Merge "Fix multisim issue with sim call manager."
diff --git a/src/com/android/phone/CallFeaturesSetting.java b/src/com/android/phone/CallFeaturesSetting.java
index 92af129..6c69a33 100644
--- a/src/com/android/phone/CallFeaturesSetting.java
+++ b/src/com/android/phone/CallFeaturesSetting.java
@@ -38,6 +38,7 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
+import android.telephony.ims.ProvisioningManager;
 import android.telephony.ims.feature.ImsFeature;
 import android.util.Log;
 import android.view.MenuItem;
@@ -214,10 +215,32 @@
         }
     };
 
+    private final ProvisioningManager.Callback mProvisioningCallback =
+            new ProvisioningManager.Callback() {
+        @Override
+        public void onProvisioningIntChanged(int item, int value) {
+            if (item == ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED
+                    || item == ImsConfig.ConfigConstants.VLT_SETTING_ENABLED
+                    || item == ImsConfig.ConfigConstants.LVC_SETTING_ENABLED) {
+                updateVtWfc();
+            }
+        }
+    };
+
     @Override
     protected void onPause() {
         super.onPause();
         listenPhoneState(false);
+
+        // Remove callback for provisioning changes.
+        try {
+            if (mImsMgr != null) {
+                mImsMgr.getConfigInterface().removeConfigCallback(
+                        mProvisioningCallback.getBinder());
+            }
+        } catch (ImsException e) {
+            Log.w(LOG_TAG, "onPause: Unable to remove callback for provisioning changes");
+        }
     }
 
     @Override
@@ -310,7 +333,24 @@
                 }
             }
         }
+        updateVtWfc();
 
+        // Register callback for provisioning changes.
+        try {
+            if (mImsMgr != null) {
+                mImsMgr.getConfigInterface().addConfigCallback(mProvisioningCallback);
+            }
+        } catch (ImsException e) {
+            Log.w(LOG_TAG, "onResume: Unable to register callback for provisioning changes.");
+        }
+    }
+
+    private void updateVtWfc() {
+        PreferenceScreen prefSet = getPreferenceScreen();
+        TelephonyManager telephonyManager = getSystemService(TelephonyManager.class)
+                .createForSubscriptionId(mPhone.getSubId());
+        PersistableBundle carrierConfig =
+                PhoneGlobals.getInstance().getCarrierConfigForSubId(mPhone.getSubId());
         if (mImsMgr.isVtEnabledByPlatform() && mImsMgr.isVtProvisionedOnDevice()
                 && (carrierConfig.getBoolean(
                         CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS)
@@ -320,6 +360,7 @@
                     ? mImsMgr.isVtEnabledByUser() : false;
             mEnableVideoCalling.setChecked(currentValue);
             mEnableVideoCalling.setOnPreferenceChangeListener(this);
+            prefSet.addPreference(mEnableVideoCalling);
         } else {
             prefSet.removePreference(mEnableVideoCalling);
         }
@@ -335,6 +376,7 @@
                     mButtonWifiCalling.setTitle(resolutions.get(0).loadLabel(pm));
                     mButtonWifiCalling.setSummary(null);
                     mButtonWifiCalling.setIntent(intent);
+                    prefSet.addPreference(mButtonWifiCalling);
                 } else {
                     prefSet.removePreference(mButtonWifiCalling);
                 }
@@ -363,6 +405,7 @@
                 }
             }
             mButtonWifiCalling.setSummary(resId);
+            prefSet.addPreference(mButtonWifiCalling);
         }
 
         try {
diff --git a/src/com/android/phone/MobileNetworkSettings.java b/src/com/android/phone/MobileNetworkSettings.java
index df0a527..a7da9db 100644
--- a/src/com/android/phone/MobileNetworkSettings.java
+++ b/src/com/android/phone/MobileNetworkSettings.java
@@ -57,6 +57,7 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.telephony.euicc.EuiccManager;
+import android.telephony.ims.ProvisioningManager;
 import android.telephony.ims.feature.ImsFeature;
 import android.text.TextUtils;
 import android.util.Log;
@@ -891,6 +892,18 @@
             }
         }
 
