ServiceStateProvider: sanitize location info if caller has no permission

Telephony#ServiceStateTable requires no read permission when
applications try to get the ServiceState for supported subscriptions.
This may leak location sensitive infos and bring security and
privacy concern.

The fix follows the same policy as TelephonyManager#getServiceState
and scrubs out location info from ServiceState when needed before
returning to clients.

Bug: 182384053
Test: atest com.android.phone.ServiceStateProviderTest
Test: atest android.telephonyprovider.cts.ServiceStateTest

Change-Id: I176fb3fbe02bcdc36b900c62f8a59344426f077b
diff --git a/src/com/android/phone/ServiceStateProvider.java b/src/com/android/phone/ServiceStateProvider.java
index 32562fa..08f0907 100644
--- a/src/com/android/phone/ServiceStateProvider.java
+++ b/src/com/android/phone/ServiceStateProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.phone;
 
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.provider.Telephony.ServiceStateTable;
 import static android.provider.Telephony.ServiceStateTable.CONTENT_URI;
 import static android.provider.Telephony.ServiceStateTable.DATA_NETWORK_TYPE;
@@ -26,6 +27,7 @@
 import static android.provider.Telephony.ServiceStateTable.getUriForSubscriptionId;
 import static android.provider.Telephony.ServiceStateTable.getUriForSubscriptionIdAndField;
 
+import android.Manifest;
 import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.Context;
@@ -33,16 +35,21 @@
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
 import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
 import android.os.Parcel;
+import android.telephony.LocationAccessPolicy;
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionManager;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.TelephonyPermissions;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * The class to provide base facility to access ServiceState related content,
@@ -223,7 +230,9 @@
     public static final String OPERATOR_ALPHA_SHORT_RAW = "operator_alpha_short_raw";
 
     private final HashMap<Integer, ServiceState> mServiceStates = new HashMap<>();
