diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 18b5f5d6ac38c..3677b8f9e66ee 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -63,6 +63,7 @@ applications that come with the platform + diff --git a/location/java/com/android/internal/location/ProviderRequest.java b/location/java/com/android/internal/location/ProviderRequest.java index 8d8df4533ebe0..b2314c585e6e8 100644 --- a/location/java/com/android/internal/location/ProviderRequest.java +++ b/location/java/com/android/internal/location/ProviderRequest.java @@ -23,11 +23,10 @@ import android.os.Parcelable; import android.os.WorkSource; import android.util.TimeUtils; -import com.android.internal.util.Preconditions; - import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; /** @hide */ public final class ProviderRequest implements Parcelable { @@ -76,8 +75,8 @@ public final class ProviderRequest implements Parcelable { this.interval = interval; this.lowPowerMode = lowPowerMode; this.locationSettingsIgnored = locationSettingsIgnored; - this.locationRequests = Preconditions.checkNotNull(locationRequests); - this.workSource = Preconditions.checkNotNull(workSource); + this.locationRequests = Objects.requireNonNull(locationRequests); + this.workSource = Objects.requireNonNull(workSource); } public static final Parcelable.Creator CREATOR = @@ -155,40 +154,50 @@ public final class ProviderRequest implements Parcelable { return mInterval; } - public void setInterval(long interval) { + /** Sets the request interval. */ + public Builder setInterval(long interval) { this.mInterval = interval; + return this; } public boolean isLowPowerMode() { return mLowPowerMode; } - public void setLowPowerMode(boolean lowPowerMode) { + /** Sets whether low power mode is enabled. */ + public Builder setLowPowerMode(boolean lowPowerMode) { this.mLowPowerMode = lowPowerMode; + return this; } public boolean isLocationSettingsIgnored() { return mLocationSettingsIgnored; } - public void setLocationSettingsIgnored(boolean locationSettingsIgnored) { + /** Sets whether location settings should be ignored. */ + public Builder setLocationSettingsIgnored(boolean locationSettingsIgnored) { this.mLocationSettingsIgnored = locationSettingsIgnored; + return this; } public List getLocationRequests() { return mLocationRequests; } - public void setLocationRequests(List locationRequests) { - this.mLocationRequests = Preconditions.checkNotNull(locationRequests); + /** Sets the {@link LocationRequest}s associated with this request. */ + public Builder setLocationRequests(List locationRequests) { + this.mLocationRequests = Objects.requireNonNull(locationRequests); + return this; } public WorkSource getWorkSource() { return mWorkSource; } - public void setWorkSource(WorkSource workSource) { - mWorkSource = Preconditions.checkNotNull(workSource); + /** Sets the work source. */ + public Builder setWorkSource(WorkSource workSource) { + mWorkSource = Objects.requireNonNull(workSource); + return this; } /** diff --git a/location/lib/java/com/android/location/provider/ProviderRequestUnbundled.java b/location/lib/java/com/android/location/provider/ProviderRequestUnbundled.java index d12d6b7778564..b650efc91907f 100644 --- a/location/lib/java/com/android/location/provider/ProviderRequestUnbundled.java +++ b/location/lib/java/com/android/location/provider/ProviderRequestUnbundled.java @@ -34,6 +34,7 @@ import java.util.List; * of this package for more information. */ public final class ProviderRequestUnbundled { + private final ProviderRequest mRequest; /** @hide */ diff --git a/packages/FusedLocation/Android.bp b/packages/FusedLocation/Android.bp index e794f726dba62..c70ab716aa443 100644 --- a/packages/FusedLocation/Android.bp +++ b/packages/FusedLocation/Android.bp @@ -14,9 +14,33 @@ android_app { name: "FusedLocation", - srcs: ["**/*.java"], + srcs: ["src/**/*.java"], libs: ["com.android.location.provider"], platform_apis: true, certificate: "platform", privileged: true, } + +android_test { + name: "FusedLocationTests", + manifest: "test/AndroidManifest.xml", + test_config: "test/AndroidTest.xml", + srcs: [ + "test/src/**/*.java", + "src/**/*.java", // include real sources because we're forced to test this directly + ], + libs: [ + "android.test.base", + "android.test.runner", + "com.android.location.provider", + ], + static_libs: [ + "androidx.test.core", + "androidx.test.rules", + "androidx.test.ext.junit", + "androidx.test.ext.truth", + "mockito-target-minus-junit4", + "truth-prebuilt", + ], + test_suites: ["device-tests"] +} \ No newline at end of file diff --git a/packages/FusedLocation/AndroidManifest.xml b/packages/FusedLocation/AndroidManifest.xml index a8319ab233ac1..bad0497c487d0 100644 --- a/packages/FusedLocation/AndroidManifest.xml +++ b/packages/FusedLocation/AndroidManifest.xml @@ -23,6 +23,8 @@ + + diff --git a/packages/FusedLocation/TEST_MAPPING b/packages/FusedLocation/TEST_MAPPING new file mode 100644 index 0000000000000..e810d6aea8af5 --- /dev/null +++ b/packages/FusedLocation/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "FusedLocationTests" + } + ] +} \ No newline at end of file diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java index be817d60e55b6..fb7dbc8aca5c6 100644 --- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java +++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java @@ -16,70 +16,307 @@ package com.android.location.fused; +import static android.content.Intent.ACTION_USER_SWITCHED; +import static android.location.LocationManager.GPS_PROVIDER; +import static android.location.LocationManager.NETWORK_PROVIDER; + +import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.location.Criteria; -import android.os.Handler; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationRequest; +import android.os.Bundle; import android.os.Looper; -import android.os.UserHandle; +import android.os.Parcelable; import android.os.WorkSource; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.location.ProviderRequest; import com.android.location.provider.LocationProviderBase; +import com.android.location.provider.LocationRequestUnbundled; import com.android.location.provider.ProviderPropertiesUnbundled; import com.android.location.provider.ProviderRequestUnbundled; -import java.io.FileDescriptor; import java.io.PrintWriter; -class FusedLocationProvider extends LocationProviderBase implements FusionEngine.Callback { +/** Basic fused location provider implementation. */ +public class FusedLocationProvider extends LocationProviderBase { + private static final String TAG = "FusedLocationProvider"; - private static ProviderPropertiesUnbundled PROPERTIES = ProviderPropertiesUnbundled.create( - false, false, false, false, true, true, true, Criteria.POWER_LOW, - Criteria.ACCURACY_FINE); + private static final ProviderPropertiesUnbundled PROPERTIES = + ProviderPropertiesUnbundled.create( + /* requiresNetwork = */ false, + /* requiresSatellite = */ false, + /* requiresCell = */ false, + /* hasMonetaryCost = */ false, + /* supportsAltitude = */ true, + /* supportsSpeed = */ true, + /* supportsBearing = */ true, + Criteria.POWER_LOW, + Criteria.ACCURACY_FINE + ); + + private static final long MAX_LOCATION_COMPARISON_NS = 11 * 1000000000L; // 11 seconds + + private final Object mLock = new Object(); private final Context mContext; - private final Handler mHandler; - private final FusionEngine mEngine; + private final LocationManager mLocationManager; + private final LocationListener mGpsListener; + private final LocationListener mNetworkListener; + private final BroadcastReceiver mUserChangeReceiver; - private final BroadcastReceiver mUserSwitchReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_USER_SWITCHED.equals(action)) { - mEngine.switchUser(); + @GuardedBy("mLock") + private ProviderRequestUnbundled mRequest; + @GuardedBy("mLock") + private WorkSource mWorkSource; + @GuardedBy("mLock") + private long mGpsInterval; + @GuardedBy("mLock") + private long mNetworkInterval; + + @GuardedBy("mLock") + @Nullable private Location mFusedLocation; + @GuardedBy("mLock") + @Nullable private Location mGpsLocation; + @GuardedBy("mLock") + @Nullable private Location mNetworkLocation; + + public FusedLocationProvider(Context context) { + super(TAG, PROPERTIES); + mContext = context; + mLocationManager = context.getSystemService(LocationManager.class); + + mGpsListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + synchronized (mLock) { + mGpsLocation = location; + reportBestLocationLocked(); + } + } + + @Override + public void onProviderDisabled(String provider) { + synchronized (mLock) { + // if satisfying a bypass request, don't clear anything + if (mRequest.getReportLocation() && mRequest.isLocationSettingsIgnored()) { + return; + } + + mGpsLocation = null; + } + } + }; + + mNetworkListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + synchronized (mLock) { + mNetworkLocation = location; + reportBestLocationLocked(); + } + } + + @Override + public void onProviderDisabled(String provider) { + synchronized (mLock) { + // if satisfying a bypass request, don't clear anything + if (mRequest.getReportLocation() && mRequest.isLocationSettingsIgnored()) { + return; + } + + mNetworkLocation = null; + } + } + }; + + mUserChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!ACTION_USER_SWITCHED.equals(intent.getAction())) { + return; + } + + onUserChanged(); + } + }; + + mRequest = new ProviderRequestUnbundled(ProviderRequest.EMPTY_REQUEST); + mWorkSource = new WorkSource(); + mGpsInterval = Long.MAX_VALUE; + mNetworkInterval = Long.MAX_VALUE; + } + + void start() { + mContext.registerReceiver(mUserChangeReceiver, new IntentFilter(ACTION_USER_SWITCHED)); + } + + void stop() { + mContext.unregisterReceiver(mUserChangeReceiver); + + synchronized (mLock) { + mRequest = new ProviderRequestUnbundled(ProviderRequest.EMPTY_REQUEST); + updateRequirementsLocked(); + } + } + + @Override + public void onSetRequest(ProviderRequestUnbundled request, WorkSource workSource) { + synchronized (mLock) { + mRequest = request; + mWorkSource = workSource; + updateRequirementsLocked(); + } + } + + @GuardedBy("mLock") + private void updateRequirementsLocked() { + long gpsInterval = Long.MAX_VALUE; + long networkInterval = Long.MAX_VALUE; + if (mRequest.getReportLocation()) { + for (LocationRequestUnbundled request : mRequest.getLocationRequests()) { + switch (request.getQuality()) { + case LocationRequestUnbundled.ACCURACY_FINE: + case LocationRequestUnbundled.POWER_HIGH: + if (request.getInterval() < gpsInterval) { + gpsInterval = request.getInterval(); + } + if (request.getInterval() < networkInterval) { + networkInterval = request.getInterval(); + } + break; + case LocationRequestUnbundled.ACCURACY_BLOCK: + case LocationRequestUnbundled.ACCURACY_CITY: + case LocationRequestUnbundled.POWER_LOW: + if (request.getInterval() < networkInterval) { + networkInterval = request.getInterval(); + } + break; + } } } - }; - FusedLocationProvider(Context context) { - super(TAG, PROPERTIES); - - mContext = context; - mHandler = new Handler(Looper.myLooper()); - mEngine = new FusionEngine(context, Looper.myLooper(), this); + if (gpsInterval != mGpsInterval) { + resetProviderRequestLocked(GPS_PROVIDER, mGpsInterval, gpsInterval, mGpsListener); + mGpsInterval = gpsInterval; + } + if (networkInterval != mNetworkInterval) { + resetProviderRequestLocked(NETWORK_PROVIDER, mNetworkInterval, networkInterval, + mNetworkListener); + mNetworkInterval = networkInterval; + } } - void init() { - // listen for user change - mContext.registerReceiverAsUser(mUserSwitchReceiver, UserHandle.ALL, - new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler); + @GuardedBy("mLock") + private void resetProviderRequestLocked(String provider, long oldInterval, long newInterval, + LocationListener listener) { + if (oldInterval != Long.MAX_VALUE) { + mLocationManager.removeUpdates(listener); + } + if (newInterval != Long.MAX_VALUE) { + LocationRequest request = LocationRequest.createFromDeprecatedProvider( + provider, newInterval, 0, false); + if (mRequest.isLocationSettingsIgnored()) { + request.setLocationSettingsIgnored(true); + } + request.setWorkSource(mWorkSource); + mLocationManager.requestLocationUpdates(request, listener, Looper.getMainLooper()); + } } - void destroy() { - mContext.unregisterReceiver(mUserSwitchReceiver); - mHandler.post(() -> mEngine.setRequest(null)); + @GuardedBy("mLock") + private void reportBestLocationLocked() { + Location bestLocation = chooseBestLocation(mGpsLocation, mNetworkLocation); + if (bestLocation == mFusedLocation) { + return; + } + + mFusedLocation = bestLocation; + if (mFusedLocation == null) { + return; + } + + // copy NO_GPS_LOCATION extra from mNetworkLocation into mFusedLocation + if (mNetworkLocation != null) { + Bundle srcExtras = mNetworkLocation.getExtras(); + if (srcExtras != null) { + Parcelable srcParcelable = + srcExtras.getParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION); + if (srcParcelable instanceof Location) { + Bundle dstExtras = mFusedLocation.getExtras(); + if (dstExtras == null) { + dstExtras = new Bundle(); + mFusedLocation.setExtras(dstExtras); + } + dstExtras.putParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION, + srcParcelable); + } + } + } + + reportLocation(mFusedLocation); } - @Override - public void onSetRequest(ProviderRequestUnbundled request, WorkSource source) { - mHandler.post(() -> mEngine.setRequest(request)); + private void onUserChanged() { + // clear cached locations when the user changes to prevent leaking user information + synchronized (mLock) { + mFusedLocation = null; + mGpsLocation = null; + mNetworkLocation = null; + } } - @Override - public void onDump(FileDescriptor fd, PrintWriter pw, String[] args) { - mEngine.dump(fd, pw, args); + void dump(PrintWriter writer) { + synchronized (mLock) { + writer.println("request: " + mRequest); + if (mGpsInterval != Long.MAX_VALUE) { + writer.println(" gps interval: " + mGpsInterval); + } + if (mNetworkInterval != Long.MAX_VALUE) { + writer.println(" network interval: " + mNetworkInterval); + } + if (mGpsLocation != null) { + writer.println(" last gps location: " + mGpsLocation); + } + if (mNetworkLocation != null) { + writer.println(" last network location: " + mNetworkLocation); + } + } + } + + @Nullable + private static Location chooseBestLocation( + @Nullable Location locationA, + @Nullable Location locationB) { + if (locationA == null) { + return locationB; + } + if (locationB == null) { + return locationA; + } + + if (locationA.getElapsedRealtimeNanos() + > locationB.getElapsedRealtimeNanos() + MAX_LOCATION_COMPARISON_NS) { + return locationA; + } + if (locationB.getElapsedRealtimeNanos() + > locationA.getElapsedRealtimeNanos() + MAX_LOCATION_COMPARISON_NS) { + return locationB; + } + + if (!locationA.hasAccuracy()) { + return locationB; + } + if (!locationB.hasAccuracy()) { + return locationA; + } + return locationA.getAccuracy() < locationB.getAccuracy() ? locationA : locationB; } } diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationService.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationService.java index 75bb5eceab6d2..1fa3824f23213 100644 --- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationService.java +++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationService.java @@ -16,19 +16,23 @@ package com.android.location.fused; +import android.annotation.Nullable; import android.app.Service; import android.content.Intent; import android.os.IBinder; +import java.io.FileDescriptor; +import java.io.PrintWriter; + public class FusedLocationService extends Service { - private FusedLocationProvider mProvider; + @Nullable private FusedLocationProvider mProvider; @Override public IBinder onBind(Intent intent) { if (mProvider == null) { mProvider = new FusedLocationProvider(this); - mProvider.init(); + mProvider.start(); } return mProvider.getBinder(); @@ -37,8 +41,15 @@ public class FusedLocationService extends Service { @Override public void onDestroy() { if (mProvider != null) { - mProvider.destroy(); + mProvider.stop(); mProvider = null; } } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + if (mProvider != null) { + mProvider.dump(writer); + } + } } diff --git a/packages/FusedLocation/src/com/android/location/fused/FusionEngine.java b/packages/FusedLocation/src/com/android/location/fused/FusionEngine.java deleted file mode 100644 index e4610cf44636b..0000000000000 --- a/packages/FusedLocation/src/com/android/location/fused/FusionEngine.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.location.fused; - -import android.content.Context; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Bundle; -import android.os.Looper; -import android.os.Parcelable; -import android.os.SystemClock; -import android.util.Log; - -import com.android.location.provider.LocationProviderBase; -import com.android.location.provider.LocationRequestUnbundled; -import com.android.location.provider.ProviderRequestUnbundled; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.HashMap; - -public class FusionEngine implements LocationListener { - public interface Callback { - void reportLocation(Location location); - } - - private static final String TAG = "FusedLocation"; - private static final String NETWORK = LocationManager.NETWORK_PROVIDER; - private static final String GPS = LocationManager.GPS_PROVIDER; - private static final String FUSED = LocationProviderBase.FUSED_PROVIDER; - - public static final long SWITCH_ON_FRESHNESS_CLIFF_NS = 11 * 1000000000L; // 11 seconds - - private final LocationManager mLocationManager; - private final Looper mLooper; - private final Callback mCallback; - - // all fields are only used on mLooper thread. except for in dump() which is not thread-safe - private Location mFusedLocation; - private Location mGpsLocation; - private Location mNetworkLocation; - - private ProviderRequestUnbundled mRequest; - - private final HashMap mStats = new HashMap<>(); - - FusionEngine(Context context, Looper looper, Callback callback) { - mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - mNetworkLocation = new Location(""); - mNetworkLocation.setAccuracy(Float.MAX_VALUE); - mGpsLocation = new Location(""); - mGpsLocation.setAccuracy(Float.MAX_VALUE); - mLooper = looper; - mCallback = callback; - - mStats.put(GPS, new ProviderStats()); - mStats.put(NETWORK, new ProviderStats()); - } - - /** Called on mLooper thread */ - public void setRequest(ProviderRequestUnbundled request) { - mRequest = request; - updateRequirements(); - } - - private static class ProviderStats { - public boolean requested; - public long requestTime; - public long minTime; - - @Override - public String toString() { - return (requested ? " REQUESTED" : " ---"); - } - } - - private void enableProvider(String name, long minTime) { - ProviderStats stats = mStats.get(name); - if (stats == null) return; - - if (mLocationManager.isProviderEnabled(name)) { - if (!stats.requested) { - stats.requestTime = SystemClock.elapsedRealtime(); - stats.requested = true; - stats.minTime = minTime; - mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper); - } else if (stats.minTime != minTime) { - stats.minTime = minTime; - mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper); - } - } - } - - private void disableProvider(String name) { - ProviderStats stats = mStats.get(name); - if (stats == null) return; - - if (stats.requested) { - stats.requested = false; - mLocationManager.removeUpdates(this); //TODO GLOBAL - } - } - - private void updateRequirements() { - if (mRequest == null || !mRequest.getReportLocation()) { - mRequest = null; - disableProvider(NETWORK); - disableProvider(GPS); - return; - } - - long networkInterval = Long.MAX_VALUE; - long gpsInterval = Long.MAX_VALUE; - for (LocationRequestUnbundled request : mRequest.getLocationRequests()) { - switch (request.getQuality()) { - case LocationRequestUnbundled.ACCURACY_FINE: - case LocationRequestUnbundled.POWER_HIGH: - if (request.getInterval() < gpsInterval) { - gpsInterval = request.getInterval(); - } - if (request.getInterval() < networkInterval) { - networkInterval = request.getInterval(); - } - break; - case LocationRequestUnbundled.ACCURACY_BLOCK: - case LocationRequestUnbundled.ACCURACY_CITY: - case LocationRequestUnbundled.POWER_LOW: - if (request.getInterval() < networkInterval) { - networkInterval = request.getInterval(); - } - break; - } - } - - if (gpsInterval < Long.MAX_VALUE) { - enableProvider(GPS, gpsInterval); - } else { - disableProvider(GPS); - } - if (networkInterval < Long.MAX_VALUE) { - enableProvider(NETWORK, networkInterval); - } else { - disableProvider(NETWORK); - } - } - - /** - * Test whether one location (a) is better to use than another (b). - */ - private static boolean isBetterThan(Location locationA, Location locationB) { - if (locationA == null) { - return false; - } - if (locationB == null) { - return true; - } - // A provider is better if the reading is sufficiently newer. Heading - // underground can cause GPS to stop reporting fixes. In this case it's - // appropriate to revert to cell, even when its accuracy is less. - if (locationA.getElapsedRealtimeNanos() - > locationB.getElapsedRealtimeNanos() + SWITCH_ON_FRESHNESS_CLIFF_NS) { - return true; - } - - // A provider is better if it has better accuracy. Assuming both readings - // are fresh (and by that accurate), choose the one with the smaller - // accuracy circle. - if (!locationA.hasAccuracy()) { - return false; - } - if (!locationB.hasAccuracy()) { - return true; - } - return locationA.getAccuracy() < locationB.getAccuracy(); - } - - private void updateFusedLocation() { - // may the best location win! - if (isBetterThan(mGpsLocation, mNetworkLocation)) { - mFusedLocation = new Location(mGpsLocation); - } else { - mFusedLocation = new Location(mNetworkLocation); - } - mFusedLocation.setProvider(FUSED); - if (mNetworkLocation != null) { - // copy NO_GPS_LOCATION extra from mNetworkLocation into mFusedLocation - Bundle srcExtras = mNetworkLocation.getExtras(); - if (srcExtras != null) { - Parcelable srcParcelable = - srcExtras.getParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION); - if (srcParcelable instanceof Location) { - Bundle dstExtras = mFusedLocation.getExtras(); - if (dstExtras == null) { - dstExtras = new Bundle(); - mFusedLocation.setExtras(dstExtras); - } - dstExtras.putParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION, - srcParcelable); - } - } - } - - if (mCallback != null) { - mCallback.reportLocation(mFusedLocation); - } else { - Log.w(TAG, "Location updates received while fusion engine not started"); - } - } - - /** Called on mLooper thread */ - @Override - public void onLocationChanged(Location location) { - if (GPS.equals(location.getProvider())) { - mGpsLocation = location; - updateFusedLocation(); - } else if (NETWORK.equals(location.getProvider())) { - mNetworkLocation = location; - updateFusedLocation(); - } - } - - /** Called on mLooper thread */ - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - } - - /** Called on mLooper thread */ - @Override - public void onProviderEnabled(String provider) { - } - - /** Called on mLooper thread */ - @Override - public void onProviderDisabled(String provider) { - } - - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - StringBuilder s = new StringBuilder(); - s.append(mRequest).append('\n'); - s.append("fused=").append(mFusedLocation).append('\n'); - s.append(String.format("gps %s\n", mGpsLocation)); - s.append(" ").append(mStats.get(GPS)).append('\n'); - s.append(String.format("net %s\n", mNetworkLocation)); - s.append(" ").append(mStats.get(NETWORK)).append('\n'); - pw.append(s); - } - - /** Called on mLooper thread */ - public void switchUser() { - // reset state to prevent location data leakage - mFusedLocation = null; - mGpsLocation = null; - mNetworkLocation = null; - } -} diff --git a/packages/FusedLocation/test/AndroidManifest.xml b/packages/FusedLocation/test/AndroidManifest.xml new file mode 100644 index 0000000000000..d6c4107a6e4aa --- /dev/null +++ b/packages/FusedLocation/test/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/packages/FusedLocation/test/AndroidTest.xml b/packages/FusedLocation/test/AndroidTest.xml new file mode 100644 index 0000000000000..f88e595e9763d --- /dev/null +++ b/packages/FusedLocation/test/AndroidTest.xml @@ -0,0 +1,30 @@ + + + + + + + \ No newline at end of file diff --git a/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java b/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java new file mode 100644 index 0000000000000..33126510bc53c --- /dev/null +++ b/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.location.fused.tests; + +import static android.location.LocationManager.FUSED_PROVIDER; +import static android.location.LocationManager.GPS_PROVIDER; +import static android.location.LocationManager.NETWORK_PROVIDER; + +import static androidx.test.ext.truth.location.LocationSubject.assertThat; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationManager; +import android.location.LocationRequest; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.os.WorkSource; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.location.ILocationProvider; +import com.android.internal.location.ILocationProviderManager; +import com.android.internal.location.ProviderProperties; +import com.android.internal.location.ProviderRequest; +import com.android.location.fused.FusedLocationProvider; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +public class FusedLocationServiceTest { + + private static final String TAG = "FusedLocationServiceTest"; + + private static final long TIMEOUT_MS = 5000; + + private Context mContext; + private Random mRandom; + private LocationManager mLocationManager; + + private ILocationProvider mProvider; + private LocationProviderManagerCapture mManager; + + @Before + public void setUp() throws Exception { + long seed = System.currentTimeMillis(); + Log.i(TAG, "location seed: " + seed); + + mContext = InstrumentationRegistry.getTargetContext(); + mRandom = new Random(seed); + mLocationManager = mContext.getSystemService(LocationManager.class); + + setMockLocation(true); + + mManager = new LocationProviderManagerCapture(); + mProvider = ILocationProvider.Stub.asInterface( + new FusedLocationProvider(mContext).getBinder()); + mProvider.setLocationProviderManager(mManager); + + mLocationManager.addTestProvider(NETWORK_PROVIDER, + true, + false, + true, + false, + false, + false, + false, + Criteria.POWER_MEDIUM, + Criteria.ACCURACY_FINE); + mLocationManager.setTestProviderEnabled(NETWORK_PROVIDER, true); + mLocationManager.addTestProvider(GPS_PROVIDER, + true, + false, + true, + false, + false, + false, + false, + Criteria.POWER_MEDIUM, + Criteria.ACCURACY_FINE); + mLocationManager.setTestProviderEnabled(GPS_PROVIDER, true); + } + + @After + public void tearDown() throws Exception { + for (String provider : mLocationManager.getAllProviders()) { + mLocationManager.removeTestProvider(provider); + } + + setMockLocation(false); + } + + @Test + public void testNetworkRequest() throws Exception { + LocationRequest request = LocationRequest.createFromDeprecatedProvider(FUSED_PROVIDER, 1000, + 0, false); + + mProvider.setRequest( + new ProviderRequest.Builder() + .setInterval(1000) + .setLocationRequests(Collections.singletonList(request)) + .build(), + new WorkSource()); + + Location location = createLocation(NETWORK_PROVIDER, mRandom); + mLocationManager.setTestProviderLocation(NETWORK_PROVIDER, location); + + assertThat(mManager.getNextLocation(TIMEOUT_MS)).isEqualTo(location); + } + + @Test + public void testGpsRequest() throws Exception { + LocationRequest request = LocationRequest.createFromDeprecatedProvider(FUSED_PROVIDER, 1000, + 0, false).setQuality(LocationRequest.POWER_HIGH); + + mProvider.setRequest( + new ProviderRequest.Builder() + .setInterval(1000) + .setLocationRequests(Collections.singletonList(request)) + .build(), + new WorkSource()); + + Location location = createLocation(GPS_PROVIDER, mRandom); + mLocationManager.setTestProviderLocation(GPS_PROVIDER, location); + + assertThat(mManager.getNextLocation(TIMEOUT_MS)).isEqualTo(location); + } + + @Test + public void testBypassRequest() throws Exception { + LocationRequest request = LocationRequest.createFromDeprecatedProvider(FUSED_PROVIDER, 1000, + 0, false).setQuality(LocationRequest.POWER_HIGH).setLocationSettingsIgnored(true); + + mProvider.setRequest( + new ProviderRequest.Builder() + .setInterval(1000) + .setLocationSettingsIgnored(true) + .setLocationRequests(Collections.singletonList(request)) + .build(), + new WorkSource()); + + boolean containsNetworkBypass = false; + for (LocationRequest iRequest : mLocationManager.getTestProviderCurrentRequests( + NETWORK_PROVIDER)) { + if (iRequest.isLocationSettingsIgnored()) { + containsNetworkBypass = true; + break; + } + } + + boolean containsGpsBypass = false; + for (LocationRequest iRequest : mLocationManager.getTestProviderCurrentRequests( + GPS_PROVIDER)) { + if (iRequest.isLocationSettingsIgnored()) { + containsGpsBypass = true; + break; + } + } + + assertThat(containsNetworkBypass).isTrue(); + assertThat(containsGpsBypass).isTrue(); + } + + private static class LocationProviderManagerCapture extends ILocationProviderManager.Stub { + + private final LinkedBlockingQueue mLocations; + + private LocationProviderManagerCapture() { + mLocations = new LinkedBlockingQueue<>(); + } + + @Override + public void onSetAdditionalProviderPackages(List packageNames) { + + } + + @Override + public void onSetEnabled(boolean enabled) { + + } + + @Override + public void onSetProperties(ProviderProperties properties) { + + } + + @Override + public void onReportLocation(Location location) { + mLocations.add(location); + } + + public Location getNextLocation(long timeoutMs) throws InterruptedException { + return mLocations.poll(timeoutMs, TimeUnit.MILLISECONDS); + } + } + + private static final double MIN_LATITUDE = -90D; + private static final double MAX_LATITUDE = 90D; + private static final double MIN_LONGITUDE = -180D; + private static final double MAX_LONGITUDE = 180D; + + private static final float MIN_ACCURACY = 1; + private static final float MAX_ACCURACY = 100; + + private static Location createLocation(String provider, Random random) { + return createLocation(provider, + MIN_LATITUDE + random.nextDouble() * (MAX_LATITUDE - MIN_LATITUDE), + MIN_LONGITUDE + random.nextDouble() * (MAX_LONGITUDE - MIN_LONGITUDE), + MIN_ACCURACY + random.nextFloat() * (MAX_ACCURACY - MIN_ACCURACY)); + } + + private static Location createLocation(String provider, double latitude, double longitude, + float accuracy) { + Location location = new Location(provider); + location.setLatitude(latitude); + location.setLongitude(longitude); + location.setAccuracy(accuracy); + location.setTime(System.currentTimeMillis()); + location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); + return location; + } + + private static void setMockLocation(boolean allowed) throws IOException { + ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation() + .executeShellCommand("appops set " + + InstrumentationRegistry.getTargetContext().getPackageName() + + " android:mock_location " + (allowed ? "allow" : "deny")); + try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + byte[] buffer = new byte[32768]; + int count; + try { + while ((count = fis.read(buffer)) != -1) { + os.write(buffer, 0, count); + } + fis.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + Log.e(TAG, new String(os.toByteArray())); + } + } +}