Merge "Overhaul FusedLocationProvider"

This commit is contained in:
TreeHugger Robot
2020-01-22 17:25:31 +00:00
committed by Android (Google) Code Review
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()));
}
}
}