-    private static final String[] sColumns = {
+
+    @VisibleForTesting
+    /* package */ static final String[] ALL_COLUMNS = {
         VOICE_REG_STATE,
         DATA_REG_STATE,
         VOICE_ROAMING_TYPE,
@@ -252,6 +261,34 @@
         DUPLEX_MODE,
     };
 
+    /**
+     * Columns that are exposed to public surface.
+     * These are the columns accessible to apps target S+ and lack
+     * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE} permission.
+     */
+    @VisibleForTesting
+    /* package */ static final String[] PUBLIC_COLUMNS = {
+            VOICE_REG_STATE,
+            DATA_REG_STATE,
+            VOICE_OPERATOR_NUMERIC,
+            IS_MANUAL_NETWORK_SELECTION,
+            DATA_NETWORK_TYPE,
+            DUPLEX_MODE
+    };
+
+    /**
+     * Columns protected by location permissions (either FINE or COARSE).
+     * SecurityException will throw if applications without location permissions try to put those
+     * columns explicitly into cursor (e.g. through {@code projection} parameter in
+     * {@link #query(Uri, String[], String, String[], String)} method).
+     * Default (scrub-out) value will return if applications try to put all columns into cursor by
+     * specifying null of {@code projection} parameter and get values through the returned cursor.
+     */
+    private static final Set<String> LOCATION_PROTECTED_COLUMNS_SET = Set.of(
+            NETWORK_ID,
+            SYSTEM_ID
+    );
+
     @Override
     public boolean onCreate() {
         return true;
@@ -354,12 +391,55 @@
             }
 
             // Get the service state
-            ServiceState ss = getServiceState(subId);
-            if (ss == null) {
+            ServiceState unredactedServiceState = getServiceState(subId);
+            if (unredactedServiceState == null) {
                 Log.d(TAG, "returning null");
                 return null;
             }
 
+            final boolean targetingAtLeastS = TelephonyPermissions.getTargetSdk(getContext(),
+                    getCallingPackage()) >= Build.VERSION_CODES.S;
+            final boolean canReadPrivilegedPhoneState = getContext().checkCallingOrSelfPermission(
+                    Manifest.permission.READ_PRIVILEGED_PHONE_STATE) == PERMISSION_GRANTED;
+
+            final String[] availableColumns;
+            final ServiceState ss;
+            if (targetingAtLeastS && !canReadPrivilegedPhoneState) {
+                // targetSdkVersion S+ without read privileged phone state permission can only
+                // access public columns which have no location sensitive info.
+                availableColumns = PUBLIC_COLUMNS;
+                ss = unredactedServiceState;
+            } else {
+                availableColumns = ALL_COLUMNS;
+
+                final boolean hasLocationPermission =
+                        hasFineLocationPermission() || hasCoarseLocationPermission();
+                if (hasLocationPermission) {
+                    ss = unredactedServiceState;
+                } else {
+                    // The caller has no location permission but explicitly requires for location
+                    // protected columns. Throw SecurityException to fail loudly.
+                    if (projection != null) {
+                        for (String requiredColumn : projection) {
+                            if (LOCATION_PROTECTED_COLUMNS_SET.contains(requiredColumn)) {
+                                throw new SecurityException("Column " + requiredColumn
+                                        + "requires location permissions to access.");
+                            }
+                        }
+                    }
+
+                    // The caller has no location permission but only requires columns without
+                    // location sensitive info or "all" columns, return result that scrub out all
+                    // sensitive info. In later case, we will not know which columns will be fetched
+                    // from the returned cursor until the result has been returned.
+                    ss = unredactedServiceState.createLocationInfoSanitizedCopy(
+                            true /*removeCoarseLocation*/);
+                    // TODO(b/188061647): remove the additional redaction once it is fixed in SS
+                    ss.setCdmaSystemAndNetworkId(ServiceState.UNKNOWN_ID,
+                            ServiceState.UNKNOWN_ID);
+                }
+            }
+
             // Build the result
             final int voice_reg_state = ss.getState();
             final int data_reg_state = ss.getDataRegistrationState();
@@ -388,7 +468,8 @@
             final int data_network_type = ss.getDataNetworkType();
             final int duplex_mode = ss.getDuplexMode();
 
-            return buildSingleRowResult(projection, sColumns, new Object[] {
+            Object[] data = availableColumns == ALL_COLUMNS ? new Object[]{
+                    // data for all columns
                     voice_reg_state,
                     data_reg_state,
                     voice_roaming_type,
@@ -415,7 +496,17 @@
                     operator_alpha_short_raw,
                     data_network_type,
                     duplex_mode,
-            });
+            } : new Object[]{
+                    // data for public columns only
+                    voice_reg_state,
+                    data_reg_state,
+                    voice_operator_numeric,
+                    is_manual_network_selection,
+                    data_network_type,
+                    duplex_mode,
+            };
+
+            return buildSingleRowResult(projection, availableColumns, data);
         }
     }
 
@@ -569,4 +660,38 @@
         values.put(SERVICE_STATE, p.marshall());
         return values;
     }
+
+    private boolean hasFineLocationPermission() {
+        LocationAccessPolicy.LocationPermissionResult fineLocationResult =
+                LocationAccessPolicy.checkLocationPermission(getContext(),
+                        new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                                .setCallingPackage(getCallingPackage())
+                                .setCallingFeatureId(getCallingAttributionTag())
+                                .setCallingPid(Binder.getCallingPid())
+                                .setCallingUid(Binder.getCallingUid())
+                                .setMethod("ServiceStateProvider#query")
+                                .setLogAsInfo(true)
+                                .setMinSdkVersionForFine(Build.VERSION_CODES.S)
+                                .setMinSdkVersionForCoarse(Build.VERSION_CODES.S)
+                                .setMinSdkVersionForEnforcement(Build.VERSION_CODES.S)
+                                .build());
+        return fineLocationResult == LocationAccessPolicy.LocationPermissionResult.ALLOWED;
+    }
+
+    private boolean hasCoarseLocationPermission() {
+        LocationAccessPolicy.LocationPermissionResult coarseLocationResult =
+                LocationAccessPolicy.checkLocationPermission(getContext(),
+                        new LocationAccessPolicy.LocationPermissionQuery.Builder()
+                                .setCallingPackage(getCallingPackage())
+                                .setCallingFeatureId(getCallingAttributionTag())
+                                .setCallingPid(Binder.getCallingPid())
+                                .setCallingUid(Binder.getCallingUid())
+                                .setMethod("ServiceStateProvider#query")
+                                .setLogAsInfo(true)
+                                .setMinSdkVersionForCoarse(Build.VERSION_CODES.S)
+                                .setMinSdkVersionForFine(Integer.MAX_VALUE)
+                                .setMinSdkVersionForEnforcement(Build.VERSION_CODES.S)
+                                .build());
+        return coarseLocationResult == LocationAccessPolicy.LocationPermissionResult.ALLOWED;
+    }
 }
