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; + } +}