diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index bcf423537bb66..e01567fdefaae 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9055,8 +9055,12 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val
"location_time_zone_detection_enabled";
/**
- * The accuracy in meters used for coarsening location for clients with only the coarse
- * location permission.
+ * The nominal accuracy in meters used to scale the random offset applied before population
+ * density coarsening.
+ *
+ *
The provider directly selects the S2 coarsening level. This value can indirectly
+ * change that level when the offset crosses a population density boundary. The offset's
+ * standard deviation is one quarter of this value, subject to a minimum scale.
*
* @hide
*/
diff --git a/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java b/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
index 54eb611700fa0..82704fd83a23d 100644
--- a/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
+++ b/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
@@ -159,10 +159,22 @@ public String toString() {
*/
public static CurrentUserServiceSupplier createFromConfig(Context context, String action,
@BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId) {
+ return createFromConfig(context, action, enableOverlayResId, nonOverlayPackageResId,
+ /* callerPermission= */ null, /* servicePermission= */ null);
+ }
+
+ /**
+ * Creates an instance using config resources and permission requirements.
+ *
+ * @see #create(Context, String, String, String, String)
+ */
+ public static CurrentUserServiceSupplier createFromConfig(Context context, String action,
+ @BoolRes int enableOverlayResId, @StringRes int nonOverlayPackageResId,
+ @Nullable String callerPermission, @Nullable String servicePermission) {
String explicitPackage = retrieveExplicitPackage(context, enableOverlayResId,
nonOverlayPackageResId);
return CurrentUserServiceSupplier.create(context, action, explicitPackage,
- /*callerPermission=*/null, /*servicePermission=*/null);
+ callerPermission, servicePermission);
}
/**
@@ -326,7 +338,7 @@ public BoundServiceInfo getServiceInfo() {
if (mContext.checkPermission(mServicePermission, Process.INVALID_PID,
serviceInfo.mUid) != PERMISSION_GRANTED) {
Log.d(TAG, serviceInfo.getComponentName().flattenToShortString()
- + " disqualified due to not holding " + mCallerPermission);
+ + " disqualified due to not holding " + mServicePermission);
continue;
}
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index fa2c3f0534c0d..0a8c1f18dfdaa 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2308,14 +2308,8 @@
-
app.grapheneos.networklocation
-
- com.android.location.populationdensity
+
+ app.grapheneos.networklocation
-
- true
+
+ false
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index e6fba2fadd1ec..81dfff116824f 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -112,7 +112,6 @@
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.location.eventlog.LocationEventLog;
-import com.android.server.location.fudger.LocationFudgerCache;
import com.android.server.location.geofence.GeofenceManager;
import com.android.server.location.geofence.GeofenceProxy;
import com.android.server.location.gnss.GnssConfiguration;
@@ -266,10 +265,7 @@ void onCurrentUserChanged(int fromUserId, int toUserId) {
private volatile @Nullable GnssManagerService mGnssManagerService = null;
private ProxyGeocodeProvider mGeocodeProvider;
- private @Nullable ProxyPopulationDensityProvider mPopulationDensityProvider = null;
-
- // A cache for population density lookups. Used if density-based coarse locations are enabled.
- private @Nullable LocationFudgerCache mLocationFudgerCache = null;
+ private volatile @Nullable ProxyPopulationDensityProvider mPopulationDensityProvider = null;
private final Object mDeprecatedGnssBatchingLock = new Object();
@GuardedBy("mDeprecatedGnssBatchingLock")
@@ -404,6 +400,13 @@ void addLocationProviderManager(
manager.setRealProvider(realProvider);
}
mProviderManagers.add(manager);
+
+ // Managers added after onSystemThirdPartyAppsCanStart has wired the boot-time
+ // managers must also receive the population density provider; otherwise their
+ // fudger fails closed and suppresses every coarse fix.
+ if (mPopulationDensityProvider != null) {
+ manager.setPopulationDensityProvider(mPopulationDensityProvider);
+ }
}
}
@@ -413,10 +416,10 @@ protected void setProxyPopulationDensityProvider(ProxyPopulationDensityProvider
}
@VisibleForTesting
- protected void setLocationFudgerCache(LocationFudgerCache cache) {
- mLocationFudgerCache = cache;
+ protected void setPopulationDensityProviderOnFudgers(
+ @Nullable ProxyPopulationDensityProvider provider) {
for (LocationProviderManager manager : mProviderManagers) {
- manager.setLocationFudgerCache(cache);
+ manager.setPopulationDensityProvider(provider);
}
}
@@ -575,18 +578,16 @@ void onSystemThirdPartyAppsCanStart() {
}
long startTime = System.currentTimeMillis();
- setProxyPopulationDensityProvider(
- ProxyPopulationDensityProvider.createAndRegister(mContext));
+ ProxyPopulationDensityProvider populationDensityProvider =
+ ProxyPopulationDensityProvider.createAndRegister(mContext);
+ setProxyPopulationDensityProvider(populationDensityProvider);
int duration = (int) (System.currentTimeMillis() - startTime);
- if (mPopulationDensityProvider == null) {
- Log.e(TAG, "no population density provider found");
- }
+ // The proxy registers its watcher even when no provider currently resolves (it binds one
+ // that appears later), so resolution state at boot is diagnostic only.
FrameworkStatsLog.write(FrameworkStatsLog.POPULATION_DENSITY_PROVIDER_LOADING_REPORTED,
- /* provider_null= */ (mPopulationDensityProvider == null),
+ /* provider_null= */ !populationDensityProvider.isServiceResolved(),
/* provider_start_time_millis= */ duration);
- if (mPopulationDensityProvider != null) {
- setLocationFudgerCache(new LocationFudgerCache(mPopulationDensityProvider));
- }
+ setPopulationDensityProviderOnFudgers(populationDensityProvider);
if (!Flags.disableHardwareAr()) {
// bind to hardware activity recognition
diff --git a/services/core/java/com/android/server/location/eventlog/LocationEventLog.java b/services/core/java/com/android/server/location/eventlog/LocationEventLog.java
index 87e193f895712..2c2b7198eba0a 100644
--- a/services/core/java/com/android/server/location/eventlog/LocationEventLog.java
+++ b/services/core/java/com/android/server/location/eventlog/LocationEventLog.java
@@ -237,6 +237,13 @@ public void logProviderDeliveredLocations(String provider, int numLocations,
getAggregateStats(provider, identity).markLocationDelivered();
}
+ /** Logs a suppressed delivery to a client whose location could not be coarsened. */
+ public void logProviderCoarseningSuppressed(String provider, CallerIdentity identity) {
+ synchronized (this) {
+ mLocationsLog.logProviderCoarseningSuppressed(provider, identity);
+ }
+ }
+
/** Logs that a provider has entered or exited stationary throttling. */
public void logProviderStationaryThrottled(String provider, boolean throttled,
ProviderRequest request) {
@@ -460,6 +467,22 @@ public String toString() {
}
}
+ private static final class ProviderCoarseningSuppressedEvent extends ProviderEvent {
+
+ private final CallerIdentity mIdentity;
+
+ ProviderCoarseningSuppressedEvent(String provider, CallerIdentity identity) {
+ super(provider);
+ mIdentity = identity;
+ }
+
+ @Override
+ public String toString() {
+ return mProvider + " provider delivery suppressed (coarsening failed) for "
+ + mIdentity;
+ }
+ }
+
private static final class ProviderStationaryThrottledEvent extends ProviderEvent {
private final boolean mStationaryThrottled;
@@ -643,6 +666,10 @@ public void logProviderDeliveredLocations(String provider, int numLocations,
addLog(new ProviderDeliverLocationEvent(provider, numLocations, identity));
}
+ public void logProviderCoarseningSuppressed(String provider, CallerIdentity identity) {
+ addLog(new ProviderCoarseningSuppressedEvent(provider, identity));
+ }
+
private void addLog(Object logEvent) {
this.addLog(SystemClock.elapsedRealtime(), logEvent);
}
diff --git a/services/core/java/com/android/server/location/fudger/LocationFudger.java b/services/core/java/com/android/server/location/fudger/LocationFudger.java
index 5f999062a3bda..a5b3ee56b5955 100644
--- a/services/core/java/com/android/server/location/fudger/LocationFudger.java
+++ b/services/core/java/com/android/server/location/fudger/LocationFudger.java
@@ -22,15 +22,19 @@
import android.annotation.Nullable;
import android.location.Location;
import android.location.LocationResult;
-import android.location.flags.Flags;
import android.os.SystemClock;
+import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.location.geometry.S2CellIdUtils;
+import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider;
+import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider.PopulationDensityUnavailableException;
import java.security.SecureRandom;
import java.time.Clock;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Random;
/**
@@ -39,7 +43,9 @@
*/
public class LocationFudger {
- // minimum accuracy a coarsened location can have
+ private static final String TAG = "LocationFudger";
+
+ // Minimum scale for the random coarsening offset.
private static final float MIN_ACCURACY_M = 200.0f;
// how often random offsets are updated
@@ -68,7 +74,7 @@ public class LocationFudger {
90.0 - (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR);
// The average edge length in km of an S2 cell, indexed by S2 levels 0 to
- // 13. Level 13 is the highest level used for coarsening.
+ // 12. Level 12 is the highest level used for coarsening.
// This approximation assumes the S2 cells are squares.
// For density-based coarsening, we use the edge to set the accuracy of the
// coarsened location.
@@ -76,7 +82,21 @@ public class LocationFudger {
// We take square root of the average area.
private static final float[] S2_CELL_AVG_EDGE_PER_LEVEL = new float[] {
9220.14f, 4610.07f, 2305.04f, 1152.52f, 576.26f, 288.13f, 144.06f,
- 72.03f, 36.02f, 20.79f, 9f, 5.05f, 2.25f, 1.13f, 0.57f};
+ 72.03f, 36.02f, 18.01f, 9f, 4.50f, 2.25f};
+
+ // Also limits the location precision sent to the provider.
+ private static final int MAX_COARSENING_S2_LEVEL = S2_CELL_AVG_EDGE_PER_LEVEL.length - 1;
+
+ // Bound batch work before each location. One in-flight query may extend past this duration.
+ @VisibleForTesting
+ static final long MAX_BATCH_COARSENING_DURATION_MS = 100;
+
+ // Rate-limit persistent provider faults.
+ private static final long FAULT_LOG_INTERVAL_MS = 60 * 1000;
+
+ // Re-query cached failures after transient faults that do not trigger a provider rebind.
+ @VisibleForTesting
+ static final long NEGATIVE_CACHE_TTL_MS = 2000;
private final float mAccuracyM;
private final Clock mClock;
@@ -88,20 +108,47 @@ public class LocationFudger {
private double mLongitudeOffsetM;
@GuardedBy("this")
private long mNextUpdateRealtimeMs;
+ @GuardedBy("this")
+ private long mOffsetGeneration;
+ // Cache the latest input by identity to share one outcome across registrations.
@GuardedBy("this")
@Nullable private Location mCachedFineLocation;
@GuardedBy("this")
@Nullable private Location mCachedCoarseLocation;
+ @GuardedBy("this")
+ private long mCachedLocationGeneration;
+ @GuardedBy("this")
+ private long mCachedLocationOffsetGeneration;
+ @GuardedBy("this")
+ @Nullable private ProxyPopulationDensityProvider mCachedLocationProvider;
+ @GuardedBy("this")
+ private long mCachedLocationFailureRealtimeMs;
@GuardedBy("this")
@Nullable private LocationResult mCachedFineLocationResult;
@GuardedBy("this")
@Nullable private LocationResult mCachedCoarseLocationResult;
+ @GuardedBy("this")
+ private long mCachedLocationResultGeneration;
+ @GuardedBy("this")
+ private long mCachedLocationResultOffsetGeneration;
+ @GuardedBy("this")
+ @Nullable private ProxyPopulationDensityProvider mCachedLocationResultProvider;
+ @GuardedBy("this")
+ private long mCachedLocationResultFailureRealtimeMs;
+
+ @GuardedBy("this")
+ private long mNextFaultLogRealtimeMs;
@GuardedBy("this")
- @Nullable private LocationFudgerCache mLocationFudgerCache = null;
+ @Nullable private ProxyPopulationDensityProvider mPopulationDensityProvider = null;
+ /**
+ * Creates a location fudger with the given random-offset scale.
+ *
+ * The population density provider selects the S2 coarsening level.
+ */
public LocationFudger(float accuracyM) {
this(accuracyM, SystemClock.elapsedRealtimeClock(), new SecureRandom());
}
@@ -115,12 +162,14 @@ public LocationFudger(float accuracyM) {
resetOffsets();
}
- /**
- * Provides the optional {@link LocationFudgerCache} for coarsening based on population density.
- */
- public void setLocationFudgerCache(LocationFudgerCache cache) {
+ /** Sets the population density provider, or null to make coarsening fail closed. */
+ public void setPopulationDensityProvider(@Nullable ProxyPopulationDensityProvider provider) {
synchronized (this) {
- mLocationFudgerCache = cache;
+ if (mPopulationDensityProvider == provider) {
+ return;
+ }
+ mPopulationDensityProvider = provider;
+ clearCachedLocationsLocked();
}
}
@@ -128,52 +177,140 @@ public void setLocationFudgerCache(LocationFudgerCache cache) {
* Resets the random offsets completely.
*/
public void resetOffsets() {
- mLatitudeOffsetM = nextRandomOffset();
- mLongitudeOffsetM = nextRandomOffset();
- mNextUpdateRealtimeMs = mClock.millis() + OFFSET_UPDATE_INTERVAL_MS;
+ synchronized (this) {
+ mLatitudeOffsetM = nextRandomOffset();
+ mLongitudeOffsetM = nextRandomOffset();
+ mNextUpdateRealtimeMs = mClock.millis() + OFFSET_UPDATE_INTERVAL_MS;
+ mOffsetGeneration++;
+ clearCachedLocationsLocked();
+ }
+ }
+
+ @GuardedBy("this")
+ private void clearCachedLocationsLocked() {
+ mCachedFineLocation = null;
+ mCachedCoarseLocation = null;
+ mCachedLocationProvider = null;
+ mCachedFineLocationResult = null;
+ mCachedCoarseLocationResult = null;
+ mCachedLocationResultProvider = null;
}
/**
- * Coarsens a LocationResult by coarsening every location within the location result with
- * {@link #createCoarse(Location)}.
+ * Coarsens a location result from oldest to newest.
+ *
+ *
A provider fault suppresses the result. Reaching the batch deadline before a location
+ * returns the completed prefix, or null before the first location. This deadline also applies
+ * to a one-entry result; {@link #createCoarse(Location)} has no batch deadline.
*/
- public LocationResult createCoarse(LocationResult fineLocationResult) {
+ public @Nullable LocationResult createCoarse(LocationResult fineLocationResult) {
+ ProxyPopulationDensityProvider provider;
+ long providerGeneration;
+ double latitudeOffsetM;
+ double longitudeOffsetM;
+ long offsetGeneration;
synchronized (this) {
- if (fineLocationResult == mCachedFineLocationResult
- || fineLocationResult == mCachedCoarseLocationResult) {
- return mCachedCoarseLocationResult;
+ updateOffsets();
+ provider = mPopulationDensityProvider;
+ providerGeneration = currentBindingGeneration();
+ latitudeOffsetM = mLatitudeOffsetM;
+ longitudeOffsetM = mLongitudeOffsetM;
+ offsetGeneration = mOffsetGeneration;
+ if (isCurrentStateLocked(provider, providerGeneration, offsetGeneration)
+ && (fineLocationResult == mCachedFineLocationResult
+ || fineLocationResult == mCachedCoarseLocationResult)) {
+ if (mCachedLocationResultProvider == provider
+ && mCachedLocationResultGeneration == providerGeneration
+ && mCachedLocationResultOffsetGeneration == offsetGeneration
+ && (mCachedCoarseLocationResult != null
+ || mClock.millis() - mCachedLocationResultFailureRealtimeMs
+ < NEGATIVE_CACHE_TTL_MS)) {
+ return mCachedCoarseLocationResult;
+ }
}
}
- LocationResult coarseLocationResult = fineLocationResult.map(this::createCoarse);
+ List fineLocations = fineLocationResult.asList();
+ ArrayList coarseLocations = new ArrayList<>(fineLocations.size());
+ long batchDeadlineRealtimeMs = mClock.millis() + MAX_BATCH_COARSENING_DURATION_MS;
+ for (Location fineLocation : fineLocations) {
+ if (mClock.millis() >= batchDeadlineRealtimeMs) {
+ logCoarseningFault("batch exceeded coarsening deadline");
+ break;
+ }
+ Location coarseLocation = createCoarse(
+ fineLocation, provider, providerGeneration, latitudeOffsetM, longitudeOffsetM,
+ offsetGeneration);
+ if (coarseLocation == null) {
+ return recordBatchFailure(fineLocationResult, provider, providerGeneration,
+ offsetGeneration);
+ }
+ coarseLocations.add(coarseLocation);
+ }
+ if (coarseLocations.isEmpty()) {
+ return recordBatchFailure(fineLocationResult, provider, providerGeneration,
+ offsetGeneration);
+ }
+ LocationResult coarseLocationResult = LocationResult.wrap(coarseLocations);
synchronized (this) {
+ if (!isCurrentStateLocked(provider, providerGeneration, offsetGeneration)) {
+ return recordBatchFailure(fineLocationResult, provider, providerGeneration,
+ offsetGeneration);
+ }
mCachedFineLocationResult = fineLocationResult;
mCachedCoarseLocationResult = coarseLocationResult;
+ mCachedLocationResultProvider = provider;
+ mCachedLocationResultGeneration = providerGeneration;
+ mCachedLocationResultOffsetGeneration = offsetGeneration;
}
return coarseLocationResult;
}
/**
- * Create a coarse location using two technique, random offsets and snap-to-grid.
+ * Creates a density-coarsened location, or returns null on a provider fault.
*
- * First we add a random offset to mitigate against detecting grid transitions. Without a random
- * offset it is possible to detect a user's position quite accurately when they cross a grid
- * boundary. The random offset changes very slowly over time, to mitigate against taking many
- * location samples and averaging them out. Second we snap-to-grid (quantize). This has the nice
- * property of producing stable results, and mitigating against taking many samples to average
- * out a random offset.
+ * The provider sees the center of the offset point's finest accepted S2 cell. Only its
+ * returned level is trusted; the output cell is derived locally.
*/
- public Location createCoarse(Location fine) {
+ public @Nullable Location createCoarse(Location fine) {
+ ProxyPopulationDensityProvider provider;
+ long providerGeneration;
+ double latitudeOffsetM;
+ double longitudeOffsetM;
+ long offsetGeneration;
synchronized (this) {
- if (fine == mCachedFineLocation || fine == mCachedCoarseLocation) {
- return mCachedCoarseLocation;
- }
+ updateOffsets();
+ provider = mPopulationDensityProvider;
+ providerGeneration = currentBindingGeneration();
+ latitudeOffsetM = mLatitudeOffsetM;
+ longitudeOffsetM = mLongitudeOffsetM;
+ offsetGeneration = mOffsetGeneration;
}
+ return createCoarse(fine, provider, providerGeneration, latitudeOffsetM, longitudeOffsetM,
+ offsetGeneration);
+ }
- // update the offsets in use
- updateOffsets();
+ private @Nullable Location createCoarse(Location fine,
+ @Nullable ProxyPopulationDensityProvider provider,
+ long providerGeneration, double latitudeOffsetM, double longitudeOffsetM,
+ long offsetGeneration) {
+ synchronized (this) {
+ if (isCurrentStateLocked(provider, providerGeneration, offsetGeneration)
+ && (fine == mCachedFineLocation || fine == mCachedCoarseLocation)
+ && mCachedLocationProvider == provider
+ && mCachedLocationGeneration == providerGeneration
+ && mCachedLocationOffsetGeneration == offsetGeneration) {
+ if (mCachedCoarseLocation != null) {
+ return mCachedCoarseLocation;
+ }
+ if (mClock.millis() - mCachedLocationFailureRealtimeMs
+ < NEGATIVE_CACHE_TTL_MS) {
+ return null;
+ }
+ }
+ }
// Build the coarse location from an allowlist: start from a fresh location and copy only
// non-sensitive fields, so no present or future Location field can leak fine-grained data
@@ -192,50 +329,134 @@ public Location createCoarse(Location fine) {
double longitude = wrapLongitude(fine.getLongitude());
// add offsets - update longitude first using the non-offset latitude
- longitude += wrapLongitude(metersToDegreesLongitude(mLongitudeOffsetM, latitude));
- latitude += wrapLatitude(metersToDegreesLatitude(mLatitudeOffsetM));
+ longitude += wrapLongitude(metersToDegreesLongitude(longitudeOffsetM, latitude));
+ latitude += wrapLatitude(metersToDegreesLatitude(latitudeOffsetM));
+
+ // The sums can leave the valid coordinate ranges (only the base coordinates and the
+ // offsets were normalized individually), so re-normalize before deriving cells.
+ latitude = wrapLatitude(latitude);
+ longitude = wrapLongitude(longitude);
+
+ // Limit provider input precision without changing any accepted parent cell.
+ long queryS2CellId = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(latitude, longitude), MAX_COARSENING_S2_LEVEL);
+ double[] queryPoint = new double[] {0.0, 0.0};
+ S2CellIdUtils.toLatLngDegrees(queryS2CellId, queryPoint);
+
+ if (provider == null) {
+ logCoarseningFault("no population density provider configured");
+ return recordFailure(fine, provider, providerGeneration, offsetGeneration);
+ }
+ if (!isCurrentState(provider, providerGeneration, offsetGeneration)) {
+ return recordFailure(fine, provider, providerGeneration, offsetGeneration);
+ }
- // We copy a reference to the cache, so even if mLocationFudgerCache is concurrently set
- // to null, we can continue executing the condition below.
- LocationFudgerCache cacheCopy = null;
- synchronized (this) {
- cacheCopy = mLocationFudgerCache;
+ long s2CellId;
+ try {
+ s2CellId = provider.getCoarsenedS2CellId(
+ queryPoint[LAT_INDEX], queryPoint[LNG_INDEX]);
+ } catch (PopulationDensityUnavailableException e) {
+ logCoarseningFault("density query failed: " + e.getMessage());
+ return recordFailure(fine, provider, providerGeneration, offsetGeneration);
}
- double[] coarsened = new double[] {0.0, 0.0};
- // The new algorithm is applied if and only if (1) the flag is on, (2) the cache has been
- // set, and (3) the cache has successfully queried the provider for the default coarsening
- // value.
- float accuracy = mAccuracyM;
- if (cacheCopy != null) {
- if (cacheCopy.hasDefaultValue()) {
- // New algorithm that snaps to the center of a S2 cell.
- int level = cacheCopy.getCoarseningLevel(latitude, longitude);
- coarsened = snapToCenterOfS2Cell(latitude, longitude, level);
- accuracy = getS2CellApproximateEdge(level);
- } else {
- // Try to fetch the default value. The answer won't come in time, but will be used
- // for the next location to coarsen.
- cacheCopy.onDefaultCoarseningLevelNotSet();
- // Previous algorithm that snaps to a grid of width mAccuracyM.
- coarsened = snapToGrid(latitude, longitude);
- }
- } else {
- // Previous algorithm that snaps to a grid of width mAccuracyM.
- coarsened = snapToGrid(latitude, longitude);
+ if (!isCurrentState(provider, providerGeneration, offsetGeneration)) {
+ return recordFailure(fine, provider, providerGeneration, offsetGeneration);
+ }
+ // Trust only the returned level; derive the cell locally from the query point.
+ int level = S2CellIdUtils.getLevel(s2CellId);
+ if (level < 0 || level > MAX_COARSENING_S2_LEVEL) {
+ logCoarseningFault("provider returned invalid coarsening level " + level);
+ return recordFailure(fine, provider, providerGeneration, offsetGeneration);
}
+ snapToCenterOfS2Cell(queryPoint[LAT_INDEX], queryPoint[LNG_INDEX], level, queryPoint);
+ float accuracy = getS2CellApproximateEdge(level);
- coarse.setLatitude(coarsened[LAT_INDEX]);
- coarse.setLongitude(coarsened[LNG_INDEX]);
+ coarse.setLatitude(queryPoint[LAT_INDEX]);
+ coarse.setLongitude(queryPoint[LNG_INDEX]);
coarse.setAccuracy(Math.max(accuracy, fine.getAccuracy()));
synchronized (this) {
+ if (!isCurrentStateLocked(provider, providerGeneration, offsetGeneration)) {
+ return recordFailure(fine, provider, providerGeneration, offsetGeneration);
+ }
mCachedFineLocation = fine;
mCachedCoarseLocation = coarse;
+ mCachedLocationProvider = provider;
+ mCachedLocationGeneration = providerGeneration;
+ mCachedLocationOffsetGeneration = offsetGeneration;
}
return coarse;
}
+ private @Nullable Location recordFailure(Location fine,
+ @Nullable ProxyPopulationDensityProvider provider, long providerGeneration,
+ long offsetGeneration) {
+ synchronized (this) {
+ if (!isCurrentStateLocked(provider, providerGeneration, offsetGeneration)) {
+ return null;
+ }
+ mCachedFineLocation = fine;
+ mCachedCoarseLocation = null;
+ mCachedLocationProvider = provider;
+ mCachedLocationGeneration = providerGeneration;
+ mCachedLocationOffsetGeneration = offsetGeneration;
+ mCachedLocationFailureRealtimeMs = mClock.millis();
+ }
+ return null;
+ }
+
+ @GuardedBy("this")
+ private long currentBindingGeneration() {
+ return mPopulationDensityProvider == null
+ ? 0 : mPopulationDensityProvider.getBindingGeneration();
+ }
+
+ private boolean isCurrentState(@Nullable ProxyPopulationDensityProvider provider,
+ long providerGeneration, long offsetGeneration) {
+ synchronized (this) {
+ return isCurrentStateLocked(provider, providerGeneration, offsetGeneration);
+ }
+ }
+
+ @GuardedBy("this")
+ private boolean isCurrentStateLocked(@Nullable ProxyPopulationDensityProvider provider,
+ long providerGeneration, long offsetGeneration) {
+ return mOffsetGeneration == offsetGeneration
+ && mPopulationDensityProvider == provider
+ && (provider == null
+ || provider.getBindingGeneration() == providerGeneration);
+ }
+
+ private @Nullable LocationResult recordBatchFailure(LocationResult fineLocationResult,
+ @Nullable ProxyPopulationDensityProvider provider, long providerGeneration,
+ long offsetGeneration) {
+ synchronized (this) {
+ if (!isCurrentStateLocked(provider, providerGeneration, offsetGeneration)) {
+ return null;
+ }
+ mCachedFineLocationResult = fineLocationResult;
+ mCachedCoarseLocationResult = null;
+ mCachedLocationResultProvider = provider;
+ mCachedLocationResultGeneration = providerGeneration;
+ mCachedLocationResultOffsetGeneration = offsetGeneration;
+ mCachedLocationResultFailureRealtimeMs = mClock.millis();
+ }
+ return null;
+ }
+
+ // Rate-limit faults because each suppressed fix can reach this path.
+ private void logCoarseningFault(String reason) {
+ synchronized (this) {
+ long nowMs = mClock.millis();
+ if (nowMs < mNextFaultLogRealtimeMs) {
+ return;
+ }
+ mNextFaultLogRealtimeMs = nowMs + FAULT_LOG_INTERVAL_MS;
+ }
+ Log.w(TAG, "coarse location suppressed: " + reason);
+ }
+
// Returns the average edge length in meters of an S2 cell at the given
// level. This is computed as if the S2 cell were a square. We do not need
// an exact value, only a rough approximation.
@@ -249,28 +470,19 @@ protected float getS2CellApproximateEdge(int level) {
return S2_CELL_AVG_EDGE_PER_LEVEL[level] * 1000;
}
- // quantize location by snapping to a grid. this is the primary means of obfuscation. it
- // gives nice consistent results and is very effective at hiding the true location (as
- // long as you are not sitting on a grid boundary, which the random offsets mitigate).
- //
- // note that we quantize the latitude first, since the longitude quantization depends on
- // the latitude value and so leaks information about the latitude
- private double[] snapToGrid(double latitude, double longitude) {
+ // Derive the cell locally; accept only the provider-supplied level.
+ @VisibleForTesting
+ protected double[] snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level) {
double[] center = new double[] {0.0, 0.0};
- double latGranularity = metersToDegreesLatitude(mAccuracyM);
- center[LAT_INDEX] = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity);
- double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude);
- center[LNG_INDEX] = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity);
+ snapToCenterOfS2Cell(latDegrees, lngDegrees, level, center);
return center;
}
- @VisibleForTesting
- protected double[] snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level) {
+ private void snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level,
+ double[] center) {
long leafCell = S2CellIdUtils.fromLatLngDegrees(latDegrees, lngDegrees);
long coarsenedCell = S2CellIdUtils.getParent(leafCell, level);
- double[] center = new double[] {0.0, 0.0};
S2CellIdUtils.toLatLngDegrees(coarsenedCell, center);
- return center;
}
/**
@@ -285,7 +497,8 @@ protected double[] snapToCenterOfS2Cell(double latDegrees, double lngDegrees, in
* mechanism. It just needs to be large enough to stop information leakage as we cross grid
* boundaries.
*/
- private synchronized void updateOffsets() {
+ @GuardedBy("this")
+ private void updateOffsets() {
long now = mClock.millis();
if (now < mNextUpdateRealtimeMs) {
return;
@@ -294,6 +507,8 @@ private synchronized void updateOffsets() {
mLatitudeOffsetM = (OLD_WEIGHT * mLatitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset());
mLongitudeOffsetM = (OLD_WEIGHT * mLongitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset());
mNextUpdateRealtimeMs = now + OFFSET_UPDATE_INTERVAL_MS;
+ mOffsetGeneration++;
+ clearCachedLocationsLocked();
}
private double nextRandomOffset() {
diff --git a/services/core/java/com/android/server/location/fudger/LocationFudgerCache.java b/services/core/java/com/android/server/location/fudger/LocationFudgerCache.java
deleted file mode 100644
index 33e3e70d729c4..0000000000000
--- a/services/core/java/com/android/server/location/fudger/LocationFudgerCache.java
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * Copyright (C) 2024 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.server.location.fudger;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.location.flags.Flags;
-import android.location.provider.IS2CellIdsCallback;
-import android.location.provider.IS2LevelCallback;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.location.geometry.S2CellIdUtils;
-import com.android.internal.util.FrameworkStatsLog;
-import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider;
-
-import java.util.Objects;
-
-/**
- * A cache for returning the coarsening level to be used. The coarsening level depends on the user
- * location. If the cache contains the requested latitude/longitude, the s2 level of the cached
- * cell id is returned. If not, a default value is returned.
- * This class has a {@link ProxyPopulationDensityProvider} used to refresh the cache.
- * This cache exists because {@link ProxyPopulationDensityProvider} must be queried asynchronously,
- * whereas a synchronous answer is needed.
- * The cache is first-in, first-out, and has a fixed size. Cache entries are valid until evicted by
- * another value.
- */
-public class LocationFudgerCache {
-
- // The maximum number of S2 cell ids stored in the cache.
- // Each cell id is a long, so the memory requirement is 8*MAX_CACHE_SIZE bytes.
- protected static final int MAX_CACHE_SIZE = 20;
-
- private final Object mLock = new Object();
-
- // mCache is a circular buffer of size MAX_CACHE_SIZE. The next position to be written to is
- // mPosInCache. Initially, the cache is filled with INVALID_CELL_IDs.
- @GuardedBy("mLock")
- private final long[] mCache = new long[MAX_CACHE_SIZE];
-
- @GuardedBy("mLock")
- private int mPosInCache = 0;
-
- @GuardedBy("mLock")
- private int mCacheSize = 0;
-
- // The S2 level to coarsen to, if the cache doesn't contain a better answer.
- // Updated concurrently by callbacks.
- @GuardedBy("mLock")
- private Integer mDefaultCoarseningLevel = null;
-
- // The provider that asynchronously provides what is stored in the cache.
- private final ProxyPopulationDensityProvider mPopulationDensityProvider;
-
- // If two calls to logDensityBasedLocsUsed are made in an interval shorter than this value,
- // the second is dropped.
- protected static final int LOG_DENSITY_BASED_LOCS_USED_RATE_LIMIT_MS = 1000 * 60 * 10; // 10 min
-
- // The system time at which the last query to logDensityBasedLocsUsed was made.
- // Initialized to -LOG_DENSITY_BASED_LOCS_USED_RATE_LIMIT_MS, so even if made at time 0, the
- // first call succeeds.
- private long mLastQueryToLogDensityBasedLocsUsedMs = -LOG_DENSITY_BASED_LOCS_USED_RATE_LIMIT_MS;
-
- private static String sTAG = "LocationFudgerCache";
-
- public LocationFudgerCache(@NonNull ProxyPopulationDensityProvider provider) {
- mPopulationDensityProvider = Objects.requireNonNull(provider);
-
- asyncFetchDefaultCoarseningLevel();
- }
-
- /**
- * Called by the LocationFudger when a query couldn't be fulfilled because the cache isn't set.
- */
- public void onDefaultCoarseningLevelNotSet() {
- if (!hasDefaultValue()) {
- asyncFetchDefaultCoarseningLevel();
- }
- logDensityBasedLocsUsed(/* nowMs=*/ System.currentTimeMillis(),
- /* skippedNoDefault= */ true,
- /* isCacheHit= */ false,
- /* defaultCoarseningLevel= */ -1);
- }
-
- /** Returns true if the cache has successfully received a default value from the provider. */
- public boolean hasDefaultValue() {
- synchronized (mLock) {
- return (mDefaultCoarseningLevel != null);
- }
- }
-
- /**
- * Returns the S2 level to which the provided location should be coarsened.
- * The answer comes from the cache if available, otherwise the default value is returned.
- */
- public int getCoarseningLevel(double latitudeDegrees, double longitudeDegrees) {
- // If we still haven't received the default level from the provider, try fetching it again.
- // The answer wouldn't come in time, but it will be used for the following queries.
- if (!hasDefaultValue()) {
- asyncFetchDefaultCoarseningLevel();
- }
- Long s2CellId = readCacheForLatLng(latitudeDegrees, longitudeDegrees);
- int defaultLevel = getDefaultCoarseningLevel();
- if (s2CellId == null) {
- // Asynchronously queries the density from the provider. The answer won't come in time,
- // but it will update the cache for the following queries.
- refreshCache(latitudeDegrees, longitudeDegrees);
-
- logDensityBasedLocsUsed(/* nowMs=*/ System.currentTimeMillis(),
- /* skippedNoDefault= */ false,
- /* isCacheHit= */ false,
- /* defaultCoarseningLevel= */ defaultLevel);
- return defaultLevel;
- }
- logDensityBasedLocsUsed(/* nowMs=*/ System.currentTimeMillis(),
- /* skippedNoDefault= */ false,
- /* isCacheHit= */ true,
- /* defaultCoarseningLevel= */ defaultLevel);
- return S2CellIdUtils.getLevel(s2CellId);
- }
-
- /**
- * A simple wrapper around FrameworkStatsLog.write() that rate-limits the calls.
- * Returns true on success, false if the call was dropped.
- */
- protected boolean logDensityBasedLocsUsed(long nowMs, boolean skippedNoDefault,
- boolean isCacheHit, int defaultCoarseningLevel) {
-
- if (nowMs - mLastQueryToLogDensityBasedLocsUsedMs
- < LOG_DENSITY_BASED_LOCS_USED_RATE_LIMIT_MS) {
- return false;
- }
- mLastQueryToLogDensityBasedLocsUsedMs = nowMs;
-
- FrameworkStatsLog.write(FrameworkStatsLog.DENSITY_BASED_COARSE_LOCATIONS_USAGE_REPORTED,
- /* skipped_no_default= */ skippedNoDefault,
- /* is_cache_hit= */ isCacheHit,
- /* default_coarsening_level= */ defaultCoarseningLevel);
- return true;
- }
-
- /**
- * If the cache contains the current location, returns the corresponding S2 cell id.
- * Otherwise, returns null.
- */
- @Nullable
- private Long readCacheForLatLng(double latDegrees, double lngDegrees) {
- synchronized (mLock) {
- for (int i = 0; i < mCacheSize; i++) {
- if (S2CellIdUtils.containsLatLngDegrees(mCache[i], latDegrees, lngDegrees)) {
- return mCache[i];
- }
- }
- }
- return null;
- }
-
- /** Adds the provided s2 cell id to the cache. This might evict other values from the cache. */
- public void addToCache(long s2CellId) {
- addToCache(new long[] {s2CellId});
- }
-
- /**
- * Adds the provided s2 cell ids to the cache. This might evict other values from the cache.
- * If more than MAX_CACHE_SIZE elements are provided, only the first elements are copied.
- * The first element of the input is added last into the FIFO cache, so it gets evicted last.
- */
- public void addToCache(long[] s2CellIds) {
- synchronized (mLock) {
- // Only copy up to MAX_CACHE_SIZE elements
- int end = Math.min(s2CellIds.length, MAX_CACHE_SIZE);
- mCacheSize = Math.min(mCacheSize + end, MAX_CACHE_SIZE);
-
- // Add in reverse so the first cell of s2CellIds is the last evicted
- for (int i = end - 1; i >= 0; i--) {
- mCache[mPosInCache] = s2CellIds[i];
- mPosInCache = (mPosInCache + 1) % MAX_CACHE_SIZE;
- }
- }
- }
-
- /**
- * Queries the population density provider for the default coarsening level (to be used if the
- * cache doesn't contain a better answer), and updates mDefaultCoarseningLevel with the answer.
- */
- private void asyncFetchDefaultCoarseningLevel() {
- IS2LevelCallback callback = new IS2LevelCallback.Stub() {
- @Override
- public void onResult(int s2level) {
- synchronized (mLock) {
- mDefaultCoarseningLevel = Integer.valueOf(s2level);
- }
- }
-
- @Override
- public void onError() {
- Log.e(sTAG, "could not get default population density");
- }
- };
- mPopulationDensityProvider.getDefaultCoarseningLevel(callback);
- }
-
- /**
- * Queries the population density provider and store the result in the cache.
- */
- private void refreshCache(double latitude, double longitude) {
- long startTime = System.currentTimeMillis();
- IS2CellIdsCallback callback = new IS2CellIdsCallback.Stub() {
- @Override
- public void onResult(long[] s2CellIds) {
- int durationMs = (int) (System.currentTimeMillis() - startTime);
- FrameworkStatsLog.write(
- FrameworkStatsLog.DENSITY_BASED_COARSE_LOCATIONS_PROVIDER_QUERY_REPORTED,
- /* query_duration_millis= */ durationMs,
- /* is_error= */ false);
- addToCache(s2CellIds);
- }
-
- @Override
- public void onError() {
- Log.e(sTAG, "could not get population density");
- int durationMs = (int) (System.currentTimeMillis() - startTime);
- FrameworkStatsLog.write(
- FrameworkStatsLog.DENSITY_BASED_COARSE_LOCATIONS_PROVIDER_QUERY_REPORTED,
- /* query_duration_millis= */ durationMs,
- /* is_error= */ true);
- }
- };
- mPopulationDensityProvider.getCoarsenedS2Cells(latitude, longitude, MAX_CACHE_SIZE - 1,
- callback);
- }
-
- /**
- * Returns the default S2 level to coarsen to. This should be used if the cache
- * does not provide a better answer.
- */
- private int getDefaultCoarseningLevel() {
- synchronized (mLock) {
- // The minimum valid level is 0.
- if (mDefaultCoarseningLevel == null) {
- return 0;
- }
- return mDefaultCoarseningLevel;
- }
- }
-}
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index 76cf820ff7915..78448cf1f36b4 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -105,7 +105,6 @@
import com.android.server.location.LocationPermissions;
import com.android.server.location.LocationPermissions.PermissionLevel;
import com.android.server.location.fudger.LocationFudger;
-import com.android.server.location.fudger.LocationFudgerCache;
import com.android.server.location.injector.AlarmHelper;
import com.android.server.location.injector.AppForegroundHelper;
import com.android.server.location.injector.AppForegroundHelper.AppForegroundListener;
@@ -127,6 +126,7 @@
import com.android.server.location.injector.UserInfoHelper.UserListener;
import com.android.server.location.listeners.ListenerMultiplexer;
import com.android.server.location.listeners.RemovableListenerRegistration;
+import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider;
import com.android.server.location.settings.LocationSettings;
import com.android.server.location.settings.LocationUserSettings;
@@ -948,8 +948,13 @@ public void onAlarm() {
return null;
}
- LocationResult permittedLocationResult = Objects.requireNonNull(
- getPermittedLocationResult(fineLocationResult, getPermissionLevel()));
+ LocationResult permittedLocationResult =
+ getPermittedLocationResult(fineLocationResult, getPermissionLevel());
+ if (permittedLocationResult == null) {
+ // Suppress the delivery when coarsening fails.
+ EVENT_LOG.logProviderCoarseningSuppressed(mName, getIdentity());
+ return null;
+ }
LocationResult locationResult = permittedLocationResult.filter(
new Predicate() {
@@ -1360,8 +1365,21 @@ public void onAlarm() {
fineLocationResult = null;
}
- // lastly - note app ops
if (fineLocationResult != null) {
+ // Coarsen only the last location retained by this one-shot request.
+ fineLocationResult = fineLocationResult.asLastLocationResult();
+ }
+
+ LocationResult permittedLocationResult = getPermittedLocationResult(
+ fineLocationResult, getPermissionLevel());
+ if (fineLocationResult != null && permittedLocationResult == null) {
+ // Keep the one-shot registration for a later fix when coarsening fails.
+ EVENT_LOG.logProviderCoarseningSuppressed(mName, getIdentity());
+ return null;
+ }
+
+ // Note app ops only for a location that can be delivered.
+ if (permittedLocationResult != null) {
int op =
isOnlyBypassPermitted()
? AppOpsManager.OP_EMERGENCY_LOCATION
@@ -1370,16 +1388,11 @@ public void onAlarm() {
if (D) {
Log.w(TAG, "noteOp denied for " + getIdentity());
}
- fineLocationResult = null;
+ permittedLocationResult = null;
}
}
- if (fineLocationResult != null) {
- fineLocationResult = fineLocationResult.asLastLocationResult();
- }
-
- LocationResult locationResult = getPermittedLocationResult(fineLocationResult,
- getPermissionLevel());
+ LocationResult locationResult = permittedLocationResult;
// deliver location
return new ListenerOperation() {
@@ -1656,11 +1669,9 @@ public boolean isEnabled(int userId) {
}
}
- /**
- * Provides the optional {@link LocationFudgerCache} for coarsening based on population density.
- */
- public void setLocationFudgerCache(LocationFudgerCache cache) {
- mLocationFudger.setLocationFudgerCache(cache);
+ /** Sets the population density provider used for coarse locations. */
+ public void setPopulationDensityProvider(@Nullable ProxyPopulationDensityProvider provider) {
+ mLocationFudger.setPopulationDensityProvider(provider);
}
/**
@@ -1802,6 +1813,7 @@ public void setMockProviderLocation(Location location) {
return null;
}
+ // Return null when the cached fix cannot be coarsened.
Location location = getPermittedLocation(
getLastLocationUnsafe(
identity.getUserId(),
@@ -2819,6 +2831,11 @@ private void onEnabledChanged(int userId) {
updateRegistrations(registration -> registration.getIdentity().getUserId() == userId);
}
+ /**
+ * Returns a deliverable location, or null when none can be produced.
+ *
+ * Coarse requests fail closed when population density coarsening fails.
+ */
@Nullable Location getPermittedLocation(@Nullable Location fineLocation,
@PermissionLevel int permissionLevel) {
switch (permissionLevel) {
@@ -2832,6 +2849,11 @@ private void onEnabledChanged(int userId) {
}
}
+ /**
+ * Returns a deliverable location result, or null when none can be produced.
+ *
+ *
Coarse requests fail closed and may return a deadline-limited prefix.
+ */
@Nullable LocationResult getPermittedLocationResult(
@Nullable LocationResult fineLocationResult, @PermissionLevel int permissionLevel) {
switch (permissionLevel) {
diff --git a/services/core/java/com/android/server/location/provider/proxy/ProxyPopulationDensityProvider.java b/services/core/java/com/android/server/location/provider/proxy/ProxyPopulationDensityProvider.java
index 7b454e481ddaa..5e86b223edd1b 100644
--- a/services/core/java/com/android/server/location/provider/proxy/ProxyPopulationDensityProvider.java
+++ b/services/core/java/com/android/server/location/provider/proxy/ProxyPopulationDensityProvider.java
@@ -16,105 +16,217 @@
package com.android.server.location.provider.proxy;
+import static android.Manifest.permission.BIND_POPULATION_DENSITY_PROVIDER_SERVICE;
import static android.location.provider.PopulationDensityProviderBase.ACTION_POPULATION_DENSITY_PROVIDER;
import android.annotation.Nullable;
import android.content.Context;
import android.location.provider.IPopulationDensityProvider;
import android.location.provider.IS2CellIdsCallback;
-import android.location.provider.IS2LevelCallback;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.servicewatcher.CurrentUserServiceSupplier;
+import com.android.server.servicewatcher.CurrentUserServiceSupplier.BoundServiceInfo;
import com.android.server.servicewatcher.ServiceWatcher;
+import com.android.server.servicewatcher.ServiceWatcher.ServiceListener;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
/**
- * Proxy for IPopulationDensityProvider implementations.
+ * Proxy for {@link IPopulationDensityProvider} implementations.
+ *
+ *
Each query waits at most {@link #QUERY_TIMEOUT_MILLIS} for the asynchronous provider callback.
*/
-public class ProxyPopulationDensityProvider {
+public class ProxyPopulationDensityProvider implements ServiceListener {
- public static final String TAG = "ProxyPopulationDensityProvider";
+ private static final String TAG = "ProxyPopulationDensityProvider";
+ private static final long QUERY_TIMEOUT_MILLIS = 100;
- final ServiceWatcher mServiceWatcher;
+ @Nullable private final ServiceWatcher mServiceWatcher;
+ private final long mQueryTimeoutMillis;
- /**
- * Creates and registers this proxy. If no suitable service is available for the proxy, returns
- * null.
- */
- @Nullable
+ private final Object mBindingLock = new Object();
+
+ @GuardedBy("mBindingLock")
+ private long mNextBindingGeneration;
+
+ private volatile BindingToken mBindingToken =
+ new BindingToken(/* provider= */ null, /* generation= */ 0);
+
+ /** Creates, registers, and returns the proxy. */
public static ProxyPopulationDensityProvider createAndRegister(Context context) {
ProxyPopulationDensityProvider proxy = new ProxyPopulationDensityProvider(context);
- if (proxy.register()) {
- return proxy;
- } else {
- return null;
- }
+ proxy.register();
+ return proxy;
}
private ProxyPopulationDensityProvider(Context context) {
- mServiceWatcher = ServiceWatcher.create(
- context,
- "PopulationDensityProxy",
- CurrentUserServiceSupplier.createFromConfig(
+ mQueryTimeoutMillis = QUERY_TIMEOUT_MILLIS;
+ mServiceWatcher =
+ ServiceWatcher.create(
context,
- ACTION_POPULATION_DENSITY_PROVIDER,
- com.android.internal.R.bool.config_enablePopulationDensityProviderOverlay,
- com.android.internal.R.string.config_populationDensityProviderPackageName),
- null);
+ "PopulationDensityProxy",
+ CurrentUserServiceSupplier.createFromConfig(
+ context,
+ ACTION_POPULATION_DENSITY_PROVIDER,
+ com.android.internal.R.bool
+ .config_enablePopulationDensityProviderOverlay,
+ com.android.internal.R.string
+ .config_populationDensityProviderPackageName,
+ BIND_POPULATION_DENSITY_PROVIDER_SERVICE,
+ /* servicePermission= */ null),
+ this);
+ }
+
+ @VisibleForTesting
+ ProxyPopulationDensityProvider() {
+ this(QUERY_TIMEOUT_MILLIS);
}
- private boolean register() {
- boolean resolves = mServiceWatcher.checkServiceResolves();
- if (resolves) {
- mServiceWatcher.register();
+ @VisibleForTesting
+ ProxyPopulationDensityProvider(long queryTimeoutMillis) {
+ if (queryTimeoutMillis <= 0) {
+ throw new IllegalArgumentException("query timeout must be positive");
}
- return resolves;
+ mQueryTimeoutMillis = queryTimeoutMillis;
+ mServiceWatcher = null;
}
- /** Gets the default coarsening level. */
- public void getDefaultCoarseningLevel(IS2LevelCallback callback) {
- mServiceWatcher.runOnBinder(
- new ServiceWatcher.BinderOperation() {
- @Override
- public void run(IBinder binder) throws RemoteException {
- IPopulationDensityProvider.Stub.asInterface(binder)
- .getDefaultCoarseningLevel(callback);
- }
+ private void register() {
+ ServiceWatcher serviceWatcher =
+ Objects.requireNonNull(mServiceWatcher, "no service watcher on test instance");
+ if (!serviceWatcher.checkServiceResolves()) {
+ Log.e(
+ TAG,
+ "no population density provider currently resolves; coarse location is"
+ + " suppressed until one becomes available");
+ }
+ serviceWatcher.register();
+ }
- @Override
- public void onError(Throwable t) {
- try {
- callback.onError();
- } catch (RemoteException e) {
- Log.w(TAG, "remote exception while querying default coarsening level");
- }
- }
- });
+ /** Returns whether a population density provider service currently resolves. */
+ public boolean isServiceResolved() {
+ return Objects.requireNonNull(mServiceWatcher, "no service watcher on test instance")
+ .checkServiceResolves();
+ }
+
+ /** Returns the generation of the current bound or unbound provider state. */
+ public long getBindingGeneration() {
+ return mBindingToken.mGeneration;
+ }
+
+ @Override
+ public void onBind(IBinder binder, BoundServiceInfo boundServiceInfo) {
+ IPopulationDensityProvider provider = IPopulationDensityProvider.Stub.asInterface(binder);
+ synchronized (mBindingLock) {
+ mBindingToken = new BindingToken(provider, ++mNextBindingGeneration);
+ }
+ }
+
+ @Override
+ public void onUnbind() {
+ synchronized (mBindingLock) {
+ mBindingToken = new BindingToken(/* provider= */ null, ++mNextBindingGeneration);
+ }
}
+ /**
+ * Returns the coarsening cell for the given normalized S2 cell center.
+ *
+ * @throws PopulationDensityUnavailableException if the provider is unavailable, the query
+ * fails, or the query times out.
+ */
+ public long getCoarsenedS2CellId(double latitudeDegrees, double longitudeDegrees)
+ throws PopulationDensityUnavailableException {
+ BindingToken bindingToken = mBindingToken;
+ IPopulationDensityProvider provider = bindingToken.mProvider;
+ if (provider == null) {
+ throw new PopulationDensityUnavailableException("provider not bound");
+ }
+
+ CompletableFuture query = new CompletableFuture<>();
+ requestCoarsenedS2Cells(provider, latitudeDegrees, longitudeDegrees, query);
- /** Gets the population density at the requested location. */
- public void getCoarsenedS2Cells(double latitudeDegrees, double longitudeDegrees,
- int numAdditionalCells, IS2CellIdsCallback callback) {
- mServiceWatcher.runOnBinder(
- new ServiceWatcher.BinderOperation() {
+ try {
+ long s2CellId = query.get(mQueryTimeoutMillis, TimeUnit.MILLISECONDS);
+ if (mBindingToken != bindingToken) {
+ throw new PopulationDensityUnavailableException("provider binding changed");
+ }
+ return s2CellId;
+ } catch (InterruptedException exception) {
+ Thread.currentThread().interrupt();
+ query.cancel(false);
+ throw new PopulationDensityUnavailableException("query interrupted", exception);
+ } catch (ExecutionException exception) {
+ throw new PopulationDensityUnavailableException("query failed", exception.getCause());
+ } catch (TimeoutException exception) {
+ query.cancel(false);
+ throw new PopulationDensityUnavailableException("query timed out", exception);
+ }
+ }
+
+ private static void requestCoarsenedS2Cells(
+ IPopulationDensityProvider provider,
+ double latitudeDegrees,
+ double longitudeDegrees,
+ CompletableFuture query) {
+ IS2CellIdsCallback callback =
+ new IS2CellIdsCallback.Stub() {
@Override
- public void run(IBinder binder) throws RemoteException {
- IPopulationDensityProvider.Stub.asInterface(binder)
- .getCoarsenedS2Cells(latitudeDegrees, longitudeDegrees,
- numAdditionalCells, callback);
+ public void onResult(long[] s2CellIds) {
+ if (s2CellIds == null || s2CellIds.length == 0) {
+ query.completeExceptionally(
+ new IllegalStateException(
+ "population density provider returned no S2 cells"));
+ return;
+ }
+ query.complete(s2CellIds[0]);
}
@Override
- public void onError(Throwable t) {
- try {
- callback.onError();
- } catch (RemoteException e) {
- Log.w(TAG, "remote exception while querying coarsened S2 cell");
- }
+ public void onError() {
+ query.completeExceptionally(
+ new IllegalStateException(
+ "population density provider reported an error"));
}
- });
+ };
+
+ try {
+ provider.getCoarsenedS2Cells(
+ latitudeDegrees, longitudeDegrees, /* numAdditionalCells= */ 0, callback);
+ } catch (RemoteException | RuntimeException exception) {
+ query.completeExceptionally(exception);
+ }
+ }
+
+ /** Indicates that the population density provider could not produce a coarsening cell. */
+ public static final class PopulationDensityUnavailableException extends Exception {
+ public PopulationDensityUnavailableException(String message) {
+ super(message);
+ }
+
+ public PopulationDensityUnavailableException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ /** Identifies one bound or unbound provider state. */
+ private static final class BindingToken {
+
+ private final @Nullable IPopulationDensityProvider mProvider;
+ private final long mGeneration;
+
+ private BindingToken(@Nullable IPopulationDensityProvider provider, long generation) {
+ mProvider = provider;
+ mGeneration = generation;
+ }
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/LocationManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/location/LocationManagerServiceTest.java
index 4b43e2c902497..8654c1110d669 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/LocationManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/LocationManagerServiceTest.java
@@ -45,7 +45,6 @@
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.server.LocalServices;
-import com.android.server.location.fudger.LocationFudgerCache;
import com.android.server.location.injector.FakeUserInfoHelper;
import com.android.server.location.injector.TestInjector;
import com.android.server.location.provider.AbstractLocationProvider;
@@ -182,15 +181,26 @@ public void testHasProvider() {
}
@Test
- public void testSetLocationFudgerCache_isCalled() {
+ public void testSetPopulationDensityProviderOnFudgers_isForwardedToManagers() {
LocationProviderManager manager = mock(LocationProviderManager.class);
ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
mLocationManagerService.addLocationProviderManager(manager, /* provider = */ null);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
- mLocationManagerService.setLocationFudgerCache(cache);
+ mLocationManagerService.setPopulationDensityProviderOnFudgers(provider);
- verify(manager).setLocationFudgerCache(cache);
+ verify(manager).setPopulationDensityProvider(provider);
+ }
+
+ @Test
+ public void testAddLocationProviderManager_wiresLateManagerWithExistingProvider() {
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ mLocationManagerService.setProxyPopulationDensityProvider(provider);
+
+ LocationProviderManager manager = mock(LocationProviderManager.class);
+ mLocationManagerService.addLocationProviderManager(manager, /* realProvider = */ null);
+
+ // A manager added after the provider was wired at boot is wired inline.
+ verify(manager).setPopulationDensityProvider(provider);
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerCacheTest.java b/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerCacheTest.java
deleted file mode 100644
index 57703c66725f5..0000000000000
--- a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerCacheTest.java
+++ /dev/null
@@ -1,425 +0,0 @@
-/*
- * Copyright (C) 2024 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.server.location.fudger;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyDouble;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.location.flags.Flags;
-import android.location.provider.IS2CellIdsCallback;
-import android.location.provider.IS2LevelCallback;
-import android.os.RemoteException;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.location.geometry.S2CellIdUtils;
-import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class LocationFudgerCacheTest {
-
- private static final String TAG = "LocationFudgerCacheTest";
-
- private static final long TIMES_SQUARE_S2_ID =
- S2CellIdUtils.fromLatLngDegrees(40.758896, -73.985130);
-
- private static final double[] POINT_IN_TIMES_SQUARE = {40.75889599346095, -73.9851300385147};
-
- private static final double[] POINT_OUTSIDE_TIMES_SQUARE = {48.858093, 2.294694};
-
- @Test
- public void hasDefaultValue_isInitiallyFalse()
- throws RemoteException {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- assertThat(cache.hasDefaultValue()).isFalse();
- }
-
- @Test
- public void hasDefaultValue_uponQueryError_isStillFalse()
- throws RemoteException {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(
- IS2LevelCallback.class);
- verify(provider).getDefaultCoarseningLevel(argumentCaptor.capture());
-
- IS2LevelCallback cb = argumentCaptor.getValue();
- cb.onError();
-
- assertThat(cache.hasDefaultValue()).isFalse();
- }
-
- @Test
- public void hasDefaultValue_afterSuccessfulQuery_isTrue()
- throws RemoteException {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(
- IS2LevelCallback.class);
- verify(provider).getDefaultCoarseningLevel(argumentCaptor.capture());
-
- IS2LevelCallback cb = argumentCaptor.getValue();
- cb.onResult(10);
-
- assertThat(cache.hasDefaultValue()).isTrue();
- }
-
- @Test
- public void locationFudgerCache_whenQueriedOutsideOfCache_returnsDefault()
- throws RemoteException {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
- int level = 10;
- int defaultLevel = 2;
- Long s2Cell = S2CellIdUtils.getParent(TIMES_SQUARE_S2_ID, level);
-
- ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(
- IS2LevelCallback.class);
- verify(provider).getDefaultCoarseningLevel(argumentCaptor.capture());
-
- IS2LevelCallback cb = argumentCaptor.getValue();
- cb.onResult(defaultLevel);
-
- cache.addToCache(s2Cell);
-
- assertThat(cache.getCoarseningLevel(POINT_OUTSIDE_TIMES_SQUARE[0],
- POINT_OUTSIDE_TIMES_SQUARE[1])).isEqualTo(defaultLevel);
- }
-
- @Test
- public void locationFudgerCache_whenQueriedValueIsCached_returnsCachedValue() {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
- int level = 10;
- Long s2Cell = S2CellIdUtils.getParent(TIMES_SQUARE_S2_ID, level);
-
- cache.addToCache(s2Cell);
-
- assertThat(cache.getCoarseningLevel(POINT_IN_TIMES_SQUARE[0], POINT_IN_TIMES_SQUARE[1]))
- .isEqualTo(level);
- }
-
- @Test
- public void locationFudgerCache_whenStarting_queriesDefaultValue() {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- verify(provider).getDefaultCoarseningLevel(any());
- }
-
- @Test
- public void locationFudgerCache_ifDidntGetDefaultValue_queriesItAgain() {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- verify(provider, times(1)).getDefaultCoarseningLevel(any());
-
- cache.getCoarseningLevel(90.0, 0.0);
-
- verify(provider, times(2)).getDefaultCoarseningLevel(any());
- }
-
- @Test
- public void locationFudgerCache_ifReceivedDefaultValue_doesNotQueriesIt()
- throws RemoteException {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(
- IS2LevelCallback.class);
- verify(provider, times(1)).getDefaultCoarseningLevel(argumentCaptor.capture());
-
- IS2LevelCallback cb = argumentCaptor.getValue();
- cb.onResult(10);
-
- cache.getCoarseningLevel(90.0, 0.0);
-
- // Verify getDefaultCoarseningLevel did not get called again
- verify(provider, times(1)).getDefaultCoarseningLevel(any());
- }
-
- @Test
- public void locationFudgerCache_whenSuccessfullyQueriesDefaultValue_storesResult()
- throws RemoteException {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
- int level = 10;
-
- ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(
- IS2LevelCallback.class);
- verify(provider).getDefaultCoarseningLevel(argumentCaptor.capture());
-
- IS2LevelCallback cb = argumentCaptor.getValue();
- cb.onResult(level);
-
- // Query any uncached location
- assertThat(cache.getCoarseningLevel(0.0, 0.0)).isEqualTo(level);
- }
-
- @Test
- public void locationFudgerCache_whenQueryingDefaultValueFails_returnsDefault()
- throws RemoteException {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(
- IS2LevelCallback.class);
- verify(provider).getDefaultCoarseningLevel(argumentCaptor.capture());
-
- IS2LevelCallback cb = argumentCaptor.getValue();
- cb.onError();
-
- // Query any uncached location. The default value is 0
- assertThat(cache.getCoarseningLevel(0.0, 0.0)).isEqualTo(0);
- }
-
- @Test
- public void locationFudgerCache_whenQueryIsNotCached_queriesProvider() {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- cache.getCoarseningLevel(POINT_IN_TIMES_SQUARE[0], POINT_IN_TIMES_SQUARE[1]);
-
- verify(provider).getCoarsenedS2Cells(eq(POINT_IN_TIMES_SQUARE[0]),
- eq(POINT_IN_TIMES_SQUARE[1]), anyInt(), any());
- }
-
- @Test
- public void locationFudgerCache_whenProviderIsQueried_resultIsCached() throws RemoteException {
- double lat = POINT_IN_TIMES_SQUARE[0];
- double lng = POINT_IN_TIMES_SQUARE[1];
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- int level = cache.getCoarseningLevel(lat, lng);
- assertThat(level).isEqualTo(0); // default value
-
- ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(
- IS2CellIdsCallback.class);
- verify(provider).getCoarsenedS2Cells(eq(POINT_IN_TIMES_SQUARE[0]),
- eq(POINT_IN_TIMES_SQUARE[1]), anyInt(), argumentCaptor.capture());
-
- // Results from the proxy should set the cache
- int expectedLevel = 4;
- long leafCell = S2CellIdUtils.fromLatLngDegrees(lat, lng);
- Long s2CellId = S2CellIdUtils.getParent(leafCell, expectedLevel);
- IS2CellIdsCallback cb = argumentCaptor.getValue();
- long[] answer = new long[] {s2CellId};
- cb.onResult(answer);
-
- int level2 = cache.getCoarseningLevel(lat, lng);
- assertThat(level2).isEqualTo(expectedLevel);
- }
-
- @Test
- public void locationFudgerCache_whenQueryIsCached_doesNotRefreshIt() {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- cache.addToCache(TIMES_SQUARE_S2_ID);
-
- verify(provider, never()).getCoarsenedS2Cells(anyDouble(), anyDouble(), anyInt(), any());
- }
-
- @Test
- public void locationFudgerCache_whenQueryIsCached_askForMaxCacheSizeElems() {
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
- int numAdditionalCells = cache.MAX_CACHE_SIZE - 1;
-
- cache.getCoarseningLevel(POINT_IN_TIMES_SQUARE[0], POINT_IN_TIMES_SQUARE[1]);
-
- verify(provider).getCoarsenedS2Cells(eq(POINT_IN_TIMES_SQUARE[0]),
- eq(POINT_IN_TIMES_SQUARE[1]), eq(numAdditionalCells), any());
- }
-
- @Test
- public void onDefaultCoarseningLevelNotSet_withDefaultValue_doesNotQueryProvider()
- throws RemoteException {
- // Arrange.
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(
- IS2LevelCallback.class);
- verify(provider, times(1)).getDefaultCoarseningLevel(argumentCaptor.capture());
-
- IS2LevelCallback cb = argumentCaptor.getValue();
- cb.onResult(10);
-
- assertThat(cache.hasDefaultValue()).isTrue();
-
- // Act.
- cache.onDefaultCoarseningLevelNotSet();
-
- // Assert. The method is not called again.
- verify(provider, times(1)).getDefaultCoarseningLevel(any());
- }
-
- @Test
- public void onDefaultCoarseningLevelNotSet_withoutDefaultValue_doesQueryProvider()
- throws RemoteException {
- // Arrange.
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
-
- ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(
- IS2LevelCallback.class);
- verify(provider, times(1)).getDefaultCoarseningLevel(argumentCaptor.capture());
-
- IS2LevelCallback cb = argumentCaptor.getValue();
- cb.onError();
-
- assertThat(cache.hasDefaultValue()).isFalse();
-
- // Act.
- cache.onDefaultCoarseningLevelNotSet();
-
- // Assert. The method is called again.
- verify(provider, times(2)).getDefaultCoarseningLevel(any());
- }
-
- @Test
- public void locationFudgerCache_canContainUpToMaxSizeItems() {
- // This test has two sequences of arrange-act-assert.
- // The first checks that the cache correctly store up to MAX_CACHE_SIZE items.
- // The second checks that any new element replaces the oldest in the cache.
-
- // Arrange.
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
- int size = cache.MAX_CACHE_SIZE;
-
- double[][] latlngs = new double[size][2];
- long[] cells = new long[size];
- int[] expectedLevels = new int[size];
-
- for (int i = 0; i < size; i++) {
- // Create arbitrary lat/lngs.
- latlngs[i][0] = 10.0 * i;
- latlngs[i][1] = 10.0 * i;
-
- expectedLevels[i] = 10; // we set some arbitrary S2 level for each latlng.
-
- long leafCell = S2CellIdUtils.fromLatLngDegrees(latlngs[i][0], latlngs[i][1]);
- long s2CellId = S2CellIdUtils.getParent(leafCell, expectedLevels[i]);
- cells[i] = s2CellId;
- }
-
- // Act.
- cache.addToCache(cells);
-
- // Assert: check that the cache contains these latlngs and returns the correct level.
- for (int i = 0; i < size; i++) {
- assertThat(cache.getCoarseningLevel(latlngs[i][0], latlngs[i][1]))
- .isEqualTo(expectedLevels[i]);
- }
-
- // Second assertion: A new value evicts the oldest one.
-
- // Arrange.
- int expectedLevel = 25;
- long leafCell = S2CellIdUtils.fromLatLngDegrees(-10.0, -180.0);
- long s2CellId = S2CellIdUtils.getParent(leafCell, expectedLevel);
-
- // Act.
- cache.addToCache(s2CellId);
-
- // Assert: the new point is in the cache.
- assertThat(cache.getCoarseningLevel(-10.0, -180.0)).isEqualTo(expectedLevel);
- // Assert: all but the oldest point are still in cache.
- for (int i = 0; i < size - 1; i++) {
- assertThat(cache.getCoarseningLevel(latlngs[i][0], latlngs[i][1]))
- .isEqualTo(expectedLevels[i]);
- }
- // Assert: the oldest point has been evicted.
- assertThat(cache.getCoarseningLevel(latlngs[size - 1][0], latlngs[size - 1][1]))
- .isEqualTo(0);
- }
-
- @Test
- public void logDensityBasedLocsUsed_rateLimitsTheSecondCall() {
- // To avoid having to mock the logger, logDensityBasedLocsUsed returns a boolean indicating
- // if the log was successful or rate-limited.
-
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
- boolean skippedNoDefault = false;
- boolean isCacheHit = false;
- int defaultCoarseningLevel = 3;
- long time1 = 0;
- // 7 min later. Can be any value < time1 + LOG_DENSITY_BASED_LOCS_USED_RATE_LIMIT_MS
- long time2 = time1 + 7 * 60 * 1000;
-
- boolean success1 = cache.logDensityBasedLocsUsed(time1, skippedNoDefault, isCacheHit,
- defaultCoarseningLevel);
- boolean success2 = cache.logDensityBasedLocsUsed(time2, skippedNoDefault, isCacheHit,
- defaultCoarseningLevel);
-
- assertThat(success1).isTrue(); // log OK
- assertThat(success2).isFalse(); // dropped
- }
-
- @Test
- public void logDensityBasedLocsUsed_rateLimitOf3rdCall_isNotAffectedByDropped2ndCall() {
- // To avoid having to mock the logger, logDensityBasedLocsUsed returns a boolean indicating
- // if the log was successful or rate-limited.
-
- ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
- boolean skippedNoDefault = false;
- boolean isCacheHit = false;
- int defaultCoarseningLevel = 3;
- long time1 = 0;
- // 7 min later. Can be any value < time1 + LOG_DENSITY_BASED_LOCS_USED_RATE_LIMIT_MS
- long time2 = time1 + 7 * 60 * 1000;
- // 11 min later. Can be any value >= time1 + LOG_DENSITY_BASED_LOCS_USED_RATE_LIMIT_MS
- long time3 = time1 + 11 * 60 * 1000;
-
- boolean success1 = cache.logDensityBasedLocsUsed(time1, skippedNoDefault, isCacheHit,
- defaultCoarseningLevel);
- boolean success2 = cache.logDensityBasedLocsUsed(time2, skippedNoDefault, isCacheHit,
- defaultCoarseningLevel);
- boolean success3 = cache.logDensityBasedLocsUsed(time3, skippedNoDefault, isCacheHit,
- defaultCoarseningLevel);
-
- assertThat(success1).isTrue(); // log OK
- assertThat(success2).isFalse(); // dropped
- assertThat(success3).isTrue(); // log OK
- }
-
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java
index 696a718090fbd..01b1ac859ebb7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java
@@ -19,24 +19,31 @@
import static androidx.test.ext.truth.location.LocationSubject.assertThat;
import static com.android.server.location.LocationUtils.createLocation;
+import static com.android.server.location.LocationUtils.createLocationResult;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyDouble;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.location.Location;
-import android.location.flags.Flags;
+import android.location.LocationResult;
import android.os.Bundle;
import android.platform.test.annotations.Presubmit;
-import android.util.Log;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.location.geometry.S2CellIdUtils;
+import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider;
+import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider.PopulationDensityUnavailableException;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -45,19 +52,24 @@
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
+import java.util.List;
import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class LocationFudgerTest {
- private static final String TAG = "LocationFudgerTest";
-
private static final double APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111_000;
private static final float ACCURACY_M = 2000;
- private static final float MAX_COARSE_FUDGE_DISTANCE_M =
- (float) Math.sqrt(2 * ACCURACY_M * ACCURACY_M) + ACCURACY_M / 4f;
+ private static final long COORDINATION_TIMEOUT_SECONDS = 10;
+
+ private static final int TEST_COARSENING_LEVEL = 12;
private Random mRandom;
@@ -65,10 +77,7 @@ public class LocationFudgerTest {
@Before
public void setUp() {
- long seed = System.currentTimeMillis();
- Log.i(TAG, "location random seed: " + seed);
-
- mRandom = new Random(seed);
+ mRandom = new Random(0);
mFudger = new LocationFudger(
ACCURACY_M,
Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()),
@@ -76,7 +85,13 @@ public void setUp() {
}
@Test
- public void testCoarsen() {
+ public void testCoarsen() throws Exception {
+ mFudger.setPopulationDensityProvider(fixedLevelProvider(TEST_COARSENING_LEVEL));
+ float cellEdge = mFudger.getS2CellApproximateEdge(TEST_COARSENING_LEVEL);
+ // This is a deterministic sanity threshold, not a geometric upper bound for S2 cells or
+ // Gaussian offsets.
+ float sanityDistanceM = (float) Math.sqrt(2) * cellEdge + ACCURACY_M;
+
// Coarsening must drop every position-sensitive field while carrying only the
// non-sensitive fields that downstream code and LocationResult.validate() rely on. Every
// sensitive field is set on the fine input, so a regression that copies any of them is
@@ -123,14 +138,15 @@ public void testCoarsen() {
.isEqualTo(fine.getElapsedRealtimeUncertaintyNanos());
assertThat(coarse.isMock()).isTrue();
- assertThat(coarse.getAccuracy()).isEqualTo(ACCURACY_M);
+ assertThat(coarse.getAccuracy()).isEqualTo(cellEdge);
assertThat(coarse.distanceTo(fine)).isGreaterThan(1F);
- assertThat(coarse).isNearby(fine, MAX_COARSE_FUDGE_DISTANCE_M);
+ assertThat(coarse).isNearby(fine, sanityDistanceM);
}
}
@Test
- public void testCoarsen_Consistent() {
+ public void testCoarsen_Consistent() throws Exception {
+ mFudger.setPopulationDensityProvider(fixedLevelProvider(TEST_COARSENING_LEVEL));
// test that coarsening the same location will always return the same coarse location
// (and thus that averaging to eliminate random noise won't work)
for (int i = 0; i < 100; i++) {
@@ -142,7 +158,8 @@ public void testCoarsen_Consistent() {
}
@Test
- public void testCoarsen_AvgMany() {
+ public void testCoarsen_AvgMany() throws Exception {
+ mFudger.setPopulationDensityProvider(fixedLevelProvider(TEST_COARSENING_LEVEL));
// test that a set of locations normally distributed around the user's real location still
// cannot be easily average to reveal the user's real location
@@ -184,10 +201,10 @@ public void testCoarsen_AvgMany() {
}
}
- // very generally speaking, the closer the initial fine point is to a grid point, the more
+ // very generally speaking, the closer the initial fine point is to a cell center, the more
// accurate the coarsened average will be. we use 70% as a lower bound by -very- roughly
- // taking the area within a grid where we expect a reasonable percentage of points generated
- // by step() to fall in another grid square. this likely doesn't have much mathematical
+ // taking the area within a cell where we expect a reasonable percentage of points generated
+ // by step() to fall in another cell. this likely doesn't have much mathematical
// validity, but it serves as a validity test as least.
assertThat(passed / (double) iterations).isGreaterThan(.70);
}
@@ -203,87 +220,691 @@ private Location step(Location input, double distanceM) {
0);
}
+ private static ProxyPopulationDensityProvider fixedLevelProvider(int level)
+ throws PopulationDensityUnavailableException {
+ long cell = S2CellIdUtils.getParent(S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), level);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doReturn(cell).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ return provider;
+ }
+
+ /** Creates a clock backed by the given mutable time source. */
+ private static Clock advancingClock(AtomicLong currentTimeMillis) {
+ return new Clock() {
+ @Override
+ public ZoneId getZone() {
+ return ZoneId.systemDefault();
+ }
+
+ @Override
+ public Clock withZone(ZoneId zone) {
+ return this;
+ }
+
+ @Override
+ public Instant instant() {
+ return Instant.ofEpochMilli(currentTimeMillis.get());
+ }
+
+ @Override
+ public long millis() {
+ return currentTimeMillis.get();
+ }
+ };
+ }
+
+ @Test
+ public void testNoProviderConfigured_suppressesFix() {
+ Location coarse = mFudger.createCoarse(createLocation("test", mRandom));
+
+ assertThat(coarse).isNull();
+ }
+
+ @Test
+ public void testCoarsen_nearAntimeridianAndPoles_normalizesAndCoarsens() throws Exception {
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ ArrayList queriedPoints = new ArrayList<>();
+ doAnswer(invocation -> {
+ queriedPoints.add(
+ new double[] {invocation.getArgument(0), invocation.getArgument(1)});
+ return cell;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mFudger.setPopulationDensityProvider(provider);
+
+ // Cover both offset signs.
+ assertThat(mFudger.createCoarse(createLocation("test", 89.9999, 179.9999, 1f)))
+ .isNotNull();
+ assertThat(mFudger.createCoarse(createLocation("test", -89.9999, -179.9999, 1f)))
+ .isNotNull();
+
+ for (double[] queriedPoint : queriedPoints) {
+ assertThat(queriedPoint[0]).isAtLeast(-90.0);
+ assertThat(queriedPoint[0]).isAtMost(90.0);
+ assertThat(queriedPoint[1]).isAtLeast(-180.0);
+ assertThat(queriedPoint[1]).isAtMost(180.0);
+ }
+ }
+
+ @Test
+ public void testDensityBasedCoarsening_providerCellPositionIgnored() throws Exception {
+ int level = 12;
+ long s2CellId = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(40.758896, -73.985130), level);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ double[] queried = new double[2];
+ doAnswer(invocation -> {
+ queried[0] = invocation.getArgument(0);
+ queried[1] = invocation.getArgument(1);
+ return s2CellId;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+
+ mFudger.setPopulationDensityProvider(provider);
+
+ Location fine = createLocation("test", 1.0, 1.0, /* accuracy= */ 1f);
+ Location coarse = mFudger.createCoarse(fine);
+
+ verify(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ double[] expectedCenter = mFudger.snapToCenterOfS2Cell(queried[0], queried[1], level);
+ assertThat(coarse).isNotNull();
+ assertThat(coarse.getLatitude()).isEqualTo(expectedCenter[0]);
+ assertThat(coarse.getLongitude()).isEqualTo(expectedCenter[1]);
+ assertThat(coarse.getAccuracy()).isEqualTo(mFudger.getS2CellApproximateEdge(level));
+ }
+
@Test
- public void testDensityBasedCoarsening_noDefaultValue_cacheIsNotUsed() {
- LocationFudgerCache cache = mock(LocationFudgerCache.class);
- doReturn(false).when(cache).hasDefaultValue();
+ public void testDensityBasedCoarsening_queryQuantizedToFinestAcceptedLevel()
+ throws Exception {
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ double[] queried = new double[2];
+ doAnswer(invocation -> {
+ queried[0] = invocation.getArgument(0);
+ queried[1] = invocation.getArgument(1);
+ return cell;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mFudger.setPopulationDensityProvider(provider);
+
+ Location coarse = mFudger.createCoarse(createLocation("test", 40.758896, -73.985130, 1f));
+
+ assertThat(coarse).isNotNull();
+ double[] requantized =
+ mFudger.snapToCenterOfS2Cell(queried[0], queried[1], TEST_COARSENING_LEVEL);
+ assertThat(requantized[0]).isEqualTo(queried[0]);
+ assertThat(requantized[1]).isEqualTo(queried[1]);
+ }
+
+ @Test
+ public void testDensityBasedCoarsening_malformedCell_suppressesFix() throws Exception {
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doReturn(0L).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+
+ mFudger.setPopulationDensityProvider(provider);
+
+ Location coarse = mFudger.createCoarse(createLocation("test", mRandom));
+
+ assertThat(coarse).isNull();
+ }
- mFudger.setLocationFudgerCache(cache);
+ @Test
+ public void testDensityBasedCoarsening_tooFineLevel_suppressesFix() throws Exception {
+ long s2CellId = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(40.758896, -73.985130), 13);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doReturn(s2CellId).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+
+ mFudger.setPopulationDensityProvider(provider);
- mFudger.createCoarse(createLocation("test", mRandom));
+ Location coarse = mFudger.createCoarse(createLocation("test", mRandom));
- verify(cache, never()).getCoarseningLevel(anyDouble(), anyDouble());
+ assertThat(coarse).isNull();
}
@Test
- public void testDensityBasedCoarsening_noDefaultValue_defaultIsFetched() {
- LocationFudgerCache cache = mock(LocationFudgerCache.class);
- doReturn(false).when(cache).hasDefaultValue();
+ public void testDensityBasedCoarsening_finestSupportedLevel_coarsens() throws Exception {
+ int level = 12;
+ long s2CellId = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(40.758896, -73.985130), level);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doReturn(s2CellId).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
- mFudger.setLocationFudgerCache(cache);
+ mFudger.setPopulationDensityProvider(provider);
- mFudger.createCoarse(createLocation("test", mRandom));
+ Location fine = createLocation("test", 1.0, 1.0, /* accuracy= */ 1f);
+ Location coarse = mFudger.createCoarse(fine);
- verify(cache).onDefaultCoarseningLevelNotSet();
+ assertThat(coarse).isNotNull();
+ assertThat(coarse.getAccuracy()).isEqualTo(mFudger.getS2CellApproximateEdge(level));
}
@Test
- public void testDensityBasedCoarsening_defaultIsSet_cacheIsUsed() {
- LocationFudgerCache cache = mock(LocationFudgerCache.class);
- doReturn(true).when(cache).hasDefaultValue();
+ public void testDensityBasedCoarsening_providerFault_suppressesFix() throws Exception {
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
- mFudger.setLocationFudgerCache(cache);
+ mFudger.setPopulationDensityProvider(provider);
+ Location coarse = mFudger.createCoarse(createLocation("test", mRandom));
+
+ assertThat(coarse).isNull();
+ }
+
+ @Test
+ public void testDensityBasedCoarsening_providerFault_notRetried() throws Exception {
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mFudger.setPopulationDensityProvider(provider);
Location fine = createLocation("test", mRandom);
- mFudger.createCoarse(fine);
- // We can't verify that the coordinates of "fine" are passed to the API due to the addition
- // of the offset. We must use anyDouble().
- verify(cache).getCoarseningLevel(anyDouble(), anyDouble());
+ assertThat(mFudger.createCoarse(fine)).isNull();
+ assertThat(mFudger.createCoarse(fine)).isNull();
+ assertThat(mFudger.createCoarse(fine)).isNull();
+
+ verify(provider, times(1)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
+
+ @Test
+ public void testDensityBasedCoarsening_providerRebind_reCoarsensSameFix() throws Exception {
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .doReturn(cell)
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ AtomicLong bindingGeneration = new AtomicLong();
+ doAnswer(invocation -> bindingGeneration.get()).when(provider).getBindingGeneration();
+ mFudger.setPopulationDensityProvider(provider);
+ Location fine = createLocation("test", mRandom);
+
+ assertThat(mFudger.createCoarse(fine)).isNull();
+ bindingGeneration.incrementAndGet();
+ assertThat(mFudger.createCoarse(fine)).isNotNull();
+
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
+
+ @Test
+ public void testDensityBasedCoarsening_bindingChangesDuringCacheLookup_suppressesStaleResult()
+ throws Exception {
+ ProxyPopulationDensityProvider provider = fixedLevelProvider(TEST_COARSENING_LEVEL);
+ AtomicLong bindingGeneration = new AtomicLong();
+ AtomicBoolean advanceGenerationOnNextRead = new AtomicBoolean();
+ doAnswer(invocation -> {
+ long generation = bindingGeneration.get();
+ if (advanceGenerationOnNextRead.compareAndSet(true, false)) {
+ bindingGeneration.incrementAndGet();
+ }
+ return generation;
+ }).when(provider).getBindingGeneration();
+ mFudger.setPopulationDensityProvider(provider);
+ Location fine = createLocation("test", mRandom);
+
+ assertThat(mFudger.createCoarse(fine)).isNotNull();
+ advanceGenerationOnNextRead.set(true);
+ assertThat(mFudger.createCoarse(fine)).isNull();
+ assertThat(mFudger.createCoarse(fine)).isNotNull();
+
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
}
@Test
- public void testDensityBasedCoarsening_newAlgorithm_snapsToCenterOfS2Cell_testVector() {
- // NB: a complete test vector is in
- // frameworks/base/services/tests/mockingservicestests/src/com/android/server/...
- // location/geometry/S2CellIdUtilsTest.java
+ public void testDensityBasedCoarsening_faultThenTtlExpiry_reQueriesSameFix() throws Exception {
+ AtomicLong currentTimeMillis = new AtomicLong();
+ LocationFudger fudger = new LocationFudger(
+ ACCURACY_M, advancingClock(currentTimeMillis), new Random(0));
+
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .doReturn(cell)
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ doReturn(0L).when(provider).getBindingGeneration();
+ fudger.setPopulationDensityProvider(provider);
+ Location fine = createLocation("test", new Random(0));
+
+ assertThat(fudger.createCoarse(fine)).isNull();
+ currentTimeMillis.set(LocationFudger.NEGATIVE_CACHE_TTL_MS - 1);
+ assertThat(fudger.createCoarse(fine)).isNull();
+ verify(provider, times(1)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ currentTimeMillis.set(LocationFudger.NEGATIVE_CACHE_TTL_MS);
+ assertThat(fudger.createCoarse(fine)).isNotNull();
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
+
+ @Test
+ public void testDensityBasedCoarsening_faultThenRecovery_nextFixCoarsens() throws Exception {
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .doReturn(cell)
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mFudger.setPopulationDensityProvider(provider);
+
+ assertThat(mFudger.createCoarse(createLocation("test", mRandom))).isNull();
+ assertThat(mFudger.createCoarse(createLocation("test", mRandom))).isNotNull();
+ }
+
+ @Test
+ public void testDensityBasedCoarsening_failureMemoryCoversOnlyLatestInput() throws Exception {
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .doReturn(cell)
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mFudger.setPopulationDensityProvider(provider);
+ Location failedFine = createLocation("test", mRandom);
+
+ assertThat(mFudger.createCoarse(failedFine)).isNull();
+ assertThat(mFudger.createCoarse(createLocation("test", mRandom))).isNotNull();
+ assertThat(mFudger.createCoarse(failedFine)).isNotNull();
+
+ verify(provider, times(3)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
+
+ @Test
+ public void testCoarsenLocationResult_allCoarsen_returnsFullBatch() throws Exception {
+ mFudger.setPopulationDensityProvider(fixedLevelProvider(TEST_COARSENING_LEVEL));
+ LocationResult fine = createLocationResult("test", mRandom, 3);
+
+ LocationResult coarse = mFudger.createCoarse(fine);
+
+ assertThat(coarse).isNotNull();
+ assertThat(coarse.asList()).hasSize(3);
+ assertThat(coarse.get(0).getAccuracy())
+ .isEqualTo(mFudger.getS2CellApproximateEdge(TEST_COARSENING_LEVEL));
+ }
+
+ @Test
+ public void testCoarsenLocationResult_midBatchFault_suppressesWholeBatch() throws Exception {
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doReturn(cell)
+ .doThrow(new PopulationDensityUnavailableException("mid-batch fault"))
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mFudger.setPopulationDensityProvider(provider);
+
+ LocationResult coarse = mFudger.createCoarse(createLocationResult("test", mRandom, 3));
+
+ assertThat(coarse).isNull();
+ }
+
+ @Test
+ public void testCoarsenLocationResult_failedResult_notRetried() throws Exception {
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mFudger.setPopulationDensityProvider(provider);
+ LocationResult fine = createLocationResult("test", mRandom, 3);
+
+ assertThat(mFudger.createCoarse(fine)).isNull();
+ assertThat(mFudger.createCoarse(fine)).isNull();
+ assertThat(mFudger.createCoarse(fine)).isNull();
+
+ verify(provider, times(1)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
+
+ @Test
+ public void testCoarsenLocationResult_failedResult_retriesAfterBindingChange()
+ throws Exception {
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ AtomicLong generation = new AtomicLong();
+ doAnswer(invocation -> generation.get()).when(provider).getBindingGeneration();
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .doReturn(cell)
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mFudger.setPopulationDensityProvider(provider);
+ LocationResult fine = LocationResult.wrap(
+ List.of(createLocation("test", new Random(0))));
+
+ assertThat(mFudger.createCoarse(fine)).isNull();
+ generation.incrementAndGet();
+ assertThat(mFudger.createCoarse(fine)).isNotNull();
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
+
+ @Test
+ public void testCoarsenLocationResult_rebindDuringCacheLookup_discardsStaleResult()
+ throws Exception {
+ AtomicLong generation = new AtomicLong();
+ AtomicBoolean rebindAfterRead = new AtomicBoolean();
+ ProxyPopulationDensityProvider provider = fixedLevelProvider(TEST_COARSENING_LEVEL);
+ doAnswer(invocation -> {
+ long capturedGeneration = generation.get();
+ if (rebindAfterRead.getAndSet(false)) {
+ generation.incrementAndGet();
+ }
+ return capturedGeneration;
+ }).when(provider).getBindingGeneration();
+ mFudger.setPopulationDensityProvider(provider);
+ LocationResult fine = LocationResult.wrap(
+ List.of(createLocation("test", new Random(0))));
+
+ assertThat(mFudger.createCoarse(fine)).isNotNull();
+ rebindAfterRead.set(true);
+ assertThat(mFudger.createCoarse(fine)).isNull();
+ assertThat(mFudger.createCoarse(fine)).isNotNull();
+
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
+
+ @Test
+ public void testCoarsenLocationResult_providerReplacement_reCoarsensSameInput()
+ throws Exception {
+ int firstLevel = TEST_COARSENING_LEVEL;
+ int secondLevel = TEST_COARSENING_LEVEL - 2;
+ ProxyPopulationDensityProvider firstProvider = fixedLevelProvider(firstLevel);
+ ProxyPopulationDensityProvider secondProvider = fixedLevelProvider(secondLevel);
+ doReturn(0L).when(firstProvider).getBindingGeneration();
+ doReturn(0L).when(secondProvider).getBindingGeneration();
+ LocationResult fine = LocationResult.wrap(
+ List.of(createLocation("test", new Random(0))));
+
+ mFudger.setPopulationDensityProvider(firstProvider);
+ LocationResult firstCoarse = mFudger.createCoarse(fine);
+ assertThat(firstCoarse).isNotNull();
+ assertThat(firstCoarse.get(0).getAccuracy())
+ .isEqualTo(mFudger.getS2CellApproximateEdge(firstLevel));
+ mFudger.setPopulationDensityProvider(secondProvider);
+ LocationResult secondCoarse = mFudger.createCoarse(fine);
+ assertThat(secondCoarse).isNotNull();
+ assertThat(secondCoarse.get(0).getAccuracy())
+ .isEqualTo(mFudger.getS2CellApproximateEdge(secondLevel));
+
+ verify(firstProvider, times(1)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ verify(secondProvider, times(1)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
- // Arbitrary location in Times Square, NYC
- double[] latLng = new double[] {40.758896, -73.985130};
- int s2Level = 1;
- // The level-2 S2 cell around this location is "8c", its center is:
- double[] expected = { 21.037511025421814, -67.38013505195958 };
+ @Test
+ public void testCoarsenLocationResult_resetOffsets_reCoarsensSameInput() throws Exception {
+ Random random = mock(Random.class);
+ doReturn(0.0, 0.0, 10.0, 10.0).when(random).nextGaussian();
+ LocationFudger fudger = new LocationFudger(
+ ACCURACY_M,
+ Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()),
+ random);
+ ProxyPopulationDensityProvider provider = fixedLevelProvider(TEST_COARSENING_LEVEL);
+ fudger.setPopulationDensityProvider(provider);
+ LocationResult fine = LocationResult.wrap(
+ List.of(createLocation("test", 0.0, 0.0, 1.0f)));
+
+ LocationResult firstCoarse = fudger.createCoarse(fine);
+ assertThat(firstCoarse).isNotNull();
+ fudger.resetOffsets();
+ LocationResult secondCoarse = fudger.createCoarse(fine);
+
+ assertThat(secondCoarse).isNotNull();
+ long firstCellId = S2CellIdUtils.fromLatLngDegrees(
+ firstCoarse.get(0).getLatitude(), firstCoarse.get(0).getLongitude());
+ long secondCellId = S2CellIdUtils.fromLatLngDegrees(
+ secondCoarse.get(0).getLatitude(), secondCoarse.get(0).getLongitude());
+ assertThat(secondCellId).isNotEqualTo(firstCellId);
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
- double[] center = mFudger.snapToCenterOfS2Cell(latLng[0], latLng[1], s2Level);
+ @Test
+ public void testCoarsenLocationResult_scheduledOffsetUpdate_reCoarsensSameInput()
+ throws Exception {
+ AtomicLong currentTimeMillis = new AtomicLong();
+ Random random = mock(Random.class);
+ doReturn(0.0, 0.0, 1.0, 1.0).when(random).nextGaussian();
+ LocationFudger fudger =
+ new LocationFudger(
+ ACCURACY_M, advancingClock(currentTimeMillis), random);
+ ProxyPopulationDensityProvider provider = fixedLevelProvider(TEST_COARSENING_LEVEL);
+ fudger.setPopulationDensityProvider(provider);
+ LocationResult fine = LocationResult.wrap(
+ List.of(createLocation("test", 0.0, 0.0, 1.0f)));
+
+ assertThat(fudger.createCoarse(fine)).isNotNull();
+ currentTimeMillis.set(LocationFudger.OFFSET_UPDATE_INTERVAL_MS);
+ assertThat(fudger.createCoarse(fine)).isNotNull();
+
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
- assertThat(center[0]).isEqualTo(expected[0]);
- assertThat(center[1]).isEqualTo(expected[1]);
+ @Test
+ public void testCoarsenLocation_resetDuringQuery_discardsStaleResult() throws Exception {
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ CountDownLatch queryStarted = new CountDownLatch(1);
+ CountDownLatch releaseQuery = new CountDownLatch(1);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doAnswer(invocation -> {
+ queryStarted.countDown();
+ assertThat(releaseQuery.await(
+ COORDINATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
+ return cell;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mFudger.setPopulationDensityProvider(provider);
+ Location fine = createLocation("test", 0.0, 0.0, 1.0f);
+ FutureTask queryResult = new FutureTask<>(() -> mFudger.createCoarse(fine));
+ Thread queryThread = new Thread(queryResult, "LocationFudgerTest-query");
+ queryThread.start();
+
+ try {
+ assertThat(queryStarted.await(
+ COORDINATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
+ mFudger.resetOffsets();
+ releaseQuery.countDown();
+ assertThat(queryResult.get(
+ COORDINATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isNull();
+ assertThat(mFudger.createCoarse(fine)).isNotNull();
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ } finally {
+ queryResult.cancel(true);
+ releaseQuery.countDown();
+ queryThread.join(TimeUnit.SECONDS.toMillis(COORDINATION_TIMEOUT_SECONDS));
+ assertThat(queryThread.isAlive()).isFalse();
+ }
}
@Test
- public void getS2CellApproximateEdge_returnsCorrectRadius() {
- int level = 10;
+ public void testCoarsenLocationResult_failedResult_retriesAfterTtl() throws Exception {
+ AtomicLong currentTimeMillis = new AtomicLong(0);
+ LocationFudger fudger =
+ new LocationFudger(
+ ACCURACY_M, advancingClock(currentTimeMillis), new Random(0));
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .doReturn(cell)
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ fudger.setPopulationDensityProvider(provider);
+ LocationResult fine = LocationResult.wrap(
+ List.of(createLocation("test", new Random(0))));
+
+ assertThat(fudger.createCoarse(fine)).isNull();
+ currentTimeMillis.set(LocationFudger.NEGATIVE_CACHE_TTL_MS - 1);
+ assertThat(fudger.createCoarse(fine)).isNull();
+ currentTimeMillis.set(LocationFudger.NEGATIVE_CACHE_TTL_MS);
+ assertThat(fudger.createCoarse(fine)).isNotNull();
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
- float radius = mFudger.getS2CellApproximateEdge(level);
+ @Test
+ public void testCoarsenLocationResult_slowBatch_deliversCoarsenedPrefix() throws Exception {
+ AtomicLong currentTimeMillis = new AtomicLong(0);
+ LocationFudger fudger =
+ new LocationFudger(
+ ACCURACY_M, advancingClock(currentTimeMillis), new Random(0));
+
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ long perQueryAdvanceMillis =
+ LocationFudger.MAX_BATCH_COARSENING_DURATION_MS / 2 + 1;
+ doAnswer(invocation -> {
+ currentTimeMillis.addAndGet(perQueryAdvanceMillis);
+ return cell;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ fudger.setPopulationDensityProvider(provider);
+
+ LocationResult coarse =
+ fudger.createCoarse(createLocationResult("test", new Random(0), 3));
+
+ assertThat(coarse).isNotNull();
+ assertThat(coarse.asList()).hasSize(2);
+ verify(provider, times(2)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
- assertThat(radius).isEqualTo(9000); // in meters
+ @Test
+ public void testCoarsenLocationResult_withinDeadline_returnsFullBatch() throws Exception {
+ AtomicLong currentTimeMillis = new AtomicLong(0);
+ LocationFudger fudger =
+ new LocationFudger(
+ ACCURACY_M, advancingClock(currentTimeMillis), new Random(0));
+
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ long perQueryAdvanceMillis =
+ LocationFudger.MAX_BATCH_COARSENING_DURATION_MS / 10;
+ doAnswer(invocation -> {
+ currentTimeMillis.addAndGet(perQueryAdvanceMillis);
+ return cell;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ fudger.setPopulationDensityProvider(provider);
+
+ LocationResult coarse =
+ fudger.createCoarse(createLocationResult("test", new Random(0), 3));
+
+ assertThat(coarse).isNotNull();
+ assertThat(coarse.asList()).hasSize(3);
+ verify(provider, times(3)).getCoarsenedS2CellId(anyDouble(), anyDouble());
}
@Test
- public void getS2CellApproximateEdge_doesNotThrow() {
- int level = -1;
+ public void testCoarsenLocationResult_singleSlowLocation_stillDelivers() throws Exception {
+ AtomicLong currentTimeMillis = new AtomicLong(0);
+ LocationFudger fudger =
+ new LocationFudger(
+ ACCURACY_M, advancingClock(currentTimeMillis), new Random(0));
+
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doAnswer(invocation -> {
+ currentTimeMillis.addAndGet(
+ LocationFudger.MAX_BATCH_COARSENING_DURATION_MS + 1);
+ return cell;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ fudger.setPopulationDensityProvider(provider);
+
+ LocationResult coarse =
+ fudger.createCoarse(createLocationResult("test", new Random(0), 1));
+
+ assertThat(coarse).isNotNull();
+ assertThat(coarse.asList()).hasSize(1);
+ }
- mFudger.getS2CellApproximateEdge(level);
+ @Test
+ public void testCoarsenLocationResult_deadlineExpiresBeforeFirst_suppressesResult()
+ throws Exception {
+ Clock clock = mock(Clock.class);
+ doReturn(0L, 0L, 0L, LocationFudger.MAX_BATCH_COARSENING_DURATION_MS)
+ .when(clock).millis();
+ LocationFudger fudger = new LocationFudger(ACCURACY_M, clock, new Random(0));
+ ProxyPopulationDensityProvider provider = fixedLevelProvider(TEST_COARSENING_LEVEL);
+ fudger.setPopulationDensityProvider(provider);
+
+ Location fine = createLocation("test", new Random(0));
+ LocationResult coarse = fudger.createCoarse(LocationResult.wrap(List.of(fine)));
+
+ assertThat(coarse).isNull();
+ verify(provider, never()).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
- // No exception thrown.
+ @Test
+ public void testCoarsenLocation_directLocation_hasNoBatchDeadline() throws Exception {
+ Clock clock = mock(Clock.class);
+ doReturn(0L, LocationFudger.MAX_BATCH_COARSENING_DURATION_MS)
+ .when(clock).millis();
+ LocationFudger fudger = new LocationFudger(ACCURACY_M, clock, new Random(0));
+ ProxyPopulationDensityProvider provider = fixedLevelProvider(TEST_COARSENING_LEVEL);
+ fudger.setPopulationDensityProvider(provider);
+
+ Location coarse = fudger.createCoarse(createLocation("test", new Random(0)));
+
+ assertThat(coarse).isNotNull();
+ verify(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
}
@Test
- public void getS2CellApproximateEdge_doesNotThrow2() {
- int level = 14;
+ public void testCoarsenLocationResult_deadlineReachedExactly_deliversPrefix() throws Exception {
+ AtomicLong currentTimeMillis = new AtomicLong(0);
+ LocationFudger fudger =
+ new LocationFudger(
+ ACCURACY_M, advancingClock(currentTimeMillis), new Random(0));
+
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doAnswer(invocation -> {
+ currentTimeMillis.addAndGet(LocationFudger.MAX_BATCH_COARSENING_DURATION_MS);
+ return cell;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ fudger.setPopulationDensityProvider(provider);
+
+ LocationResult coarse =
+ fudger.createCoarse(createLocationResult("test", new Random(0), 2));
+
+ assertThat(coarse).isNotNull();
+ assertThat(coarse.asList()).hasSize(1);
+ verify(provider, times(1)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
- mFudger.getS2CellApproximateEdge(level);
+ @Test
+ public void testCoarsenLocationResult_repeatedLocation_consumesNoBudget() throws Exception {
+ AtomicLong currentTimeMillis = new AtomicLong(0);
+ LocationFudger fudger =
+ new LocationFudger(
+ ACCURACY_M, advancingClock(currentTimeMillis), new Random(0));
+
+ long cell = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ long perQueryAdvanceMillis =
+ LocationFudger.MAX_BATCH_COARSENING_DURATION_MS / 2 + 1;
+ doAnswer(invocation -> {
+ currentTimeMillis.addAndGet(perQueryAdvanceMillis);
+ return cell;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ fudger.setPopulationDensityProvider(provider);
+
+ Location fine = createLocation("test", new Random(0));
+ LocationResult coarse = fudger.createCoarse(
+ LocationResult.wrap(List.of(fine, fine, fine)));
+
+ assertThat(coarse).isNotNull();
+ assertThat(coarse.asList()).hasSize(3);
+ verify(provider, times(1)).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ }
- // No exception thrown.
+ @Test
+ public void getS2CellApproximateEdge_returnsCorrectEdge() {
+ assertThat(mFudger.getS2CellApproximateEdge(9)).isWithin(1f).of(18010f);
+ assertThat(mFudger.getS2CellApproximateEdge(10)).isWithin(1f).of(9000f);
+ assertThat(mFudger.getS2CellApproximateEdge(11)).isWithin(1f).of(4500f);
+ }
+
+ @Test
+ public void getS2CellApproximateEdge_belowRange_clampsToLevelZero() {
+ assertThat(mFudger.getS2CellApproximateEdge(-1)).isEqualTo(9_220_140f);
+ }
+
+ @Test
+ public void getS2CellApproximateEdge_aboveRange_clampsToLastLevel() {
+ assertThat(mFudger.getS2CellApproximateEdge(14)).isEqualTo(2_250f);
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
index 2763580238e37..9e6f2f616b509 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.LOCATION_BYPASS;
+import static android.app.AppOpsManager.OP_COARSE_LOCATION;
import static android.app.AppOpsManager.OP_FINE_LOCATION;
import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
@@ -48,7 +49,9 @@
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -72,7 +75,6 @@
import android.location.LocationRequest;
import android.location.LocationResult;
import android.location.provider.IProviderRequestListener;
-import android.location.provider.IS2LevelCallback;
import android.location.provider.ProviderProperties;
import android.location.provider.ProviderRequest;
import android.location.util.identity.CallerIdentity;
@@ -96,12 +98,13 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.R;
+import com.android.internal.location.geometry.S2CellIdUtils;
import com.android.server.FgThread;
import com.android.server.LocalServices;
-import com.android.server.location.fudger.LocationFudgerCache;
import com.android.server.location.injector.FakeUserInfoHelper;
import com.android.server.location.injector.TestInjector;
import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider;
+import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider.PopulationDensityUnavailableException;
import org.junit.After;
import org.junit.Before;
@@ -134,6 +137,9 @@ public class LocationProviderManagerTest {
private static final int CURRENT_USER = FakeUserInfoHelper.DEFAULT_USERID;
private static final int OTHER_USER = CURRENT_USER + 10;
+ // S2 level with approximately 2.25 km cells.
+ private static final int TEST_COARSENING_LEVEL = 12;
+
private static final String NAME = "test";
private static final ProviderProperties PROPERTIES = new ProviderProperties.Builder()
.setHasAltitudeSupport(true)
@@ -223,6 +229,14 @@ private void createManager(String name, Collection requiredPermissions)
mManager.setRealProvider(mProvider);
}
+ private void setFixedLevelCoarseningProvider() throws PopulationDensityUnavailableException {
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ long s2CellId = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ doReturn(s2CellId).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mManager.setPopulationDensityProvider(provider);
+ }
+
@After
public void tearDown() throws Exception {
DeviceConfig.resetToDefaults(Settings.RESET_MODE_PACKAGE_DEFAULTS,
@@ -348,7 +362,8 @@ public void testGetLastLocation_Fine() {
}
@Test
- public void testGetLastLocation_Coarse() {
+ public void testGetLastLocation_Coarse() throws Exception {
+ setFixedLevelCoarseningProvider();
assertThat(mManager.getLastLocation(new LastLocationRequest.Builder().build(), IDENTITY,
PERMISSION_FINE)).isNull();
@@ -744,6 +759,7 @@ public void testRegisterListener_Wakelock() throws Exception {
@Test
public void testRegisterListener_Coarse() throws Exception {
+ setFixedLevelCoarseningProvider();
ILocationListener listener = createMockLocationListener();
mManager.registerLocationRequest(
new LocationRequest.Builder(0).setWorkSource(WORK_SOURCE).build(),
@@ -759,6 +775,7 @@ public void testRegisterListener_Coarse() throws Exception {
@Test
public void testRegisterListener_Coarse_Passive() throws Exception {
+ setFixedLevelCoarseningProvider();
ILocationListener listener = createMockLocationListener();
mManager.registerLocationRequest(
new LocationRequest.Builder(PASSIVE_INTERVAL)
@@ -774,6 +791,35 @@ public void testRegisterListener_Coarse_Passive() throws Exception {
.onLocationChanged(any(List.class), nullable(IRemoteCallback.class));
}
+ @Test
+ public void testRegisterListener_Coarse_suppressedFixIsRetried() throws Exception {
+ Random random = new Random(0);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ long s2CellId = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ doThrow(new PopulationDensityUnavailableException("provider not bound"))
+ .doReturn(s2CellId)
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mManager.setPopulationDensityProvider(provider);
+
+ ILocationListener listener = createMockLocationListener();
+ mManager.registerLocationRequest(
+ new LocationRequest.Builder(0).setWorkSource(WORK_SOURCE).build(),
+ IDENTITY,
+ PERMISSION_COARSE,
+ listener);
+
+ mProvider.setProviderLocation(createLocation(NAME, random));
+ verify(listener, never())
+ .onLocationChanged(nullable(List.class), nullable(IRemoteCallback.class));
+
+ mProvider.setProviderLocation(createLocation(NAME, random));
+ verify(listener, times(1))
+ .onLocationChanged(any(List.class), nullable(IRemoteCallback.class));
+ verify(listener, never())
+ .onLocationChanged(isNull(), nullable(IRemoteCallback.class));
+ }
+
@Test
public void testProviderRequestListener() throws Exception {
IProviderRequestListener requestListener = mock(IProviderRequestListener.class);
@@ -872,6 +918,58 @@ public void testGetCurrentLocation_InvisibleUser() throws Exception {
verify(listener).onLocation(isNull());
}
+ @Test
+ public void testGetCurrentLocation_Coarse_suppressedFixIsRetried() throws Exception {
+ Random random = new Random(0);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ long s2CellId = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(0.0, 0.0), TEST_COARSENING_LEVEL);
+ doThrow(new PopulationDensityUnavailableException("provider not bound"))
+ .doReturn(s2CellId)
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mManager.setPopulationDensityProvider(provider);
+
+ ILocationCallback listener = createMockGetCurrentLocationListener();
+ LocationRequest request = new LocationRequest.Builder(0).setWorkSource(WORK_SOURCE).build();
+ mManager.getCurrentLocation(request, IDENTITY, PERMISSION_COARSE, listener);
+
+ mProvider.setProviderLocation(createLocation(NAME, random));
+ verify(listener, never()).onLocation(nullable(Location.class));
+ assertThat(mInjector.getAppOpsHelper()
+ .getAppOpNoteCount(OP_COARSE_LOCATION, IDENTITY.getPackageName())).isEqualTo(0);
+
+ mProvider.setProviderLocation(createLocation(NAME, random));
+ verify(listener, times(1)).onLocation(any(Location.class));
+ verify(listener, never()).onLocation(isNull());
+ assertThat(mInjector.getAppOpsHelper()
+ .getAppOpNoteCount(OP_COARSE_LOCATION, IDENTITY.getPackageName())).isEqualTo(1);
+
+ mProvider.setProviderLocation(createLocation(NAME, random));
+ verify(listener, times(1)).onLocation(any(Location.class));
+ }
+
+ @Test
+ public void testGetCurrentLocation_Coarse_persistentFaultTimesOut() throws Exception {
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("provider not bound"))
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mManager.setPopulationDensityProvider(provider);
+
+ ILocationCallback listener = createMockGetCurrentLocationListener();
+ LocationRequest request = new LocationRequest.Builder(0).setWorkSource(WORK_SOURCE).build();
+ mManager.getCurrentLocation(request, IDENTITY, PERMISSION_COARSE, listener);
+
+ mProvider.setProviderLocation(createLocation(NAME, new Random(0)));
+ verify(listener, never()).onLocation(nullable(Location.class));
+ assertThat(mInjector.getAppOpsHelper()
+ .getAppOpNoteCount(OP_COARSE_LOCATION, IDENTITY.getPackageName())).isEqualTo(0);
+
+ mInjector.getAlarmHelper().incrementAlarmTime(TimeUnit.MINUTES.toMillis(1));
+ verify(listener, times(1)).onLocation(isNull());
+ assertThat(mInjector.getAppOpsHelper()
+ .getAppOpNoteCount(OP_COARSE_LOCATION, IDENTITY.getPackageName())).isEqualTo(0);
+ }
+
@Test
public void testFlush() throws Exception {
ILocationListener listener = createMockLocationListener();
@@ -1427,52 +1525,90 @@ public void testValidateLocation_futureLocation() {
}
@Test
- public void testLocationFudger_noDefaults_oldAlgoIsUsed()
- throws RemoteException {
+ public void testLocationFudger_providerCellPositionIgnored() throws Exception {
createManager("some-other-name");
ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
+ // Return a distant cell to verify that only its level is used.
+ long s2CellId = S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(40.758896, -73.985130), TEST_COARSENING_LEVEL);
+ double[] queriedCoordinates = new double[2];
+ doAnswer(invocation -> {
+ queriedCoordinates[0] = invocation.getArgument(0);
+ queriedCoordinates[1] = invocation.getArgument(1);
+ return s2CellId;
+ }).when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+
+ mManager.setPopulationDensityProvider(provider);
- mManager.setLocationFudgerCache(cache);
+ Location test = new Location("any-provider");
+ test.setLatitude(10.0);
+ test.setLongitude(20.0);
+ Location coarse = mManager.getPermittedLocation(test, PERMISSION_COARSE);
- ArgumentCaptor captor = ArgumentCaptor.forClass(IS2LevelCallback.class);
- verify(provider).getDefaultCoarseningLevel(captor.capture());
+ verify(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ double[] expectedCenter = new double[] {0.0, 0.0};
+ S2CellIdUtils.toLatLngDegrees(S2CellIdUtils.getParent(
+ S2CellIdUtils.fromLatLngDegrees(
+ queriedCoordinates[0], queriedCoordinates[1]), TEST_COARSENING_LEVEL),
+ expectedCenter);
+ double[] providerCellCenter = new double[] {0.0, 0.0};
+ S2CellIdUtils.toLatLngDegrees(s2CellId, providerCellCenter);
+ assertThat(coarse).isNotNull();
+ assertThat(coarse.getLatitude()).isEqualTo(expectedCenter[0]);
+ assertThat(coarse.getLongitude()).isEqualTo(expectedCenter[1]);
+ assertThat(coarse.getLatitude()).isNotEqualTo(providerCellCenter[0]);
+ assertThat(coarse.getLongitude()).isNotEqualTo(providerCellCenter[1]);
+ }
- IS2LevelCallback cb = captor.getValue();
+ @Test
+ public void testLocationFudger_providerFault_suppressesCoarseLocation() throws Exception {
+ createManager("some-other-name");
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
- // Act: the provider didn't provide a default
- cb.onError();
+ mManager.setPopulationDensityProvider(provider);
Location test = new Location("any-provider");
- mManager.getPermittedLocation(test, PERMISSION_COARSE);
+ test.setLatitude(10.0);
+ test.setLongitude(20.0);
- verify(provider, never()).getCoarsenedS2Cells(anyDouble(), anyDouble(), anyInt(), any());
+ assertThat(mManager.getPermittedLocation(test, PERMISSION_COARSE)).isNull();
}
@Test
- public void testLocationFudger_cacheIsSetAndNewAlgoIsUsed()
- throws RemoteException {
- createManager("some-other-name");
+ public void testLocationFudger_providerFault_suppressesCoarseDelivery() throws Exception {
ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
- LocationFudgerCache cache = new LocationFudgerCache(provider);
- int defaultLevel = 2;
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mManager.setPopulationDensityProvider(provider);
+
+ ILocationListener listener = createMockLocationListener();
+ mManager.registerLocationRequest(
+ new LocationRequest.Builder(0).setWorkSource(WORK_SOURCE).build(),
+ IDENTITY,
+ PERMISSION_COARSE,
+ listener);
- mManager.setLocationFudgerCache(cache);
+ mProvider.setProviderLocation(createLocation(NAME, new Random(0)));
- ArgumentCaptor captor = ArgumentCaptor.forClass(IS2LevelCallback.class);
- verify(provider).getDefaultCoarseningLevel(captor.capture());
+ verify(listener, never())
+ .onLocationChanged(nullable(List.class), nullable(IRemoteCallback.class));
+ }
- IS2LevelCallback cb = captor.getValue();
- cb.onResult(defaultLevel);
+ @Test
+ public void testLocationFudger_providerFault_suppressesCoarseLastLocation() throws Exception {
+ mProvider.setProviderLocation(createLocation(NAME, new Random(0)));
+ assertThat(mManager.getLastLocation(new LastLocationRequest.Builder().build(), IDENTITY,
+ PERMISSION_FINE)).isNotNull();
- Location test = new Location("any-provider");
- test.setLatitude(10.0);
- test.setLongitude(20.0);
- mManager.getPermittedLocation(test, PERMISSION_COARSE);
+ ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class);
+ doThrow(new PopulationDensityUnavailableException("test"))
+ .when(provider).getCoarsenedS2CellId(anyDouble(), anyDouble());
+ mManager.setPopulationDensityProvider(provider);
- // We can't test that 10.0, 20.0 was passed due to the offset. We only test that a call
- // happened.
- verify(provider).getCoarsenedS2Cells(anyDouble(), anyDouble(), anyInt(), any());
+ assertThat(mManager.getLastLocation(new LastLocationRequest.Builder().build(), IDENTITY,
+ PERMISSION_COARSE)).isNull();
}
@MediumTest
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/proxy/ProxyPopulationDensityProviderTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/proxy/ProxyPopulationDensityProviderTest.java
new file mode 100644
index 0000000000000..e8233773bf24b
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/proxy/ProxyPopulationDensityProviderTest.java
@@ -0,0 +1,376 @@
+package com.android.server.location.provider.proxy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.location.provider.IPopulationDensityProvider;
+import android.location.provider.IS2CellIdsCallback;
+import android.location.provider.IS2LevelCallback;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider.PopulationDensityUnavailableException;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ProxyPopulationDensityProviderTest {
+
+ private static final long BLOCKED_QUERY_TIMEOUT_SECONDS = 10;
+ private static final long COORDINATION_QUERY_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(30);
+ private static final long EXPLICIT_QUERY_TIMEOUT_MILLIS = 100;
+
+ private static final long ARBITRARY_CELL_ID = 0x1234_5678_9abc_def0L;
+ private static final long OTHER_CELL_ID = 0x0fed_cba9_8765_4321L;
+
+ @Test
+ public void getCoarsenedS2CellId_notBound_throws() {
+ ProxyPopulationDensityProvider proxy = new ProxyPopulationDensityProvider();
+
+ assertThrows(
+ PopulationDensityUnavailableException.class,
+ () -> proxy.getCoarsenedS2CellId(1.0, 2.0));
+ }
+
+ @Test
+ public void getCoarsenedS2CellId_bound_returnsFirstCellAndForwardsQuery() throws Exception {
+ double[] queriedCoordinates = new double[2];
+ int[] queriedAdditionalCells = new int[1];
+ IPopulationDensityProvider.Stub provider =
+ new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback)
+ throws RemoteException {
+ queriedCoordinates[0] = latitudeDegrees;
+ queriedCoordinates[1] = longitudeDegrees;
+ queriedAdditionalCells[0] = numAdditionalCells;
+ callback.onResult(new long[] {ARBITRARY_CELL_ID, OTHER_CELL_ID});
+ }
+ };
+ ProxyPopulationDensityProvider proxy = new ProxyPopulationDensityProvider();
+ proxy.onBind(provider.asBinder(), null);
+
+ long cell = proxy.getCoarsenedS2CellId(12.5, -34.25);
+
+ assertThat(cell).isEqualTo(ARBITRARY_CELL_ID);
+ assertThat(queriedCoordinates[0]).isEqualTo(12.5);
+ assertThat(queriedCoordinates[1]).isEqualTo(-34.25);
+ assertThat(queriedAdditionalCells[0]).isEqualTo(0);
+ }
+
+ @Test
+ public void getCoarsenedS2CellId_providerReportsError_throws() {
+ IPopulationDensityProvider.Stub provider =
+ new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback)
+ throws RemoteException {
+ callback.onError();
+ }
+ };
+ ProxyPopulationDensityProvider proxy = new ProxyPopulationDensityProvider();
+ proxy.onBind(provider.asBinder(), null);
+
+ PopulationDensityUnavailableException exception =
+ assertThrows(
+ PopulationDensityUnavailableException.class,
+ () -> proxy.getCoarsenedS2CellId(1.0, 2.0));
+
+ assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void getCoarsenedS2CellId_providerReturnsNoCells_throws() {
+ for (long[] returnedCells : new long[][] {null, new long[0]}) {
+ IPopulationDensityProvider.Stub provider =
+ new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback)
+ throws RemoteException {
+ callback.onResult(returnedCells);
+ }
+ };
+ ProxyPopulationDensityProvider proxy = new ProxyPopulationDensityProvider();
+ proxy.onBind(provider.asBinder(), null);
+
+ PopulationDensityUnavailableException exception =
+ assertThrows(
+ PopulationDensityUnavailableException.class,
+ () -> proxy.getCoarsenedS2CellId(1.0, 2.0));
+
+ assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class);
+ }
+ }
+
+ @Test
+ public void getCoarsenedS2CellId_runtimeException_wrapsAsUnavailable() {
+ RuntimeException providerFailure = new RuntimeException("provider failure");
+ IPopulationDensityProvider.Stub provider =
+ new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback) {
+ throw providerFailure;
+ }
+ };
+ ProxyPopulationDensityProvider proxy = new ProxyPopulationDensityProvider();
+ proxy.onBind(provider.asBinder(), null);
+
+ PopulationDensityUnavailableException exception =
+ assertThrows(
+ PopulationDensityUnavailableException.class,
+ () -> proxy.getCoarsenedS2CellId(1.0, 2.0));
+
+ assertThat(exception).hasCauseThat().isSameInstanceAs(providerFailure);
+ }
+
+ @Test
+ public void getCoarsenedS2CellId_remoteException_wrapsAsUnavailable() {
+ RemoteException providerFailure = new RemoteException("provider failure");
+ IPopulationDensityProvider.Stub provider =
+ new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback)
+ throws RemoteException {
+ throw providerFailure;
+ }
+ };
+ ProxyPopulationDensityProvider proxy = new ProxyPopulationDensityProvider();
+ proxy.onBind(provider.asBinder(), null);
+
+ PopulationDensityUnavailableException exception =
+ assertThrows(
+ PopulationDensityUnavailableException.class,
+ () -> proxy.getCoarsenedS2CellId(1.0, 2.0));
+
+ assertThat(exception).hasCauseThat().isSameInstanceAs(providerFailure);
+ }
+
+ @Test
+ public void getCoarsenedS2CellId_providerTimesOut_nextQueryRetries() throws Exception {
+ AtomicInteger providerCalls = new AtomicInteger();
+ IPopulationDensityProvider.Stub provider =
+ new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback)
+ throws RemoteException {
+ if (providerCalls.incrementAndGet() > 1) {
+ callback.onResult(new long[] {ARBITRARY_CELL_ID});
+ }
+ }
+ };
+ ProxyPopulationDensityProvider proxy =
+ new ProxyPopulationDensityProvider(EXPLICIT_QUERY_TIMEOUT_MILLIS);
+ proxy.onBind(provider.asBinder(), null);
+
+ assertThrows(
+ PopulationDensityUnavailableException.class,
+ () -> proxy.getCoarsenedS2CellId(1.0, 2.0));
+
+ assertThat(proxy.getCoarsenedS2CellId(1.0, 2.0)).isEqualTo(ARBITRARY_CELL_ID);
+ assertThat(providerCalls.get()).isEqualTo(2);
+ }
+
+ @Test
+ public void getCoarsenedS2CellId_sequentialSamePoint_doesNotRetainAnswer() throws Exception {
+ AtomicInteger providerCalls = new AtomicInteger();
+ IPopulationDensityProvider.Stub provider =
+ new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback)
+ throws RemoteException {
+ long cell =
+ providerCalls.incrementAndGet() == 1
+ ? ARBITRARY_CELL_ID
+ : OTHER_CELL_ID;
+ callback.onResult(new long[] {cell});
+ }
+ };
+ ProxyPopulationDensityProvider proxy = new ProxyPopulationDensityProvider();
+ proxy.onBind(provider.asBinder(), null);
+
+ assertThat(proxy.getCoarsenedS2CellId(12.5, -34.25)).isEqualTo(ARBITRARY_CELL_ID);
+ assertThat(proxy.getCoarsenedS2CellId(12.5, -34.25)).isEqualTo(OTHER_CELL_ID);
+ assertThat(providerCalls.get()).isEqualTo(2);
+ }
+
+ @Test
+ public void getCoarsenedS2CellId_queryCompletingAfterRebind_failsClosed() throws Exception {
+ CountDownLatch firstQueryStarted = new CountDownLatch(1);
+ AtomicReference firstCallback = new AtomicReference<>();
+ IPopulationDensityProvider.Stub firstProvider =
+ new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback) {
+ firstCallback.set(callback);
+ firstQueryStarted.countDown();
+ }
+ };
+ ProxyPopulationDensityProvider proxy =
+ new ProxyPopulationDensityProvider(COORDINATION_QUERY_TIMEOUT_MILLIS);
+ proxy.onBind(firstProvider.asBinder(), null);
+ FutureTask firstResult =
+ new FutureTask<>(() -> proxy.getCoarsenedS2CellId(12.5, -34.25));
+ Thread firstThread = startTask(firstResult, "first-binding");
+
+ try {
+ assertThat(firstQueryStarted.await(BLOCKED_QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS))
+ .isTrue();
+ proxy.onBind(constantProvider(OTHER_CELL_ID).asBinder(), null);
+ assertThat(proxy.getCoarsenedS2CellId(12.5, -34.25)).isEqualTo(OTHER_CELL_ID);
+
+ firstCallback.get().onResult(new long[] {ARBITRARY_CELL_ID});
+
+ ExecutionException exception =
+ assertThrows(
+ ExecutionException.class,
+ () -> firstResult.get(BLOCKED_QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ assertThat(exception)
+ .hasCauseThat()
+ .isInstanceOf(PopulationDensityUnavailableException.class);
+ } finally {
+ cancelAndJoinTask(firstResult, firstThread);
+ }
+ }
+
+ @Test
+ public void getCoarsenedS2CellId_interruptedQuery_canRetry() throws Exception {
+ AtomicInteger providerCalls = new AtomicInteger();
+ CountDownLatch firstQueryStarted = new CountDownLatch(1);
+ AtomicReference firstCallback = new AtomicReference<>();
+ IPopulationDensityProvider.Stub provider =
+ new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback)
+ throws RemoteException {
+ if (providerCalls.incrementAndGet() == 1) {
+ firstCallback.set(callback);
+ firstQueryStarted.countDown();
+ } else {
+ callback.onResult(new long[] {ARBITRARY_CELL_ID});
+ }
+ }
+ };
+ ProxyPopulationDensityProvider proxy =
+ new ProxyPopulationDensityProvider(COORDINATION_QUERY_TIMEOUT_MILLIS);
+ proxy.onBind(provider.asBinder(), null);
+ FutureTask interruptedResult =
+ new FutureTask<>(
+ () ->
+ assertThrows(
+ PopulationDensityUnavailableException.class,
+ () -> proxy.getCoarsenedS2CellId(1.0, 2.0)));
+ Thread queryThread = startTask(interruptedResult, "interrupted-query");
+
+ try {
+ assertThat(firstQueryStarted.await(BLOCKED_QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS))
+ .isTrue();
+ queryThread.interrupt();
+ PopulationDensityUnavailableException exception =
+ interruptedResult.get(BLOCKED_QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ assertThat(exception).hasMessageThat().isEqualTo("query interrupted");
+
+ firstCallback.get().onResult(new long[] {OTHER_CELL_ID});
+ assertThat(proxy.getCoarsenedS2CellId(1.0, 2.0)).isEqualTo(ARBITRARY_CELL_ID);
+ assertThat(providerCalls.get()).isEqualTo(2);
+ } finally {
+ cancelAndJoinTask(interruptedResult, queryThread);
+ }
+ }
+
+ @Test
+ public void getBindingGeneration_advancesOnBindAndUnbind() {
+ ProxyPopulationDensityProvider proxy = new ProxyPopulationDensityProvider();
+ long initialGeneration = proxy.getBindingGeneration();
+
+ proxy.onBind(constantProvider(ARBITRARY_CELL_ID).asBinder(), null);
+ long boundGeneration = proxy.getBindingGeneration();
+ proxy.onUnbind();
+
+ assertThat(boundGeneration).isGreaterThan(initialGeneration);
+ assertThat(proxy.getBindingGeneration()).isGreaterThan(boundGeneration);
+ }
+
+ private abstract static class CellProvider extends IPopulationDensityProvider.Stub {
+ @Override
+ public void getDefaultCoarseningLevel(IS2LevelCallback callback) throws RemoteException {
+ callback.onError();
+ }
+ }
+
+ private static IPopulationDensityProvider.Stub constantProvider(long cellId) {
+ return new CellProvider() {
+ @Override
+ public void getCoarsenedS2Cells(
+ double latitudeDegrees,
+ double longitudeDegrees,
+ int numAdditionalCells,
+ IS2CellIdsCallback callback)
+ throws RemoteException {
+ callback.onResult(new long[] {cellId});
+ }
+ };
+ }
+
+ private static Thread startTask(FutureTask> task, String threadName) {
+ Thread thread = new Thread(task, threadName);
+ thread.start();
+ return thread;
+ }
+
+ private static void cancelAndJoinTask(FutureTask> task, Thread thread)
+ throws InterruptedException {
+ task.cancel(true);
+ thread.join(TimeUnit.SECONDS.toMillis(BLOCKED_QUERY_TIMEOUT_SECONDS));
+ assertThat(thread.isAlive()).isFalse();
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/servicewatcher/CurrentUserServiceSupplierTest.java b/services/tests/mockingservicestests/src/com/android/server/servicewatcher/CurrentUserServiceSupplierTest.java
new file mode 100644
index 0000000000000..41cf62cdb1162
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/servicewatcher/CurrentUserServiceSupplierTest.java
@@ -0,0 +1,157 @@
+package com.android.server.servicewatcher;
+
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.os.Process;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CurrentUserServiceSupplierTest {
+
+ private static final String ACTION = "test.action.POPULATION_DENSITY";
+ private static final String PACKAGE_NAME = "test.provider";
+ private static final String CALLER_PERMISSION = "test.permission.BIND_PROVIDER";
+ private static final String WRONG_CALLER_PERMISSION = "test.permission.WRONG_BIND_PROVIDER";
+ private static final String SERVICE_PERMISSION = "test.permission.PROVIDE_DENSITY";
+ private static final int ENABLE_OVERLAY_RES_ID = 1;
+ private static final int PACKAGE_NAME_RES_ID = 2;
+ private static final int USER_ID = 10;
+ private static final int PROVIDER_UID = 100_000;
+
+ private ActivityManagerInternal mOriginalActivityManager;
+ private ActivityManagerInternal mActivityManager;
+ private Context mContext;
+ private PackageManager mPackageManager;
+
+ @Before
+ public void setUp() {
+ mOriginalActivityManager = LocalServices.getService(ActivityManagerInternal.class);
+ if (mOriginalActivityManager != null) {
+ LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+ }
+ mActivityManager = mock(ActivityManagerInternal.class);
+ LocalServices.addService(ActivityManagerInternal.class, mActivityManager);
+
+ mContext = mock(Context.class);
+ mPackageManager = mock(PackageManager.class);
+ Resources resources = mock(Resources.class);
+ when(mContext.getResources()).thenReturn(resources);
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ when(resources.getBoolean(ENABLE_OVERLAY_RES_ID)).thenReturn(true);
+ when(mActivityManager.getCurrentUserId()).thenReturn(USER_ID);
+ }
+
+ @After
+ public void tearDown() {
+ LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+ if (mOriginalActivityManager != null) {
+ LocalServices.addService(ActivityManagerInternal.class, mOriginalActivityManager);
+ }
+ }
+
+ @Test
+ public void createFromConfig_forwardsPermissionRequirements() {
+ ResolveInfo resolveInfo = createResolveInfo(CALLER_PERMISSION);
+ when(mPackageManager.queryIntentServicesAsUser(any(Intent.class), anyInt(), eq(USER_ID)))
+ .thenReturn(List.of(resolveInfo));
+ when(mContext.checkPermission(SERVICE_PERMISSION, Process.INVALID_PID, PROVIDER_UID))
+ .thenReturn(PERMISSION_GRANTED);
+
+ CurrentUserServiceSupplier supplier =
+ CurrentUserServiceSupplier.createFromConfig(
+ mContext,
+ ACTION,
+ ENABLE_OVERLAY_RES_ID,
+ PACKAGE_NAME_RES_ID,
+ CALLER_PERMISSION,
+ SERVICE_PERMISSION);
+
+ assertThat(supplier.getServiceInfo()).isNotNull();
+ verify(mContext).checkPermission(SERVICE_PERMISSION, Process.INVALID_PID, PROVIDER_UID);
+ }
+
+ @Test
+ public void createFromConfig_rejectsWrongCallerPermission() {
+ ResolveInfo resolveInfo = createResolveInfo(WRONG_CALLER_PERMISSION);
+ when(mPackageManager.queryIntentServicesAsUser(any(Intent.class), anyInt(), eq(USER_ID)))
+ .thenReturn(List.of(resolveInfo));
+ when(mContext.checkPermission(SERVICE_PERMISSION, Process.INVALID_PID, PROVIDER_UID))
+ .thenReturn(PERMISSION_GRANTED);
+
+ CurrentUserServiceSupplier supplier =
+ CurrentUserServiceSupplier.createFromConfig(
+ mContext,
+ ACTION,
+ ENABLE_OVERLAY_RES_ID,
+ PACKAGE_NAME_RES_ID,
+ CALLER_PERMISSION,
+ SERVICE_PERMISSION);
+
+ assertThat(supplier.getServiceInfo()).isNull();
+ }
+
+ @Test
+ public void createFromConfig_rejectsDeniedServicePermission() {
+ ResolveInfo resolveInfo = createResolveInfo(CALLER_PERMISSION);
+ when(mPackageManager.queryIntentServicesAsUser(any(Intent.class), anyInt(), eq(USER_ID)))
+ .thenReturn(List.of(resolveInfo));
+ when(mContext.checkPermission(SERVICE_PERMISSION, Process.INVALID_PID, PROVIDER_UID))
+ .thenReturn(PERMISSION_DENIED);
+
+ CurrentUserServiceSupplier supplier =
+ CurrentUserServiceSupplier.createFromConfig(
+ mContext,
+ ACTION,
+ ENABLE_OVERLAY_RES_ID,
+ PACKAGE_NAME_RES_ID,
+ CALLER_PERMISSION,
+ SERVICE_PERMISSION);
+
+ assertThat(supplier.getServiceInfo()).isNull();
+ verify(mContext).checkPermission(SERVICE_PERMISSION, Process.INVALID_PID, PROVIDER_UID);
+ }
+
+ private static ResolveInfo createResolveInfo(String callerPermission) {
+ ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.packageName = PACKAGE_NAME;
+ serviceInfo.name = PACKAGE_NAME + ".PopulationDensityProvider";
+ serviceInfo.permission = callerPermission;
+ serviceInfo.applicationInfo = new ApplicationInfo();
+ serviceInfo.applicationInfo.uid = PROVIDER_UID;
+ ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.serviceInfo = serviceInfo;
+ return resolveInfo;
+ }
+}