diff --git a/tests/src/com/android/phone/ServiceStateProviderTest.java b/tests/src/com/android/phone/ServiceStateProviderTest.java
index d85976a..76d6ecc 100644
--- a/tests/src/com/android/phone/ServiceStateProviderTest.java
+++ b/tests/src/com/android/phone/ServiceStateProviderTest.java
@@ -16,22 +16,44 @@
 
 package com.android.phone;
 
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.provider.Telephony.ServiceStateTable;
+import static android.provider.Telephony.ServiceStateTable.DATA_NETWORK_TYPE;
+import static android.provider.Telephony.ServiceStateTable.DATA_REG_STATE;
+import static android.provider.Telephony.ServiceStateTable.DUPLEX_MODE;
+import static android.provider.Telephony.ServiceStateTable.VOICE_OPERATOR_NUMERIC;
+import static android.provider.Telephony.ServiceStateTable.VOICE_REG_STATE;
 import static android.provider.Telephony.ServiceStateTable.getUriForSubscriptionId;
 import static android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_HOME;
 
+import static com.android.phone.ServiceStateProvider.DATA_ROAMING_TYPE;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+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.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
+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.content.pm.ProviderInfo;
 import android.database.ContentObserver;
 import android.database.Cursor;
+import android.location.LocationManager;
 import android.net.Uri;
+import android.os.Build;
+import android.os.UserHandle;
 import android.telephony.AccessNetworkConstants;
 import android.telephony.NetworkRegistrationInfo;
 import android.telephony.ServiceState;
@@ -40,8 +62,13 @@
 import android.test.mock.MockContentResolver;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 /**
  * Tests for simple queries of ServiceStateProvider.
@@ -53,41 +80,18 @@
  *         runtest --path tests/src/com/android/phone/ServiceStateProviderTest.java \
  *                 --test-method testGetServiceState
  */
