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:
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,7 @@ import java.util.List;
|
||||
* of this package for more information.
|
||||
*/
|
||||
public final class ProviderRequestUnbundled {
|
||||
|
||||
private final ProviderRequest mRequest;
|
||||
|
||||
/** @hide */
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
|
||||
7
packages/FusedLocation/TEST_MAPPING
Normal file
7
packages/FusedLocation/TEST_MAPPING
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"presubmit": [
|
||||
{
|
||||
"name": "FusedLocationTests"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
19
packages/FusedLocation/test/AndroidManifest.xml
Normal file
19
packages/FusedLocation/test/AndroidManifest.xml
Normal 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>
|
||||
30
packages/FusedLocation/test/AndroidTest.xml
Normal file
30
packages/FusedLocation/test/AndroidTest.xml
Normal 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>
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user