Allow LOCATION_BYPASS permission to be used for location

During an emergency call, allow the LOCATION_BYPASS permission to bypass
normal location permission, using a noteOp to OP_EMERGENCY_LOCATION
instead.

Bug: 301150056
Test: atest LocationProviderManagerTest
Change-Id: Ie52134caa840bc8b90d6b1aea30c255731463a66
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index f33bcb7..5b110e5 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -8,6 +8,13 @@
 }
 
 flag {
+    name: "enable_location_bypass"
+    namespace: "location"
+    description: "Enable location bypass feature"
+    bug: "301150056"
+}
+
+flag {
     name: "location_bypass"
     namespace: "location"
     description: "Enable location bypass appops behavior"
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index a608049..6e991b4 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -17,6 +17,7 @@
 package com.android.server.location;
 
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.LOCATION_BYPASS;
 import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
 import static android.app.compat.CompatChanges.isChangeEnabled;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
@@ -34,6 +35,7 @@
 
 import static com.android.server.location.LocationPermissions.PERMISSION_COARSE;
 import static com.android.server.location.LocationPermissions.PERMISSION_FINE;
+import static com.android.server.location.LocationPermissions.PERMISSION_NONE;
 import static com.android.server.location.eventlog.LocationEventLog.EVENT_LOG;
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
@@ -73,6 +75,7 @@
 import android.location.LocationProvider;
 import android.location.LocationRequest;
 import android.location.LocationTime;
+import android.location.flags.Flags;
 import android.location.provider.ForwardGeocodeRequest;
 import android.location.provider.IGeocodeCallback;
 import android.location.provider.IProviderRequestListener;
@@ -776,8 +779,19 @@
                 listenerId);
         int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(),
                 identity.getPid());
-        LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel,
-                PERMISSION_COARSE);
+        if (Flags.enableLocationBypass()) {
+            if (permissionLevel == PERMISSION_NONE) {
+                if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) {
+                    LocationPermissions.enforceLocationPermission(
+                            identity.getUid(), permissionLevel, PERMISSION_COARSE);
+                } else {
+                    permissionLevel = PERMISSION_FINE;
+                }
+            }
+        } else {
+            LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel,
+                    PERMISSION_COARSE);
+        }
 
         // clients in the system process must have an attribution tag set
         Preconditions.checkState(identity.getPid() != Process.myPid() || attributionTag != null);
@@ -805,8 +819,19 @@
                 listenerId);
         int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(),
                 identity.getPid());
-        LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel,
-                PERMISSION_COARSE);
+        if (Flags.enableLocationBypass()) {
+            if (permissionLevel == PERMISSION_NONE) {
+                if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) {
+                    LocationPermissions.enforceLocationPermission(
+                            identity.getUid(), permissionLevel, PERMISSION_COARSE);
+                } else {
+                    permissionLevel = PERMISSION_FINE;
+                }
+            }
+        } else {
+            LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel,
+                    PERMISSION_COARSE);
+        }
 
         // clients in the system process should have an attribution tag set
         if (identity.getPid() == Process.myPid() && attributionTag == null) {
@@ -830,8 +855,19 @@
                 AppOpsManager.toReceiverId(pendingIntent));
         int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(),
                 identity.getPid());
-        LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel,
-                PERMISSION_COARSE);
+        if (Flags.enableLocationBypass()) {
+            if (permissionLevel == PERMISSION_NONE) {
+                if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) {
+                    LocationPermissions.enforceLocationPermission(
+                            identity.getUid(), permissionLevel, PERMISSION_COARSE);
+                } else {
+                    permissionLevel = PERMISSION_FINE;
+                }
+            }
+        } else {
+            LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel,
+                    PERMISSION_COARSE);
+        }
 
         // clients in the system process must have an attribution tag set
         Preconditions.checkArgument(identity.getPid() != Process.myPid() || attributionTag != null);
@@ -982,8 +1018,19 @@
         CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
         int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(),
                 identity.getPid());
-        LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel,
-                PERMISSION_COARSE);
+        if (Flags.enableLocationBypass()) {
+            if (permissionLevel == PERMISSION_NONE) {
+                if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) {
+                    LocationPermissions.enforceLocationPermission(
+                            identity.getUid(), permissionLevel, PERMISSION_COARSE);
+                } else {
+                    permissionLevel = PERMISSION_FINE;
+                }
+            }
+        } else {
+            LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel,
+                    PERMISSION_COARSE);
+        }
 
         // clients in the system process must have an attribution tag set
         Preconditions.checkArgument(identity.getPid() != Process.myPid() || attributionTag != null);
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index 40e538b..542a29a 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -16,6 +16,7 @@
 
 package com.android.server.location.provider;
 
