Merge "Fix background request throttling + jitter calculation"
This commit is contained in:
committed by
Android (Google) Code Review
commit
f20b5f0303
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
* <p>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.
|
||||
*
|
||||
* <p>See {@link LocationRequest} documentation for an explanation of various request parameters
|
||||
* and how they can affect the received locations.
|
||||
*
|
||||
* <p> If your application wants to passively observe location updates from any provider, then
|
||||
* <p>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 {
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p> When location callbacks are invoked, the system will hold a wakelock on your
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>To unregister for location updates, use {@link #removeUpdates(LocationListener)}.
|
||||
*
|
||||
* @param provider a provider listed by {@link #getAllProviders()}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Registration> 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<Registration> 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<Registration> providerRegistrations = new ArrayList<>(registrations.size());
|
||||
|
||||
long intervalMs = Long.MAX_VALUE;
|
||||
boolean locationSettingsIgnored = false;
|
||||
boolean lowPower = true;
|
||||
ArrayList<LocationRequest> 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<Registration> 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;
|
||||
|
||||
@@ -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<Registration> registrations) {
|
||||
ProviderRequest.Builder providerRequest = new ProviderRequest.Builder()
|
||||
.setIntervalMillis(0);
|
||||
|
||||
ArrayList<LocationRequest> 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<Registration> registrations) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ import java.util.function.Predicate;
|
||||
* <li>{@link #onRegister()}</li>
|
||||
* <li>{@link ListenerRegistration#onRegister(Object)}</li>
|
||||
* <li>{@link #onRegistrationAdded(Object, ListenerRegistration)}</li>
|
||||
* <li>{@link #onRegistrationReplaced(Object, ListenerRegistration, ListenerRegistration)} (only
|
||||
* invoked if this registration is replacing a prior registration)</li>
|
||||
* <li>{@link #onActive()}</li>
|
||||
* <li>{@link ListenerRegistration#onActive()}</li>
|
||||
* <li>{@link ListenerRegistration#onInactive()}</li>
|
||||
@@ -182,6 +184,17 @@ public abstract class ListenerMultiplexer<TKey, TListener,
|
||||
*/
|
||||
protected void onRegistrationAdded(@NonNull TKey key, @NonNull TRegistration registration) {}
|
||||
|
||||
/**
|
||||
* Invoked instead of {@link #onRegistrationAdded(Object, ListenerRegistration)} if a
|
||||
* registration is replacing an old registration. The old registration will have already been
|
||||
* unregistered. Invoked while holding the multiplexer's internal lock. The default behavior is
|
||||
* simply to call into {@link #onRegistrationAdded(Object, ListenerRegistration)}.
|
||||
*/
|
||||
protected void onRegistrationReplaced(@NonNull TKey key, @NonNull TRegistration oldRegistration,
|
||||
@NonNull TRegistration newRegistration) {
|
||||
onRegistrationAdded(key, newRegistration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a registration is removed. Invoked while holding the multiplexer's internal
|
||||
* lock.
|
||||
@@ -227,9 +240,10 @@ public abstract class ListenerMultiplexer<TKey, TListener,
|
||||
|
||||
boolean wasEmpty = mRegistrations.isEmpty();
|
||||
|
||||
TRegistration oldRegistration = null;
|
||||
int index = mRegistrations.indexOfKey(key);
|
||||
if (index >= 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<TKey, TListener,
|
||||
onRegister();
|
||||
}
|
||||
registration.onRegister(key);
|
||||
onRegistrationAdded(key, registration);
|
||||
if (oldRegistration == null) {
|
||||
onRegistrationAdded(key, registration);
|
||||
} else {
|
||||
onRegistrationReplaced(key, oldRegistration, registration);
|
||||
}
|
||||
onRegistrationActiveChanged(registration);
|
||||
}
|
||||
}
|
||||
@@ -320,7 +338,7 @@ public abstract class ListenerMultiplexer<TKey, TListener,
|
||||
}
|
||||
|
||||
@GuardedBy("mRegistrations")
|
||||
private void removeRegistration(int index, boolean removeEntry) {
|
||||
private TRegistration removeRegistration(int index, boolean removeEntry) {
|
||||
if (Build.IS_DEBUGGABLE) {
|
||||
Preconditions.checkState(Thread.holdsLock(mRegistrations));
|
||||
}
|
||||
@@ -347,6 +365,8 @@ public abstract class ListenerMultiplexer<TKey, TListener,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return registration;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 com.android.internal.util.Preconditions;
|
||||
|
||||
/**
|
||||
* Helps manage alarms.
|
||||
*/
|
||||
public abstract class AlarmHelper {
|
||||
|
||||
/**
|
||||
* Sets a wakeup alarm that will fire after the given delay.
|
||||
*/
|
||||
public final void setDelayedAlarm(long delayMs, OnAlarmListener listener,
|
||||
WorkSource workSource) {
|
||||
// helps ensure that we're not wasting system resources by setting alarms in the past/now
|
||||
Preconditions.checkArgument(delayMs > 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);
|
||||
}
|
||||
@@ -28,6 +28,9 @@ public interface Injector {
|
||||
/** Returns a UserInfoHelper. */
|
||||
UserInfoHelper getUserInfoHelper();
|
||||
|
||||
/** Returns an AlarmHelper. */
|
||||
AlarmHelper getAlarmHelper();
|
||||
|
||||
/** Returns an AppOpsHelper. */
|
||||
AppOpsHelper getAppOpsHelper();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.frameworks.mockingservicestests">
|
||||
|
||||
<uses-sdk android:targetSdkVersion="30" />
|
||||
<uses-sdk android:targetSdkVersion="31" />
|
||||
|
||||
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
|
||||
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
|
||||
|
||||
@@ -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<Long> timeoutCapture = ArgumentCaptor.forClass(Long.class);
|
||||
ArgumentCaptor<OnAlarmListener> 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<OnAlarmListener> 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();
|
||||
|
||||
@@ -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<Alarm> 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<Alarm> it = mAlarms.iterator();
|
||||
while (it.hasNext()) {
|
||||
Alarm alarm = it.next();
|
||||
alarm.delayMs -= incrementMs;
|
||||
if (alarm.delayMs <= 0) {
|
||||
it.remove();
|
||||
alarm.listener.onAlarm();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user