diff options
author | Soonil Nagarkar <sooniln@google.com> | 2020-01-21 15:16:51 -0800 |
---|---|---|
committer | Soonil Nagarkar <sooniln@google.com> | 2020-01-22 17:22:54 +0000 |
commit | 4c0b85ba06b81206cdaa9d44e6d38f5ed61081d5 (patch) | |
tree | 8c682f8acad769b1b9303d877100f4eda4f2a835 /packages/FusedLocation | |
parent | b1f8ccfb122bca5931aa0e1fdfb775f5d75fd225 (diff) |
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
Diffstat (limited to 'packages/FusedLocation')
9 files changed, 642 insertions, 309 deletions
diff --git a/packages/FusedLocation/Android.bp b/packages/FusedLocation/Android.bp index e794f726dba6..c70ab716aa44 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 a8319ab233ac..bad0497c487d 100644 --- a/packages/FusedLocation/AndroidManifest.xml +++ b/packages/FusedLocation/AndroidManifest.xml @@ -23,6 +23,8 @@ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> + <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" /> <uses-permission android:name="android.permission.INSTALL_LOCATION_PROVIDER" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> diff --git a/packages/FusedLocation/TEST_MAPPING b/packages/FusedLocation/TEST_MAPPING new file mode 100644 index 000000000000..e810d6aea8af --- /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 be817d60e55b..fb7dbc8aca5c 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 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(); - } - } - }; + private final LocationManager mLocationManager; + private final LocationListener mGpsListener; + private final LocationListener mNetworkListener; + private final BroadcastReceiver mUserChangeReceiver; - FusedLocationProvider(Context context) { - super(TAG, PROPERTIES); + @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; - mHandler = new Handler(Looper.myLooper()); - mEngine = new FusionEngine(context, Looper.myLooper(), this); + 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 init() { - // listen for user change - mContext.registerReceiverAsUser(mUserSwitchReceiver, UserHandle.ALL, - new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler); + void start() { + mContext.registerReceiver(mUserChangeReceiver, new IntentFilter(ACTION_USER_SWITCHED)); } - void destroy() { - mContext.unregisterReceiver(mUserSwitchReceiver); - mHandler.post(() -> mEngine.setRequest(null)); + void stop() { + mContext.unregisterReceiver(mUserChangeReceiver); + + synchronized (mLock) { + mRequest = new ProviderRequestUnbundled(ProviderRequest.EMPTY_REQUEST); + updateRequirementsLocked(); + } } @Override - public void onSetRequest(ProviderRequestUnbundled request, WorkSource source) { - mHandler.post(() -> mEngine.setRequest(request)); + public void onSetRequest(ProviderRequestUnbundled request, WorkSource workSource) { + synchronized (mLock) { + mRequest = request; + mWorkSource = workSource; + updateRequirementsLocked(); + } } - @Override - public void onDump(FileDescriptor fd, PrintWriter pw, String[] args) { - mEngine.dump(fd, pw, args); + @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; + } + } + } + + if (gpsInterval != mGpsInterval) { + resetProviderRequestLocked(GPS_PROVIDER, mGpsInterval, gpsInterval, mGpsListener); + mGpsInterval = gpsInterval; + } + if (networkInterval != mNetworkInterval) { + resetProviderRequestLocked(NETWORK_PROVIDER, mNetworkInterval, networkInterval, + mNetworkListener); + mNetworkInterval = networkInterval; + } + } + + @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()); + } + } + + @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); + } + + private void onUserChanged() { + // clear cached locations when the user changes to prevent leaking user information + synchronized (mLock) { + mFusedLocation = null; + mGpsLocation = null; + mNetworkLocation = null; + } + } + + 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 75bb5eceab6d..1fa3824f2321 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 e4610cf44636..000000000000 --- 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<String, ProviderStats> 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 000000000000..d6c4107a6e4a --- /dev/null +++ b/packages/FusedLocation/test/AndroidManifest.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.location.fused.tests"> + + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> + <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" /> + + <application android:label="FusedLocation Tests"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.location.fused.tests" + android:label="FusedLocation Tests" /> +</manifest> diff --git a/packages/FusedLocation/test/AndroidTest.xml b/packages/FusedLocation/test/AndroidTest.xml new file mode 100644 index 000000000000..f88e595e9763 --- /dev/null +++ b/packages/FusedLocation/test/AndroidTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<configuration description="FusedLocation Tests"> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="FusedLocationTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="FusedLocationTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.location.fused.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration>
\ 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 000000000000..33126510bc53 --- /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<Location> mLocations; + + private LocationProviderManagerCapture() { + mLocations = new LinkedBlockingQueue<>(); + } + + @Override + public void onSetAdditionalProviderPackages(List<String> 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())); + } + } +} |