+import static android.Manifest.permission.LOCATION_BYPASS;
 import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
 import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
 import static android.app.compat.CompatChanges.isChangeEnabled;
@@ -51,6 +52,7 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.AlarmManager.OnAlarmListener;
+import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -66,6 +68,7 @@
 import android.location.LocationResult;
 import android.location.LocationResult.BadLocationException;
 import android.location.altitude.AltitudeConverter;
+import android.location.flags.Flags;
 import android.location.provider.IProviderRequestListener;
 import android.location.provider.ProviderProperties;
 import android.location.provider.ProviderRequest;
@@ -106,6 +109,7 @@
 import com.android.server.location.injector.AppForegroundHelper;
 import com.android.server.location.injector.AppForegroundHelper.AppForegroundListener;
 import com.android.server.location.injector.AppOpsHelper;
+import com.android.server.location.injector.EmergencyHelper;
 import com.android.server.location.injector.Injector;
 import com.android.server.location.injector.LocationPermissionsHelper;
 import com.android.server.location.injector.LocationPermissionsHelper.LocationPermissionsListener;
@@ -375,8 +379,13 @@
         // we cache these values because checking/calculating on the fly is more expensive
         @GuardedBy("mMultiplexerLock")
         private boolean mPermitted;
+
+        @GuardedBy("mMultiplexerLock")
+        private boolean mBypassPermitted;
+
         @GuardedBy("mMultiplexerLock")
         private boolean mForeground;
+
         @GuardedBy("mMultiplexerLock")
         private LocationRequest mProviderLocationRequest;
         @GuardedBy("mMultiplexerLock")
@@ -421,8 +430,8 @@
             EVENT_LOG.logProviderClientRegistered(mName, getIdentity(), mBaseRequest);
 
             // initialization order is important as there are ordering dependencies
-            mPermitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel,
-                    getIdentity());
+            onLocationPermissionsChanged();
+            onBypassLocationPermissionsChanged(mEmergencyHelper.isInEmergency(0));
             mForeground = mAppForegroundHelper.isAppForeground(getIdentity().getUid());
             mProviderLocationRequest = calculateProviderLocationRequest();
             mIsUsingHighPower = isUsingHighPower();
@@ -491,7 +500,13 @@
 
         public final boolean isPermitted() {
             synchronized (mMultiplexerLock) {
-                return mPermitted;
+                return mPermitted || mBypassPermitted;
+            }
+        }
+
+        public final boolean isOnlyBypassPermitted() {
+            synchronized (mMultiplexerLock) {
+                return mBypassPermitted && !mPermitted;
             }
         }
 
@@ -562,6 +577,33 @@
             }
         }
 
+        boolean onBypassLocationPermissionsChanged(boolean isInEmergency) {
+            synchronized (mMultiplexerLock) {
+                boolean bypassPermitted =
+                        Flags.enableLocationBypass() && isInEmergency
+                                && mContext.checkPermission(
+                                LOCATION_BYPASS, mIdentity.getPid(), mIdentity.getUid())
+                                == PERMISSION_GRANTED;
+                if (mBypassPermitted != bypassPermitted) {
+                    if (D) {
+                        Log.v(
+                                TAG,
+                                mName
+                                        + " provider package "
+                                        + getIdentity().getPackageName()
+                                        + " bypass permitted = "
+                                        + bypassPermitted);
+                    }
+
+                    mBypassPermitted = bypassPermitted;
+
+                    return true;
+                }
+
+                return false;
+            }
+        }
+
         @GuardedBy("mMultiplexerLock")
         private boolean onLocationPermissionsChanged() {
             boolean permitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel,
@@ -941,8 +983,11 @@
             }
 
             // note app ops
