From 6e410506126f5fbb5425a847ea760d9b97661924 Mon Sep 17 00:00:00 2001 From: Soonil Nagarkar Date: Wed, 16 Sep 2020 17:51:54 -0700 Subject: [PATCH] Fix background request throttling + jitter calculation Prevent applications from bypassing background location throttling by adding/removing requests. This allows provider managers to delay requests in the event the interval has decreased so that power can be saved. Clients that target S and above may now receive historical locations in exchange, so that they are not losing locations overall. Locationp providers can optionally implement delays themselves in the event of an interval increase. Also updates jitter calculation to be a capped percentage of the request interval, rather than a flat cap. Bug: 73144566 Test: manual + presubmits Change-Id: I243e813f0c0c504850e2a3e777787f49fc6f7a57 --- location/java/android/location/Location.java | 12 + .../android/location/LocationManager.java | 29 +- .../android/location/LocationRequest.java | 13 + .../fused/tests/FusedLocationServiceTest.java | 2 - .../location/LocationManagerService.java | 50 +-- .../location/LocationProviderManager.java | 397 +++++++++++++----- .../PassiveLocationProviderManager.java | 23 +- .../listeners/ListenerMultiplexer.java | 26 +- .../server/location/util/AlarmHelper.java | 47 +++ .../server/location/util/Injector.java | 3 + .../location/util/SystemAlarmHelper.java | 57 +++ .../mockingservicestests/AndroidManifest.xml | 2 +- .../location/LocationProviderManagerTest.java | 66 +-- .../server/location/util/FakeAlarmHelper.java | 61 +++ .../server/location/util/TestInjector.java | 7 + 15 files changed, 621 insertions(+), 174 deletions(-) create mode 100644 services/core/java/com/android/server/location/util/AlarmHelper.java create mode 100644 services/core/java/com/android/server/location/util/SystemAlarmHelper.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/location/util/FakeAlarmHelper.java diff --git a/location/java/android/location/Location.java b/location/java/android/location/Location.java index 9aa0c870e5126..46bd22148fb2e 100644 --- a/location/java/android/location/Location.java +++ b/location/java/android/location/Location.java @@ -16,6 +16,8 @@ package android.location; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; @@ -585,6 +587,11 @@ public class Location implements Parcelable { return mElapsedRealtimeNanos; } + /** @hide */ + public long getElapsedRealtimeMillis() { + return NANOSECONDS.toMillis(getElapsedRealtimeNanos()); + } + /** @hide */ public long getElapsedRealtimeAgeNanos(long referenceRealtimeNs) { return referenceRealtimeNs - mElapsedRealtimeNanos; @@ -595,6 +602,11 @@ public class Location implements Parcelable { return getElapsedRealtimeAgeNanos(SystemClock.elapsedRealtimeNanos()); } + /** @hide */ + public long getElapsedRealtimeAgeMillis() { + return NANOSECONDS.toMillis(getElapsedRealtimeAgeNanos()); + } + /** * Set the time of this fix, in elapsed real-time since system boot. * diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java index 6e597b2c1d631..ff004094ec59d 100644 --- a/location/java/android/location/LocationManager.java +++ b/location/java/android/location/LocationManager.java @@ -88,6 +88,16 @@ import java.util.function.Consumer; @RequiresFeature(PackageManager.FEATURE_LOCATION) public class LocationManager { + /** + * For apps targeting Android S and above, location clients may receive historical locations + * (from before the present time) under some circumstances. + * + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R) + public static final long DELIVER_HISTORICAL_LOCATIONS = 73144566L; + /** * For apps targeting Android R and above, {@link #getProvider(String)} will no longer throw any * security exceptions. @@ -1256,13 +1266,15 @@ public class LocationManager { * arguments. The same listener may be used across multiple providers with different requests * for each provider. * - *

It may take a while to receive the first location update. If an immediate location is - * required, applications may use the {@link #getLastKnownLocation(String)} method. + *

It may take some time to receive the first location update depending on the conditions the + * device finds itself in. In order to take advantage of cached locations, application may + * consider using {@link #getLastKnownLocation(String)} or {@link #getCurrentLocation(String, + * LocationRequest, CancellationSignal, Executor, Consumer)} instead. * *

See {@link LocationRequest} documentation for an explanation of various request parameters * and how they can affect the received locations. * - *

If your application wants to passively observe location updates from any provider, then + *

If your application wants to passively observe location updates from all providers, then * use the {@link #PASSIVE_PROVIDER}. This provider does not turn on or modify active location * providers, so you do not need to be as careful about minimum time and minimum distance * parameters. However, if your application performs heavy work on a location update (such as @@ -1271,13 +1283,20 @@ public class LocationManager { * *

In case the provider you have selected is disabled, location updates will cease, and a * provider availability update will be sent. As soon as the provider is enabled again, another - * provider availability update will be sent and location updates will immediately resume. + * provider availability update will be sent and location updates will resume. * - *

When location callbacks are invoked, the system will hold a wakelock on your + *

When location callbacks are invoked, the system will hold a wakelock on your * application's behalf for some period of time, but not indefinitely. If your application * requires a long running wakelock within the location callback, you should acquire it * yourself. * + *

Spamming location requests is a drain on system resources, and the system has preventative + * measures in place to ensure that this behavior will never result in more locations than could + * be achieved with a single location request with an equivalent interval that is left in place + * the whole time. As part of this amelioration, applications that target Android S and above + * may receive cached or historical locations through their listener. These locations will never + * be older than the interval of the location request. + * *

To unregister for location updates, use {@link #removeUpdates(LocationListener)}. * * @param provider a provider listed by {@link #getAllProviders()} diff --git a/location/java/android/location/LocationRequest.java b/location/java/android/location/LocationRequest.java index 0521b10a2530a..c57794f0f04a0 100644 --- a/location/java/android/location/LocationRequest.java +++ b/location/java/android/location/LocationRequest.java @@ -27,6 +27,8 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledAfter; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; import android.os.Parcel; @@ -44,6 +46,17 @@ import java.util.Objects; */ public final class LocationRequest implements Parcelable { + /** + * For apps targeting Android S and above, all LocationRequest objects marked as low power will + * throw exceptions if the caller does not have the LOCATION_HARDWARE permission, instead of + * silently dropping the low power part of the request. + * + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R) + public static final long LOW_POWER_EXCEPTIONS = 168936375L; + /** * Represents a passive only request. Such a request will not trigger any active locations or * power usage itself, but may receive locations generated in response to other requests. diff --git a/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java b/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java index e05bd3c22ae86..d3aa977f85b1a 100644 --- a/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java +++ b/packages/FusedLocation/test/src/com/android/location/fused/tests/FusedLocationServiceTest.java @@ -21,8 +21,6 @@ import static android.location.LocationManager.NETWORK_PROVIDER; import static androidx.test.ext.truth.location.LocationSubject.assertThat; -import static com.google.common.truth.Truth.assertThat; - import android.content.Context; import android.location.Criteria; import android.location.Location; diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index 72734c47873eb..0329c3c78e20c 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -23,10 +23,10 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.location.LocationManager.FUSED_PROVIDER; import static android.location.LocationManager.GPS_PROVIDER; import static android.location.LocationManager.NETWORK_PROVIDER; +import static android.location.LocationRequest.LOW_POWER_EXCEPTIONS; import static com.android.server.location.LocationPermissions.PERMISSION_COARSE; import static com.android.server.location.LocationPermissions.PERMISSION_FINE; -import static com.android.server.location.LocationProviderManager.FASTEST_COARSE_INTERVAL_MS; import static java.util.concurrent.TimeUnit.NANOSECONDS; @@ -36,6 +36,7 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.PendingIntent; +import android.app.compat.CompatChanges; import android.content.Context; import android.content.Intent; import android.location.Criteria; @@ -82,12 +83,12 @@ import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; import com.android.server.LocalServices; import com.android.server.SystemService; -import com.android.server.location.LocationPermissions.PermissionLevel; import com.android.server.location.LocationRequestStatistics.PackageProviderKey; import com.android.server.location.LocationRequestStatistics.PackageStatistics; import com.android.server.location.geofence.GeofenceManager; import com.android.server.location.geofence.GeofenceProxy; import com.android.server.location.gnss.GnssManagerService; +import com.android.server.location.util.AlarmHelper; import com.android.server.location.util.AppForegroundHelper; import com.android.server.location.util.AppOpsHelper; import com.android.server.location.util.Injector; @@ -97,6 +98,7 @@ import com.android.server.location.util.LocationPowerSaveModeHelper; import com.android.server.location.util.LocationUsageLogger; import com.android.server.location.util.ScreenInteractiveHelper; import com.android.server.location.util.SettingsHelper; +import com.android.server.location.util.SystemAlarmHelper; import com.android.server.location.util.SystemAppForegroundHelper; import com.android.server.location.util.SystemAppOpsHelper; import com.android.server.location.util.SystemLocationPermissionsHelper; @@ -569,7 +571,7 @@ public class LocationManagerService extends ILocationManager.Stub { new IllegalArgumentException()); } - request = validateAndSanitizeLocationRequest(request, permissionLevel); + request = validateLocationRequest(request); LocationProviderManager manager = getLocationProviderManager(provider); Preconditions.checkArgument(manager != null, @@ -591,7 +593,7 @@ public class LocationManagerService extends ILocationManager.Stub { // clients in the system process must have an attribution tag set Preconditions.checkArgument(identity.getPid() != Process.myPid() || attributionTag != null); - request = validateAndSanitizeLocationRequest(request, permissionLevel); + request = validateLocationRequest(request); LocationProviderManager manager = getLocationProviderManager(provider); Preconditions.checkArgument(manager != null, @@ -600,8 +602,7 @@ public class LocationManagerService extends ILocationManager.Stub { manager.registerLocationRequest(request, identity, permissionLevel, pendingIntent); } - private LocationRequest validateAndSanitizeLocationRequest(LocationRequest request, - @PermissionLevel int permissionLevel) { + private LocationRequest validateLocationRequest(LocationRequest request) { WorkSource workSource = request.getWorkSource(); if (workSource != null && !workSource.isEmpty()) { mContext.enforceCallingOrSelfPermission( @@ -620,26 +621,20 @@ public class LocationManagerService extends ILocationManager.Stub { } LocationRequest.Builder sanitized = new LocationRequest.Builder(request); - if (mContext.checkCallingPermission(permission.LOCATION_HARDWARE) != PERMISSION_GRANTED) { - sanitized.setLowPower(false); - } - if (permissionLevel < PERMISSION_FINE) { - switch (request.getQuality()) { - case LocationRequest.ACCURACY_FINE: - sanitized.setQuality(LocationRequest.ACCURACY_BLOCK); - break; - case LocationRequest.POWER_HIGH: - sanitized.setQuality(LocationRequest.POWER_LOW); - break; - } - if (request.getIntervalMillis() < FASTEST_COARSE_INTERVAL_MS) { - sanitized.setIntervalMillis(FASTEST_COARSE_INTERVAL_MS); + if (CompatChanges.isChangeEnabled(LOW_POWER_EXCEPTIONS, Binder.getCallingUid())) { + if (request.isLowPower()) { + mContext.enforceCallingOrSelfPermission( + permission.LOCATION_HARDWARE, + "low power request requires " + permission.LOCATION_HARDWARE); } - if (request.getMinUpdateIntervalMillis() < FASTEST_COARSE_INTERVAL_MS) { - sanitized.clearMinUpdateIntervalMillis(); + } else { + if (mContext.checkCallingPermission(permission.LOCATION_HARDWARE) + != PERMISSION_GRANTED) { + sanitized.setLowPower(false); } } + if (request.getWorkSource() != null) { if (request.getWorkSource().isEmpty()) { sanitized.setWorkSource(null); @@ -716,7 +711,7 @@ public class LocationManagerService extends ILocationManager.Stub { // clients in the system process must have an attribution tag set Preconditions.checkState(identity.getPid() != Process.myPid() || attributionTag != null); - request = validateAndSanitizeLocationRequest(request, permissionLevel); + request = validateLocationRequest(request); LocationProviderManager manager = getLocationProviderManager(provider); Preconditions.checkArgument(manager != null, @@ -735,7 +730,7 @@ public class LocationManagerService extends ILocationManager.Stub { // use fine permission level to avoid creating unnecessary coarse locations Location location = gpsManager.getLastLocationUnsafe(UserHandle.USER_ALL, - PERMISSION_FINE, false); + PERMISSION_FINE, false, Long.MAX_VALUE); if (location == null) { return null; } @@ -1237,6 +1232,7 @@ public class LocationManagerService extends ILocationManager.Stub { private static class SystemInjector implements Injector { private final UserInfoHelper mUserInfoHelper; + private final AlarmHelper mAlarmHelper; private final SystemAppOpsHelper mAppOpsHelper; private final SystemLocationPermissionsHelper mLocationPermissionsHelper; private final SystemSettingsHelper mSettingsHelper; @@ -1249,6 +1245,7 @@ public class LocationManagerService extends ILocationManager.Stub { SystemInjector(Context context, UserInfoHelper userInfoHelper) { mUserInfoHelper = userInfoHelper; + mAlarmHelper = new SystemAlarmHelper(context); mAppOpsHelper = new SystemAppOpsHelper(context); mLocationPermissionsHelper = new SystemLocationPermissionsHelper(context, mAppOpsHelper); @@ -1275,6 +1272,11 @@ public class LocationManagerService extends ILocationManager.Stub { return mUserInfoHelper; } + @Override + public AlarmHelper getAlarmHelper() { + return mAlarmHelper; + } + @Override public AppOpsHelper getAppOpsHelper() { return mAppOpsHelper; diff --git a/services/core/java/com/android/server/location/LocationProviderManager.java b/services/core/java/com/android/server/location/LocationProviderManager.java index 138301ae934d5..cd8bf4a0154d3 100644 --- a/services/core/java/com/android/server/location/LocationProviderManager.java +++ b/services/core/java/com/android/server/location/LocationProviderManager.java @@ -16,13 +16,14 @@ package com.android.server.location; -import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; -import static android.app.AlarmManager.WINDOW_EXACT; +import static android.app.compat.CompatChanges.isChangeEnabled; +import static android.location.LocationManager.DELIVER_HISTORICAL_LOCATIONS; import static android.location.LocationManager.FUSED_PROVIDER; import static android.location.LocationManager.GPS_PROVIDER; import static android.location.LocationManager.KEY_LOCATION_CHANGED; import static android.location.LocationManager.KEY_PROVIDER_ENABLED; import static android.location.LocationManager.PASSIVE_PROVIDER; +import static android.location.LocationRequest.PASSIVE_INTERVAL; import static android.os.IPowerManager.LOCATION_MODE_NO_CHANGE; import static android.os.PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF; import static android.os.PowerManager.LOCATION_MODE_FOREGROUND_ONLY; @@ -36,11 +37,11 @@ import static com.android.server.location.LocationPermissions.PERMISSION_COARSE; import static com.android.server.location.LocationPermissions.PERMISSION_FINE; import static com.android.server.location.LocationPermissions.PERMISSION_NONE; +import static java.lang.Math.max; import static java.lang.Math.min; -import static java.util.concurrent.TimeUnit.NANOSECONDS; import android.annotation.Nullable; -import android.app.AlarmManager; +import android.app.AlarmManager.OnAlarmListener; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -88,6 +89,7 @@ import com.android.server.PendingIntentUtils; import com.android.server.location.LocationPermissions.PermissionLevel; import com.android.server.location.listeners.ListenerMultiplexer; import com.android.server.location.listeners.RemoteListenerRegistration; +import com.android.server.location.util.AlarmHelper; import com.android.server.location.util.AppForegroundHelper; import com.android.server.location.util.AppForegroundHelper.AppForegroundListener; import com.android.server.location.util.AppOpsHelper; @@ -118,12 +120,12 @@ class LocationProviderManager extends LocationProviderManager.Registration, ProviderRequest> implements AbstractLocationProvider.Listener { - // fastest interval at which clients may receive coarse locations - public static final long FASTEST_COARSE_INTERVAL_MS = 10 * 60 * 1000; - private static final String WAKELOCK_TAG = "*location*"; private static final long WAKELOCK_TIMEOUT_MS = 30 * 1000; + // fastest interval at which clients may receive coarse locations + private static final long MIN_COARSE_INTERVAL_MS = 10 * 60 * 1000; + // max interval to be considered "high power" request private static final long MAX_HIGH_POWER_INTERVAL_MS = 5 * 60 * 1000; @@ -133,8 +135,15 @@ class LocationProviderManager extends // max timeout allowed for getting the current location private static final long GET_CURRENT_LOCATION_MAX_TIMEOUT_MS = 30 * 1000; - // max jitter allowed for fastest interval evaluation - private static final int MAX_FASTEST_INTERVAL_JITTER_MS = 100; + // max jitter allowed for min update interval as a percentage of the interval + private static final float FASTEST_INTERVAL_JITTER_PERCENTAGE = .10f; + + // max absolute jitter allowed for min update interval evaluation + private static final int MAX_FASTEST_INTERVAL_JITTER_MS = 5 * 1000; + + // minimum amount of request delay in order to respect the delay, below this value the request + // will just be scheduled immediately + private static final long MIN_REQUEST_DELAY_MS = 30 * 1000; protected interface LocationTransport { @@ -221,6 +230,7 @@ class LocationProviderManager extends /** * Must be implemented to return the location this operation intends to deliver. */ + @Nullable Location getLocation(); } @@ -312,7 +322,7 @@ class LocationProviderManager extends mLocationAttributionHelper.reportLocationStart(getIdentity(), getName(), getKey()); } onHighPowerUsageChanged(); - return null; + return onProviderListenerActive(); } @Override @@ -325,6 +335,22 @@ class LocationProviderManager extends if (!getRequest().isHiddenFromAppOps()) { mLocationAttributionHelper.reportLocationStop(getIdentity(), getName(), getKey()); } + return onProviderListenerInactive(); + } + + /** + * Subclasses may override this instead of {@link #onActive()}. + */ + @GuardedBy("mLock") + protected LocationListenerOperation onProviderListenerActive() { + return null; + } + + /** + * Subclasses may override this instead of {@link #onInactive()} ()}. + */ + @GuardedBy("mLock") + protected LocationListenerOperation onProviderListenerInactive() { return null; } @@ -333,6 +359,14 @@ class LocationProviderManager extends return mProviderLocationRequest; } + @GuardedBy("mLock") + final void initializeLastLocation(@Nullable Location location) { + if (mLastLocation == null) { + mLastLocation = location; + } + } + + @GuardedBy("mLock") public final Location getLastDeliveredLocation() { return mLastLocation; } @@ -465,9 +499,27 @@ class LocationProviderManager extends } private LocationRequest calculateProviderLocationRequest() { - LocationRequest.Builder builder = new LocationRequest.Builder(super.getRequest()); + LocationRequest baseRequest = super.getRequest(); + LocationRequest.Builder builder = new LocationRequest.Builder(baseRequest); - if (super.getRequest().isLocationSettingsIgnored()) { + if (mPermissionLevel < PERMISSION_FINE) { + switch (baseRequest.getQuality()) { + case LocationRequest.ACCURACY_FINE: + builder.setQuality(LocationRequest.ACCURACY_BLOCK); + break; + case LocationRequest.POWER_HIGH: + builder.setQuality(LocationRequest.POWER_LOW); + break; + } + if (baseRequest.getIntervalMillis() < MIN_COARSE_INTERVAL_MS) { + builder.setIntervalMillis(MIN_COARSE_INTERVAL_MS); + } + if (baseRequest.getMinUpdateIntervalMillis() < MIN_COARSE_INTERVAL_MS) { + builder.clearMinUpdateIntervalMillis(); + } + } + + if (baseRequest.isLocationSettingsIgnored()) { // if we are not currently allowed use location settings ignored, disable it if (!mSettingsHelper.getIgnoreSettingsPackageWhitelist().contains( getIdentity().getPackageName()) && !mLocationManagerInternal.isProvider( @@ -476,10 +528,10 @@ class LocationProviderManager extends } } - if (!super.getRequest().isLocationSettingsIgnored() && !isThrottlingExempt()) { + if (!baseRequest.isLocationSettingsIgnored() && !isThrottlingExempt()) { // throttle in the background if (!mForeground) { - builder.setIntervalMillis(Math.max(super.getRequest().getIntervalMillis(), + builder.setIntervalMillis(max(baseRequest.getIntervalMillis(), mSettingsHelper.getBackgroundThrottleIntervalMs())); } } @@ -534,7 +586,7 @@ class LocationProviderManager extends } protected abstract class LocationRegistration extends Registration implements - AlarmManager.OnAlarmListener, ProviderEnabledListener { + OnAlarmListener, ProviderEnabledListener { private final PowerManager.WakeLock mWakeLock; @@ -561,17 +613,15 @@ class LocationProviderManager extends @GuardedBy("mLock") @Override protected final void onProviderListenerRegister() { - mExpirationRealtimeMs = getRequest().getExpirationRealtimeMs( - SystemClock.elapsedRealtime()); + long registerTimeMs = SystemClock.elapsedRealtime(); + mExpirationRealtimeMs = getRequest().getExpirationRealtimeMs(registerTimeMs); // add alarm for expiration - if (mExpirationRealtimeMs < SystemClock.elapsedRealtime()) { - remove(); + if (mExpirationRealtimeMs <= registerTimeMs) { + onAlarm(); } else if (mExpirationRealtimeMs < Long.MAX_VALUE) { - AlarmManager alarmManager = Objects.requireNonNull( - mContext.getSystemService(AlarmManager.class)); - alarmManager.set(ELAPSED_REALTIME_WAKEUP, mExpirationRealtimeMs, WINDOW_EXACT, - 0, this, FgThread.getHandler(), getWorkSource()); + mAlarmHelper.setDelayedAlarm(mExpirationRealtimeMs - registerTimeMs, this, + getWorkSource()); } // start listening for provider enabled/disabled events @@ -594,9 +644,7 @@ class LocationProviderManager extends // remove alarm for expiration if (mExpirationRealtimeMs < Long.MAX_VALUE) { - AlarmManager alarmManager = Objects.requireNonNull( - mContext.getSystemService(AlarmManager.class)); - alarmManager.cancel(this); + mAlarmHelper.cancel(this); } onLocationListenerUnregister(); @@ -614,6 +662,39 @@ class LocationProviderManager extends @GuardedBy("mLock") protected void onLocationListenerUnregister() {} + @GuardedBy("mLock") + @Override + protected final LocationListenerOperation onProviderListenerActive() { + // a new registration may not get a location immediately, the provider request may be + // delayed. therefore we deliver a historical location if available. since delivering an + // older location could be considered a breaking change for some applications, we only + // do so for apps targeting S+. + if (isChangeEnabled(DELIVER_HISTORICAL_LOCATIONS, getIdentity().getUid())) { + long maxLocationAgeMs = getRequest().getIntervalMillis(); + Location lastDeliveredLocation = getLastDeliveredLocation(); + if (lastDeliveredLocation != null) { + // ensure that location is fresher than the last delivered location + maxLocationAgeMs = min(maxLocationAgeMs, + lastDeliveredLocation.getElapsedRealtimeAgeMillis() - 1); + } + + // requests are never delayed less than MIN_REQUEST_DELAY_MS, so it only makes sense + // to deliver historical locations to clients with a last location older than that + if (maxLocationAgeMs > MIN_REQUEST_DELAY_MS) { + Location lastLocation = getLastLocationUnsafe( + getIdentity().getUserId(), + PERMISSION_FINE, // acceptLocationChange() handles coarsening this + getRequest().isLocationSettingsIgnored(), + maxLocationAgeMs); + if (lastLocation != null) { + return acceptLocationChange(lastLocation); + } + } + } + + return null; + } + @Override public void onAlarm() { if (D) { @@ -624,6 +705,8 @@ class LocationProviderManager extends synchronized (mLock) { remove(); + // no need to remove alarm after it's fired + mExpirationRealtimeMs = Long.MAX_VALUE; } } @@ -658,11 +741,11 @@ class LocationProviderManager extends Location lastDeliveredLocation = getLastDeliveredLocation(); if (lastDeliveredLocation != null) { // check fastest interval - long deltaMs = NANOSECONDS.toMillis( - location.getElapsedRealtimeNanos() - - lastDeliveredLocation.getElapsedRealtimeNanos()); - if (deltaMs < getRequest().getMinUpdateIntervalMillis() - - MAX_FASTEST_INTERVAL_JITTER_MS) { + long deltaMs = location.getElapsedRealtimeMillis() + - lastDeliveredLocation.getElapsedRealtimeMillis(); + long maxJitterMs = min((long) (FASTEST_INTERVAL_JITTER_PERCENTAGE + * getRequest().getIntervalMillis()), MAX_FASTEST_INTERVAL_JITTER_MS); + if (deltaMs < getRequest().getMinUpdateIntervalMillis() - maxJitterMs) { return null; } @@ -871,7 +954,7 @@ class LocationProviderManager extends } protected final class GetCurrentLocationListenerRegistration extends Registration implements - IBinder.DeathRecipient, ProviderEnabledListener, AlarmManager.OnAlarmListener { + IBinder.DeathRecipient, ProviderEnabledListener, OnAlarmListener { private volatile LocationTransport mTransport; @@ -902,15 +985,15 @@ class LocationProviderManager extends remove(); } - mExpirationRealtimeMs = getRequest().getExpirationRealtimeMs( - SystemClock.elapsedRealtime()); + long registerTimeMs = SystemClock.elapsedRealtime(); + mExpirationRealtimeMs = getRequest().getExpirationRealtimeMs(registerTimeMs); // add alarm for expiration - if (mExpirationRealtimeMs < Long.MAX_VALUE) { - AlarmManager alarmManager = Objects.requireNonNull( - mContext.getSystemService(AlarmManager.class)); - alarmManager.set(ELAPSED_REALTIME_WAKEUP, mExpirationRealtimeMs, WINDOW_EXACT, - 0, this, FgThread.getHandler(), getWorkSource()); + if (mExpirationRealtimeMs <= registerTimeMs) { + onAlarm(); + } else if (mExpirationRealtimeMs < Long.MAX_VALUE) { + mAlarmHelper.setDelayedAlarm(mExpirationRealtimeMs - registerTimeMs, this, + getWorkSource()); } // if this request is ignoring location settings, then we don't want to immediately fail @@ -935,9 +1018,7 @@ class LocationProviderManager extends // remove alarm for expiration if (mExpirationRealtimeMs < Long.MAX_VALUE) { - AlarmManager alarmManager = Objects.requireNonNull( - mContext.getSystemService(AlarmManager.class)); - alarmManager.cancel(this); + mAlarmHelper.cancel(this); } ((IBinder) getKey()).unlinkToDeath(this, 0); @@ -953,6 +1034,8 @@ class LocationProviderManager extends synchronized (mLock) { deliverLocation(null); + // no need to remove alarm after it's fired + mExpirationRealtimeMs = Long.MAX_VALUE; } } @@ -964,6 +1047,12 @@ class LocationProviderManager extends Preconditions.checkState(Thread.holdsLock(mLock)); } + // check expiration time - alarm is not guaranteed to go off at the right time, + // especially for short intervals + if (SystemClock.elapsedRealtime() >= mExpirationRealtimeMs) { + fineLocation = null; + } + // lastly - note app ops Location location; if (fineLocation == null) { @@ -1077,6 +1166,7 @@ class LocationProviderManager extends protected final LocationManagerInternal mLocationManagerInternal; protected final SettingsHelper mSettingsHelper; protected final UserInfoHelper mUserInfoHelper; + protected final AlarmHelper mAlarmHelper; protected final AppOpsHelper mAppOpsHelper; protected final LocationPermissionsHelper mLocationPermissionsHelper; protected final AppForegroundHelper mAppForegroundHelper; @@ -1120,6 +1210,9 @@ class LocationProviderManager extends // acquiring mLock makes operations on mProvider atomic, but is otherwise unnecessary protected final MockableLocationProvider mProvider; + @GuardedBy("mLock") + @Nullable private OnAlarmListener mDelayedRegister; + LocationProviderManager(Context context, Injector injector, String name, @Nullable PassiveLocationProviderManager passiveManager) { mContext = context; @@ -1135,6 +1228,7 @@ class LocationProviderManager extends LocalServices.getService(LocationManagerInternal.class)); mSettingsHelper = injector.getSettingsHelper(); mUserInfoHelper = injector.getUserInfoHelper(); + mAlarmHelper = injector.getAlarmHelper(); mAppOpsHelper = injector.getAppOpsHelper(); mLocationPermissionsHelper = injector.getLocationPermissionsHelper(); mAppForegroundHelper = injector.getAppForegroundHelper(); @@ -1344,7 +1438,7 @@ class LocationProviderManager extends } Location location = getLastLocationUnsafe(identity.getUserId(), permissionLevel, - ignoreLocationSettings); + ignoreLocationSettings, Long.MAX_VALUE); // we don't note op here because we don't know what the client intends to do with the // location, the client is responsible for noting if necessary @@ -1364,13 +1458,14 @@ class LocationProviderManager extends */ @Nullable public Location getLastLocationUnsafe(int userId, @PermissionLevel int permissionLevel, - boolean ignoreLocationSettings) { + boolean ignoreLocationSettings, long maximumAgeMs) { if (userId == UserHandle.USER_ALL) { + // find the most recent location across all users Location lastLocation = null; final int[] runningUserIds = mUserInfoHelper.getRunningUserIds(); for (int i = 0; i < runningUserIds.length; i++) { Location next = getLastLocationUnsafe(runningUserIds[i], permissionLevel, - ignoreLocationSettings); + ignoreLocationSettings, maximumAgeMs); if (lastLocation == null || (next != null && next.getElapsedRealtimeNanos() > lastLocation.getElapsedRealtimeNanos())) { lastLocation = next; @@ -1381,18 +1476,30 @@ class LocationProviderManager extends Preconditions.checkArgument(userId >= 0); + Location location; synchronized (mLock) { LastLocation lastLocation = mLastLocations.get(userId); if (lastLocation == null) { - return null; + location = null; + } else { + location = lastLocation.get(permissionLevel, ignoreLocationSettings); } - return lastLocation.get(permissionLevel, ignoreLocationSettings); } + + if (location == null) { + return null; + } + + if (location.getElapsedRealtimeAgeMillis() > maximumAgeMs) { + return null; + } + + return location; } public void injectLastLocation(Location location, int userId) { synchronized (mLock) { - if (getLastLocationUnsafe(userId, PERMISSION_FINE, false) == null) { + if (getLastLocationUnsafe(userId, PERMISSION_FINE, false, Long.MAX_VALUE) == null) { setLastLocation(location, userId); } } @@ -1455,22 +1562,14 @@ class LocationProviderManager extends return null; } - Location lastLocation = getLastLocationUnsafe(callerIdentity.getUserId(), - permissionLevel, request.isLocationSettingsIgnored()); + Location lastLocation = getLastLocationUnsafe( + callerIdentity.getUserId(), + permissionLevel, + request.isLocationSettingsIgnored(), + MAX_CURRENT_LOCATION_AGE_MS); if (lastLocation != null) { - long locationAgeMs = NANOSECONDS.toMillis( - SystemClock.elapsedRealtimeNanos() - - lastLocation.getElapsedRealtimeNanos()); - if (locationAgeMs < MAX_CURRENT_LOCATION_AGE_MS) { - registration.deliverLocation(lastLocation); - return null; - } - - if (!mAppForegroundHelper.isAppForeground(Binder.getCallingUid()) - && locationAgeMs < mSettingsHelper.getBackgroundThrottleIntervalMs()) { - registration.deliverLocation(null); - return null; - } + registration.deliverLocation(lastLocation); + return null; } // if last location isn't good enough then we add a location request @@ -1627,6 +1726,16 @@ class LocationProviderManager extends registration.isForeground()); } + @GuardedBy("mLock") + @Override + protected void onRegistrationReplaced(Object key, Registration oldRegistration, + Registration newRegistration) { + // by saving the last delivered location state we are able to potentially delay the + // resulting provider request longer and save additional power + newRegistration.initializeLastLocation(oldRegistration.getLastDeliveredLocation()); + super.onRegistrationReplaced(key, oldRegistration, newRegistration); + } + @GuardedBy("mLock") @Override protected void onRegistrationRemoved(Object key, Registration registration) { @@ -1652,21 +1761,61 @@ class LocationProviderManager extends @GuardedBy("mLock") @Override - protected boolean registerWithService(ProviderRequest mergedRequest, + protected boolean registerWithService(ProviderRequest request, Collection registrations) { - if (Build.IS_DEBUGGABLE) { - Preconditions.checkState(Thread.holdsLock(mLock)); - } - - mProvider.setRequest(mergedRequest); - return true; + return reregisterWithService(EMPTY_REQUEST, request, registrations); } @GuardedBy("mLock") @Override protected boolean reregisterWithService(ProviderRequest oldRequest, ProviderRequest newRequest, Collection registrations) { - return registerWithService(newRequest, registrations); + if (Build.IS_DEBUGGABLE) { + Preconditions.checkState(Thread.holdsLock(mLock)); + } + + if (mDelayedRegister != null) { + mAlarmHelper.cancel(mDelayedRegister); + mDelayedRegister = null; + } + + // calculate how long the new request should be delayed before sending it off to the + // provider, under the assumption that once we send the request off, the provider will + // immediately attempt to deliver a new location satisfying that request. + long delayMs; + if (!oldRequest.isLocationSettingsIgnored() && newRequest.isLocationSettingsIgnored()) { + delayMs = 0; + } else if (newRequest.getIntervalMillis() > oldRequest.getIntervalMillis()) { + // if the interval has increased, tell the provider immediately, so it can save power + // (even though technically this could burn extra power in the short term by producing + // an extra location - the provider itself is free to detect an increasing interval and + // delay its own location) + delayMs = 0; + } else { + delayMs = calculateRequestDelayMillis(newRequest.getIntervalMillis(), registrations); + } + + // the delay should never exceed the new interval + Preconditions.checkState(delayMs >= 0 && delayMs <= newRequest.getIntervalMillis()); + + if (delayMs < MIN_REQUEST_DELAY_MS) { + mProvider.setRequest(newRequest); + } else { + mDelayedRegister = new OnAlarmListener() { + @Override + public void onAlarm() { + synchronized (mLock) { + if (mDelayedRegister == this) { + mProvider.setRequest(newRequest); + mDelayedRegister = null; + } + } + } + }; + mAlarmHelper.setDelayedAlarm(delayMs, mDelayedRegister, newRequest.getWorkSource()); + } + + return true; } @GuardedBy("mLock") @@ -1733,42 +1882,40 @@ class LocationProviderManager extends Preconditions.checkState(Thread.holdsLock(mLock)); } - ArrayList providerRegistrations = new ArrayList<>(registrations.size()); - long intervalMs = Long.MAX_VALUE; boolean locationSettingsIgnored = false; boolean lowPower = true; ArrayList locationRequests = new ArrayList<>(registrations.size()); - for (Registration registration : registrations) { - LocationRequest locationRequest = registration.getRequest(); - // passive requests do not contribute to the provider - if (locationRequest.getIntervalMillis() == LocationRequest.PASSIVE_INTERVAL) { + for (Registration registration : registrations) { + LocationRequest request = registration.getRequest(); + + // passive requests do not contribute to the provider request + if (request.getIntervalMillis() == PASSIVE_INTERVAL) { continue; } - providerRegistrations.add(registration); - intervalMs = min(locationRequest.getIntervalMillis(), intervalMs); - locationSettingsIgnored |= locationRequest.isLocationSettingsIgnored(); - lowPower &= locationRequest.isLowPower(); - locationRequests.add(locationRequest); + intervalMs = min(request.getIntervalMillis(), intervalMs); + locationSettingsIgnored |= request.isLocationSettingsIgnored(); + lowPower &= request.isLowPower(); + locationRequests.add(request); } // calculate who to blame for power in a somewhat arbitrary fashion. we pick a threshold // interval slightly higher that the minimum interval, and spread the blame across all // contributing registrations under that threshold (since worksource does not allow us to // represent differing power blame ratios). - WorkSource workSource = new WorkSource(); long thresholdIntervalMs = (intervalMs + 1000) * 3 / 2; - if (thresholdIntervalMs < 0) { - // handle overflow by setting to one below the passive interval - thresholdIntervalMs = Long.MAX_VALUE - 1; + if (thresholdIntervalMs < 0 || thresholdIntervalMs >= PASSIVE_INTERVAL) { + // check for and handle overflow by setting to one below the passive interval so passive + // requests are automatically skipped + thresholdIntervalMs = PASSIVE_INTERVAL - 1; } - final int providerRegistrationsSize = providerRegistrations.size(); - for (int i = 0; i < providerRegistrationsSize; i++) { - Registration registration = providerRegistrations.get(i); + + WorkSource workSource = new WorkSource(); + for (Registration registration : registrations) { if (registration.getRequest().getIntervalMillis() <= thresholdIntervalMs) { - workSource.add(providerRegistrations.get(i).getWorkSource()); + workSource.add(registration.getWorkSource()); } } @@ -1781,6 +1928,47 @@ class LocationProviderManager extends .build(); } + @GuardedBy("mLock") + protected long calculateRequestDelayMillis(long newIntervalMs, + Collection registrations) { + // calculate the minimum delay across all registrations, ensuring that it is not more than + // the requested interval + long delayMs = newIntervalMs; + for (Registration registration : registrations) { + if (delayMs == 0) { + break; + } + + LocationRequest locationRequest = registration.getRequest(); + Location last = registration.getLastDeliveredLocation(); + + if (last == null && !locationRequest.isLocationSettingsIgnored()) { + // if this request has never gotten any location and it's not ignoring location + // settings, then we pretend that this request has gotten the last applicable cached + // location for our calculations instead. this prevents spammy add/remove behavior + last = getLastLocationUnsafe( + registration.getIdentity().getUserId(), + PERMISSION_FINE, + false, + locationRequest.getIntervalMillis()); + } + + long registrationDelayMs; + if (last == null) { + // if this request has never gotten any location then there's no delay + registrationDelayMs = 0; + } else { + // otherwise the delay is the amount of time until the next location is expected + registrationDelayMs = max(0, + locationRequest.getIntervalMillis() - last.getElapsedRealtimeAgeMillis()); + } + + delayMs = min(delayMs, registrationDelayMs); + } + + return delayMs; + } + private void onUserChanged(int userId, int change) { synchronized (mLock) { switch (change) { @@ -2068,7 +2256,7 @@ class LocationProviderManager extends ipw.increaseIndent(); } ipw.print("last location="); - ipw.println(getLastLocationUnsafe(userId, PERMISSION_FINE, false)); + ipw.println(getLastLocationUnsafe(userId, PERMISSION_FINE, false, Long.MAX_VALUE)); ipw.print("enabled="); ipw.println(isEnabled(userId)); if (userIds.length != 1) { @@ -2126,24 +2314,37 @@ class LocationProviderManager extends } } - public void set(Location location, Location coarseLocation) { - mFineLocation = location; + public void set(Location fineLocation, Location coarseLocation) { + mFineLocation = calculateNextFine(mFineLocation, fineLocation); mCoarseLocation = calculateNextCoarse(mCoarseLocation, coarseLocation); } - public void setBypass(Location location, Location coarseLocation) { - mFineBypassLocation = location; + public void setBypass(Location fineLocation, Location coarseLocation) { + mFineBypassLocation = calculateNextFine(mFineBypassLocation, fineLocation); mCoarseBypassLocation = calculateNextCoarse(mCoarseBypassLocation, coarseLocation); } + private Location calculateNextFine(@Nullable Location oldFine, Location newFine) { + if (oldFine == null) { + return newFine; + } + + // update last fine interval only if more recent + if (newFine.getElapsedRealtimeNanos() > oldFine.getElapsedRealtimeNanos()) { + return newFine; + } else { + return oldFine; + } + } + private Location calculateNextCoarse(@Nullable Location oldCoarse, Location newCoarse) { if (oldCoarse == null) { return newCoarse; } + // update last coarse interval only if enough time has passed - long timeDeltaMs = NANOSECONDS.toMillis(newCoarse.getElapsedRealtimeNanos()) - - NANOSECONDS.toMillis(oldCoarse.getElapsedRealtimeNanos()); - if (timeDeltaMs > FASTEST_COARSE_INTERVAL_MS) { + if (newCoarse.getElapsedRealtimeNanos() - MIN_COARSE_INTERVAL_MS + > oldCoarse.getElapsedRealtimeNanos()) { return newCoarse; } else { return oldCoarse; diff --git a/services/core/java/com/android/server/location/PassiveLocationProviderManager.java b/services/core/java/com/android/server/location/PassiveLocationProviderManager.java index afeb6444c40b2..2870d41e5248f 100644 --- a/services/core/java/com/android/server/location/PassiveLocationProviderManager.java +++ b/services/core/java/com/android/server/location/PassiveLocationProviderManager.java @@ -20,14 +20,12 @@ import android.annotation.Nullable; import android.content.Context; import android.location.Location; import android.location.LocationManager; -import android.location.LocationRequest; import android.os.Binder; import com.android.internal.location.ProviderRequest; import com.android.internal.util.Preconditions; import com.android.server.location.util.Injector; -import java.util.ArrayList; import java.util.Collection; class PassiveLocationProviderManager extends LocationProviderManager { @@ -65,17 +63,20 @@ class PassiveLocationProviderManager extends LocationProviderManager { @Override protected ProviderRequest mergeRegistrations(Collection registrations) { - ProviderRequest.Builder providerRequest = new ProviderRequest.Builder() - .setIntervalMillis(0); - - ArrayList requests = new ArrayList<>(registrations.size()); + boolean locationSettingsIgnored = false; for (Registration registration : registrations) { - requests.add(registration.getRequest()); - if (registration.getRequest().isLocationSettingsIgnored()) { - providerRequest.setLocationSettingsIgnored(true); - } + locationSettingsIgnored |= registration.getRequest().isLocationSettingsIgnored(); } - return providerRequest.setLocationRequests(requests).build(); + return new ProviderRequest.Builder() + .setIntervalMillis(0) + .setLocationSettingsIgnored(locationSettingsIgnored) + .build(); + } + + @Override + protected long calculateRequestDelayMillis(long newIntervalMs, + Collection registrations) { + return 0; } } diff --git a/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java b/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java index 87d668a07d703..0cda57cf1936a 100644 --- a/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java +++ b/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java @@ -57,6 +57,8 @@ import java.util.function.Predicate; *

  • {@link #onRegister()}
  • *
  • {@link ListenerRegistration#onRegister(Object)}
  • *
  • {@link #onRegistrationAdded(Object, ListenerRegistration)}
  • + *
  • {@link #onRegistrationReplaced(Object, ListenerRegistration, ListenerRegistration)} (only + * invoked if this registration is replacing a prior registration)
  • *
  • {@link #onActive()}
  • *
  • {@link ListenerRegistration#onActive()}
  • *
  • {@link ListenerRegistration#onInactive()}
  • @@ -182,6 +184,17 @@ public abstract class ListenerMultiplexer= 0) { - removeRegistration(index, false); + oldRegistration = removeRegistration(index, false); mRegistrations.setValueAt(index, registration); } else { mRegistrations.put(key, registration); @@ -239,7 +253,11 @@ public abstract class ListenerMultiplexer 0); + Preconditions.checkArgument(workSource != null); + setDelayedAlarmInternal(delayMs, listener, workSource); + } + + protected abstract void setDelayedAlarmInternal(long delayMs, OnAlarmListener listener, + WorkSource workSource); + + /** + * Cancels an alarm. + */ + public abstract void cancel(OnAlarmListener listener); +} diff --git a/services/core/java/com/android/server/location/util/Injector.java b/services/core/java/com/android/server/location/util/Injector.java index 379b303bbfc34..d9c73ba464441 100644 --- a/services/core/java/com/android/server/location/util/Injector.java +++ b/services/core/java/com/android/server/location/util/Injector.java @@ -28,6 +28,9 @@ public interface Injector { /** Returns a UserInfoHelper. */ UserInfoHelper getUserInfoHelper(); + /** Returns an AlarmHelper. */ + AlarmHelper getAlarmHelper(); + /** Returns an AppOpsHelper. */ AppOpsHelper getAppOpsHelper(); diff --git a/services/core/java/com/android/server/location/util/SystemAlarmHelper.java b/services/core/java/com/android/server/location/util/SystemAlarmHelper.java new file mode 100644 index 0000000000000..81849794c472f --- /dev/null +++ b/services/core/java/com/android/server/location/util/SystemAlarmHelper.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.location.util; + +import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; +import static android.app.AlarmManager.WINDOW_EXACT; + +import android.app.AlarmManager; +import android.content.Context; +import android.os.SystemClock; +import android.os.WorkSource; + +import com.android.server.FgThread; + +import java.util.Objects; + +/** + * Provides helpers for alarms. + */ +public class SystemAlarmHelper extends AlarmHelper { + + private final Context mContext; + + public SystemAlarmHelper(Context context) { + mContext = context; + } + + @Override + public void setDelayedAlarmInternal(long delayMs, AlarmManager.OnAlarmListener listener, + WorkSource workSource) { + AlarmManager alarmManager = Objects.requireNonNull( + mContext.getSystemService(AlarmManager.class)); + alarmManager.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delayMs, + WINDOW_EXACT, 0, listener, FgThread.getHandler(), workSource); + } + + @Override + public void cancel(AlarmManager.OnAlarmListener listener) { + AlarmManager alarmManager = Objects.requireNonNull( + mContext.getSystemService(AlarmManager.class)); + alarmManager.cancel(listener); + } +} diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml index a398961db4c3e..182fe9ae94767 100644 --- a/services/tests/mockingservicestests/AndroidManifest.xml +++ b/services/tests/mockingservicestests/AndroidManifest.xml @@ -17,7 +17,7 @@ - + diff --git a/services/tests/mockingservicestests/src/com/android/server/location/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/LocationProviderManagerTest.java index b4e8825c3902f..be258dc5963ec 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/LocationProviderManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/LocationProviderManagerTest.java @@ -16,8 +16,6 @@ package com.android.server.location; -import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; -import static android.app.AlarmManager.WINDOW_EXACT; 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; @@ -41,7 +39,6 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.after; @@ -55,8 +52,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.MockitoAnnotations.initMocks; -import android.app.AlarmManager; -import android.app.AlarmManager.OnAlarmListener; import android.content.Context; import android.location.ILocationCallback; import android.location.ILocationListener; @@ -66,14 +61,11 @@ import android.location.LocationManagerInternal.ProviderEnabledListener; import android.location.LocationRequest; import android.location.util.identity.CallerIdentity; import android.os.Bundle; -import android.os.Handler; import android.os.ICancellationSignal; import android.os.IRemoteCallback; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; -import android.os.SystemClock; -import android.os.WorkSource; import android.platform.test.annotations.Presubmit; import android.util.Log; @@ -127,8 +119,6 @@ public class LocationProviderManagerTest { @Mock private Context mContext; @Mock - private AlarmManager mAlarmManager; - @Mock private PowerManager mPowerManager; @Mock private PowerManager.WakeLock mWakeLock; @@ -151,7 +141,6 @@ public class LocationProviderManagerTest { LocalServices.addService(LocationManagerInternal.class, mInternal); doReturn("android").when(mContext).getPackageName(); - doReturn(mAlarmManager).when(mContext).getSystemService(AlarmManager.class); doReturn(mPowerManager).when(mContext).getSystemService(PowerManager.class); doReturn(mWakeLock).when(mPowerManager).newWakeLock(anyInt(), anyString()); @@ -505,19 +494,8 @@ public class LocationProviderManagerTest { ILocationListener listener = createMockLocationListener(); LocationRequest request = new LocationRequest.Builder(0).setDurationMillis(5000).build(); mManager.registerLocationRequest(request, IDENTITY, PERMISSION_FINE, listener); - long baseTimeMs = SystemClock.elapsedRealtime(); - - ArgumentCaptor timeoutCapture = ArgumentCaptor.forClass(Long.class); - ArgumentCaptor listenerCapture = ArgumentCaptor.forClass( - OnAlarmListener.class); - verify(mAlarmManager).set(eq(ELAPSED_REALTIME_WAKEUP), timeoutCapture.capture(), - eq(WINDOW_EXACT), eq(0L), listenerCapture.capture(), any(Handler.class), - any(WorkSource.class)); - - assertThat(timeoutCapture.getValue()).isAtLeast(baseTimeMs + 4000); - assertThat(timeoutCapture.getValue()).isAtMost(baseTimeMs + 5000); - listenerCapture.getValue().onAlarm(); + mInjector.getAlarmHelper().incrementAlarmTime(5000); mProvider.setProviderLocation(createLocation(NAME, mRandom)); verify(listener, never()).onLocationChanged(any(Location.class), nullable(IRemoteCallback.class)); @@ -684,13 +662,7 @@ public class LocationProviderManagerTest { LocationRequest locationRequest = new LocationRequest.Builder(0).build(); mManager.getCurrentLocation(locationRequest, IDENTITY, PERMISSION_FINE, listener); - ArgumentCaptor listenerCapture = ArgumentCaptor.forClass( - OnAlarmListener.class); - verify(mAlarmManager).set(eq(ELAPSED_REALTIME_WAKEUP), anyLong(), - eq(WINDOW_EXACT), eq(0L), listenerCapture.capture(), any(Handler.class), - any(WorkSource.class)); - listenerCapture.getValue().onAlarm(); - + mInjector.getAlarmHelper().incrementAlarmTime(60000); verify(listener, times(1)).onLocation(isNull()); } @@ -768,6 +740,40 @@ public class LocationProviderManagerTest { assertThat(mProvider.getRequest().getLocationRequests()).isEmpty(); } + @Test + public void testProviderRequest_DelayedRequest() throws Exception { + mProvider.setProviderLocation(createLocation(NAME, mRandom)); + + ILocationListener listener1 = createMockLocationListener(); + LocationRequest request1 = new LocationRequest.Builder(60000).build(); + mManager.registerLocationRequest(request1, IDENTITY, PERMISSION_FINE, listener1); + + verify(listener1).onLocationChanged(any(Location.class), nullable(IRemoteCallback.class)); + + assertThat(mProvider.getRequest().isActive()).isFalse(); + + mInjector.getAlarmHelper().incrementAlarmTime(60000); + assertThat(mProvider.getRequest().isActive()).isTrue(); + assertThat(mProvider.getRequest().getIntervalMillis()).isEqualTo(60000); + } + + @Test + public void testProviderRequest_SpamRequesting() { + mProvider.setProviderLocation(createLocation(NAME, mRandom)); + + ILocationListener listener1 = createMockLocationListener(); + LocationRequest request1 = new LocationRequest.Builder(60000).build(); + + mManager.registerLocationRequest(request1, IDENTITY, PERMISSION_FINE, listener1); + assertThat(mProvider.getRequest().isActive()).isFalse(); + mManager.unregisterLocationRequest(listener1); + assertThat(mProvider.getRequest().isActive()).isFalse(); + mManager.registerLocationRequest(request1, IDENTITY, PERMISSION_FINE, listener1); + assertThat(mProvider.getRequest().isActive()).isFalse(); + mManager.unregisterLocationRequest(listener1); + assertThat(mProvider.getRequest().isActive()).isFalse(); + } + @Test public void testProviderRequest_BackgroundThrottle() { ILocationListener listener1 = createMockLocationListener(); diff --git a/services/tests/mockingservicestests/src/com/android/server/location/util/FakeAlarmHelper.java b/services/tests/mockingservicestests/src/com/android/server/location/util/FakeAlarmHelper.java new file mode 100644 index 0000000000000..0e3e6ef02a18c --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/location/util/FakeAlarmHelper.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.location.util; + +import android.app.AlarmManager.OnAlarmListener; +import android.os.WorkSource; + +import java.util.ArrayList; +import java.util.Iterator; + +public class FakeAlarmHelper extends AlarmHelper { + + private static class Alarm { + public long delayMs; + public final OnAlarmListener listener; + + Alarm(long delayMs, OnAlarmListener listener) { + this.delayMs = delayMs; + this.listener = listener; + } + } + + private final ArrayList mAlarms = new ArrayList<>(); + + @Override + public void setDelayedAlarmInternal(long delayMs, OnAlarmListener listener, + WorkSource workSource) { + mAlarms.add(new Alarm(delayMs, listener)); + } + + @Override + public void cancel(OnAlarmListener listener) { + mAlarms.removeIf(alarm -> alarm.listener == listener); + } + + public void incrementAlarmTime(long incrementMs) { + Iterator it = mAlarms.iterator(); + while (it.hasNext()) { + Alarm alarm = it.next(); + alarm.delayMs -= incrementMs; + if (alarm.delayMs <= 0) { + it.remove(); + alarm.listener.onAlarm(); + } + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/location/util/TestInjector.java b/services/tests/mockingservicestests/src/com/android/server/location/util/TestInjector.java index 1867be0b9f3b9..69f7376a43098 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/util/TestInjector.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/util/TestInjector.java @@ -21,6 +21,7 @@ import com.android.server.location.LocationRequestStatistics; public class TestInjector implements Injector { private final FakeUserInfoHelper mUserInfoHelper; + private final FakeAlarmHelper mAlarmHelper; private final FakeAppOpsHelper mAppOpsHelper; private final FakeLocationPermissionsHelper mLocationPermissionsHelper; private final FakeSettingsHelper mSettingsHelper; @@ -33,6 +34,7 @@ public class TestInjector implements Injector { public TestInjector() { mUserInfoHelper = new FakeUserInfoHelper(); + mAlarmHelper = new FakeAlarmHelper(); mAppOpsHelper = new FakeAppOpsHelper(); mLocationPermissionsHelper = new FakeLocationPermissionsHelper(mAppOpsHelper); mSettingsHelper = new FakeSettingsHelper(); @@ -49,6 +51,11 @@ public class TestInjector implements Injector { return mUserInfoHelper; } + @Override + public FakeAlarmHelper getAlarmHelper() { + return mAlarmHelper; + } + @Override public FakeAppOpsHelper getAppOpsHelper() { return mAppOpsHelper;