From 4c0b85ba06b81206cdaa9d44e6d38f5ed61081d5 Mon Sep 17 00:00:00 2001 From: Soonil Nagarkar Date: Tue, 21 Jan 2020 15:16:51 -0800 Subject: [PATCH] Overhaul FusedLocationProvider Fix some minor bugs and ensure fused location provider correctly supports location bypass. This is especially important for when location bypass is invoked in direct boot. The added UPDATE_DEVICE_STATS permission is necessary for FusedLocation to correctly update WorkSources. FusedLocation receives work from LMS and then further delegates that work to other location providers. The other location providers should be informed of the correct applications for battery blame, and should not be blaming the FusedLocation package. 1) This is the minimally scoped permission necessary to battery blame correctly. 2) There is no way to attribute battery blame without this permission. 3) This is the correct permission - as required by LocationManager, and this permission will likely never be removed (FusedLocation will always need to battery blame). Test: atest FusedLocationTests Change-Id: If7126fffaae5577ddf8e366a0b5c17b3e5286582 --- data/etc/privapp-permissions-platform.xml | 1 + .../internal/location/ProviderRequest.java | 31 +- .../provider/ProviderRequestUnbundled.java | 1 + packages/FusedLocation/Android.bp | 26 +- packages/FusedLocation/AndroidManifest.xml | 2 + packages/FusedLocation/TEST_MAPPING | 7 + .../location/fused/FusedLocationProvider.java | 307 ++++++++++++++++-- .../location/fused/FusedLocationService.java | 17 +- .../android/location/fused/FusionEngine.java | 270 --------------- .../FusedLocation/test/AndroidManifest.xml | 19 ++ packages/FusedLocation/test/AndroidTest.xml | 30 ++ .../fused/tests/FusedLocationServiceTest.java | 273 ++++++++++++++++ 12 files changed, 664 insertions(+), 320 deletions(-) create mode 100644 packages/FusedLocation/TEST_MAPPING delete mode 100644 packages/FusedLocation/src/com/android/location/fused/FusionEngine.java create mode 100644 packages/FusedLocation/test/AndroidManifest.xml create mode 100644 packages/FusedLocation/test/AndroidTest.xml create mode 100644 packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java 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())); + } + } +}