-            if (!mAppOpsHelper.noteOpNoThrow(LocationPermissions.asAppOp(getPermissionLevel()),
-                    getIdentity())) {
+            int op =
+                    Flags.enableLocationBypass() && isOnlyBypassPermitted()
+                            ? AppOpsManager.OP_EMERGENCY_LOCATION
+                            : LocationPermissions.asAppOp(getPermissionLevel());
+            if (!mAppOpsHelper.noteOpNoThrow(op, getIdentity())) {
                 if (D) {
                     Log.w(TAG,
                             mName + " provider registration " + getIdentity() + " noteOp denied");
@@ -1292,12 +1337,17 @@
             }
 
             // lastly - note app ops
-            if (fineLocationResult != null && !mAppOpsHelper.noteOpNoThrow(
-                    LocationPermissions.asAppOp(getPermissionLevel()), getIdentity())) {
-                if (D) {
-                    Log.w(TAG, "noteOp denied for " + getIdentity());
+            if (fineLocationResult != null) {
+                int op =
+                        Flags.enableLocationBypass() && isOnlyBypassPermitted()
+                                ? AppOpsManager.OP_EMERGENCY_LOCATION
+                                : LocationPermissions.asAppOp(getPermissionLevel());
+                if (!mAppOpsHelper.noteOpNoThrow(op, getIdentity())) {
+                    if (D) {
+                        Log.w(TAG, "noteOp denied for " + getIdentity());
+                    }
+                    fineLocationResult = null;
                 }
-                fineLocationResult = null;
             }
 
             if (fineLocationResult != null) {
@@ -1399,6 +1449,7 @@
     protected final ScreenInteractiveHelper mScreenInteractiveHelper;
     protected final LocationUsageLogger mLocationUsageLogger;
     protected final LocationFudger mLocationFudger;
+    protected final EmergencyHelper mEmergencyHelper;
     private final PackageResetHelper mPackageResetHelper;
 
     private final UserListener mUserChangedListener = this::onUserChanged;
@@ -1434,6 +1485,8 @@
             this::onLocationPowerSaveModeChanged;
     private final ScreenInteractiveChangedListener mScreenInteractiveChangedListener =
             this::onScreenInteractiveChanged;
+    private final EmergencyHelper.EmergencyStateChangedListener mEmergencyStateChangedListener =
+            this::onEmergencyStateChanged;
     private final PackageResetHelper.Responder mPackageResetResponder =
             new PackageResetHelper.Responder() {
                 @Override
@@ -1507,6 +1560,7 @@
         mScreenInteractiveHelper = injector.getScreenInteractiveHelper();
         mLocationUsageLogger = injector.getLocationUsageLogger();
         mLocationFudger = new LocationFudger(mSettingsHelper.getCoarseLocationAccuracyM());
+        mEmergencyHelper = injector.getEmergencyHelper();
         mPackageResetHelper = injector.getPackageResetHelper();
 
         mProvider = new MockableLocationProvider(mMultiplexerLock);
@@ -1757,8 +1811,17 @@
 
         if (location != null) {
             // lastly - note app ops
-            if (!mAppOpsHelper.noteOpNoThrow(LocationPermissions.asAppOp(permissionLevel),
-                    identity)) {
+            int op =
+                    (Flags.enableLocationBypass()
+                            && !mLocationPermissionsHelper.hasLocationPermissions(
+                                    permissionLevel, identity)
+                            && mEmergencyHelper.isInEmergency(0)
+                            && mContext.checkPermission(
+                                    LOCATION_BYPASS, identity.getPid(), identity.getUid())
+                            == PERMISSION_GRANTED)
+                            ? AppOpsManager.OP_EMERGENCY_LOCATION
+                            : LocationPermissions.asAppOp(permissionLevel);
+            if (!mAppOpsHelper.noteOpNoThrow(op, identity)) {
                 return null;
             }
 
@@ -2069,6 +2132,9 @@
         mAppForegroundHelper.addListener(mAppForegroundChangedListener);
         mLocationPowerSaveModeHelper.addListener(mLocationPowerSaveModeChangedListener);
         mScreenInteractiveHelper.addListener(mScreenInteractiveChangedListener);
+        if (Flags.enableLocationBypass()) {
+            mEmergencyHelper.addOnEmergencyStateChangedListener(mEmergencyStateChangedListener);
+        }
         mPackageResetHelper.register(mPackageResetResponder);
     }
 
@@ -2088,6 +2154,9 @@
         mAppForegroundHelper.removeListener(mAppForegroundChangedListener);
         mLocationPowerSaveModeHelper.removeListener(mLocationPowerSaveModeChangedListener);
         mScreenInteractiveHelper.removeListener(mScreenInteractiveChangedListener);
+        if (Flags.enableLocationBypass()) {
+            mEmergencyHelper.removeOnEmergencyStateChangedListener(mEmergencyStateChangedListener);
+        }
         mPackageResetHelper.unregister(mPackageResetResponder);
     }
 
@@ -2466,6 +2535,12 @@
         }
     }
 
+    private void onEmergencyStateChanged() {
+        boolean inEmergency = mEmergencyHelper.isInEmergency(0);
+        updateRegistrations(
+                registration -> registration.onBypassLocationPermissionsChanged(inEmergency));
+    }
+
     private void onBackgroundThrottlePackageWhitelistChanged() {
         updateRegistrations(Registration::onProviderLocationRequestChanged);
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java
index ca73091..7f1a0bb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java
@@ -110,7 +110,7 @@
     }
 
     @Override
-    public EmergencyHelper getEmergencyHelper() {
+    public FakeEmergencyHelper getEmergencyHelper() {
         return mEmergencyHelper;
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
index 32878b3..0928264 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.location.provider;
 
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.LOCATION_BYPASS;
 import static android.app.AppOpsManager.OP_FINE_LOCATION;
 import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
 import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
@@ -1170,6 +1173,63 @@
     }
 
     @Test
+    public void testProviderRequest_IgnoreLocationSettings_LocationBypass() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LOCATION_BYPASS);
+
+        doReturn(PackageManager.PERMISSION_GRANTED)
+                .when(mContext)
+                .checkPermission(LOCATION_BYPASS, IDENTITY.getPid(), IDENTITY.getUid());
+        mInjector.getLocationPermissionsHelper()
+                .revokePermission(IDENTITY.getPackageName(), ACCESS_FINE_LOCATION);
+        mInjector.getLocationPermissionsHelper()
+                .revokePermission(IDENTITY.getPackageName(), ACCESS_COARSE_LOCATION);
+        mInjector
+                .getSettingsHelper()
+                .setIgnoreSettingsAllowlist(
+                        new PackageTagsList.Builder().add(IDENTITY.getPackageName()).build());
+
+        ILocationListener listener = createMockLocationListener();
+        LocationRequest request =
+                new LocationRequest.Builder(1)
+                        .setLocationSettingsIgnored(true)
+                        .setWorkSource(WORK_SOURCE)
+                        .build();
+        mManager.registerLocationRequest(request, IDENTITY, PERMISSION_FINE, listener);
+
+        assertThat(mProvider.getRequest().isActive()).isFalse();
+    }
+
+    @Test
+    public void testProviderRequest_IgnoreLocationSettings_LocationBypass_EmergencyCall() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LOCATION_BYPASS);
+
+        doReturn(PackageManager.PERMISSION_GRANTED)
+                .when(mContext)
+                .checkPermission(LOCATION_BYPASS, IDENTITY.getPid(), IDENTITY.getUid());
+        mInjector.getLocationPermissionsHelper()
+                .revokePermission(IDENTITY.getPackageName(), ACCESS_FINE_LOCATION);
+        mInjector.getLocationPermissionsHelper()
+                .revokePermission(IDENTITY.getPackageName(), ACCESS_COARSE_LOCATION);
+        mInjector.getEmergencyHelper().setInEmergency(true);
+        mInjector
+                .getSettingsHelper()
+                .setIgnoreSettingsAllowlist(
+                        new PackageTagsList.Builder().add(IDENTITY.getPackageName()).build());
+
+        ILocationListener listener = createMockLocationListener();
+        LocationRequest request =
+                new LocationRequest.Builder(1)
+                        .setLocationSettingsIgnored(true)
+                        .setWorkSource(WORK_SOURCE)
+                        .build();
+        mManager.registerLocationRequest(request, IDENTITY, PERMISSION_FINE, listener);
+
+        assertThat(mProvider.getRequest().isActive()).isTrue();
+        assertThat(mProvider.getRequest().getIntervalMillis()).isEqualTo(1);
+        assertThat(mProvider.getRequest().isLocationSettingsIgnored()).isTrue();
+    }
+
+    @Test
     public void testProviderRequest_BackgroundThrottle_IgnoreLocationSettings() {
         mInjector.getSettingsHelper().setIgnoreSettingsAllowlist(
                 new PackageTagsList.Builder().add(