+@RunWith(AndroidJUnit4.class)
 public class ServiceStateProviderTest {
     private static final String TAG = "ServiceStateProviderTest";
 
-    private Context mContext;
     private MockContentResolver mContentResolver;
     private ServiceState mTestServiceState;
     private ServiceState mTestServiceStateForSubId1;
 
-    private final String[] mTestProjection =
-    {
-        ServiceStateTable.VOICE_REG_STATE,
-        ServiceStateTable.DATA_REG_STATE,
-        ServiceStateProvider.VOICE_OPERATOR_ALPHA_LONG,
-        ServiceStateProvider.VOICE_OPERATOR_ALPHA_SHORT,
-        ServiceStateTable.VOICE_OPERATOR_NUMERIC,
-        ServiceStateProvider.DATA_OPERATOR_ALPHA_LONG,
-        ServiceStateProvider.DATA_OPERATOR_ALPHA_SHORT,
-        ServiceStateProvider.DATA_OPERATOR_NUMERIC,
-        ServiceStateTable.IS_MANUAL_NETWORK_SELECTION,
-        ServiceStateProvider.RIL_VOICE_RADIO_TECHNOLOGY,
-        ServiceStateProvider.RIL_DATA_RADIO_TECHNOLOGY,
-        ServiceStateProvider.CSS_INDICATOR,
-        ServiceStateProvider.NETWORK_ID,
-        ServiceStateProvider.SYSTEM_ID,
-        ServiceStateProvider.CDMA_ROAMING_INDICATOR,
-        ServiceStateProvider.CDMA_DEFAULT_ROAMING_INDICATOR,
-        ServiceStateProvider.CDMA_ERI_ICON_INDEX,
-        ServiceStateProvider.CDMA_ERI_ICON_MODE,
-        ServiceStateProvider.IS_EMERGENCY_ONLY,
-        ServiceStateProvider.IS_USING_CARRIER_AGGREGATION,
-        ServiceStateProvider.OPERATOR_ALPHA_LONG_RAW,
-        ServiceStateProvider.OPERATOR_ALPHA_SHORT_RAW,
-        ServiceStateTable.DATA_NETWORK_TYPE,
-        ServiceStateTable.DUPLEX_MODE,
-    };
+    @Mock Context mContext;
+    @Mock AppOpsManager mAppOpsManager;
+    @Mock LocationManager mLocationManager;
+    @Mock PackageManager mPackageManager;
 
     // Exception used internally to verify if the Resolver#notifyChange has been called.
     private class TestNotifierException extends RuntimeException {
@@ -98,7 +102,11 @@
 
     @Before
     public void setUp() throws Exception {
-        mContext = mock(Context.class);
+        MockitoAnnotations.initMocks(this);
+        mockSystemService(AppOpsManager.class, mAppOpsManager, Context.APP_OPS_SERVICE);
+        mockSystemService(LocationManager.class, mLocationManager, Context.LOCATION_SERVICE);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+
         mContentResolver = new MockContentResolver() {
             @Override
             public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
@@ -141,62 +149,165 @@
         providerInfo.authority = "service-state";
         provider.attachInfoForTesting(mContext, providerInfo);
         mContentResolver.addProvider("service-state", provider);
-    }
 
-    @Test
-    @SmallTest
-    public void testQueryServiceStateWithNoSubId() {
-        // Verify that when calling query with no subId in the uri the default ServiceState is
-        // returned.
-        // In this case the subId is set to 0 and the expected service state is
-        // mTestServiceState.
-        verifyServiceStateForSubId(ServiceStateTable.CONTENT_URI, mTestServiceState);
-    }
-
-    @Test
-    @SmallTest
-    public void testGetServiceStateWithDefaultSubId() {
-        // Verify that when calling with the DEFAULT_SUBSCRIPTION_ID the correct ServiceState is
-        // returned
-        // In this case the subId is set to 0 and the expected service state is
-        // mTestServiceState.
-        verifyServiceStateForSubId(
-                getUriForSubscriptionId(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID),
-                mTestServiceState);
+        // By default, test with app target R, no READ_PRIVILEGED_PHONE_STATE permission
+        setTargetSdkVersion(Build.VERSION_CODES.R);
+        setCanReadPrivilegedPhoneState(false);
     }
 
     /**
-     * Test querying the service state for a given subId
+     * Verify that when calling query with no subId in the uri the default ServiceState is returned.
+     * In this case the subId is set to 0 and the expected service state is mTestServiceState.
      */
     @Test
     @SmallTest
-    public void testGetServiceStateForSubId() {
-        // Verify that when calling with a specific subId the correct ServiceState is returned
-        // In this case the subId is set to 1 and the expected service state is
-        // mTestServiceStateForSubId1
-        verifyServiceStateForSubId(getUriForSubscriptionId(1), mTestServiceStateForSubId1);
+    public void testQueryServiceState_withNoSubId_withoutLocation() {
+        setLocationPermissions(false);
+
+        verifyServiceStateForSubId(ServiceStateTable.CONTENT_URI, mTestServiceState,
+                false /*hasLocation*/);
     }
 
-    private void verifyServiceStateForSubId(Uri uri, ServiceState ss) {
-        Cursor cursor = mContentResolver.query(uri, mTestProjection, "",
+    @Test
+    @SmallTest
+    public void testQueryServiceState_withNoSubId_withLocation() {
+        setLocationPermissions(true);
+
+        verifyServiceStateForSubId(ServiceStateTable.CONTENT_URI, mTestServiceState,
+                true /*hasLocation*/);
+    }
+
+    /**
+     * Verify that when calling with the DEFAULT_SUBSCRIPTION_ID the correct ServiceState is
+     * returned. In this case the subId is set to 0 and the expected service state is
+     * mTestServiceState.
+     */
+    @Test
+    @SmallTest
+    public void testGetServiceState_withDefaultSubId_withoutLocation() {
+        setLocationPermissions(false);
+
+        verifyServiceStateForSubId(
+                getUriForSubscriptionId(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID),
+                mTestServiceState, false /*hasLocation*/);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetServiceState_withDefaultSubId_withLocation() {
+        setLocationPermissions(true);
+
+        verifyServiceStateForSubId(
+                getUriForSubscriptionId(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID),
+                mTestServiceState, true /*hasLocation*/);
+    }
+
+    /**
+     * Verify that when calling with a specific subId the correct ServiceState is returned. In this
+     * case the subId is set to 1 and the expected service state is mTestServiceStateForSubId1
+     */
+    @Test
+    @SmallTest
+    public void testGetServiceStateForSubId_withoutLocation() {
+        setLocationPermissions(false);
+
+        verifyServiceStateForSubId(getUriForSubscriptionId(1), mTestServiceStateForSubId1,
+                false /*hasLocation*/);
+    }
+
+    @Test
+    @SmallTest
+    public void testGetServiceStateForSubId_withLocation() {
+        setLocationPermissions(true);
+
+        verifyServiceStateForSubId(getUriForSubscriptionId(1), mTestServiceStateForSubId1,
+                true /*hasLocation*/);
+    }
+
+    /**
+     * Verify that apps target S+ without READ_PRIVILEGED_PHONE_STATE permission can only access
+     * the public columns of ServiceStateTable.
+     */
+    @Test
+    public void testQueryAllColumns_targetS_noReadPrivilege() {
+        setTargetSdkVersion(Build.VERSION_CODES.S);
+        setCanReadPrivilegedPhoneState(false);
+
+        verifyServiceStateWithPublicColumns(mTestServiceState, null /*projection*/);
+    }
+
+    /**
+     * Verify that apps target S+ without READ_PRIVILEGED_PHONE_STATE permission try to access
+     * non-public columns should throw IllegalArgumentException.
+     */
+    @Test
+    public void testQueryNonPublicColumn_targetS_noReadPrivilege() {
+        setTargetSdkVersion(Build.VERSION_CODES.S);
+        setCanReadPrivilegedPhoneState(false);
+
+        // DATA_ROAMING_TYPE is a non-public column
+        String[] projection = new String[]{DATA_ROAMING_TYPE};
+
+        assertThrows(IllegalArgumentException.class,
+                () -> verifyServiceStateWithPublicColumns(mTestServiceState, projection));
+    }
+
+    /**
+     * Verify that apps target S+ with READ_PRIVILEGED_PHONE_STATE and location permissions should
+     * be able to access all columns.
+     */
+    @Test
+    public void testQueryAllColumn_targetS_withAllPermission() {
+        setTargetSdkVersion(Build.VERSION_CODES.S);
+        setCanReadPrivilegedPhoneState(true);
+        setLocationPermissions(true);
+
+        verifyServiceStateForSubId(
+                getUriForSubscriptionId(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID),
+                mTestServiceState, true /*hasPermission*/);
+    }
+
+    private void verifyServiceStateWithPublicColumns(ServiceState ss, String[] projection) {
+        try (Cursor cursor = mContentResolver.query(ServiceStateTable.CONTENT_URI, projection, null,
+                null)) {
+            assertNotNull(cursor);
+            assertEquals(cursor.getColumnCount(), ServiceStateProvider.PUBLIC_COLUMNS.length);
+
+            cursor.moveToFirst();
+            assertEquals(ss.getVoiceRegState(),
+                    cursor.getInt(cursor.getColumnIndex(VOICE_REG_STATE)));
+            assertEquals(ss.getDataRegistrationState(),
+                    cursor.getInt(cursor.getColumnIndex(DATA_REG_STATE)));
+            assertEquals(ss.getOperatorNumeric(),
+                    cursor.getString(cursor.getColumnIndex(VOICE_OPERATOR_NUMERIC)));
+            assertEquals(ss.getDataNetworkType(),
+                    cursor.getInt(cursor.getColumnIndex(DATA_NETWORK_TYPE)));
+            assertEquals(ss.getDuplexMode(), cursor.getInt(cursor.getColumnIndex(DUPLEX_MODE)));
+        }
+    }
+
+    private void verifyServiceStateForSubId(Uri uri, ServiceState ss, boolean hasLocation) {
+        Cursor cursor = mContentResolver.query(uri, ServiceStateProvider.ALL_COLUMNS, "",
                 null, null);
         assertNotNull(cursor);
         cursor.moveToFirst();
 
         final int voiceRegState = ss.getState();
         final int dataRegState = ss.getDataRegistrationState();
-        final String voiceOperatorAlphaLong = ss.getOperatorAlphaLong();
-        final String voiceOperatorAlphaShort = ss.getOperatorAlphaShort();
-        final String voiceOperatorNumeric = ss.getOperatorNumeric();
-        final String dataOperatorAlphaLong = ss.getOperatorAlphaLong();
-        final String dataOperatorAlphaShort = ss.getOperatorAlphaShort();
-        final String dataOperatorNumeric = ss.getOperatorNumeric();
+        final int voiceRoamingType = ss.getVoiceRoamingType();
+        final int dataRoamingType = ss.getDataRoamingType();
+        final String voiceOperatorAlphaLong = hasLocation ? ss.getOperatorAlphaLong() : null;
+        final String voiceOperatorAlphaShort = hasLocation ? ss.getOperatorAlphaShort() : null;
+        final String voiceOperatorNumeric = hasLocation ? ss.getOperatorNumeric() : null;
+        final String dataOperatorAlphaLong = hasLocation ? ss.getOperatorAlphaLong() : null;
+        final String dataOperatorAlphaShort = hasLocation ? ss.getOperatorAlphaShort() : null;
+        final String dataOperatorNumeric = hasLocation ? ss.getOperatorNumeric() : null;
         final int isManualNetworkSelection = (ss.getIsManualSelection()) ? 1 : 0;
         final int rilVoiceRadioTechnology = ss.getRilVoiceRadioTechnology();
         final int rilDataRadioTechnology = ss.getRilDataRadioTechnology();
         final int cssIndicator = ss.getCssIndicator();
-        final int networkId = ss.getCdmaNetworkId();
-        final int systemId = ss.getCdmaSystemId();
+        final int networkId = hasLocation ? ss.getCdmaNetworkId() : ServiceState.UNKNOWN_ID;
+        final int systemId = hasLocation ? ss.getCdmaSystemId() : ServiceState.UNKNOWN_ID;
         final int cdmaRoamingIndicator = ss.getCdmaRoamingIndicator();
         final int cdmaDefaultRoamingIndicator = ss.getCdmaDefaultRoamingIndicator();
         final int cdmaEriIconIndex = ss.getCdmaEriIconIndex();
@@ -210,28 +321,30 @@
 
         assertEquals(voiceRegState, cursor.getInt(0));
         assertEquals(dataRegState, cursor.getInt(1));
-        assertEquals(voiceOperatorAlphaLong, cursor.getString(2));
-        assertEquals(voiceOperatorAlphaShort, cursor.getString(3));
-        assertEquals(voiceOperatorNumeric, cursor.getString(4));
-        assertEquals(dataOperatorAlphaLong, cursor.getString(5));
-        assertEquals(dataOperatorAlphaShort, cursor.getString(6));
-        assertEquals(dataOperatorNumeric, cursor.getString(7));
-        assertEquals(isManualNetworkSelection, cursor.getInt(8));
-        assertEquals(rilVoiceRadioTechnology, cursor.getInt(9));
-        assertEquals(rilDataRadioTechnology, cursor.getInt(10));
-        assertEquals(cssIndicator, cursor.getInt(11));
-        assertEquals(networkId, cursor.getInt(12));
-        assertEquals(systemId, cursor.getInt(13));
-        assertEquals(cdmaRoamingIndicator, cursor.getInt(14));
-        assertEquals(cdmaDefaultRoamingIndicator, cursor.getInt(15));
-        assertEquals(cdmaEriIconIndex, cursor.getInt(16));
-        assertEquals(cdmaEriIconMode, cursor.getInt(17));
-        assertEquals(isEmergencyOnly, cursor.getInt(18));
-        assertEquals(isUsingCarrierAggregation, cursor.getInt(19));
-        assertEquals(operatorAlphaLongRaw, cursor.getString(20));
-        assertEquals(operatorAlphaShortRaw, cursor.getString(21));
-        assertEquals(dataNetworkType, cursor.getInt(22));
-        assertEquals(duplexMode, cursor.getInt(23));
+        assertEquals(voiceRoamingType, cursor.getInt(2));
+        assertEquals(dataRoamingType, cursor.getInt(3));
+        assertEquals(voiceOperatorAlphaLong, cursor.getString(4));
+        assertEquals(voiceOperatorAlphaShort, cursor.getString(5));
+        assertEquals(voiceOperatorNumeric, cursor.getString(6));
+        assertEquals(dataOperatorAlphaLong, cursor.getString(7));
+        assertEquals(dataOperatorAlphaShort, cursor.getString(8));
+        assertEquals(dataOperatorNumeric, cursor.getString(9));
+        assertEquals(isManualNetworkSelection, cursor.getInt(10));
+        assertEquals(rilVoiceRadioTechnology, cursor.getInt(11));
+        assertEquals(rilDataRadioTechnology, cursor.getInt(12));
+        assertEquals(cssIndicator, cursor.getInt(13));
+        assertEquals(networkId, cursor.getInt(14));
+        assertEquals(systemId, cursor.getInt(15));
+        assertEquals(cdmaRoamingIndicator, cursor.getInt(16));
+        assertEquals(cdmaDefaultRoamingIndicator, cursor.getInt(17));
+        assertEquals(cdmaEriIconIndex, cursor.getInt(18));
+        assertEquals(cdmaEriIconMode, cursor.getInt(19));
+        assertEquals(isEmergencyOnly, cursor.getInt(20));
+        assertEquals(isUsingCarrierAggregation, cursor.getInt(21));
+        assertEquals(operatorAlphaLongRaw, cursor.getString(22));
+        assertEquals(operatorAlphaShortRaw, cursor.getString(23));
+        assertEquals(dataNetworkType, cursor.getInt(24));
+        assertEquals(duplexMode, cursor.getInt(25));
     }
 
     /**
@@ -370,4 +483,50 @@
         }
         return false;
     }
+
+    private void setLocationPermissions(boolean hasPermission) {
+        if (!hasPermission) {
+            // System location off, LocationAccessPolicy#checkLocationPermission returns DENIED_SOFT
+            when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class)))
+                    .thenReturn(false);
+        } else {
+            // Turn on all to let LocationAccessPolicy#checkLocationPermission returns ALLOWED
+            when(mContext.checkPermission(eq(Manifest.permission.ACCESS_FINE_LOCATION),
+                    anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_GRANTED);
+
+            when(mContext.checkPermission(eq(Manifest.permission.ACCESS_COARSE_LOCATION),
+                    anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_GRANTED);
+
+            when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OPSTR_FINE_LOCATION),
+                    anyInt(), anyString(), nullable(String.class), nullable(String.class)))
+                    .thenReturn(AppOpsManager.MODE_ALLOWED);
+            when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OPSTR_COARSE_LOCATION),
+                    anyInt(), anyString(), nullable(String.class), nullable(String.class)))
+                    .thenReturn(AppOpsManager.MODE_ALLOWED);
+
+            when(mLocationManager.isLocationEnabledForUser(any(UserHandle.class))).thenReturn(true);
+            when(mContext.checkPermission(eq(Manifest.permission.INTERACT_ACROSS_USERS_FULL),
+                    anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_GRANTED);
+        }
+    }
+
+    private <T> void mockSystemService(Class<T> clazz , T obj, String serviceName) {
+        when(mContext.getSystemServiceName(eq(clazz))).thenReturn(serviceName);
+        when(mContext.getSystemService(eq(serviceName))).thenReturn(obj);
+    }
+
+    private void setTargetSdkVersion(int version) {
+        ApplicationInfo testAppInfo = new ApplicationInfo();
+        testAppInfo.targetSdkVersion = version;
+        try {
+            when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), any()))
+                    .thenReturn(testAppInfo);
+        } catch (Exception ignored) {
+        }
+    }
+
+    private void setCanReadPrivilegedPhoneState(boolean granted) {
+        doReturn(granted ? PERMISSION_GRANTED : PERMISSION_DENIED).when(mContext)
+                .checkCallingOrSelfPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+    }
 }