Merge "Overhaul FusedLocationProvider"
This commit is contained in:
committed by
Android (Google) Code Review
commit
7dea96217e
@@ -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