diff options
18 files changed, 933 insertions, 778 deletions
diff --git a/api/system-current.txt b/api/system-current.txt index 8adc66394a52..d0da42c49226 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4136,7 +4136,7 @@ package android.location { method public boolean isComplete(); method public void makeComplete(); method public void setIsFromMockProvider(boolean); - field public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; + field @Deprecated public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; } public class LocationManager { diff --git a/api/test-current.txt b/api/test-current.txt index 8598c167cab0..225f3dc3ee5c 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -1338,7 +1338,7 @@ package android.location { public class Location implements android.os.Parcelable { method public void makeComplete(); - field public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; + field @Deprecated public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; } public class LocationManager { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 713cfc64afe7..44c2fbbd9ce7 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6257,16 +6257,19 @@ public final class Settings { * @hide */ public static final String LOCATION_CHANGER = "location_changer"; + /** * The location changer is unknown or unable to detect. * @hide */ public static final int LOCATION_CHANGER_UNKNOWN = 0; + /** * Location settings in system settings. * @hide */ public static final int LOCATION_CHANGER_SYSTEM_SETTINGS = 1; + /** * The location icon in drop down notification drawer. * @hide @@ -6314,6 +6317,14 @@ public final class Settings { public static final int LOCATION_MODE_ON = LOCATION_MODE_HIGH_ACCURACY; /** + * The accuracy in meters used for coarsening location for clients with only the coarse + * location permission. + * + * @hide + */ + public static final String LOCATION_COARSE_ACCURACY_M = "locationCoarseAccuracy"; + + /** * A flag containing settings used for biometric weak * @hide */ diff --git a/location/java/android/location/ILocationManager.aidl b/location/java/android/location/ILocationManager.aidl index a99e68fbb7b6..7de4d89dfa4f 100644 --- a/location/java/android/location/ILocationManager.aidl +++ b/location/java/android/location/ILocationManager.aidl @@ -93,7 +93,7 @@ interface ILocationManager boolean startGnssBatch(long periodNanos, boolean wakeOnFifoFull, String packageName); void flushGnssBatch(String packageName); boolean stopGnssBatch(); - boolean injectLocation(in Location location); + void injectLocation(in Location location); @UnsupportedAppUsage List<String> getAllProviders(); diff --git a/location/java/android/location/Location.java b/location/java/android/location/Location.java index eb76c29301c6..6724324bfcb9 100644 --- a/location/java/android/location/Location.java +++ b/location/java/android/location/Location.java @@ -64,23 +64,18 @@ public class Location implements Parcelable { public static final int FORMAT_SECONDS = 2; /** - * Bundle key for a version of the location that has been fed through - * LocationFudger. Allows location providers to flag locations as being - * safe for use with ACCESS_COARSE_LOCATION permission. - * - * @hide - */ - public static final String EXTRA_COARSE_LOCATION = "coarseLocation"; - - /** * Bundle key for a version of the location containing no GPS data. * Allows location providers to flag locations as being safe to * feed to LocationFudger. * * @hide + * @deprecated As of Android R, this extra is longer in use, since it is not necessary to keep + * gps locations separate from other locations for coarsening. Providers that do not need to + * support platforms below Android R should not use this constant. */ @TestApi @SystemApi + @Deprecated public static final String EXTRA_NO_GPS_LOCATION = "noGPSLocation"; /** diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java index 8ae967fe79c2..0fec611bc522 100644 --- a/location/java/android/location/LocationManager.java +++ b/location/java/android/location/LocationManager.java @@ -1215,7 +1215,7 @@ public class LocationManager { * the first fix. * * @param location newly available {@link Location} object - * @return true if the location was successfully injected, false otherwise + * @return true if the location was injected, false otherwise * * @throws IllegalArgumentException if location is null * @throws SecurityException if permissions are not present @@ -1229,7 +1229,8 @@ public class LocationManager { "incomplete location object, missing timestamp or accuracy?"); try { - return mService.injectLocation(location); + mService.injectLocation(location); + return true; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/location/java/android/location/LocationManagerInternal.java b/location/java/android/location/LocationManagerInternal.java index 69162bab3167..085602cbcd4f 100644 --- a/location/java/android/location/LocationManagerInternal.java +++ b/location/java/android/location/LocationManagerInternal.java @@ -43,6 +43,15 @@ public abstract class LocationManagerInternal { public abstract void requestSetProviderAllowed(@NonNull String provider, boolean allowed); /** + * Returns true if the given provider is enabled for the given user. + * + * @param provider A location provider as listed by {@link LocationManager#getAllProviders()} + * @param userId The user id to check + * @return True if the provider is enabled, false otherwise + */ + public abstract boolean isProviderEnabledForUser(@NonNull String provider, int userId); + + /** * Returns true if the given package belongs to a location provider, and so should be afforded * some special privileges. * diff --git a/location/lib/java/com/android/location/provider/LocationProviderBase.java b/location/lib/java/com/android/location/provider/LocationProviderBase.java index f67d08e045e2..bd29d8ac2a85 100644 --- a/location/lib/java/com/android/location/provider/LocationProviderBase.java +++ b/location/lib/java/com/android/location/provider/LocationProviderBase.java @@ -224,6 +224,19 @@ public abstract class LocationProviderBase { public void reportLocation(Location location) { ILocationProviderManager manager = mManager; if (manager != null) { + // remove deprecated extras to save on serialization + Bundle extras = location.getExtras(); + if (extras != null && (extras.containsKey("noGPSLocation") + || extras.containsKey("coarseLocation"))) { + location = new Location(location); + extras = location.getExtras(); + extras.remove("noGPSLocation"); + extras.remove("coarseLocation"); + if (extras.isEmpty()) { + location.setExtras(null); + } + } + try { manager.onReportLocation(location); } catch (RemoteException | RuntimeException e) { diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java index 2c91a11c358c..6e6c9420c5aa 100644 --- a/services/core/java/com/android/server/LocationManagerService.java +++ b/services/core/java/com/android/server/LocationManagerService.java @@ -27,9 +27,12 @@ import static android.location.LocationManager.NETWORK_PROVIDER; import static android.location.LocationManager.PASSIVE_PROVIDER; import static android.os.PowerManager.locationPowerSaveModeToString; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.PendingIntent; @@ -75,7 +78,6 @@ import android.stats.location.LocationStatsEnums; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; -import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; @@ -106,6 +108,7 @@ import com.android.server.location.MockableLocationProvider; import com.android.server.location.PassiveProvider; import com.android.server.location.SettingsHelper; import com.android.server.location.UserInfoHelper; +import com.android.server.location.UserInfoHelper.UserListener; import com.android.server.location.gnss.GnssManagerService; import com.android.server.pm.permission.PermissionManagerServiceInternal; @@ -124,7 +127,6 @@ import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; /** * The service class that manages LocationProviders and issues location @@ -154,7 +156,7 @@ public class LocationManagerService extends ILocationManager.Stub { if (phase == PHASE_SYSTEM_SERVICES_READY) { // the location service must be functioning after this boot phase mService.onSystemReady(); - } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) { + } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { // some providers rely on third party code, so we wait to initialize // providers until third party code is allowed to run mService.onSystemThirdPartyAppsCanStart(); @@ -179,6 +181,9 @@ public class LocationManagerService extends ILocationManager.Stub { // The maximum interval a location request can have and still be considered "high power". private static final long HIGH_POWER_INTERVAL_MS = 5 * 60 * 1000; + // The fastest interval that applications can receive coarse locations + private static final long FASTEST_COARSE_INTERVAL_MS = 10 * 60 * 1000; + // maximum age of a location before it is no longer considered "current" private static final long MAX_CURRENT_LOCATION_AGE_MS = 10 * 1000; @@ -207,8 +212,12 @@ public class LocationManagerService extends ILocationManager.Stub { private PackageManager mPackageManager; private PowerManager mPowerManager; - private GeofenceManager mGeofenceManager; + // TODO: sharing a location fudger with mock providers can leak information as the mock provider + // can be used to retrieve offset information. the fudger should likely be reset whenever mock + // providers are added or removed private LocationFudger mLocationFudger; + + private GeofenceManager mGeofenceManager; private GeocoderProxy mGeocodeProvider; @GuardedBy("mLock") @@ -228,16 +237,6 @@ public class LocationManagerService extends ILocationManager.Stub { private final LocationRequestStatistics mRequestStatistics = new LocationRequestStatistics(); - // mapping from provider name to last known location - @GuardedBy("mLock") - private final HashMap<String, Location> mLastLocation = new HashMap<>(); - - // same as mLastLocation, but is not updated faster than LocationFudger.FASTEST_INTERVAL_MS. - // locations stored here are not fudged for coarse permissions. - @GuardedBy("mLock") - private final HashMap<String, Location> mLastLocationCoarseInterval = - new HashMap<>(); - @GuardedBy("mLock") @PowerManager.LocationPowerSaveMode private int mBatterySaverMode; @@ -254,7 +253,7 @@ public class LocationManagerService extends ILocationManager.Stub { mAppForegroundHelper = new AppForegroundHelper(mContext); mLocationUsageLogger = new LocationUsageLogger(); - // set up passive provider - we do this early because it has no dependencies on system + // set up passive provider - we do this early because it has no dependencies on system // services or external code that isn't ready yet, and because this allows the variable to // be final. other more complex providers are initialized later, when system services are // ready @@ -281,18 +280,12 @@ public class LocationManagerService extends ILocationManager.Stub { mSettingsHelper.onSystemReady(); mAppForegroundHelper.onSystemReady(); - if (GnssManagerService.isGnssSupported()) { - mGnssManagerService = new GnssManagerService(mContext, mSettingsHelper, - mAppForegroundHelper, mLocationUsageLogger); - mGnssManagerService.onSystemReady(); - } - synchronized (mLock) { mPackageManager = mContext.getPackageManager(); mAppOps = mContext.getSystemService(AppOpsManager.class); mPowerManager = mContext.getSystemService(PowerManager.class); - mLocationFudger = new LocationFudger(mContext, mHandler); + mLocationFudger = new LocationFudger(mSettingsHelper.getCoarseLocationAccuracyM()); mGeofenceManager = new GeofenceManager(mContext, mSettingsHelper); PowerManagerInternal localPowerManager = @@ -376,10 +369,11 @@ public class LocationManagerService extends ILocationManager.Stub { } }, UserHandle.ALL, intentFilter, null, mHandler); - // switching the user from null to current here performs the bulk of the initialization - // work. the user being changed will cause a reload of all user specific settings, which - // causes initialization, and propagates changes until a steady state is reached - onUserChanged(UserHandle.USER_NULL, mUserInfoHelper.getCurrentUserId()); + // initialize the current users. we would get the user started notifications for these + // users eventually anyways, but this takes care of it as early as possible. + for (int userId: mUserInfoHelper.getCurrentUserIds()) { + onUserChanged(userId, UserListener.USER_STARTED); + } } } @@ -427,7 +421,7 @@ public class LocationManagerService extends ILocationManager.Stub { } if (D) { - Slog.d(TAG, + Log.d(TAG, "Battery Saver location mode changed from " + locationPowerSaveModeToString(mBatterySaverMode) + " to " + locationPowerSaveModeToString(newLocationMode)); @@ -456,15 +450,15 @@ public class LocationManagerService extends ILocationManager.Stub { Log.d(TAG, "[u" + userId + "] location enabled = " + enabled); } - synchronized (mLock) { - Intent intent = new Intent(LocationManager.MODE_CHANGED_ACTION) - .putExtra(LocationManager.EXTRA_LOCATION_ENABLED, enabled) - .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); + Intent intent = new Intent(LocationManager.MODE_CHANGED_ACTION) + .putExtra(LocationManager.EXTRA_LOCATION_ENABLED, enabled) + .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); + synchronized (mLock) { for (LocationProviderManager manager : mProviderManagers) { - manager.onEnabledChangedLocked(userId); + manager.onEnabledChangedLocked(userId); } } } @@ -538,12 +532,6 @@ public class LocationManagerService extends ILocationManager.Stub { @GuardedBy("mLock") private void initializeProvidersLocked() { - if (mGnssManagerService != null) { - LocationProviderManager gnssManager = new LocationProviderManager(GPS_PROVIDER); - mProviderManagers.add(gnssManager); - gnssManager.setRealProvider(mGnssManagerService.getGnssLocationProvider()); - } - LocationProviderProxy networkProvider = LocationProviderProxy.createAndRegister( mContext, NETWORK_LOCATION_SERVICE_ACTION, @@ -554,7 +542,7 @@ public class LocationManagerService extends ILocationManager.Stub { mProviderManagers.add(networkManager); networkManager.setRealProvider(networkProvider); } else { - Slog.w(TAG, "no network location provider found"); + Log.w(TAG, "no network location provider found"); } // ensure that a fused provider exists which will work in direct boot @@ -574,14 +562,13 @@ public class LocationManagerService extends ILocationManager.Stub { mProviderManagers.add(fusedManager); fusedManager.setRealProvider(fusedProvider); } else { - Slog.e(TAG, "no fused location provider found", - new IllegalStateException("Location service needs a fused location provider")); + Log.e(TAG, "no fused location provider found"); } // bind to geocoder provider mGeocodeProvider = GeocoderProxy.createAndRegister(mContext); if (mGeocodeProvider == null) { - Slog.e(TAG, "no geocoder provider found"); + Log.e(TAG, "no geocoder provider found"); } // bind to geofence proxy @@ -590,7 +577,7 @@ public class LocationManagerService extends ILocationManager.Stub { if (gpsGeofenceHardware != null) { GeofenceProxy provider = GeofenceProxy.createAndBind(mContext, gpsGeofenceHardware); if (provider == null) { - Slog.d(TAG, "unable to bind to GeofenceProxy"); + Log.e(TAG, "unable to bind to GeofenceProxy"); } } } @@ -619,22 +606,52 @@ public class LocationManagerService extends ILocationManager.Stub { Integer.parseInt(fragments[9]) /* accuracy */); addTestProvider(name, properties, mContext.getOpPackageName()); } - } - private void onUserChanged(int oldUserId, int newUserId) { - if (D) { - Log.d(TAG, "foreground user is changing to " + newUserId); - } + // initialize gnss last because it has no awareness of boot phases and blindly assumes that + // all other location providers are loaded at initialization + if (GnssManagerService.isGnssSupported()) { + mGnssManagerService = new GnssManagerService(mContext, mSettingsHelper, + mAppForegroundHelper, mLocationUsageLogger); + mGnssManagerService.onSystemReady(); - synchronized (mLock) { - for (LocationProviderManager manager : mProviderManagers) { - // update LOCATION_PROVIDERS_ALLOWED for best effort backwards compatibility - mSettingsHelper.setLocationProviderAllowed(manager.getName(), - manager.isEnabled(newUserId), newUserId); + LocationProviderManager gnssManager = new LocationProviderManager(GPS_PROVIDER); + mProviderManagers.add(gnssManager); + gnssManager.setRealProvider(mGnssManagerService.getGnssLocationProvider()); + } + } - manager.onEnabledChangedLocked(oldUserId); - manager.onEnabledChangedLocked(newUserId); - } + private void onUserChanged(@UserIdInt int userId, @UserListener.UserChange int change) { + switch (change) { + case UserListener.USER_SWITCHED: + if (D) { + Log.d(TAG, "user " + userId + " current status changed"); + } + synchronized (mLock) { + for (LocationProviderManager manager : mProviderManagers) { + manager.onEnabledChangedLocked(userId); + } + } + break; + case UserListener.USER_STARTED: + if (D) { + Log.d(TAG, "user " + userId + " started"); + } + synchronized (mLock) { + for (LocationProviderManager manager : mProviderManagers) { + manager.onUserStarted(userId); + } + } + break; + case UserListener.USER_STOPPED: + if (D) { + Log.d(TAG, "user " + userId + " stopped"); + } + synchronized (mLock) { + for (LocationProviderManager manager : mProviderManagers) { + manager.onUserStopped(userId); + } + } + break; } } @@ -645,25 +662,29 @@ public class LocationManagerService extends ILocationManager.Stub { private final String mName; - // acquiring mLock makes operations on mProvider atomic, but is otherwise unnecessary - protected final MockableLocationProvider mProvider; - - // enabled state for parent user ids, no entry implies false. location state is only kept - // for parent user ids, the location state for a profile user id is assumed to be the same - // as for the parent. if querying this structure, ensure that the user id being used is a - // parent id or the results may be incorrect. + // if the provider is enabled for a given user id - null or not present means unknown @GuardedBy("mLock") private final SparseArray<Boolean> mEnabled; + // last location for a given user + @GuardedBy("mLock") + private final SparseArray<Location> mLastLocation; + + // last coarse location for a given user + @GuardedBy("mLock") + private final SparseArray<Location> mLastCoarseLocation; + + // acquiring mLock makes operations on mProvider atomic, but is otherwise unnecessary + protected final MockableLocationProvider mProvider; + private LocationProviderManager(String name) { mName = name; - mEnabled = new SparseArray<>(1); + mEnabled = new SparseArray<>(2); + mLastLocation = new SparseArray<>(2); + mLastCoarseLocation = new SparseArray<>(2); // initialize last since this lets our reference escape mProvider = new MockableLocationProvider(mLock, this); - - // we can assume all users start with disabled location state since the initial state - // of all providers is disabled. no need to initialize mEnabled further. } public String getName() { @@ -679,7 +700,26 @@ public class LocationManagerService extends ILocationManager.Stub { } public void setMockProvider(@Nullable MockProvider provider) { - mProvider.setMockProvider(provider); + synchronized (mLock) { + mProvider.setMockProvider(provider); + + // when removing a mock provider, also clear any mock last locations + if (provider == null) { + for (int i = 0; i < mLastLocation.size(); i++) { + Location lastLocation = mLastLocation.valueAt(i); + if (lastLocation != null && lastLocation.isFromMockProvider()) { + mLastLocation.setValueAt(i, null); + } + } + + for (int i = 0; i < mLastCoarseLocation.size(); i++) { + Location lastCoarseLocation = mLastCoarseLocation.valueAt(i); + if (lastCoarseLocation != null && lastCoarseLocation.isFromMockProvider()) { + mLastCoarseLocation.setValueAt(i, null); + } + } + } + } } public Set<String> getPackages() { @@ -691,6 +731,45 @@ public class LocationManagerService extends ILocationManager.Stub { return mProvider.getState().properties; } + @Nullable + public Location getLastFineLocation(int userId) { + synchronized (mLock) { + return mLastLocation.get(userId); + } + } + + @Nullable + public Location getLastCoarseLocation(int userId) { + synchronized (mLock) { + return mLastCoarseLocation.get(userId); + } + } + + public void injectLastLocation(Location location, int userId) { + synchronized (mLock) { + if (mLastLocation.get(userId) == null) { + setLastLocation(location, userId); + } + } + } + + private void setLastLocation(Location location, int userId) { + synchronized (mLock) { + mLastLocation.put(userId, location); + + // update last coarse interval only if enough time has passed + long timeDeltaMs = Long.MAX_VALUE; + Location coarseLocation = mLastCoarseLocation.get(userId); + if (coarseLocation != null) { + timeDeltaMs = NANOSECONDS.toMillis(location.getElapsedRealtimeNanos()) + - NANOSECONDS.toMillis(coarseLocation.getElapsedRealtimeNanos()); + } + if (timeDeltaMs > FASTEST_COARSE_INTERVAL_MS) { + mLastCoarseLocation.put(userId, mLocationFudger.createCoarse(location)); + } + } + } + public void setMockProviderAllowed(boolean enabled) { synchronized (mLock) { if (!mProvider.isMock()) { @@ -743,23 +822,31 @@ public class LocationManagerService extends ILocationManager.Stub { // don't validate mock locations if (!location.isFromMockProvider()) { if (location.getLatitude() == 0 && location.getLongitude() == 0) { - Slog.w(TAG, "blocking 0,0 location from " + mName + " provider"); + Log.w(TAG, "blocking 0,0 location from " + mName + " provider"); return; } } - handleLocationChangedLocked(location, this); + if (!location.isComplete()) { + Log.w(TAG, "blocking incomplete location from " + mName + " provider"); + return; + } + + // update last location if the provider is enabled or if servicing a bypass request + boolean locationSettingsIgnored = mProvider.getCurrentRequest().locationSettingsIgnored; + for (int userId : mUserInfoHelper.getCurrentUserIds()) { + if (locationSettingsIgnored || isEnabled(userId)) { + setLastLocation(location, userId); + } + } + + handleLocationChangedLocked(this, location, mLocationFudger.createCoarse(location)); } @GuardedBy("mLock") @Override public void onReportLocation(List<Location> locations) { - if (mGnssManagerService == null) { - return; - } - - if (!GPS_PROVIDER.equals(mName) || !isEnabled()) { - Slog.w(TAG, "reportLocationBatch() called without user permission"); + if (mGnssManagerService == null || !GPS_PROVIDER.equals(mName)) { return; } @@ -770,10 +857,7 @@ public class LocationManagerService extends ILocationManager.Stub { @Override public void onStateChanged(State oldState, State newState) { if (oldState.allowed != newState.allowed) { - // it would be more correct to call this for all users, but we know this can - // only affect the current user since providers are disabled for non-current - // users - onEnabledChangedLocked(mUserInfoHelper.getCurrentUserId()); + onEnabledChangedLocked(UserHandle.USER_ALL); } } @@ -781,37 +865,74 @@ public class LocationManagerService extends ILocationManager.Stub { mProvider.requestSetAllowed(allowed); } - public boolean isEnabled() { - return isEnabled(mUserInfoHelper.getCurrentUserId()); + public void onUserStarted(int userId) { + synchronized (mLock) { + // clear the user's enabled state in order to force a reevalution of whether the + // provider is enabled or disabled for the given user. we clear the user's state + // first to ensure that a user starting never causes any change notifications. it's + // possible for us to observe a user before we observe it's been started (for + // example, another component gets a user started notification before us and + // registers a location request immediately), which would cause us to already have + // some state in place. when we eventually do get the user started notification + // ourselves we don't want to send a change notification based on the prior state + mEnabled.put(userId, null); + onEnabledChangedLocked(userId); + } + } + + public void onUserStopped(int userId) { + synchronized (mLock) { + mEnabled.remove(userId); + mLastLocation.remove(userId); + mLastCoarseLocation.remove(userId); + } } public boolean isEnabled(int userId) { + if (userId == UserHandle.USER_NULL) { + // used during initialization - ignore since many lower level operations (checking + // settings for instance) do not support the null user + return false; + } + synchronized (mLock) { - // normalize user id to always refer to parent since profile state is always the - // same as parent state - userId = mUserInfoHelper.getParentUserId(userId); - return mEnabled.get(userId, Boolean.FALSE); + Boolean enabled = mEnabled.get(userId); + if (enabled == null) { + // this generally shouldn't occur, but might be possible due to race conditions + // on when we are notified of new users + Log.w(TAG, mName + " provider saw user " + userId + " unexpectedly"); + onEnabledChangedLocked(userId); + enabled = Objects.requireNonNull(mEnabled.get(userId)); + } + + return enabled; } } @GuardedBy("mLock") public void onEnabledChangedLocked(int userId) { if (userId == UserHandle.USER_NULL) { - // only used during initialization - we don't care about the null user + // used during initialization - ignore since many lower level operations (checking + // settings for instance) do not support the null user + return; + } else if (userId == UserHandle.USER_ALL) { + // we know enabled changes can only happen for current users since providers are + // always disabled for all non-current users + for (int currentUserId : mUserInfoHelper.getCurrentUserIds()) { + onEnabledChangedLocked(currentUserId); + } return; } - // normalize user id to always refer to parent since profile state is always the same - // as parent state - userId = mUserInfoHelper.getParentUserId(userId); - // if any property that contributes to "enabled" here changes state, it MUST result // in a direct or indrect call to onEnabledChangedLocked. this allows the provider to // guarantee that it will always eventually reach the correct state. - boolean enabled = (userId == mUserInfoHelper.getCurrentUserId()) - && mSettingsHelper.isLocationEnabled(userId) && mProvider.getState().allowed; + boolean enabled = mProvider.getState().allowed + && mUserInfoHelper.isCurrentUserId(userId) + && mSettingsHelper.isLocationEnabled(userId); - if (enabled == isEnabled(userId)) { + Boolean wasEnabled = mEnabled.get(userId); + if (wasEnabled != null && wasEnabled == enabled) { return; } @@ -821,28 +942,29 @@ public class LocationManagerService extends ILocationManager.Stub { Log.d(TAG, "[u" + userId + "] " + mName + " provider enabled = " + enabled); } - // fused and passive provider never get public updates for legacy reasons - if (!FUSED_PROVIDER.equals(mName) && !PASSIVE_PROVIDER.equals(mName)) { - // update LOCATION_PROVIDERS_ALLOWED for best effort backwards compatibility - mSettingsHelper.setLocationProviderAllowed(mName, enabled, userId); - - Intent intent = new Intent(LocationManager.PROVIDERS_CHANGED_ACTION) - .putExtra(LocationManager.EXTRA_PROVIDER_NAME, mName) - .putExtra(LocationManager.EXTRA_PROVIDER_ENABLED, enabled) - .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); + // clear last locations if we become disabled and if not servicing a bypass request + if (!enabled && !mProvider.getCurrentRequest().locationSettingsIgnored) { + mLastLocation.put(userId, null); + mLastCoarseLocation.put(userId, null); } - if (!enabled) { - // If any provider has been disabled, clear all last locations for all - // providers. This is to be on the safe side in case a provider has location - // derived from this disabled provider. - mLastLocation.clear(); - mLastLocationCoarseInterval.clear(); + // update LOCATION_PROVIDERS_ALLOWED for best effort backwards compatibility + mSettingsHelper.setLocationProviderAllowed(mName, enabled, userId); + + // do not send change notifications if we just saw this user for the first time + if (wasEnabled != null) { + // fused and passive provider never get public updates for legacy reasons + if (!FUSED_PROVIDER.equals(mName) && !PASSIVE_PROVIDER.equals(mName)) { + Intent intent = new Intent(LocationManager.PROVIDERS_CHANGED_ACTION) + .putExtra(LocationManager.EXTRA_PROVIDER_NAME, mName) + .putExtra(LocationManager.EXTRA_PROVIDER_ENABLED, enabled) + .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); + } } - updateProviderEnabledLocked(this); + updateProviderEnabledLocked(this, enabled); } public void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) { @@ -855,7 +977,11 @@ public class LocationManagerService extends ILocationManager.Stub { pw.increaseIndent(); - pw.println("enabled=" + isEnabled()); + // for now we only dump for the parent user + int userId = mUserInfoHelper.getCurrentUserIds()[0]; + pw.println("last location=" + mLastLocation.get(userId)); + pw.println("last coarse location=" + mLastCoarseLocation.get(userId)); + pw.println("enabled=" + isEnabled(userId)); } mProvider.dump(fd, pw, args); @@ -1004,7 +1130,8 @@ public class LocationManagerService extends ILocationManager.Stub { if (manager == null) { continue; } - if (!manager.isEnabled() && !isSettingsExempt(updateRecord)) { + if (!manager.isEnabled(UserHandle.getUserId(mCallerIdentity.mUid)) + && !isSettingsExempt(updateRecord)) { continue; } @@ -1420,7 +1547,7 @@ public class LocationManagerService extends ILocationManager.Stub { if (FUSED_PROVIDER.equals(name)) { continue; } - if (enabledOnly && !manager.isEnabled()) { + if (enabledOnly && !manager.isEnabled(UserHandle.getCallingUserId())) { continue; } if (criteria != null @@ -1462,15 +1589,12 @@ public class LocationManagerService extends ILocationManager.Stub { } @GuardedBy("mLock") - private void updateProviderEnabledLocked(LocationProviderManager manager) { - boolean enabled = manager.isEnabled(); - + private void updateProviderEnabledLocked(LocationProviderManager manager, boolean enabled) { ArrayList<Receiver> deadReceivers = null; - ArrayList<UpdateRecord> records = mRecordsByProvider.get(manager.getName()); if (records != null) { for (UpdateRecord record : records) { - if (!mUserInfoHelper.isCurrentUserOrProfile( + if (!mUserInfoHelper.isCurrentUserId( UserHandle.getUserId(record.mReceiver.mCallerIdentity.mUid))) { continue; } @@ -1528,8 +1652,8 @@ public class LocationManagerService extends ILocationManager.Stub { // initialize the low power mode to true and set to false if any of the records requires providerRequest.setLowPowerMode(true); for (UpdateRecord record : records) { - if (!mUserInfoHelper.isCurrentUserOrProfile( - UserHandle.getUserId(record.mReceiver.mCallerIdentity.mUid))) { + int userId = UserHandle.getUserId(record.mReceiver.mCallerIdentity.mUid); + if (!mUserInfoHelper.isCurrentUserId(userId)) { continue; } if (!checkLocationAccess( @@ -1541,7 +1665,7 @@ public class LocationManagerService extends ILocationManager.Stub { } final boolean isBatterySaverDisablingLocation = shouldThrottleRequests || (isForegroundOnlyMode && !record.mIsForegroundUid); - if (!manager.isEnabled() || isBatterySaverDisablingLocation) { + if (!manager.isEnabled(userId) || isBatterySaverDisablingLocation) { if (isSettingsExempt(record)) { providerRequest.setLocationSettingsIgnored(true); providerRequest.setLowPowerMode(false); @@ -1587,7 +1711,7 @@ public class LocationManagerService extends ILocationManager.Stub { // TODO: overflow long thresholdInterval = (providerRequest.getInterval() + 1000) * 3 / 2; for (UpdateRecord record : records) { - if (mUserInfoHelper.isCurrentUserOrProfile( + if (mUserInfoHelper.isCurrentUserId( UserHandle.getUserId(record.mReceiver.mCallerIdentity.mUid))) { LocationRequest locationRequest = record.mRequest; @@ -1697,11 +1821,8 @@ public class LocationManagerService extends ILocationManager.Stub { mStackTrace = new Throwable(); } - ArrayList<UpdateRecord> records = mRecordsByProvider.get(provider); - if (records == null) { - records = new ArrayList<>(); - mRecordsByProvider.put(provider, records); - } + ArrayList<UpdateRecord> records = mRecordsByProvider.computeIfAbsent(provider, + k -> new ArrayList<>()); if (!records.contains(this)) { records.add(this); } @@ -1837,11 +1958,11 @@ public class LocationManagerService extends ILocationManager.Stub { break; } // throttle - if (sanitizedRequest.getInterval() < LocationFudger.FASTEST_INTERVAL_MS) { - sanitizedRequest.setInterval(LocationFudger.FASTEST_INTERVAL_MS); + if (sanitizedRequest.getInterval() < FASTEST_COARSE_INTERVAL_MS) { + sanitizedRequest.setInterval(FASTEST_COARSE_INTERVAL_MS); } - if (sanitizedRequest.getFastestInterval() < LocationFudger.FASTEST_INTERVAL_MS) { - sanitizedRequest.setFastestInterval(LocationFudger.FASTEST_INTERVAL_MS); + if (sanitizedRequest.getFastestInterval() < FASTEST_COARSE_INTERVAL_MS) { + sanitizedRequest.setFastestInterval(FASTEST_COARSE_INTERVAL_MS); } } // make getFastestInterval() the minimum of interval and fastest interval @@ -1916,7 +2037,7 @@ public class LocationManagerService extends ILocationManager.Stub { receiver = getReceiverLocked(listener, pid, uid, packageName, featureId, workSource, hideFromAppOps, listenerIdentifier); } - requestLocationUpdatesLocked(sanitizedRequest, receiver, uid, packageName); + requestLocationUpdatesLocked(sanitizedRequest, receiver); } finally { Binder.restoreCallingIdentity(identity); } @@ -1924,8 +2045,7 @@ public class LocationManagerService extends ILocationManager.Stub { } @GuardedBy("mLock") - private void requestLocationUpdatesLocked(LocationRequest request, Receiver receiver, - int uid, String packageName) { + private void requestLocationUpdatesLocked(LocationRequest request, Receiver receiver) { // Figure out the provider. Either its explicitly request (legacy use cases), or // use the fused provider if (request == null) request = DEFAULT_LOCATION_REQUEST; @@ -1940,20 +2060,14 @@ public class LocationManagerService extends ILocationManager.Stub { } UpdateRecord record = new UpdateRecord(name, request, receiver); - if (D) { - Log.d(TAG, "request " + Integer.toHexString(System.identityHashCode(receiver)) - + " " + name + " " + request + " from " + packageName + "(" + uid + " " - + (record.mIsForegroundUid ? "foreground" : "background") - + (isThrottlingExempt(receiver.mCallerIdentity) - ? " [whitelisted]" : "") + ")"); - } UpdateRecord oldRecord = receiver.mUpdateRecords.put(name, record); if (oldRecord != null) { oldRecord.disposeLocked(false); } - if (!manager.isEnabled() && !isSettingsExempt(record)) { + int userId = UserHandle.getUserId(receiver.mCallerIdentity.mUid); + if (!manager.isEnabled(userId) && !isSettingsExempt(record)) { // Notify the listener that updates are currently disabled - but only if the request // does not ignore location settings receiver.callProviderEnabledLocked(name, false); @@ -2030,91 +2144,65 @@ public class LocationManagerService extends ILocationManager.Stub { } @Override - public Location getLastLocation(LocationRequest r, String packageName, String featureId) { + public Location getLastLocation(LocationRequest request, String packageName, String featureId) { + if (request == null) { + request = DEFAULT_LOCATION_REQUEST; + } + enforceCallingOrSelfLocationPermission(); enforceCallingOrSelfPackageName(packageName); - synchronized (mLock) { - LocationRequest request = r != null ? r : DEFAULT_LOCATION_REQUEST; - int allowedResolutionLevel = getCallerAllowedResolutionLevel(); - // no need to sanitize this request, as only the provider name is used + int allowedResolutionLevel = getCallerAllowedResolutionLevel(); + if (!reportLocationAccessNoThrow(Binder.getCallingPid(), Binder.getCallingUid(), + packageName, featureId, allowedResolutionLevel, null)) { + if (D) { + Log.d(TAG, "not returning last loc for no op app: " + packageName); + } + return null; + } - final int pid = Binder.getCallingPid(); - final int uid = Binder.getCallingUid(); - final long identity = Binder.clearCallingIdentity(); - try { - if (mSettingsHelper.isLocationPackageBlacklisted(UserHandle.getUserId(uid), - packageName)) { - if (D) { - Log.d(TAG, "not returning last loc for blacklisted app: " - + packageName); - } - return null; - } + int userId = UserHandle.getCallingUserId(); - // Figure out the provider. Either its explicitly request (deprecated API's), - // or use the fused provider - String name = request.getProvider(); - if (name == null) name = LocationManager.FUSED_PROVIDER; - LocationProviderManager manager = getLocationProviderManager(name); - if (manager == null) return null; + if (mSettingsHelper.isLocationPackageBlacklisted(userId, packageName)) { + return null; + } - // only the current user or location providers may get location this way - if (!mUserInfoHelper.isCurrentUserOrProfile(UserHandle.getUserId(uid)) - && !mLocalService.isProviderPackage(packageName)) { - return null; - } + if (!mUserInfoHelper.isCurrentUserId(userId)) { + return null; + } - if (!manager.isEnabled()) { - return null; - } + synchronized (mLock) { + LocationProviderManager manager = getLocationProviderManager(request.getProvider()); + if (manager == null) { + return null; + } - Location location; - if (allowedResolutionLevel < RESOLUTION_LEVEL_FINE) { - // Make sure that an app with coarse permissions can't get frequent location - // updates by calling LocationManager.getLastKnownLocation repeatedly. - location = mLastLocationCoarseInterval.get(name); - } else { - location = mLastLocation.get(name); - } - if (location == null) { - return null; - } + if (!manager.isEnabled(userId) && !request.isLocationSettingsIgnored()) { + return null; + } - // Don't return stale location to apps with foreground-only location permission. - String op = resolutionLevelToOpStr(allowedResolutionLevel); - long locationAgeMs = TimeUnit.NANOSECONDS.toMillis( - SystemClock.elapsedRealtime() - location.getElapsedRealtimeNanos()); - if (locationAgeMs > mSettingsHelper.getMaxLastLocationAgeMs() - && (mAppOps.unsafeCheckOp(op, uid, packageName) - == AppOpsManager.MODE_FOREGROUND)) { - return null; - } + Location location; + if (allowedResolutionLevel < RESOLUTION_LEVEL_FINE) { + location = manager.getLastCoarseLocation(userId); + } else { + location = manager.getLastFineLocation(userId); + } + if (location == null) { + return null; + } - Location lastLocation = null; - if (allowedResolutionLevel < RESOLUTION_LEVEL_FINE) { - Location noGPSLocation = location.getExtraLocation( - Location.EXTRA_NO_GPS_LOCATION); - if (noGPSLocation != null) { - lastLocation = new Location(mLocationFudger.getOrCreate(noGPSLocation)); - } - } else { - lastLocation = new Location(location); - } - // Don't report location access if there is no last location to deliver. - if (lastLocation != null) { - if (!reportLocationAccessNoThrow(pid, uid, packageName, featureId, - allowedResolutionLevel, null)) { - if (D) { - Log.d(TAG, "not returning last loc for no op app: " + packageName); - } - lastLocation = null; - } - } - return lastLocation; - } finally { - Binder.restoreCallingIdentity(identity); + // Don't return stale location to apps with foreground-only location permission. + String op = resolutionLevelToOpStr(allowedResolutionLevel); + long locationAgeMs = NANOSECONDS.toMillis( + SystemClock.elapsedRealtime() - location.getElapsedRealtimeNanos()); + if (locationAgeMs > mSettingsHelper.getMaxLastLocationAgeMs() + && (mAppOps.unsafeCheckOp(op, Binder.getCallingUid(), packageName) + == AppOpsManager.MODE_FOREGROUND)) { + return null; } + + // make a defensive copy - the client could be in the same process as us + return new Location(location); } } @@ -2125,7 +2213,7 @@ public class LocationManagerService extends ILocationManager.Stub { // side effect of validating locationRequest and packageName Location lastLocation = getLastLocation(locationRequest, packageName, featureId); if (lastLocation != null) { - long locationAgeMs = TimeUnit.NANOSECONDS.toMillis( + long locationAgeMs = NANOSECONDS.toMillis( SystemClock.elapsedRealtimeNanos() - lastLocation.getElapsedRealtimeNanos()); if (locationAgeMs < MAX_CURRENT_LOCATION_AGE_MS) { @@ -2160,38 +2248,36 @@ public class LocationManagerService extends ILocationManager.Stub { @Override public LocationTime getGnssTimeMillis() { synchronized (mLock) { - Location location = mLastLocation.get(LocationManager.GPS_PROVIDER); + LocationProviderManager gpsManager = getLocationProviderManager(GPS_PROVIDER); + if (gpsManager == null) { + return null; + } + + Location location = gpsManager.getLastFineLocation(UserHandle.getCallingUserId()); if (location == null) { return null; } + long currentNanos = SystemClock.elapsedRealtimeNanos(); - long deltaMs = (currentNanos - location.getElapsedRealtimeNanos()) / 1000000L; + long deltaMs = NANOSECONDS.toMillis( + currentNanos - location.getElapsedRealtimeNanos()); return new LocationTime(location.getTime() + deltaMs, currentNanos); } } @Override - public boolean injectLocation(Location location) { + public void injectLocation(Location location) { mContext.enforceCallingPermission(android.Manifest.permission.LOCATION_HARDWARE, null); mContext.enforceCallingPermission(ACCESS_FINE_LOCATION, null); Preconditions.checkArgument(location.isComplete()); + int userId = UserHandle.getCallingUserId(); synchronized (mLock) { LocationProviderManager manager = getLocationProviderManager(location.getProvider()); - if (manager == null || !manager.isEnabled()) { - return false; + if (manager != null && manager.isEnabled(userId)) { + manager.injectLastLocation(Objects.requireNonNull(location), userId); } - - // NOTE: If last location is already available, location is not injected. If - // provider's normal source (like a GPS chipset) have already provided an output - // there is no need to inject this location. - if (mLastLocation.get(manager.getName()) != null) { - return false; - } - - updateLastLocationLocked(location, manager.getName()); - return true; } } @@ -2440,18 +2526,15 @@ public class LocationManagerService extends ILocationManager.Stub { } @Override - public boolean isProviderEnabledForUser(String providerName, int userId) { + public boolean isProviderEnabledForUser(String provider, int userId) { userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, false, false, "isProviderEnabledForUser", null); // Fused provider is accessed indirectly via criteria rather than the provider-based APIs, // so we discourage its use - if (FUSED_PROVIDER.equals(providerName)) return false; + if (FUSED_PROVIDER.equals(provider)) return false; - synchronized (mLock) { - LocationProviderManager manager = getLocationProviderManager(providerName); - return manager != null && manager.isEnabled(userId); - } + return mLocalService.isProviderEnabledForUser(provider, userId); } @GuardedBy("mLock") @@ -2464,7 +2547,7 @@ public class LocationManagerService extends ILocationManager.Stub { // Check whether sufficient time has passed long minTime = record.mRealRequest.getFastestInterval(); - long deltaMs = TimeUnit.NANOSECONDS.toMillis( + long deltaMs = NANOSECONDS.toMillis( loc.getElapsedRealtimeNanos() - lastLoc.getElapsedRealtimeNanos()); if (deltaMs < minTime - MAX_PROVIDER_SCHEDULING_JITTER_MS) { return false; @@ -2488,62 +2571,23 @@ public class LocationManagerService extends ILocationManager.Stub { } @GuardedBy("mLock") - private void handleLocationChangedLocked(Location location, LocationProviderManager manager) { + private void handleLocationChangedLocked(LocationProviderManager manager, Location location, + Location coarseLocation) { if (!mProviderManagers.contains(manager)) { Log.w(TAG, "received location from unknown provider: " + manager.getName()); return; } - if (!location.isComplete()) { - Log.w(TAG, "dropping incomplete location from " + manager.getName() + " provider: " - + location); - return; - } // notify passive provider if (manager != mPassiveManager) { - mPassiveManager.updateLocation(new Location(location)); + mPassiveManager.updateLocation(location); } - if (D) Log.d(TAG, "incoming location: " + location); long now = SystemClock.elapsedRealtime(); - - // only update last location for locations that come from enabled providers - if (manager.isEnabled()) { - updateLastLocationLocked(location, manager.getName()); - } - - // Update last known coarse interval location if enough time has passed. - Location lastLocationCoarseInterval = mLastLocationCoarseInterval.get( - manager.getName()); - if (lastLocationCoarseInterval == null) { - lastLocationCoarseInterval = new Location(location); - - if (manager.isEnabled()) { - mLastLocationCoarseInterval.put(manager.getName(), lastLocationCoarseInterval); - } - } - long timeDeltaMs = TimeUnit.NANOSECONDS.toMillis(location.getElapsedRealtimeNanos() - - lastLocationCoarseInterval.getElapsedRealtimeNanos()); - if (timeDeltaMs > LocationFudger.FASTEST_INTERVAL_MS) { - lastLocationCoarseInterval.set(location); - } - // Don't ever return a coarse location that is more recent than the allowed update - // interval (i.e. don't allow an app to keep registering and unregistering for - // location updates to overcome the minimum interval). - Location noGPSLocation = - lastLocationCoarseInterval.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION); - - // Skip if there are no UpdateRecords for this provider. ArrayList<UpdateRecord> records = mRecordsByProvider.get(manager.getName()); if (records == null || records.size() == 0) return; - // Fetch coarse location - Location coarseLocation = null; - if (noGPSLocation != null) { - coarseLocation = mLocationFudger.getOrCreate(noGPSLocation); - } - ArrayList<Receiver> deadReceivers = null; ArrayList<UpdateRecord> deadUpdateRecords = null; @@ -2551,22 +2595,23 @@ public class LocationManagerService extends ILocationManager.Stub { for (UpdateRecord r : records) { Receiver receiver = r.mReceiver; boolean receiverDead = false; + int userId = UserHandle.getUserId(receiver.mCallerIdentity.mUid); - if (!manager.isEnabled() && !isSettingsExempt(r)) { + + if (!manager.isEnabled(userId) && !isSettingsExempt(r)) { continue; } - int receiverUserId = UserHandle.getUserId(receiver.mCallerIdentity.mUid); - if (!mUserInfoHelper.isCurrentUserOrProfile(receiverUserId) + if (!mUserInfoHelper.isCurrentUserId(userId) && !isProviderPackage(receiver.mCallerIdentity.mPackageName)) { if (D) { - Log.d(TAG, "skipping loc update for background user " + receiverUserId + - " (app: " + receiver.mCallerIdentity.mPackageName + ")"); + Log.d(TAG, "skipping loc update for background user " + userId + + " (app: " + receiver.mCallerIdentity.mPackageName + ")"); } continue; } - if (mSettingsHelper.isLocationPackageBlacklisted(receiverUserId, + if (mSettingsHelper.isLocationPackageBlacklisted(userId, receiver.mCallerIdentity.mPackageName)) { if (D) { Log.d(TAG, "skipping loc update for blacklisted app: " + @@ -2581,39 +2626,30 @@ public class LocationManagerService extends ILocationManager.Stub { } else { notifyLocation = location; // use fine location } - if (notifyLocation != null) { - Location lastLoc = r.mLastFixBroadcast; - if ((lastLoc == null) - || shouldBroadcastSafeLocked(notifyLocation, lastLoc, r, now)) { - if (lastLoc == null) { - lastLoc = new Location(notifyLocation); - r.mLastFixBroadcast = lastLoc; - } else { - lastLoc.set(notifyLocation); - } - // Report location access before delivering location to the client. This will - // note location delivery to appOps, so it should be called only when a - // location is really being delivered to the client. - if (!reportLocationAccessNoThrow( - receiver.mCallerIdentity.mPid, - receiver.mCallerIdentity.mUid, - receiver.mCallerIdentity.mPackageName, - receiver.mCallerIdentity.mFeatureId, - receiver.mAllowedResolutionLevel, - "Location sent to " + receiver.mCallerIdentity.mListenerIdentifier)) { - if (D) { - Log.d(TAG, "skipping loc update for no op app: " - + receiver.mCallerIdentity.mPackageName); - } - continue; - } - if (!receiver.callLocationChangedLocked(notifyLocation)) { - Slog.w(TAG, "RemoteException calling onLocationChanged on " - + receiver); - receiverDead = true; + if (shouldBroadcastSafeLocked(notifyLocation, r.mLastFixBroadcast, r, now)) { + r.mLastFixBroadcast = notifyLocation; + // Report location access before delivering location to the client. This will + // note location delivery to appOps, so it should be called only when a + // location is really being delivered to the client. + if (!reportLocationAccessNoThrow( + receiver.mCallerIdentity.mPid, + receiver.mCallerIdentity.mUid, + receiver.mCallerIdentity.mPackageName, + receiver.mCallerIdentity.mFeatureId, + receiver.mAllowedResolutionLevel, + "Location sent to " + receiver.mCallerIdentity.mListenerIdentifier)) { + if (D) { + Log.d(TAG, "skipping loc update for no op app: " + + receiver.mCallerIdentity.mPackageName); } - r.mRealRequest.decrementNumUpdates(); + continue; } + if (!receiver.callLocationChangedLocked(notifyLocation)) { + Log.w(TAG, "RemoteException calling onLocationChanged on " + + receiver); + receiverDead = true; + } + r.mRealRequest.decrementNumUpdates(); } // track expired records @@ -2650,30 +2686,6 @@ public class LocationManagerService extends ILocationManager.Stub { } } - @GuardedBy("mLock") - private void updateLastLocationLocked(Location location, String provider) { - Location noGPSLocation = location.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION); - Location lastNoGPSLocation; - Location lastLocation = mLastLocation.get(provider); - if (lastLocation == null) { - lastLocation = new Location(provider); - mLastLocation.put(provider, lastLocation); - } else { - lastNoGPSLocation = lastLocation.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION); - if (noGPSLocation == null && lastNoGPSLocation != null) { - // New location has no no-GPS location: adopt last no-GPS location. This is set - // directly into location because we do not want to notify COARSE clients. - Bundle extras = location.getExtras(); - if (extras == null) { - extras = new Bundle(); - } - extras.putParcelable(Location.EXTRA_NO_GPS_LOCATION, lastNoGPSLocation); - location.setExtras(extras); - } - } - lastLocation.set(location); - } - // Geocoder @Override @@ -2743,8 +2755,6 @@ public class LocationManagerService extends ILocationManager.Stub { manager.setMockProvider(null); if (!manager.hasProvider()) { mProviderManagers.remove(manager); - mLastLocation.remove(manager.getName()); - mLastLocationCoarseInterval.remove(manager.getName()); } } } @@ -2854,8 +2864,8 @@ public class LocationManagerService extends ILocationManager.Stub { ipw.println("Historical Records by Provider:"); ipw.increaseIndent(); - TreeMap<PackageProviderKey, PackageStatistics> sorted = new TreeMap<>(); - sorted.putAll(mRequestStatistics.statistics); + TreeMap<PackageProviderKey, PackageStatistics> sorted = new TreeMap<>( + mRequestStatistics.statistics); for (Map.Entry<PackageProviderKey, PackageStatistics> entry : sorted.entrySet()) { PackageProviderKey key = entry.getKey(); @@ -2865,20 +2875,6 @@ public class LocationManagerService extends ILocationManager.Stub { mRequestStatistics.history.dump(ipw); - ipw.println("Last Known Locations:"); - ipw.increaseIndent(); - for (Map.Entry<String, Location> entry : mLastLocation.entrySet()) { - ipw.println(entry.getKey() + ": " + entry.getValue()); - } - ipw.decreaseIndent(); - - ipw.println("Last Known Coarse Locations:"); - ipw.increaseIndent(); - for (Map.Entry<String, Location> entry : mLastLocationCoarseInterval.entrySet()) { - ipw.println(entry.getKey() + ": " + entry.getValue()); - } - ipw.decreaseIndent(); - if (mGeofenceManager != null) { ipw.println("Geofences:"); ipw.increaseIndent(); @@ -2890,21 +2886,16 @@ public class LocationManagerService extends ILocationManager.Stub { ipw.println("Location Controller Extra Package: " + mExtraLocationControllerPackage + (mExtraLocationControllerPackageEnabled ? " [enabled]" : "[disabled]")); } + } - if (mLocationFudger != null) { - ipw.println("Location Fudger:"); - ipw.increaseIndent(); - mLocationFudger.dump(fd, ipw, args); - ipw.decreaseIndent(); - } - - ipw.println("Location Providers:"); - ipw.increaseIndent(); - for (LocationProviderManager manager : mProviderManagers) { - manager.dump(fd, ipw, args); - } - ipw.decreaseIndent(); + ipw.println("Location Providers:"); + ipw.increaseIndent(); + for (LocationProviderManager manager : mProviderManagers) { + manager.dump(fd, ipw, args); + } + ipw.decreaseIndent(); + synchronized (mLock) { if (mGnssManagerService != null) { ipw.println("GNSS:"); ipw.increaseIndent(); @@ -2929,6 +2920,18 @@ public class LocationManagerService extends ILocationManager.Stub { } @Override + public boolean isProviderEnabledForUser(@NonNull String provider, int userId) { + synchronized (mLock) { + LocationProviderManager manager = getLocationProviderManager(provider); + if (manager == null) { + return false; + } + + return manager.isEnabled(userId); + } + } + + @Override public boolean isProviderPackage(String packageName) { for (LocationProviderManager manager : mProviderManagers) { if (manager.getPackages().contains(packageName)) { diff --git a/services/core/java/com/android/server/location/LocationFudger.java b/services/core/java/com/android/server/location/LocationFudger.java index 04c7714f07f7..a069e7ace636 100644 --- a/services/core/java/com/android/server/location/LocationFudger.java +++ b/services/core/java/com/android/server/location/LocationFudger.java @@ -16,297 +16,165 @@ package com.android.server.location; -import android.content.Context; -import android.database.ContentObserver; +import android.annotation.Nullable; import android.location.Location; -import android.os.Bundle; -import android.os.Handler; import android.os.SystemClock; -import android.provider.Settings; -import android.util.Log; -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.security.SecureRandom; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import java.security.SecureRandom; +import java.time.Clock; +import java.util.Random; /** - * Contains the logic to obfuscate (fudge) locations for coarse applications. - * - * <p>The goal is just to prevent applications with only - * the coarse location permission from receiving a fine location. + * Contains the logic to obfuscate (fudge) locations for coarse applications. The goal is just to + * prevent applications with only the coarse location permission from receiving a fine location. */ public class LocationFudger { - private static final boolean D = false; - private static final String TAG = "LocationFudge"; - - /** - * Default coarse accuracy in meters. - */ - private static final float DEFAULT_ACCURACY_IN_METERS = 2000.0f; - - /** - * Minimum coarse accuracy in meters. - */ - private static final float MINIMUM_ACCURACY_IN_METERS = 200.0f; - - /** - * Secure settings key for coarse accuracy. - */ - private static final String COARSE_ACCURACY_CONFIG_NAME = "locationCoarseAccuracy"; - /** - * This is the fastest interval that applications can receive coarse - * locations. - */ - public static final long FASTEST_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes + // minimum accuracy a coarsened location can have + private static final float MIN_ACCURACY_M = 200.0f; - /** - * The duration until we change the random offset. - */ - private static final long CHANGE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour + // how often random offsets are updated + @VisibleForTesting + static final long OFFSET_UPDATE_INTERVAL_MS = 60 * 60 * 1000; - /** - * The percentage that we change the random offset at every interval. - * - * <p>0.0 indicates the random offset doesn't change. 1.0 - * indicates the random offset is completely replaced every interval. - */ + // the percentage that we change the random offset at every interval. 0.0 indicates the random + // offset doesn't change. 1.0 indicates the random offset is completely replaced every interval private static final double CHANGE_PER_INTERVAL = 0.03; // 3% change - // Pre-calculated weights used to move the random offset. - // - // The goal is to iterate on the previous offset, but keep - // the resulting standard deviation the same. The variance of - // two gaussian distributions summed together is equal to the - // sum of the variance of each distribution. So some quick - // algebra results in the following sqrt calculation to - // weigh in a new offset while keeping the final standard - // deviation unchanged. + // weights used to move the random offset. the goal is to iterate on the previous offset, but + // keep the resulting standard deviation the same. the variance of two gaussian distributions + // summed together is equal to the sum of the variance of each distribution. so some quick + // algebra results in the following sqrt calculation to weight in a new offset while keeping the + // final standard deviation unchanged. private static final double NEW_WEIGHT = CHANGE_PER_INTERVAL; - private static final double PREVIOUS_WEIGHT = Math.sqrt(1 - NEW_WEIGHT * NEW_WEIGHT); - - /** - * This number actually varies because the earth is not round, but - * 111,000 meters is considered generally acceptable. - */ - private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111000; - - /** - * Maximum latitude. - * - * <p>We pick a value 1 meter away from 90.0 degrees in order - * to keep cosine(MAX_LATITUDE) to a non-zero value, so that we avoid - * divide by zero fails. - */ - private static final double MAX_LATITUDE = 90.0 - - (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR); - - private final Object mLock = new Object(); - private final SecureRandom mRandom = new SecureRandom(); - - /** - * Used to monitor coarse accuracy secure setting for changes. - */ - private final ContentObserver mSettingsObserver; - - /** - * Used to resolve coarse accuracy setting. - */ - private final Context mContext; - - // all fields below protected by mLock - private double mOffsetLatitudeMeters; - private double mOffsetLongitudeMeters; - private long mNextInterval; - - /** - * Best location accuracy allowed for coarse applications. - * This value should only be set by {@link #setAccuracyInMetersLocked(float)}. - */ - private float mAccuracyInMeters; + private static final double OLD_WEIGHT = Math.sqrt(1 - NEW_WEIGHT * NEW_WEIGHT); + + // this number actually varies because the earth is not round, but 111,000 meters is considered + // generally acceptable + private static final int APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111_000; + + // we pick a value 1 meter away from 90.0 degrees in order to keep cosine(MAX_LATITUDE) to a + // non-zero value, so that we avoid divide by zero errors + private static final double MAX_LATITUDE = + 90.0 - (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR); + + private final float mAccuracyM; + private final Clock mClock; + private final Random mRandom; + + @GuardedBy("this") + private double mLatitudeOffsetM; + @GuardedBy("this") + private double mLongitudeOffsetM; + @GuardedBy("this") + private long mNextUpdateRealtimeMs; + + @GuardedBy("this") + @Nullable private Location mCachedFineLocation; + @GuardedBy("this") + @Nullable private Location mCachedCoarseLocation; + + public LocationFudger(float accuracyM) { + this(accuracyM, SystemClock.elapsedRealtimeClock(), new SecureRandom()); + } - /** - * The distance between grids for snap-to-grid. See {@link #createCoarse}. - * This value should only be set by {@link #setAccuracyInMetersLocked(float)}. - */ - private double mGridSizeInMeters; + @VisibleForTesting + LocationFudger(float accuracyM, Clock clock, Random random) { + mClock = clock; + mRandom = random; + mAccuracyM = Math.max(accuracyM, MIN_ACCURACY_M); - /** - * Standard deviation of the (normally distributed) random offset applied - * to coarse locations. It does not need to be as large as - * {@link #COARSE_ACCURACY_METERS} because snap-to-grid is the primary obfuscation - * method. See further details in the implementation. - * This value should only be set by {@link #setAccuracyInMetersLocked(float)}. - */ - private double mStandardDeviationInMeters; - - public LocationFudger(Context context, Handler handler) { - mContext = context; - mSettingsObserver = new ContentObserver(handler) { - @Override - public void onChange(boolean selfChange) { - setAccuracyInMeters(loadCoarseAccuracy()); - } - }; - mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor( - COARSE_ACCURACY_CONFIG_NAME), false, mSettingsObserver); - - float accuracy = loadCoarseAccuracy(); - synchronized (mLock) { - setAccuracyInMetersLocked(accuracy); - mOffsetLatitudeMeters = nextOffsetLocked(); - mOffsetLongitudeMeters = nextOffsetLocked(); - mNextInterval = SystemClock.elapsedRealtime() + CHANGE_INTERVAL_MS; - } + mLatitudeOffsetM = nextRandomOffset(); + mLongitudeOffsetM = nextRandomOffset(); + mNextUpdateRealtimeMs = mClock.millis() + OFFSET_UPDATE_INTERVAL_MS; } /** - * Get the cached coarse location, or generate a new one and cache it. - */ - public Location getOrCreate(Location location) { - synchronized (mLock) { - Location coarse = location.getExtraLocation(Location.EXTRA_COARSE_LOCATION); - if (coarse == null) { - return addCoarseLocationExtraLocked(location); - } - if (coarse.getAccuracy() < mAccuracyInMeters) { - return addCoarseLocationExtraLocked(location); + * Create a coarse location using two technique, random offsets and snap-to-grid. + * + * First we add a random offset to mitigate against detecting grid transitions. Without a random + * offset it is possible to detect a user's position quite accurately when they cross a grid + * boundary. The random offset changes very slowly over time, to mitigate against taking many + * location samples and averaging them out. Second we snap-to-grid (quantize). This has the nice + * property of producing stable results, and mitigating against taking many samples to average + * out a random offset. + */ + public Location createCoarse(Location fine) { + synchronized (this) { + if (fine == mCachedFineLocation) { + return new Location(mCachedCoarseLocation); } - return coarse; } - } - private Location addCoarseLocationExtraLocked(Location location) { - Location coarse = createCoarseLocked(location); - Bundle extras = location.getExtras(); - if (extras == null) { - extras = new Bundle(); - } - extras.putParcelable(Location.EXTRA_COARSE_LOCATION, coarse); - location.setExtras(extras); - return coarse; - } + // update the offsets in use + updateOffsets(); - /** - * Create a coarse location. - * - * <p>Two techniques are used: random offsets and snap-to-grid. - * - * <p>First we add a random offset. This mitigates against detecting - * grid transitions. Without a random offset it is possible to detect - * a users position very accurately when they cross a grid boundary. - * The random offset changes very slowly over time, to mitigate against - * taking many location samples and averaging them out. - * - * <p>Second we snap-to-grid (quantize). This has the nice property of - * producing stable results, and mitigating against taking many samples - * to average out a random offset. - */ - private Location createCoarseLocked(Location fine) { Location coarse = new Location(fine); - // clean all the optional information off the location, because - // this can leak detailed location information + // clear any fields that could leak more detailed location information coarse.removeBearing(); coarse.removeSpeed(); coarse.removeAltitude(); coarse.setExtras(null); - double lat = coarse.getLatitude(); - double lon = coarse.getLongitude(); + double latitude = wrapLatitude(coarse.getLatitude()); + double longitude = wrapLongitude(coarse.getLongitude()); - // wrap - lat = wrapLatitude(lat); - lon = wrapLongitude(lon); + // add offsets - update longitude first using the non-offset latitude + longitude += wrapLongitude(metersToDegreesLongitude(mLongitudeOffsetM, latitude)); + latitude += wrapLatitude(metersToDegreesLatitude(mLatitudeOffsetM)); - // Step 1) apply a random offset - // - // The goal of the random offset is to prevent the application - // from determining that the device is on a grid boundary - // when it crosses from one grid to the next. - // - // We apply the offset even if the location already claims to be - // inaccurate, because it may be more accurate than claimed. - updateRandomOffsetLocked(); - // perform lon first whilst lat is still within bounds - lon += metersToDegreesLongitude(mOffsetLongitudeMeters, lat); - lat += metersToDegreesLatitude(mOffsetLatitudeMeters); - if (D) Log.d(TAG, String.format("applied offset of %.0f, %.0f (meters)", - mOffsetLongitudeMeters, mOffsetLatitudeMeters)); - - // wrap - lat = wrapLatitude(lat); - lon = wrapLongitude(lon); - - // Step 2) Snap-to-grid (quantize) + // quantize location by snapping to a grid. this is the primary means of obfuscation. it + // gives nice consistent results and is very effective at hiding the true location (as long + // as you are not sitting on a grid boundary, which the random offsets mitigate). // - // This is the primary means of obfuscation. It gives nice consistent - // results and is very effective at hiding the true location - // (as long as you are not sitting on a grid boundary, which - // step 1 mitigates). - // - // Note we quantize the latitude first, since the longitude - // quantization depends on the latitude value and so leaks information - // about the latitude - double latGranularity = metersToDegreesLatitude(mGridSizeInMeters); - lat = Math.round(lat / latGranularity) * latGranularity; - double lonGranularity = metersToDegreesLongitude(mGridSizeInMeters, lat); - lon = Math.round(lon / lonGranularity) * lonGranularity; - - // wrap again - lat = wrapLatitude(lat); - lon = wrapLongitude(lon); - - // apply - coarse.setLatitude(lat); - coarse.setLongitude(lon); - coarse.setAccuracy(Math.max(mAccuracyInMeters, coarse.getAccuracy())); - - if (D) Log.d(TAG, "fudged " + fine + " to " + coarse); - return coarse; + // note that we quantize the latitude first, since the longitude quantization depends on the + // latitude value and so leaks information about the latitude + double latGranularity = metersToDegreesLatitude(mAccuracyM); + latitude = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity); + double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude); + longitude = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity); + + coarse.setLatitude(latitude); + coarse.setLongitude(longitude); + coarse.setAccuracy(Math.max(mAccuracyM, coarse.getAccuracy())); + + synchronized (this) { + mCachedFineLocation = fine; + mCachedCoarseLocation = coarse; + } + + return new Location(mCachedCoarseLocation); } /** - * Update the random offset over time. + * Update the random offsets over time. * - * <p>If the random offset was new for every location - * fix then an application can more easily average location results - * over time, - * especially when the location is near a grid boundary. On the - * other hand if the random offset is constant then if an application - * found a way to reverse engineer the offset they would be able - * to detect location at grid boundaries very accurately. So - * we choose a random offset and then very slowly move it, to - * make both approaches very hard. - * - * <p>The random offset does not need to be large, because snap-to-grid - * is the primary obfuscation mechanism. It just needs to be large - * enough to stop information leakage as we cross grid boundaries. - */ - private void updateRandomOffsetLocked() { - long now = SystemClock.elapsedRealtime(); - if (now < mNextInterval) { + * If the random offset was reset for every location fix then an application could more easily + * average location results over time, especially when the location is near a grid boundary. On + * the other hand if the random offset is constant then if an application finds a way to reverse + * engineer the offset they would be able to detect location at grid boundaries very accurately. + * So we choose a random offset and then very slowly move it, to make both approaches very hard. + * The random offset does not need to be large, because snap-to-grid is the primary obfuscation + * mechanism. It just needs to be large enough to stop information leakage as we cross grid + * boundaries. + */ + private synchronized void updateOffsets() { + long now = mClock.millis(); + if (now < mNextUpdateRealtimeMs) { return; } - if (D) Log.d(TAG, String.format("old offset: %.0f, %.0f (meters)", - mOffsetLongitudeMeters, mOffsetLatitudeMeters)); - - // ok, need to update the random offset - mNextInterval = now + CHANGE_INTERVAL_MS; - - mOffsetLatitudeMeters *= PREVIOUS_WEIGHT; - mOffsetLatitudeMeters += NEW_WEIGHT * nextOffsetLocked(); - mOffsetLongitudeMeters *= PREVIOUS_WEIGHT; - mOffsetLongitudeMeters += NEW_WEIGHT * nextOffsetLocked(); - - if (D) Log.d(TAG, String.format("new offset: %.0f, %.0f (meters)", - mOffsetLongitudeMeters, mOffsetLatitudeMeters)); + mLatitudeOffsetM = (OLD_WEIGHT * mLatitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset()); + mLongitudeOffsetM = (OLD_WEIGHT * mLongitudeOffsetM) + (NEW_WEIGHT * nextRandomOffset()); + mNextUpdateRealtimeMs = now + OFFSET_UPDATE_INTERVAL_MS; } - private double nextOffsetLocked() { - return mRandom.nextGaussian() * mStandardDeviationInMeters; + private double nextRandomOffset() { + return mRandom.nextGaussian() * (mAccuracyM / 4.0); } private static double wrapLatitude(double lat) { @@ -334,56 +202,8 @@ public class LocationFudger { return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR; } - /** - * Requires latitude since longitudinal distances change with distance from equator. - */ + // requires latitude since longitudinal distances change with distance from equator. private static double metersToDegreesLongitude(double distance, double lat) { return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat)); } - - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println(String.format("offset: %.0f, %.0f (meters)", mOffsetLongitudeMeters, - mOffsetLatitudeMeters)); - } - - /** - * This is the main control: call this to set the best location accuracy - * allowed for coarse applications and all derived values. - */ - private void setAccuracyInMetersLocked(float accuracyInMeters) { - mAccuracyInMeters = Math.max(accuracyInMeters, MINIMUM_ACCURACY_IN_METERS); - if (D) { - Log.d(TAG, "setAccuracyInMetersLocked: new accuracy = " + mAccuracyInMeters); - } - mGridSizeInMeters = mAccuracyInMeters; - mStandardDeviationInMeters = mGridSizeInMeters / 4.0; - } - - /** - * Same as setAccuracyInMetersLocked without the pre-lock requirement. - */ - private void setAccuracyInMeters(float accuracyInMeters) { - synchronized (mLock) { - setAccuracyInMetersLocked(accuracyInMeters); - } - } - - /** - * Loads the coarse accuracy value from secure settings. - */ - private float loadCoarseAccuracy() { - String newSetting = Settings.Secure.getString(mContext.getContentResolver(), - COARSE_ACCURACY_CONFIG_NAME); - if (D) { - Log.d(TAG, "loadCoarseAccuracy: newSetting = \"" + newSetting + "\""); - } - if (newSetting == null) { - return DEFAULT_ACCURACY_IN_METERS; - } - try { - return Float.parseFloat(newSetting); - } catch (NumberFormatException e) { - return DEFAULT_ACCURACY_IN_METERS; - } - } } diff --git a/services/core/java/com/android/server/location/MockableLocationProvider.java b/services/core/java/com/android/server/location/MockableLocationProvider.java index b0e133061fc6..f43669e488d1 100644 --- a/services/core/java/com/android/server/location/MockableLocationProvider.java +++ b/services/core/java/com/android/server/location/MockableLocationProvider.java @@ -237,6 +237,10 @@ public class MockableLocationProvider extends AbstractLocationProvider { * Dumps the current provider implementation. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + // holding the owner lock outside this could lead to deadlock since we don't run dump on the + // executor specified by the provider, we run it directly + Preconditions.checkState(!Thread.holdsLock(mOwnerLock)); + AbstractLocationProvider provider; synchronized (mOwnerLock) { provider = mProvider; diff --git a/services/core/java/com/android/server/location/SettingsHelper.java b/services/core/java/com/android/server/location/SettingsHelper.java index 6d1d1f901eb3..370a3f762d32 100644 --- a/services/core/java/com/android/server/location/SettingsHelper.java +++ b/services/core/java/com/android/server/location/SettingsHelper.java @@ -23,6 +23,7 @@ import static android.provider.Settings.Global.LOCATION_BACKGROUND_THROTTLE_PACK import static android.provider.Settings.Global.LOCATION_BACKGROUND_THROTTLE_PROXIMITY_ALERT_INTERVAL_MS; import static android.provider.Settings.Global.LOCATION_IGNORE_SETTINGS_PACKAGE_WHITELIST; import static android.provider.Settings.Global.LOCATION_LAST_LOCATION_MAX_AGE_MILLIS; +import static android.provider.Settings.Secure.LOCATION_COARSE_ACCURACY_M; import static android.provider.Settings.Secure.LOCATION_MODE; import static android.provider.Settings.Secure.LOCATION_MODE_OFF; @@ -88,6 +89,7 @@ public class SettingsHelper { private static final long DEFAULT_BACKGROUND_THROTTLE_PROXIMITY_ALERT_INTERVAL_MS = 30 * 60 * 1000; private static final long DEFAULT_MAX_LAST_LOCATION_AGE_MS = 20 * 60 * 1000; + private static final float DEFAULT_COARSE_LOCATION_ACCURACY_M = 2000.0f; private final Context mContext; @@ -268,19 +270,45 @@ public class SettingsHelper { * Retrieve the background throttling proximity alert interval. */ public long getBackgroundThrottleProximityAlertIntervalMs() { - return Settings.Global.getLong(mContext.getContentResolver(), - LOCATION_BACKGROUND_THROTTLE_PROXIMITY_ALERT_INTERVAL_MS, - DEFAULT_BACKGROUND_THROTTLE_PROXIMITY_ALERT_INTERVAL_MS); + long identity = Binder.clearCallingIdentity(); + try { + return Settings.Global.getLong(mContext.getContentResolver(), + LOCATION_BACKGROUND_THROTTLE_PROXIMITY_ALERT_INTERVAL_MS, + DEFAULT_BACKGROUND_THROTTLE_PROXIMITY_ALERT_INTERVAL_MS); + } finally { + Binder.restoreCallingIdentity(identity); + } } /** * Retrieve maximum age of the last location. */ public long getMaxLastLocationAgeMs() { - return Settings.Global.getLong( - mContext.getContentResolver(), - LOCATION_LAST_LOCATION_MAX_AGE_MILLIS, - DEFAULT_MAX_LAST_LOCATION_AGE_MS); + long identity = Binder.clearCallingIdentity(); + try { + return Settings.Global.getLong( + mContext.getContentResolver(), + LOCATION_LAST_LOCATION_MAX_AGE_MILLIS, + DEFAULT_MAX_LAST_LOCATION_AGE_MS); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + /** + * Retrieve the accuracy for coarsening location, ie, the grid size used for snap-to-grid + * coarsening. + */ + public float getCoarseLocationAccuracyM() { + long identity = Binder.clearCallingIdentity(); + try { + return Settings.Secure.getFloat( + mContext.getContentResolver(), + LOCATION_COARSE_ACCURACY_M, + DEFAULT_COARSE_LOCATION_ACCURACY_M); + } finally { + Binder.restoreCallingIdentity(identity); + } } /** diff --git a/services/core/java/com/android/server/location/UserInfoHelper.java b/services/core/java/com/android/server/location/UserInfoHelper.java index 94f3a88cb859..a33e2da4eb87 100644 --- a/services/core/java/com/android/server/location/UserInfoHelper.java +++ b/services/core/java/com/android/server/location/UserInfoHelper.java @@ -16,6 +16,9 @@ package com.android.server.location; +import static android.os.UserManager.DISALLOW_SHARE_LOCATION; + +import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; @@ -23,7 +26,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.UserInfo; import android.os.Binder; import android.os.Build; import android.os.UserHandle; @@ -36,6 +38,8 @@ import com.android.server.FgThread; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.concurrent.CopyOnWriteArrayList; @@ -47,15 +51,24 @@ public class UserInfoHelper { /** * Listener for current user changes. */ - public interface UserChangedListener { + public interface UserListener { + + int USER_SWITCHED = 1; + int USER_STARTED = 2; + int USER_STOPPED = 3; + + @IntDef({USER_SWITCHED, USER_STARTED, USER_STOPPED}) + @Retention(RetentionPolicy.SOURCE) + @interface UserChange {} + /** - * Called when the current user changes. + * Called when something has changed about the given user. */ - void onUserChanged(@UserIdInt int oldUserId, @UserIdInt int newUserId); + void onUserChanged(@UserIdInt int userId, @UserChange int change); } private final Context mContext; - private final CopyOnWriteArrayList<UserChangedListener> mListeners; + private final CopyOnWriteArrayList<UserListener> mListeners; @GuardedBy("this") @Nullable private UserManager mUserManager; @@ -86,6 +99,8 @@ public class UserInfoHelper { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_USER_SWITCHED); + intentFilter.addAction(Intent.ACTION_USER_STARTED); + intentFilter.addAction(Intent.ACTION_USER_STOPPED); intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); @@ -96,12 +111,24 @@ public class UserInfoHelper { if (action == null) { return; } + int userId; switch (action) { case Intent.ACTION_USER_SWITCHED: - int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - UserHandle.USER_NULL); + userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); + if (userId != UserHandle.USER_NULL) { + onCurrentUserChanged(userId); + } + break; + case Intent.ACTION_USER_STARTED: + userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); + if (userId != UserHandle.USER_NULL) { + onUserChanged(userId, UserListener.USER_STARTED); + } + break; + case Intent.ACTION_USER_STOPPED: + userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); if (userId != UserHandle.USER_NULL) { - onUserChanged(userId); + onUserChanged(userId, UserListener.USER_STOPPED); } break; case Intent.ACTION_MANAGED_PROFILE_ADDED: @@ -118,18 +145,18 @@ public class UserInfoHelper { /** * Adds a listener for user changed events. Callbacks occur on an unspecified thread. */ - public void addListener(UserChangedListener listener) { + public void addListener(UserListener listener) { mListeners.add(listener); } /** * Removes a listener for user changed events. */ - public void removeListener(UserChangedListener listener) { + public void removeListener(UserListener listener) { mListeners.remove(listener); } - private void onUserChanged(@UserIdInt int newUserId) { + private void onCurrentUserChanged(@UserIdInt int newUserId) { if (newUserId == mCurrentUserId) { return; } @@ -137,8 +164,13 @@ public class UserInfoHelper { int oldUserId = mCurrentUserId; mCurrentUserId = newUserId; - for (UserChangedListener listener : mListeners) { - listener.onUserChanged(oldUserId, newUserId); + onUserChanged(oldUserId, UserListener.USER_SWITCHED); + onUserChanged(newUserId, UserListener.USER_SWITCHED); + } + + private void onUserChanged(@UserIdInt int userId, @UserListener.UserChange int change) { + for (UserListener listener : mListeners) { + listener.onUserChanged(userId, change); } } @@ -151,53 +183,23 @@ public class UserInfoHelper { } /** - * Returns the user id of the current user. + * Returns an array of current user ids. This will always include the current user, and will + * also include any profiles of the current user. */ - @UserIdInt - public int getCurrentUserId() { - return mCurrentUserId; + public int[] getCurrentUserIds() { + return getProfileUserIdsForParentUser(mCurrentUserId); } /** * Returns true if the given user id is either the current user or a profile of the current * user. */ - public boolean isCurrentUserOrProfile(@UserIdInt int userId) { + public boolean isCurrentUserId(@UserIdInt int userId) { int currentUserId = mCurrentUserId; return userId == currentUserId || ArrayUtils.contains( getProfileUserIdsForParentUser(currentUserId), userId); } - /** - * Returns the parent user id of the given user id, or the user id itself if the user id either - * is a parent or has no profiles. - */ - @UserIdInt - public int getParentUserId(@UserIdInt int userId) { - synchronized (this) { - if (userId == mCachedParentUserId || ArrayUtils.contains(mCachedProfileUserIds, - userId)) { - return mCachedParentUserId; - } - - Preconditions.checkState(mUserManager != null); - } - - int parentUserId; - - long identity = Binder.clearCallingIdentity(); - try { - UserInfo userInfo = mUserManager.getProfileParent(userId); - parentUserId = userInfo != null ? userInfo.id : userId; - } finally { - Binder.restoreCallingIdentity(identity); - } - - // force profiles into cache - getProfileUserIdsForParentUser(parentUserId); - return parentUserId; - } - @GuardedBy("this") private synchronized int[] getProfileUserIdsForParentUser(@UserIdInt int parentUserId) { if (parentUserId != mCachedParentUserId) { @@ -225,8 +227,22 @@ public class UserInfoHelper { * Dump info for debugging. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - int currentUserId = mCurrentUserId; - pw.println("Current User: " + currentUserId + " " + Arrays.toString( - getProfileUserIdsForParentUser(currentUserId))); + boolean systemRunning; + synchronized (this) { + systemRunning = mUserManager != null; + } + + if (systemRunning) { + int[] currentUserIds = getProfileUserIdsForParentUser(mCurrentUserId); + pw.println("current users: " + Arrays.toString(currentUserIds)); + for (int userId : currentUserIds) { + if (mUserManager.hasUserRestrictionForUser(DISALLOW_SHARE_LOCATION, + UserHandle.of(userId))) { + pw.println(" u" + userId + " restricted"); + } + } + } else { + pw.println("current user: " + mCurrentUserId); + } } } diff --git a/services/core/java/com/android/server/location/gnss/GnssManagerService.java b/services/core/java/com/android/server/location/gnss/GnssManagerService.java index 02c1bddd8c00..87bb28e710c1 100644 --- a/services/core/java/com/android/server/location/gnss/GnssManagerService.java +++ b/services/core/java/com/android/server/location/gnss/GnssManagerService.java @@ -17,6 +17,7 @@ package com.android.server.location.gnss; import static android.app.AppOpsManager.OP_FINE_LOCATION; +import static android.location.LocationManager.GPS_PROVIDER; import android.Manifest; import android.annotation.NonNull; @@ -39,6 +40,7 @@ import android.os.IBinder; import android.os.IInterface; import android.os.Process; import android.os.RemoteException; +import android.os.UserHandle; import android.stats.location.LocationStatsEnums; import android.util.ArrayMap; import android.util.Log; @@ -588,18 +590,26 @@ public class GnssManagerService { */ public void onReportLocation(List<Location> locations) { IBatchedLocationCallback gnssBatchingCallback; + LinkedListener<IBatchedLocationCallback> gnssBatchingDeathCallback; synchronized (mGnssBatchingLock) { gnssBatchingCallback = mGnssBatchingCallback; + gnssBatchingDeathCallback = mGnssBatchingDeathCallback; } - if (gnssBatchingCallback == null) { + if (gnssBatchingCallback == null || gnssBatchingDeathCallback == null) { + return; + } + + int userId = UserHandle.getUserId(gnssBatchingDeathCallback.getCallerIdentity().mUid); + if (!mLocationManagerInternal.isProviderEnabledForUser(GPS_PROVIDER, userId)) { + Log.w(TAG, "reportLocationBatch() called without user permission"); return; } try { gnssBatchingCallback.onLocationBatch(locations); } catch (RemoteException e) { - Log.e(TAG, "mGnssBatchingCallback.onLocationBatch failed", e); + Log.e(TAG, "reportLocationBatch() failed", e); } } diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index 339ff6b8b526..ff34ebd8aa9d 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -24,6 +24,7 @@ android_test { "service-permission", "service-blobstore", "androidx.test.runner", + "androidx.test.ext.truth", "mockito-target-extended-minus-junit4", "platform-test-annotations", "truth-prebuilt", diff --git a/services/tests/mockingservicestests/src/com/android/server/location/LocationFudgerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/LocationFudgerTest.java new file mode 100644 index 000000000000..f2246dac01ca --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/location/LocationFudgerTest.java @@ -0,0 +1,165 @@ +/* + * 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; + +import static androidx.test.ext.truth.location.LocationSubject.assertThat; + +import static com.android.server.location.LocationUtils.createLocation; + +import static com.google.common.truth.Truth.assertThat; + +import android.location.Location; +import android.platform.test.annotations.Presubmit; +import android.util.Log; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Random; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class LocationFudgerTest { + + private static final String TAG = "LocationFudgerTest"; + + private static final double APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111_000; + private static final float ACCURACY_M = 2000; + private static final float MAX_COARSE_FUDGE_DISTANCE_M = + (float) Math.sqrt(2 * ACCURACY_M * ACCURACY_M); + + private Random mRandom; + + private LocationFudger mFudger; + + @Before + public void setUp() { + long seed = System.currentTimeMillis(); + Log.i(TAG, "location random seed: " + seed); + + mRandom = new Random(seed); + mFudger = new LocationFudger( + ACCURACY_M, + Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()), + mRandom); + } + + @Test + public void testCoarsen() { + // test that the coarsened location is not the same as the fine location and no leaks + for (int i = 0; i < 100; i++) { + Location fine = createLocation("test", mRandom); + fine.setBearing(1); + fine.setSpeed(1); + fine.setAltitude(1); + + Location coarse = mFudger.createCoarse(fine); + + assertThat(coarse).isNotNull(); + assertThat(coarse).isNotSameAs(fine); + assertThat(coarse.hasBearing()).isFalse(); + assertThat(coarse.hasSpeed()).isFalse(); + assertThat(coarse.hasAltitude()).isFalse(); + assertThat(coarse.getAccuracy()).isEqualTo(ACCURACY_M); + assertThat(coarse.distanceTo(fine)).isGreaterThan(1F); + assertThat(coarse).isNearby(fine, MAX_COARSE_FUDGE_DISTANCE_M); + } + } + + @Test + public void testCoarsen_Consistent() { + // test that coarsening the same location will always return the same coarse location + // (and thus that averaging to eliminate random noise won't work) + for (int i = 0; i < 100; i++) { + Location fine = createLocation("test", mRandom); + Location coarse = mFudger.createCoarse(fine); + assertThat(mFudger.createCoarse(new Location(fine))).isEqualTo(coarse); + assertThat(mFudger.createCoarse(new Location(fine))).isEqualTo(coarse); + } + } + + @Test + public void testCoarsen_AvgMany() { + // test that a set of locations normally distributed around the user's real location still + // cannot be easily average to reveal the user's real location + + int passed = 0; + int iterations = 100; + for (int j = 0; j < iterations; j++) { + Location fine = createLocation("test", mRandom); + + // generate a point cloud around a single location + ArrayList<Location> finePoints = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + finePoints.add(step(fine, mRandom.nextGaussian() * ACCURACY_M)); + } + + // generate the coarsened version of that point cloud + ArrayList<Location> coarsePoints = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + coarsePoints.add(mFudger.createCoarse(finePoints.get(i))); + } + + double avgFineLatitude = finePoints.stream().mapToDouble( + Location::getLatitude).average() + .orElseThrow(IllegalStateException::new); + double avgFineLongitude = finePoints.stream().mapToDouble( + Location::getLongitude).average() + .orElseThrow(IllegalStateException::new); + Location fineAvg = createLocation("test", avgFineLatitude, avgFineLongitude, 0); + + double avgCoarseLatitude = coarsePoints.stream().mapToDouble( + Location::getLatitude).average() + .orElseThrow(IllegalStateException::new); + double avgCoarseLongitude = coarsePoints.stream().mapToDouble( + Location::getLongitude).average() + .orElseThrow(IllegalStateException::new); + Location coarseAvg = createLocation("test", avgCoarseLatitude, avgCoarseLongitude, 0); + + if (coarseAvg.distanceTo(fine) > fineAvg.distanceTo(fine)) { + passed++; + } + } + + // very generally speaking, the closer the initial fine point is to a grid point, the more + // accurate the coarsened average will be. we use 70% as a lower bound by -very- roughly + // taking the area within a grid where we expect a reasonable percentage of points generated + // by step() to fall in another grid square. this likely doesn't have much mathematical + // validity, but it serves as a sanity test as least. + assertThat(passed / (double) iterations).isGreaterThan(.70); + } + + // step in a random direction by distance - assume cartesian + private Location step(Location input, double distanceM) { + double radians = mRandom.nextDouble() * 2 * Math.PI; + double deltaXM = Math.cos(radians) * distanceM; + double deltaYM = Math.sin(radians) * distanceM; + return createLocation("test", + input.getLatitude() + deltaXM / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR, + input.getLongitude() + deltaYM / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR, + 0); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/location/LocationUtils.java b/services/tests/mockingservicestests/src/com/android/server/location/LocationUtils.java new file mode 100644 index 000000000000..decb3a6e334a --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/location/LocationUtils.java @@ -0,0 +1,51 @@ +/* + * 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; + +import android.location.Location; +import android.os.SystemClock; + +import java.util.Random; + +public final class LocationUtils { + + private static final double MIN_LATITUDE = -90D; + private static final double MAX_LATITUDE = 90D; + private static final double MIN_LONGITUDE = -180D; + private static final double MAX_LONGITUDE = 180D; + + private static final float MIN_ACCURACY = 1; + private static final float MAX_ACCURACY = 100; + + public static Location createLocation(String provider, Random random) { + return createLocation(provider, + MIN_LATITUDE + random.nextDouble() * (MAX_LATITUDE - MIN_LATITUDE), + MIN_LONGITUDE + random.nextDouble() * (MAX_LONGITUDE - MIN_LONGITUDE), + MIN_ACCURACY + random.nextFloat() * (MAX_ACCURACY - MIN_ACCURACY)); + } + + public static Location createLocation(String provider, double latitude, double longitude, + float accuracy) { + Location location = new Location(provider); + location.setLatitude(latitude); + location.setLongitude(longitude); + location.setAccuracy(accuracy); + location.setTime(System.currentTimeMillis()); + location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); + return location; + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/location/UserInfoHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/location/UserInfoHelperTest.java index 389fdf9b0abc..71e79b331cae 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/UserInfoHelperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/UserInfoHelperTest.java @@ -26,6 +26,7 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.ActivityManager; @@ -43,6 +44,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.server.location.UserInfoHelper.UserListener; import org.junit.After; import org.junit.Before; @@ -117,62 +119,88 @@ public class UserInfoHelperTest { } } + private void startUser(int userId) { + Intent intent = new Intent(Intent.ACTION_USER_STARTED).putExtra(Intent.EXTRA_USER_HANDLE, + userId); + for (BroadcastReceiver broadcastReceiver : mBroadcastReceivers) { + broadcastReceiver.onReceive(mContext, intent); + } + } + + private void stopUser(int userId) { + Intent intent = new Intent(Intent.ACTION_USER_STOPPED).putExtra(Intent.EXTRA_USER_HANDLE, + userId); + for (BroadcastReceiver broadcastReceiver : mBroadcastReceivers) { + broadcastReceiver.onReceive(mContext, intent); + } + } + @Test - public void testListeners() { - UserInfoHelper.UserChangedListener listener = mock( - UserInfoHelper.UserChangedListener.class); + public void testListener_SwitchUser() { + UserListener listener = mock(UserListener.class); mHelper.addListener(listener); switchUser(USER1_ID); verify(listener, never()).onUserChanged(anyInt(), anyInt()); switchUser(USER2_ID); - verify(listener).onUserChanged(USER1_ID, USER2_ID); + verify(listener, times(1)).onUserChanged(USER1_ID, UserListener.USER_SWITCHED); + verify(listener, times(1)).onUserChanged(USER2_ID, UserListener.USER_SWITCHED); switchUser(USER1_ID); - verify(listener).onUserChanged(USER2_ID, USER1_ID); + verify(listener, times(2)).onUserChanged(USER1_ID, UserListener.USER_SWITCHED); + verify(listener, times(2)).onUserChanged(USER2_ID, UserListener.USER_SWITCHED); } @Test - public void testCurrentUser() { - assertThat(mHelper.getCurrentUserId()).isEqualTo(USER1_ID); + public void testListener_StartUser() { + UserListener listener = mock(UserListener.class); + mHelper.addListener(listener); - switchUser(USER2_ID); + startUser(USER1_ID); + verify(listener).onUserChanged(USER1_ID, UserListener.USER_STARTED); - assertThat(mHelper.getCurrentUserId()).isEqualTo(USER2_ID); + startUser(USER2_ID); + verify(listener).onUserChanged(USER2_ID, UserListener.USER_STARTED); + } - switchUser(USER1_ID); + @Test + public void testListener_StopUser() { + UserListener listener = mock(UserListener.class); + mHelper.addListener(listener); + + stopUser(USER1_ID); + verify(listener).onUserChanged(USER1_ID, UserListener.USER_STOPPED); - assertThat(mHelper.getCurrentUserId()).isEqualTo(USER1_ID); + stopUser(USER2_ID); + verify(listener).onUserChanged(USER2_ID, UserListener.USER_STOPPED); } @Test - public void testIsCurrentUserOrProfile() { - assertThat(mHelper.isCurrentUserOrProfile(USER1_ID)).isTrue(); - assertThat(mHelper.isCurrentUserOrProfile(USER1_MANAGED_ID)).isTrue(); - assertThat(mHelper.isCurrentUserOrProfile(USER2_ID)).isFalse(); - assertThat(mHelper.isCurrentUserOrProfile(USER2_MANAGED_ID)).isFalse(); + public void testCurrentUserIds() { + assertThat(mHelper.getCurrentUserIds()).isEqualTo(USER1_PROFILES); switchUser(USER2_ID); - assertThat(mHelper.isCurrentUserOrProfile(USER1_ID)).isFalse(); - assertThat(mHelper.isCurrentUserOrProfile(USER2_ID)).isTrue(); - assertThat(mHelper.isCurrentUserOrProfile(USER1_MANAGED_ID)).isFalse(); - assertThat(mHelper.isCurrentUserOrProfile(USER2_MANAGED_ID)).isTrue(); + assertThat(mHelper.getCurrentUserIds()).isEqualTo(USER2_PROFILES); + + switchUser(USER1_ID); + + assertThat(mHelper.getCurrentUserIds()).isEqualTo(USER1_PROFILES); } @Test - public void testGetParentUserId() { - assertThat(mHelper.getParentUserId(USER1_ID)).isEqualTo(USER1_ID); - assertThat(mHelper.getParentUserId(USER1_MANAGED_ID)).isEqualTo(USER1_ID); - assertThat(mHelper.getParentUserId(USER2_ID)).isEqualTo(USER2_ID); - assertThat(mHelper.getParentUserId(USER2_MANAGED_ID)).isEqualTo(USER2_ID); + public void testIsCurrentUserId() { + assertThat(mHelper.isCurrentUserId(USER1_ID)).isTrue(); + assertThat(mHelper.isCurrentUserId(USER1_MANAGED_ID)).isTrue(); + assertThat(mHelper.isCurrentUserId(USER2_ID)).isFalse(); + assertThat(mHelper.isCurrentUserId(USER2_MANAGED_ID)).isFalse(); switchUser(USER2_ID); - assertThat(mHelper.getParentUserId(USER1_ID)).isEqualTo(USER1_ID); - assertThat(mHelper.getParentUserId(USER2_ID)).isEqualTo(USER2_ID); - assertThat(mHelper.getParentUserId(USER1_MANAGED_ID)).isEqualTo(USER1_ID); - assertThat(mHelper.getParentUserId(USER2_MANAGED_ID)).isEqualTo(USER2_ID); + assertThat(mHelper.isCurrentUserId(USER1_ID)).isFalse(); + assertThat(mHelper.isCurrentUserId(USER2_ID)).isTrue(); + assertThat(mHelper.isCurrentUserId(USER1_MANAGED_ID)).isFalse(); + assertThat(mHelper.isCurrentUserId(USER2_MANAGED_ID)).isTrue(); } } |