diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 18b5f5d6ac38c..3677b8f9e66ee 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -63,6 +63,7 @@ applications that come with the platform
+
diff --git a/location/java/com/android/internal/location/ProviderRequest.java b/location/java/com/android/internal/location/ProviderRequest.java
index 8d8df4533ebe0..b2314c585e6e8 100644
--- a/location/java/com/android/internal/location/ProviderRequest.java
+++ b/location/java/com/android/internal/location/ProviderRequest.java
@@ -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 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 getLocationRequests() {
return mLocationRequests;
}
- public void setLocationRequests(List locationRequests) {
- this.mLocationRequests = Preconditions.checkNotNull(locationRequests);
+ /** Sets the {@link LocationRequest}s associated with this request. */
+ public Builder setLocationRequests(List 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;
}
/**
diff --git a/location/lib/java/com/android/location/provider/ProviderRequestUnbundled.java b/location/lib/java/com/android/location/provider/ProviderRequestUnbundled.java
index d12d6b7778564..b650efc91907f 100644
--- a/location/lib/java/com/android/location/provider/ProviderRequestUnbundled.java
+++ b/location/lib/java/com/android/location/provider/ProviderRequestUnbundled.java
@@ -34,6 +34,7 @@ import java.util.List;
* of this package for more information.
*/
public final class ProviderRequestUnbundled {
+
private final ProviderRequest mRequest;
/** @hide */
diff --git a/packages/FusedLocation/Android.bp b/packages/FusedLocation/Android.bp
index e794f726dba62..c70ab716aa443 100644
--- a/packages/FusedLocation/Android.bp
+++ b/packages/FusedLocation/Android.bp
@@ -14,9 +14,33 @@
android_app {
name: "FusedLocation",
- srcs: ["**/*.java"],
+ srcs: ["src/**/*.java"],
libs: ["com.android.location.provider"],
platform_apis: true,
certificate: "platform",
privileged: true,
}
+
+android_test {
+ name: "FusedLocationTests",
+ manifest: "test/AndroidManifest.xml",
+ test_config: "test/AndroidTest.xml",
+ srcs: [
+ "test/src/**/*.java",
+ "src/**/*.java", // include real sources because we're forced to test this directly
+ ],
+ libs: [
+ "android.test.base",
+ "android.test.runner",
+ "com.android.location.provider",
+ ],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "androidx.test.ext.truth",
+ "mockito-target-minus-junit4",
+ "truth-prebuilt",
+ ],
+ test_suites: ["device-tests"]
+}
\ No newline at end of file
diff --git a/packages/FusedLocation/AndroidManifest.xml b/packages/FusedLocation/AndroidManifest.xml
index a8319ab233ac1..bad0497c487d0 100644
--- a/packages/FusedLocation/AndroidManifest.xml
+++ b/packages/FusedLocation/AndroidManifest.xml
@@ -23,6 +23,8 @@
+
+
diff --git a/packages/FusedLocation/TEST_MAPPING b/packages/FusedLocation/TEST_MAPPING
new file mode 100644
index 0000000000000..e810d6aea8af5
--- /dev/null
+++ b/packages/FusedLocation/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "FusedLocationTests"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
index be817d60e55b6..fb7dbc8aca5c6 100644
--- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
+++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
@@ -16,70 +16,307 @@
package com.android.location.fused;
+import static android.content.Intent.ACTION_USER_SWITCHED;
+import static android.location.LocationManager.GPS_PROVIDER;
+import static android.location.LocationManager.NETWORK_PROVIDER;
+
+import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.Criteria;
-import android.os.Handler;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.os.Bundle;
import android.os.Looper;
-import android.os.UserHandle;
+import android.os.Parcelable;
import android.os.WorkSource;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.location.ProviderRequest;
import com.android.location.provider.LocationProviderBase;
+import com.android.location.provider.LocationRequestUnbundled;
import com.android.location.provider.ProviderPropertiesUnbundled;
import com.android.location.provider.ProviderRequestUnbundled;
-import java.io.FileDescriptor;
import java.io.PrintWriter;
-class FusedLocationProvider extends LocationProviderBase implements FusionEngine.Callback {
+/** Basic fused location provider implementation. */
+public class FusedLocationProvider extends LocationProviderBase {
+
private static final String TAG = "FusedLocationProvider";
- private static ProviderPropertiesUnbundled PROPERTIES = ProviderPropertiesUnbundled.create(
- false, false, false, false, true, true, true, Criteria.POWER_LOW,
- Criteria.ACCURACY_FINE);
+ private static final ProviderPropertiesUnbundled PROPERTIES =
+ ProviderPropertiesUnbundled.create(
+ /* requiresNetwork = */ false,
+ /* requiresSatellite = */ false,
+ /* requiresCell = */ false,
+ /* hasMonetaryCost = */ false,
+ /* supportsAltitude = */ true,
+ /* supportsSpeed = */ true,
+ /* supportsBearing = */ true,
+ Criteria.POWER_LOW,
+ Criteria.ACCURACY_FINE
+ );
+
+ private static final long MAX_LOCATION_COMPARISON_NS = 11 * 1000000000L; // 11 seconds
+
+ private final Object mLock = new Object();
private final Context mContext;
- private final Handler mHandler;
- private final FusionEngine mEngine;
+ private final 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;
}
}
diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationService.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationService.java
index 75bb5eceab6d2..1fa3824f23213 100644
--- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationService.java
+++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationService.java
@@ -16,19 +16,23 @@
package com.android.location.fused;
+import android.annotation.Nullable;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
public class FusedLocationService extends Service {
- private FusedLocationProvider mProvider;
+ @Nullable private FusedLocationProvider mProvider;
@Override
public IBinder onBind(Intent intent) {
if (mProvider == null) {
mProvider = new FusedLocationProvider(this);
- mProvider.init();
+ mProvider.start();
}
return mProvider.getBinder();
@@ -37,8 +41,15 @@ public class FusedLocationService extends Service {
@Override
public void onDestroy() {
if (mProvider != null) {
- mProvider.destroy();
+ mProvider.stop();
mProvider = null;
}
}
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (mProvider != null) {
+ mProvider.dump(writer);
+ }
+ }
}
diff --git a/packages/FusedLocation/src/com/android/location/fused/FusionEngine.java b/packages/FusedLocation/src/com/android/location/fused/FusionEngine.java
deleted file mode 100644
index e4610cf44636b..0000000000000
--- a/packages/FusedLocation/src/com/android/location/fused/FusionEngine.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.location.fused;
-
-import android.content.Context;
-import android.location.Location;
-import android.location.LocationListener;
-import android.location.LocationManager;
-import android.os.Bundle;
-import android.os.Looper;
-import android.os.Parcelable;
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.location.provider.LocationProviderBase;
-import com.android.location.provider.LocationRequestUnbundled;
-import com.android.location.provider.ProviderRequestUnbundled;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.HashMap;
-
-public class FusionEngine implements LocationListener {
- public interface Callback {
- void reportLocation(Location location);
- }
-
- private static final String TAG = "FusedLocation";
- private static final String NETWORK = LocationManager.NETWORK_PROVIDER;
- private static final String GPS = LocationManager.GPS_PROVIDER;
- private static final String FUSED = LocationProviderBase.FUSED_PROVIDER;
-
- public static final long SWITCH_ON_FRESHNESS_CLIFF_NS = 11 * 1000000000L; // 11 seconds
-
- private final LocationManager mLocationManager;
- private final Looper mLooper;
- private final Callback mCallback;
-
- // all fields are only used on mLooper thread. except for in dump() which is not thread-safe
- private Location mFusedLocation;
- private Location mGpsLocation;
- private Location mNetworkLocation;
-
- private ProviderRequestUnbundled mRequest;
-
- private final HashMap mStats = new HashMap<>();
-
- FusionEngine(Context context, Looper looper, Callback callback) {
- mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
- mNetworkLocation = new Location("");
- mNetworkLocation.setAccuracy(Float.MAX_VALUE);
- mGpsLocation = new Location("");
- mGpsLocation.setAccuracy(Float.MAX_VALUE);
- mLooper = looper;
- mCallback = callback;
-
- mStats.put(GPS, new ProviderStats());
- mStats.put(NETWORK, new ProviderStats());
- }
-
- /** Called on mLooper thread */
- public void setRequest(ProviderRequestUnbundled request) {
- mRequest = request;
- updateRequirements();
- }
-
- private static class ProviderStats {
- public boolean requested;
- public long requestTime;
- public long minTime;
-
- @Override
- public String toString() {
- return (requested ? " REQUESTED" : " ---");
- }
- }
-
- private void enableProvider(String name, long minTime) {
- ProviderStats stats = mStats.get(name);
- if (stats == null) return;
-
- if (mLocationManager.isProviderEnabled(name)) {
- if (!stats.requested) {
- stats.requestTime = SystemClock.elapsedRealtime();
- stats.requested = true;
- stats.minTime = minTime;
- mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper);
- } else if (stats.minTime != minTime) {
- stats.minTime = minTime;
- mLocationManager.requestLocationUpdates(name, minTime, 0, this, mLooper);
- }
- }
- }
-
- private void disableProvider(String name) {
- ProviderStats stats = mStats.get(name);
- if (stats == null) return;
-
- if (stats.requested) {
- stats.requested = false;
- mLocationManager.removeUpdates(this); //TODO GLOBAL
- }
- }
-
- private void updateRequirements() {
- if (mRequest == null || !mRequest.getReportLocation()) {
- mRequest = null;
- disableProvider(NETWORK);
- disableProvider(GPS);
- return;
- }
-
- long networkInterval = Long.MAX_VALUE;
- long gpsInterval = Long.MAX_VALUE;
- for (LocationRequestUnbundled request : mRequest.getLocationRequests()) {
- switch (request.getQuality()) {
- case LocationRequestUnbundled.ACCURACY_FINE:
- case LocationRequestUnbundled.POWER_HIGH:
- if (request.getInterval() < gpsInterval) {
- gpsInterval = request.getInterval();
- }
- if (request.getInterval() < networkInterval) {
- networkInterval = request.getInterval();
- }
- break;
- case LocationRequestUnbundled.ACCURACY_BLOCK:
- case LocationRequestUnbundled.ACCURACY_CITY:
- case LocationRequestUnbundled.POWER_LOW:
- if (request.getInterval() < networkInterval) {
- networkInterval = request.getInterval();
- }
- break;
- }
- }
-
- if (gpsInterval < Long.MAX_VALUE) {
- enableProvider(GPS, gpsInterval);
- } else {
- disableProvider(GPS);
- }
- if (networkInterval < Long.MAX_VALUE) {
- enableProvider(NETWORK, networkInterval);
- } else {
- disableProvider(NETWORK);
- }
- }
-
- /**
- * Test whether one location (a) is better to use than another (b).
- */
- private static boolean isBetterThan(Location locationA, Location locationB) {
- if (locationA == null) {
- return false;
- }
- if (locationB == null) {
- return true;
- }
- // A provider is better if the reading is sufficiently newer. Heading
- // underground can cause GPS to stop reporting fixes. In this case it's
- // appropriate to revert to cell, even when its accuracy is less.
- if (locationA.getElapsedRealtimeNanos()
- > locationB.getElapsedRealtimeNanos() + SWITCH_ON_FRESHNESS_CLIFF_NS) {
- return true;
- }
-
- // A provider is better if it has better accuracy. Assuming both readings
- // are fresh (and by that accurate), choose the one with the smaller
- // accuracy circle.
- if (!locationA.hasAccuracy()) {
- return false;
- }
- if (!locationB.hasAccuracy()) {
- return true;
- }
- return locationA.getAccuracy() < locationB.getAccuracy();
- }
-
- private void updateFusedLocation() {
- // may the best location win!
- if (isBetterThan(mGpsLocation, mNetworkLocation)) {
- mFusedLocation = new Location(mGpsLocation);
- } else {
- mFusedLocation = new Location(mNetworkLocation);
- }
- mFusedLocation.setProvider(FUSED);
- if (mNetworkLocation != null) {
- // copy NO_GPS_LOCATION extra from mNetworkLocation into mFusedLocation
- Bundle srcExtras = mNetworkLocation.getExtras();
- if (srcExtras != null) {
- Parcelable srcParcelable =
- srcExtras.getParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION);
- if (srcParcelable instanceof Location) {
- Bundle dstExtras = mFusedLocation.getExtras();
- if (dstExtras == null) {
- dstExtras = new Bundle();
- mFusedLocation.setExtras(dstExtras);
- }
- dstExtras.putParcelable(LocationProviderBase.EXTRA_NO_GPS_LOCATION,
- srcParcelable);
- }
- }
- }
-
- if (mCallback != null) {
- mCallback.reportLocation(mFusedLocation);
- } else {
- Log.w(TAG, "Location updates received while fusion engine not started");
- }
- }
-
- /** Called on mLooper thread */
- @Override
- public void onLocationChanged(Location location) {
- if (GPS.equals(location.getProvider())) {
- mGpsLocation = location;
- updateFusedLocation();
- } else if (NETWORK.equals(location.getProvider())) {
- mNetworkLocation = location;
- updateFusedLocation();
- }
- }
-
- /** Called on mLooper thread */
- @Override
- public void onStatusChanged(String provider, int status, Bundle extras) {
- }
-
- /** Called on mLooper thread */
- @Override
- public void onProviderEnabled(String provider) {
- }
-
- /** Called on mLooper thread */
- @Override
- public void onProviderDisabled(String provider) {
- }
-
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- StringBuilder s = new StringBuilder();
- s.append(mRequest).append('\n');
- s.append("fused=").append(mFusedLocation).append('\n');
- s.append(String.format("gps %s\n", mGpsLocation));
- s.append(" ").append(mStats.get(GPS)).append('\n');
- s.append(String.format("net %s\n", mNetworkLocation));
- s.append(" ").append(mStats.get(NETWORK)).append('\n');
- pw.append(s);
- }
-
- /** Called on mLooper thread */
- public void switchUser() {
- // reset state to prevent location data leakage
- mFusedLocation = null;
- mGpsLocation = null;
- mNetworkLocation = null;
- }
-}
diff --git a/packages/FusedLocation/test/AndroidManifest.xml b/packages/FusedLocation/test/AndroidManifest.xml
new file mode 100644
index 0000000000000..d6c4107a6e4aa
--- /dev/null
+++ b/packages/FusedLocation/test/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/FusedLocation/test/AndroidTest.xml b/packages/FusedLocation/test/AndroidTest.xml
new file mode 100644
index 0000000000000..f88e595e9763d
--- /dev/null
+++ b/packages/FusedLocation/test/AndroidTest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java b/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java
new file mode 100644
index 0000000000000..33126510bc53c
--- /dev/null
+++ b/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.location.fused.tests;
+
+import static android.location.LocationManager.FUSED_PROVIDER;
+import static android.location.LocationManager.GPS_PROVIDER;
+import static android.location.LocationManager.NETWORK_PROVIDER;
+
+import static androidx.test.ext.truth.location.LocationSubject.assertThat;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.os.WorkSource;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.location.ILocationProvider;
+import com.android.internal.location.ILocationProviderManager;
+import com.android.internal.location.ProviderProperties;
+import com.android.internal.location.ProviderRequest;
+import com.android.location.fused.FusedLocationProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class FusedLocationServiceTest {
+
+ private static final String TAG = "FusedLocationServiceTest";
+
+ private static final long TIMEOUT_MS = 5000;
+
+ private Context mContext;
+ private Random mRandom;
+ private LocationManager mLocationManager;
+
+ private ILocationProvider mProvider;
+ private LocationProviderManagerCapture mManager;
+
+ @Before
+ public void setUp() throws Exception {
+ long seed = System.currentTimeMillis();
+ Log.i(TAG, "location seed: " + seed);
+
+ mContext = InstrumentationRegistry.getTargetContext();
+ mRandom = new Random(seed);
+ mLocationManager = mContext.getSystemService(LocationManager.class);
+
+ setMockLocation(true);
+
+ mManager = new LocationProviderManagerCapture();
+ mProvider = ILocationProvider.Stub.asInterface(
+ new FusedLocationProvider(mContext).getBinder());
+ mProvider.setLocationProviderManager(mManager);
+
+ mLocationManager.addTestProvider(NETWORK_PROVIDER,
+ true,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ Criteria.POWER_MEDIUM,
+ Criteria.ACCURACY_FINE);
+ mLocationManager.setTestProviderEnabled(NETWORK_PROVIDER, true);
+ mLocationManager.addTestProvider(GPS_PROVIDER,
+ true,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ Criteria.POWER_MEDIUM,
+ Criteria.ACCURACY_FINE);
+ mLocationManager.setTestProviderEnabled(GPS_PROVIDER, true);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (String provider : mLocationManager.getAllProviders()) {
+ mLocationManager.removeTestProvider(provider);
+ }
+
+ setMockLocation(false);
+ }
+
+ @Test
+ public void testNetworkRequest() throws Exception {
+ LocationRequest request = LocationRequest.createFromDeprecatedProvider(FUSED_PROVIDER, 1000,
+ 0, false);
+
+ mProvider.setRequest(
+ new ProviderRequest.Builder()
+ .setInterval(1000)
+ .setLocationRequests(Collections.singletonList(request))
+ .build(),
+ new WorkSource());
+
+ Location location = createLocation(NETWORK_PROVIDER, mRandom);
+ mLocationManager.setTestProviderLocation(NETWORK_PROVIDER, location);
+
+ assertThat(mManager.getNextLocation(TIMEOUT_MS)).isEqualTo(location);
+ }
+
+ @Test
+ public void testGpsRequest() throws Exception {
+ LocationRequest request = LocationRequest.createFromDeprecatedProvider(FUSED_PROVIDER, 1000,
+ 0, false).setQuality(LocationRequest.POWER_HIGH);
+
+ mProvider.setRequest(
+ new ProviderRequest.Builder()
+ .setInterval(1000)
+ .setLocationRequests(Collections.singletonList(request))
+ .build(),
+ new WorkSource());
+
+ Location location = createLocation(GPS_PROVIDER, mRandom);
+ mLocationManager.setTestProviderLocation(GPS_PROVIDER, location);
+
+ assertThat(mManager.getNextLocation(TIMEOUT_MS)).isEqualTo(location);
+ }
+
+ @Test
+ public void testBypassRequest() throws Exception {
+ LocationRequest request = LocationRequest.createFromDeprecatedProvider(FUSED_PROVIDER, 1000,
+ 0, false).setQuality(LocationRequest.POWER_HIGH).setLocationSettingsIgnored(true);
+
+ mProvider.setRequest(
+ new ProviderRequest.Builder()
+ .setInterval(1000)
+ .setLocationSettingsIgnored(true)
+ .setLocationRequests(Collections.singletonList(request))
+ .build(),
+ new WorkSource());
+
+ boolean containsNetworkBypass = false;
+ for (LocationRequest iRequest : mLocationManager.getTestProviderCurrentRequests(
+ NETWORK_PROVIDER)) {
+ if (iRequest.isLocationSettingsIgnored()) {
+ containsNetworkBypass = true;
+ break;
+ }
+ }
+
+ boolean containsGpsBypass = false;
+ for (LocationRequest iRequest : mLocationManager.getTestProviderCurrentRequests(
+ GPS_PROVIDER)) {
+ if (iRequest.isLocationSettingsIgnored()) {
+ containsGpsBypass = true;
+ break;
+ }
+ }
+
+ assertThat(containsNetworkBypass).isTrue();
+ assertThat(containsGpsBypass).isTrue();
+ }
+
+ private static class LocationProviderManagerCapture extends ILocationProviderManager.Stub {
+
+ private final LinkedBlockingQueue mLocations;
+
+ private LocationProviderManagerCapture() {
+ mLocations = new LinkedBlockingQueue<>();
+ }
+
+ @Override
+ public void onSetAdditionalProviderPackages(List 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()));
+ }
+ }
+}