+        private final ProvisioningManager.Callback mProvisioningCallback =
+                new ProvisioningManager.Callback() {
+            @Override
+            public void onProvisioningIntChanged(int item, int value) {
+                if (item == ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED
+                        || item == ImsConfig.ConfigConstants.VLT_SETTING_ENABLED
+                        || item == ImsConfig.ConfigConstants.LVC_SETTING_ENABLED) {
+                    updateBody();
+                }
+            }
+        };
+
         @Override
         public void onDestroy() {
             super.onDestroy();
@@ -940,6 +953,15 @@
             context.getContentResolver().registerContentObserver(ENFORCE_MANAGED_URI, false,
                     mDpcEnforcedContentObserver);
 
+            // Register callback for provisioning changes.
+            try {
+                if (mImsMgr != null) {
+                    mImsMgr.getConfigInterface().addConfigCallback(mProvisioningCallback);
+                }
+            } catch (ImsException e) {
+                Log.w(LOG_TAG, "onResume: Unable to register callback for provisioning changes.");
+            }
+
             Log.i(LOG_TAG, "onResume:-");
 
         }
@@ -1332,6 +1354,17 @@
             final Context context = getActivity();
             context.unregisterReceiver(mPhoneChangeReceiver);
             context.getContentResolver().unregisterContentObserver(mDpcEnforcedContentObserver);
