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
This commit is contained in:
Soonil Nagarkar
2020-01-21 15:16:51 -08:00
parent b1f8ccfb12
commit 4c0b85ba06
12 changed files with 664 additions and 320 deletions

View File

@@ -63,6 +63,7 @@ applications that come with the platform
<privapp-permissions package="com.android.location.fused">
<permission name="android.permission.INSTALL_LOCATION_PROVIDER"/>
<permission name="android.permission.UPDATE_DEVICE_STATS"/>
</privapp-permissions>
<privapp-permissions package="com.android.managedprovisioning">

View File

@@ -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<ProviderRequest> 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<LocationRequest> getLocationRequests() {
return mLocationRequests;
}
public void setLocationRequests(List<LocationRequest> locationRequests) {
this.mLocationRequests = Preconditions.checkNotNull(locationRequests);
/** Sets the {@link LocationRequest}s associated with this request. */
public Builder setLocationRequests(List<LocationRequest> 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;
}
/**

View File

@@ -34,6 +34,7 @@ import java.util.List;
* of this package for more information.
*/
public final class ProviderRequestUnbundled {
private final ProviderRequest mRequest;
/** @hide */

View File

@@ -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"]
}

View File

@@ -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" />

View File

@@ -0,0 +1,7 @@
{
"presubmit": [
{
"name": "FusedLocationTests"
}
]
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()));
}
}
}