+
+            // Remove callback for provisioning changes.
+            try {
+                if (mImsMgr != null) {
+                    mImsMgr.getConfigInterface().removeConfigCallback(
+                            mProvisioningCallback.getBinder());
+                }
+            } catch (ImsException e) {
+                Log.w(LOG_TAG, "onPause: Unable to remove callback for provisioning changes");
+            }
+
             if (DBG) log("onPause:-");
         }
 
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index d080b31..13accc9 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -35,6 +35,7 @@
 import android.net.Uri;
 import android.os.AsyncResult;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -157,6 +158,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -1843,9 +1845,21 @@
     public Bundle getCellLocation(String callingPackage) {
         mApp.getSystemService(AppOpsManager.class)
                 .checkPackage(Binder.getCallingUid(), callingPackage);
-        if (!LocationAccessPolicy.canAccessCellLocation(mApp, callingPackage,
-                Binder.getCallingUid(), Binder.getCallingPid(), true)) {
-            return null;
+
+        LocationAccessPolicy.LocationPermissionResult locationResult =
+                LocationAccessPolicy.checkLocationPermission(mApp,
+                        new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                                .setCallingPackage(callingPackage)
+                                .setCallingPid(Binder.getCallingPid())
+                                .setCallingUid(Binder.getCallingUid())
+                                .setMethod("getCellLocation")
+                                .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+                                .build());
+        switch (locationResult) {
+            case DENIED_HARD:
+                throw new SecurityException("Not allowed to access cell location");
+            case DENIED_SOFT:
+                return new Bundle();
         }
 
         WorkSource workSource = getWorkSource(Binder.getCallingUid());
@@ -1995,9 +2009,21 @@
     public List<CellInfo> getAllCellInfo(String callingPackage) {
         mApp.getSystemService(AppOpsManager.class)
                 .checkPackage(Binder.getCallingUid(), callingPackage);
-        if (!LocationAccessPolicy.canAccessCellLocation(mApp,
-                callingPackage, Binder.getCallingUid(), Binder.getCallingPid(), true)) {
-            return null;
+
+        LocationAccessPolicy.LocationPermissionResult locationResult =
+                LocationAccessPolicy.checkLocationPermission(mApp,
+                        new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                                .setCallingPackage(callingPackage)
+                                .setCallingPid(Binder.getCallingPid())
+                                .setCallingUid(Binder.getCallingUid())
+                                .setMethod("getAllCellInfo")
+                                .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+                                .build());
+        switch (locationResult) {
+            case DENIED_HARD:
+                throw new SecurityException("Not allowed to access cell info");
+            case DENIED_SOFT:
+                return new ArrayList<>();
         }
 
         final int targetSdk = getTargetSdk(callingPackage);
@@ -2038,9 +2064,21 @@
             int subId, ICellInfoCallback cb, String callingPackage, WorkSource workSource) {
         mApp.getSystemService(AppOpsManager.class)
                 .checkPackage(Binder.getCallingUid(), callingPackage);
-        if (!LocationAccessPolicy.canAccessCellLocation(mApp, callingPackage,
-                Binder.getCallingUid(), Binder.getCallingPid(), true)) {
-            return;
+
+        LocationAccessPolicy.LocationPermissionResult locationResult =
+                LocationAccessPolicy.checkLocationPermission(mApp,
+                        new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                                .setCallingPackage(callingPackage)
+                                .setCallingPid(Binder.getCallingPid())
+                                .setCallingUid(Binder.getCallingUid())
+                                .setMethod("requestCellInfoUpdate")
+                                .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+                                .build());
+        switch (locationResult) {
+            case DENIED_HARD:
+                throw new SecurityException("Not allowed to access cell info");
+            case DENIED_SOFT:
+                return;
         }
 
         final Phone phone = getPhone(subId);
@@ -4189,9 +4227,24 @@
      * Scans for available networks.
      */
     @Override
-    public CellNetworkScanResult getCellNetworkScanResults(int subId) {
+    public CellNetworkScanResult getCellNetworkScanResults(int subId, String callingPackage) {
         TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
                 mApp, subId, "getCellNetworkScanResults");
+        LocationAccessPolicy.LocationPermissionResult locationResult =
+                LocationAccessPolicy.checkLocationPermission(mApp,
+                        new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                                .setCallingPackage(callingPackage)
+                                .setCallingPid(Binder.getCallingPid())
+                                .setCallingUid(Binder.getCallingUid())
+                                .setMethod("getCellNetworkScanResults")
+                                .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+                                .build());
+        switch (locationResult) {
+            case DENIED_HARD:
+                throw new SecurityException("Not allowed to access scan results -- location");
+            case DENIED_SOFT:
+                return null;
+        }
 
         long identity = Binder.clearCallingIdentity();
         try {
@@ -4214,17 +4267,29 @@
      */
     @Override
     public int requestNetworkScan(int subId, NetworkScanRequest request, Messenger messenger,
-            IBinder binder) {
+            IBinder binder, String callingPackage) {
         TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
                 mApp, subId, "requestNetworkScan");
 
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            return mNetworkScanRequestTracker.startNetworkScan(
-                    request, messenger, binder, getPhone(subId));
-        } finally {
-            Binder.restoreCallingIdentity(identity);
+        LocationAccessPolicy.LocationPermissionResult locationResult =
+                LocationAccessPolicy.checkLocationPermission(mApp,
+                        new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                                .setCallingPackage(callingPackage)
+                                .setCallingPid(Binder.getCallingPid())
+                                .setCallingUid(Binder.getCallingUid())
+                                .setMethod("requestNetworkScan")
+                                .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+                                .build());
+        switch (locationResult) {
+            case DENIED_HARD:
+                throw new SecurityException("Not allowed to request network scan -- location");
+            case DENIED_SOFT:
+                return -1;
         }
+
+        return mNetworkScanRequestTracker.startNetworkScan(
+                request, messenger, binder, getPhone(subId),
+                callingPackage);
     }
 
     /**
@@ -5265,6 +5330,31 @@
             return null;
         }
 
+        LocationAccessPolicy.LocationPermissionResult fineLocationResult =
+                LocationAccessPolicy.checkLocationPermission(mApp,
+                        new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                                .setCallingPackage(callingPackage)
+                                .setCallingPid(Binder.getCallingPid())
+                                .setCallingUid(Binder.getCallingUid())
+                                .setMethod("getServiceStateForSubscriber")
+                                .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+                                .build());
+
+        LocationAccessPolicy.LocationPermissionResult coarseLocationResult =
+                LocationAccessPolicy.checkLocationPermission(mApp,
+                        new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                                .setCallingPackage(callingPackage)
+                                .setCallingPid(Binder.getCallingPid())
+                                .setCallingUid(Binder.getCallingUid())
+                                .setMethod("getServiceStateForSubscriber")
+                                .setMinSdkVersionForCoarse(Build.VERSION_CODES.Q)
+                                .build());
+        // We don't care about hard or soft here -- all we need to know is how much info to scrub.
+        boolean hasFinePermission =
+                fineLocationResult == LocationAccessPolicy.LocationPermissionResult.ALLOWED;
+        boolean hasCoarsePermission =
+                coarseLocationResult == LocationAccessPolicy.LocationPermissionResult.ALLOWED;
+
         final long identity = Binder.clearCallingIdentity();
         try {
             final Phone phone = getPhone(subId);
@@ -5272,7 +5362,13 @@
                 return null;
             }
 
-            return phone.getServiceState();
+            ServiceState ss = phone.getServiceState();
+
+            // Scrub out the location info in ServiceState depending on what level of access
+            // the caller has.
+            if (hasFinePermission) return ss;
+            if (hasCoarsePermission) return ss.sanitizeLocationInfo(false);
+            return ss.sanitizeLocationInfo(true);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
@@ -5965,9 +6061,15 @@
 
     @Override
     public List<UiccCardInfo> getUiccCardsInfo(String callingPackage) {
-        if (checkCarrierPrivilegesForPackageAnyPhone(callingPackage)
-                != TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
-            throw new SecurityException("Caller does not have carrier privileges on any UICC.");
+        try {
+            enforceReadPrivilegedPermission("getUiccCardsInfo");
+        } catch (SecurityException e) {
+            // even without READ_PRIVILEGED_PHONE_STATE, we allow the call to continue if the caller
+            // has carrier privileges on an active UICC
+            if (checkCarrierPrivilegesForPackageAnyPhone(callingPackage)
+                        != TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) {
+                throw new SecurityException("Caller does not have carrier privileges on any UICC");
+            }
         }
 
         final long identity = Binder.clearCallingIdentity();
@@ -6542,4 +6644,24 @@
         metrics.updateEnabledModemBitmap((1 << TelephonyManager.from(mApp).getPhoneCount()) - 1);
     }
 
+    @Override
+    public int[] getSlotsMapping() {
+        enforceReadPrivilegedPermission("getSlotsMapping");
+
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            int phoneCount = TelephonyManager.getDefault().getPhoneCount();
+            // All logical slots should have a mapping to a physical slot.
+            int[] logicalSlotsMapping = new int[phoneCount];
+            UiccSlotInfo[] slotInfos = getUiccSlotsInfo();
+            for (int i = 0; i < slotInfos.length; i++) {
+                if (SubscriptionManager.isValidPhoneId(slotInfos[i].getLogicalSlotIdx())) {
+                    logicalSlotsMapping[slotInfos[i].getLogicalSlotIdx()] = i;
+                }
+            }
+            return logicalSlotsMapping;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
 }
diff --git a/testapps/ImsTestService/AndroidManifest.xml b/testapps/ImsTestService/AndroidManifest.xml
index 7662a42..eea54b8 100644
--- a/testapps/ImsTestService/AndroidManifest.xml
+++ b/testapps/ImsTestService/AndroidManifest.xml
@@ -19,6 +19,9 @@
           coreApp="true"
           package="com.android.phone.testapps.imstestapp">
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <!--Beware, declaring the below permission will cause the device to not boot unless you add
+        this app and permission to frameworks/base/data/etc/privapp-permissions-platform.xml-->
+    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
     <application
         android:label="ImsTestService"
         android:directBootAware="true">
diff --git a/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsRegistrationActivity.java b/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsRegistrationActivity.java
index e4a4e6f..84ec7b9 100644
--- a/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsRegistrationActivity.java
+++ b/testapps/ImsTestService/src/com/android/phone/testapps/imstestapp/ImsRegistrationActivity.java
@@ -19,6 +19,7 @@
 import android.app.Activity;
 import android.content.Context;
 import android.os.Bundle;
+import android.telephony.AccessNetworkConstants;
 import android.telephony.SubscriptionManager;
 import android.telephony.ims.ImsException;
 import android.telephony.ims.ImsMmTelManager;
@@ -150,8 +151,8 @@
     private static final Map<Integer, String> REG_TECH_STRING = new ArrayMap<>(2);
     static {
         REG_TECH_STRING.put(ImsRegistrationImplBase.REGISTRATION_TECH_NONE, "NONE");
-        REG_TECH_STRING.put(ImsRegistrationImplBase.REGISTRATION_TECH_LTE, "LTE");
-        REG_TECH_STRING.put(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, "IWLAN");
+        REG_TECH_STRING.put(AccessNetworkConstants.TransportType.WWAN, "WWAN");
+        REG_TECH_STRING.put(AccessNetworkConstants.TransportType.WLAN, "WLAN");
     }
 
 
diff --git a/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml b/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml
index 5145a63..6dd8bc2 100644
--- a/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml
+++ b/testapps/TelephonyManagerTestApp/res/layout/calling_method.xml
@@ -73,7 +73,6 @@
         android:layout_height="50dip">
     </Button>
 
-
     <ScrollView
         android:id="@+id/return_value_wrapper"
         android:layout_width="fill_parent"
diff --git a/testapps/TelephonyRegistryTestApp/AndroidManifest.xml b/testapps/TelephonyRegistryTestApp/AndroidManifest.xml
index 1cfd3ba..550c9f0 100644
--- a/testapps/TelephonyRegistryTestApp/AndroidManifest.xml
+++ b/testapps/TelephonyRegistryTestApp/AndroidManifest.xml
@@ -16,6 +16,9 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.phone.testapps.telephonyregistry">
+    <uses-sdk android:minSdkVersion="25"
+          android:targetSdkVersion="25"/>
+
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
     <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
diff --git a/testapps/TelephonyRegistryTestApp/src/com/android/phone/testapps/telephonyregistry/TelephonyRegistryTestApp.java b/testapps/TelephonyRegistryTestApp/src/com/android/phone/testapps/telephonyregistry/TelephonyRegistryTestApp.java
index 74cafcd..96f8bf7 100644
--- a/testapps/TelephonyRegistryTestApp/src/com/android/phone/testapps/telephonyregistry/TelephonyRegistryTestApp.java
+++ b/testapps/TelephonyRegistryTestApp/src/com/android/phone/testapps/telephonyregistry/TelephonyRegistryTestApp.java
@@ -24,6 +24,7 @@
 import android.telephony.CellInfo;
 import android.telephony.CellLocation;
 import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
 import android.util.SparseArray;
 import android.widget.Button;
@@ -76,6 +77,11 @@
             notify("onSrvccStateChanged", srvccState);
         }
 
+        @Override
+        public void onServiceStateChanged(ServiceState state) {
+            notify("onServiceStateChanged", state);
+        }
+
         private void notify(String method, Object data) {
             Notification.Builder builder = new Notification.Builder(TelephonyRegistryTestApp.this,
                     NOTIFICATION_CHANNEL);
diff --git a/tests/src/com/android/phone/LocationAccessPolicyTest.java b/tests/src/com/android/phone/LocationAccessPolicyTest.java
new file mode 100644
index 0000000..9938bf2
--- /dev/null
+++ b/tests/src/com/android/phone/LocationAccessPolicyTest.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2018 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.phone;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.os.Build;
+import android.os.UserHandle;
+import android.telephony.LocationAccessPolicy;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public class LocationAccessPolicyTest {
+    private static class Scenario {
+        static class Builder {
+            private int mAppSdkLevel;
+            private boolean mAppHasFineManifest = false;
+            private boolean mAppHasCoarseManifest = false;
+            private int mFineAppOp = AppOpsManager.MODE_IGNORED;
+            private int mCoarseAppOp = AppOpsManager.MODE_IGNORED;
+            private boolean mIsDynamicLocationEnabled;
+            private LocationAccessPolicy.LocationPermissionQuery mQuery;
+            private LocationAccessPolicy.LocationPermissionResult mExpectedResult;
+            private String mName;
+
+            public Builder setAppSdkLevel(int appSdkLevel) {
+                mAppSdkLevel = appSdkLevel;
+                return this;
+            }
+
+            public Builder setAppHasFineManifest(boolean appHasFineManifest) {
+                mAppHasFineManifest = appHasFineManifest;
+                return this;
+            }
+
+            public Builder setAppHasCoarseManifest(
+                    boolean appHasCoarseManifest) {
+                mAppHasCoarseManifest = appHasCoarseManifest;
+                return this;
+            }
+
+            public Builder setFineAppOp(int fineAppOp) {
+                mFineAppOp = fineAppOp;
+                return this;
+            }
+
+            public Builder setCoarseAppOp(int coarseAppOp) {
+                mCoarseAppOp = coarseAppOp;
+                return this;
+            }
+
+            public Builder setIsDynamicLocationEnabled(
+                    boolean isDynamicLocationEnabled) {
+                mIsDynamicLocationEnabled = isDynamicLocationEnabled;
+                return this;
+            }
+
+            public Builder setQuery(
+                    LocationAccessPolicy.LocationPermissionQuery query) {
+                mQuery = query;
+                return this;
+            }
+
+            public Builder setExpectedResult(
+                    LocationAccessPolicy.LocationPermissionResult expectedResult) {
+                mExpectedResult = expectedResult;
+                return this;
+            }
+
+            public Builder setName(String name) {
+                mName = name;
+                return this;
+            }
+
+            public Scenario build() {
+                return new Scenario(mAppSdkLevel, mAppHasFineManifest, mAppHasCoarseManifest,
+                        mFineAppOp, mCoarseAppOp, mIsDynamicLocationEnabled, mQuery,
+                        mExpectedResult, mName);
+            }
+        }
+        int appSdkLevel;
+        boolean appHasFineManifest;
+        boolean appHasCoarseManifest;
+        int fineAppOp;
+        int coarseAppOp;
+        boolean isDynamicLocationEnabled;
+        LocationAccessPolicy.LocationPermissionQuery query;
+        LocationAccessPolicy.LocationPermissionResult expectedResult;
+        String name;
+
+        private Scenario(int appSdkLevel, boolean appHasFineManifest, boolean appHasCoarseManifest,
+                int fineAppOp, int coarseAppOp,
+                boolean isDynamicLocationEnabled,
+                LocationAccessPolicy.LocationPermissionQuery query,
+                LocationAccessPolicy.LocationPermissionResult expectedResult,
+                String name) {
+            this.appSdkLevel = appSdkLevel;
+            this.appHasFineManifest = appHasFineManifest;
+            this.appHasCoarseManifest = appHasFineManifest || appHasCoarseManifest;
+            this.fineAppOp = fineAppOp;
+            this.coarseAppOp = coarseAppOp == AppOpsManager.MODE_ALLOWED ? coarseAppOp : fineAppOp;
+            this.isDynamicLocationEnabled = isDynamicLocationEnabled;
+            this.query = query;
+            this.expectedResult = expectedResult;
+            this.name = name;
+        }
+
+        @Override
+        public String toString() {
+            return name;
+        }
+    }
+
+    @Mock Context mContext;
+    @Mock AppOpsManager mAppOpsManager;
+    @Mock LocationManager mLocationManager;
+    @Mock PackageManager mPackageManager;
+    Scenario mScenario;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mockContextSystemService(AppOpsManager.class, mAppOpsManager);
+        mockContextSystemService(LocationManager.class, mLocationManager);
+        mockContextSystemService(PackageManager.class, mPackageManager);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+    }
+
+    private <T> void mockContextSystemService(Class<T> clazz , T obj) {
+        when(mContext.getSystemServiceName(eq(clazz))).thenReturn(clazz.getSimpleName());
+        when(mContext.getSystemService(clazz.getSimpleName())).thenReturn(obj);
+    }
+
+    public LocationAccessPolicyTest(Scenario scenario) {
+        mScenario = scenario;
+    }
+
+
+    @Test
+    public void test() {
+        setupScenario(mScenario);
+        assertEquals(mScenario.expectedResult,
+                LocationAccessPolicy.checkLocationPermission(mContext, mScenario.query));
+    }
+
+    private void setupScenario(Scenario s) {
+        when(mContext.checkPermission(eq(Manifest.permission.ACCESS_FINE_LOCATION),
+                anyInt(), anyInt())).thenReturn(s.appHasFineManifest
+                ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
+
+        when(mContext.checkPermission(eq(Manifest.permission.ACCESS_COARSE_LOCATION),
+                anyInt(), anyInt())).thenReturn(s.appHasCoarseManifest
+                ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
+
+        when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OP_FINE_LOCATION),
+                anyInt(), anyString()))
+                .thenReturn(s.fineAppOp);
+        when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OP_COARSE_LOCATION),
+                anyInt(), anyString()))
+                .thenReturn(s.coarseAppOp);
+
+        if (s.isDynamicLocationEnabled) {
+            when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class))).thenReturn(true);
+            when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL),
+                    anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_GRANTED);
+        } else {
+            when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class)))
+                    .thenReturn(false);
+            when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL),
+                    anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_DENIED);
+        }
+
+        ApplicationInfo fakeAppInfo = new ApplicationInfo();
+        fakeAppInfo.targetSdkVersion = s.appSdkLevel;
+
+        try {
+            when(mPackageManager.getApplicationInfo(anyString(), anyInt()))
+                    .thenReturn(fakeAppInfo);
+        } catch (Exception e) {
+            // this is a formality
+        }
+    }
+
+    private static LocationAccessPolicy.LocationPermissionQuery.Builder getDefaultQueryBuilder() {
+        return new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                .setMethod("test")
+                .setCallingPackage("com.android.test")
+                .setCallingPid(10001)
+                .setCallingUid(10001);
+    }
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Scenario> getScenarios() {
+        List<Scenario> scenarios = new ArrayList<>();
+        scenarios.add(new Scenario.Builder()
+                .setName("System location is off")
+                .setAppHasFineManifest(true)
+                .setFineAppOp(AppOpsManager.MODE_ALLOWED)
+                .setAppSdkLevel(Build.VERSION_CODES.P)
+                .setIsDynamicLocationEnabled(false)
+                .setQuery(getDefaultQueryBuilder()
+                        .setMinSdkVersionForFine(Build.VERSION_CODES.N)
+                        .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+                .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_SOFT)
+                .build());
+
+        scenarios.add(new Scenario.Builder()
+                .setName("App on latest SDK level has all proper permissions for fine")
+                .setAppHasFineManifest(true)
+                .setFineAppOp(AppOpsManager.MODE_ALLOWED)
+                .setAppSdkLevel(Build.VERSION_CODES.P)
+                .setIsDynamicLocationEnabled(true)
+                .setQuery(getDefaultQueryBuilder()
+                        .setMinSdkVersionForFine(Build.VERSION_CODES.N)
+                        .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+                .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+                .build());
+
+        scenarios.add(new Scenario.Builder()
+                .setName("App on older SDK level missing permissions for fine but has coarse")
+                .setAppHasCoarseManifest(true)
+                .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+                .setAppSdkLevel(Build.VERSION_CODES.JELLY_BEAN)
+                .setIsDynamicLocationEnabled(true)
+                .setQuery(getDefaultQueryBuilder()
+                        .setMinSdkVersionForFine(Build.VERSION_CODES.M)
+                        .setMinSdkVersionForCoarse(Build.VERSION_CODES.JELLY_BEAN).build())
+                .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+                .build());
+
+        scenarios.add(new Scenario.Builder()
+                .setName("App on latest SDK level missing fine app ops permission")
+                .setAppHasFineManifest(true)
+                .setFineAppOp(AppOpsManager.MODE_ERRORED)
+                .setAppSdkLevel(Build.VERSION_CODES.P)
+                .setIsDynamicLocationEnabled(true)
+                .setQuery(getDefaultQueryBuilder()
+                        .setMinSdkVersionForFine(Build.VERSION_CODES.N)
+                        .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+                .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
+                .build());
+
+        scenarios.add(new Scenario.Builder()
+                .setName("App has coarse permission but fine permission isn't being enforced yet")
+                .setAppHasCoarseManifest(true)
+                .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+                .setAppSdkLevel(LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1)
+                .setIsDynamicLocationEnabled(true)
+                .setQuery(getDefaultQueryBuilder()
+                        .setMinSdkVersionForFine(
+                                LocationAccessPolicy.MAX_SDK_FOR_ANY_ENFORCEMENT + 1)
+                        .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+                .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+                .build());
+
+        scenarios.add(new Scenario.Builder()
+                .setName("App on latest SDK level has coarse but missing fine when fine is req.")
+                .setAppHasCoarseManifest(true)
+                .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+                .setAppSdkLevel(Build.VERSION_CODES.P)
+                .setIsDynamicLocationEnabled(true)
+                .setQuery(getDefaultQueryBuilder()
+                        .setMinSdkVersionForFine(Build.VERSION_CODES.P)
+                        .setMinSdkVersionForCoarse(Build.VERSION_CODES.N).build())
+                .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
+                .build());
+
+        scenarios.add(new Scenario.Builder()
+                .setName("App on latest SDK level has MODE_IGNORED for app ops on fine")
+                .setAppHasCoarseManifest(true)
+                .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+                .setFineAppOp(AppOpsManager.MODE_IGNORED)
+                .setAppSdkLevel(Build.VERSION_CODES.P)
+                .setIsDynamicLocationEnabled(true)
+                .setQuery(getDefaultQueryBuilder()
+                        .setMinSdkVersionForFine(Build.VERSION_CODES.P)
+                        .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build())
+                .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.DENIED_HARD)
+                .build());
+
+        scenarios.add(new Scenario.Builder()
+                .setName("App has no permissions but it's sdk level grandfathers it in")
+                .setAppSdkLevel(Build.VERSION_CODES.N)
+                .setIsDynamicLocationEnabled(true)
+                .setQuery(getDefaultQueryBuilder()
+                        .setMinSdkVersionForFine(Build.VERSION_CODES.Q)
+                        .setMinSdkVersionForCoarse(Build.VERSION_CODES.O).build())
+                .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+                .build());
+
+        scenarios.add(new Scenario.Builder()
+                .setName("App on latest SDK level has proper permissions for coarse")
+                .setAppHasCoarseManifest(true)
+                .setCoarseAppOp(AppOpsManager.MODE_ALLOWED)
+                .setAppSdkLevel(Build.VERSION_CODES.P)
+                .setIsDynamicLocationEnabled(true)
+                .setQuery(getDefaultQueryBuilder()
+                        .setMinSdkVersionForCoarse(Build.VERSION_CODES.P).build())
+                .setExpectedResult(LocationAccessPolicy.LocationPermissionResult.ALLOWED)
+                .build());
+        return scenarios;
+    }
+}