/* * Copyright (C) 2019 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 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 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.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; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.test.mock.MockContentResolver; import android.test.suitebuilder.annotation.SmallTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** * Tests for simple queries of ServiceStateProvider. * * Build, install and run the tests by running the commands below: * atest ServiceStateProviderTest */ @RunWith(AndroidJUnit4.class) public class ServiceStateProviderTest { private static final String TAG = "ServiceStateProviderTest"; private static final int TEST_NETWORK_ID = 123; private static final int TEST_SYSTEM_ID = 123; private MockContentResolver mContentResolver; private ServiceState mTestServiceState; private ServiceState mTestServiceStateForSubId1; @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 { TestNotifierException() { super(); } } @Before public void setUp() throws Exception { 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) { throw new TestNotifierException(); } }; doReturn(mContentResolver).when(mContext).getContentResolver(); mTestServiceState = new ServiceState(); mTestServiceState.setStateOutOfService(); mTestServiceState.setCdmaSystemAndNetworkId(TEST_SYSTEM_ID, TEST_NETWORK_ID); mTestServiceStateForSubId1 = new ServiceState(); mTestServiceStateForSubId1.setStateOff(); // Add NRI to trigger SS with non-default values (e.g. duplex mode) NetworkRegistrationInfo nriWwan = new NetworkRegistrationInfo.Builder() .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WWAN) .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE) .setDomain(NetworkRegistrationInfo.DOMAIN_PS) .build(); mTestServiceStateForSubId1.addNetworkRegistrationInfo(nriWwan); mTestServiceStateForSubId1.setChannelNumber(65536); // EutranBand.BAND_65, DUPLEX_MODE_FDD // Mock out the actual phone state ServiceStateProvider provider = new ServiceStateProvider() { @Override public ServiceState getServiceState(int subId) { if (subId == 1) { return mTestServiceStateForSubId1; } else { return mTestServiceState; } } @Override public int getDefaultSubId() { return 0; } }; ProviderInfo providerInfo = new ProviderInfo(); providerInfo.authority = "service-state"; provider.attachInfoForTesting(mContext, providerInfo); mContentResolver.addProvider("service-state", provider); // By default, test with app target R, no READ_PRIVILEGED_PHONE_STATE permission setTargetSdkVersion(Build.VERSION_CODES.R); setCanReadPrivilegedPhoneState(false); // TODO(b/191995565): Turn on all ignored cases once location access is allow to be off // Do not allow phone process to always access location so we can test various scenarios // LocationAccessPolicy.alwaysAllowPrivilegedProcessToAccessLocationForTesting(false); } @After public void tearDown() throws Exception { // LocationAccessPolicy.alwaysAllowPrivilegedProcessToAccessLocationForTesting(true); } /** * 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. */ // TODO(b/191995565): Turn this on when location access can be off @Ignore @SmallTest public void testQueryServiceState_withNoSubId_withoutLocation() { setLocationPermissions(false); verifyServiceStateForSubId(ServiceStateTable.CONTENT_URI, mTestServiceState, false /*hasLocation*/); } @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. */ // TODO(b/191995565): Turn case on when location access can be off @Ignore @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 access the * public columns of ServiceStateTable. */ @Test public void query_publicColumns_targetS_noReadPrivilege_getPublicColumns() { 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 query_hideColumn_targetS_noReadPrivilege_throwIllegalArgumentException() { 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 query_allColumn_targetS_withReadPrivilegedAndLocation_getAllStateUnredacted() { setTargetSdkVersion(Build.VERSION_CODES.S); setCanReadPrivilegedPhoneState(true); setLocationPermissions(true); verifyServiceStateForSubId( getUriForSubscriptionId(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID), mTestServiceState, true /*hasPermission*/); } /** * Verify that apps target S+ with READ_PRIVILEGED_PHONE_STATE permission but no location * permission, try to access location sensitive columns should throw SecurityException. */ // TODO(b/191995565): Turn this on once b/191995565 is integrated @Ignore public void query_locationColumn_targetS_withReadPrivilegeNoLocation_throwSecurityExecption() { setTargetSdkVersion(Build.VERSION_CODES.S); setCanReadPrivilegedPhoneState(true); setLocationPermissions(false); // NETWORK_ID is a location-sensitive column String[] projection = new String[]{"network_id"}; assertThrows(SecurityException.class, () -> verifyServiceStateWithLocationColumns(mTestServiceState, projection)); } /** * Verify that apps target R- with location permissions should be able to access all columns. */ @Test public void query_allColumn_targetR_withLocation_getAllStateUnredacted() { setTargetSdkVersion(Build.VERSION_CODES.R); setLocationPermissions(true); verifyServiceStateForSubId( getUriForSubscriptionId(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID), mTestServiceState, true /*hasPermission*/); } /** * Verify that apps target R- w/o location permissions should be able to access all columns but * with redacted ServiceState. */ // TODO(b/191995565): Turn case on when location access can be off @Ignore public void query_allColumn_targetR_noLocation_getRedacted() { setTargetSdkVersion(Build.VERSION_CODES.R); setLocationPermissions(false); verifyServiceStateForSubId( getUriForSubscriptionId(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID), ServiceStateProvider.getLocationRedactedServiceState(mTestServiceState), true /*hasPermission*/); } private void verifyServiceStateWithLocationColumns(ServiceState ss, String[] projection) { try (Cursor cursor = mContentResolver.query(ServiceStateTable.CONTENT_URI, projection, null, null)) { assertNotNull(cursor); } } 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 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 = 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(); final int cdmaEriIconMode = ss.getCdmaEriIconMode(); final int isEmergencyOnly = (ss.isEmergencyOnly()) ? 1 : 0; final int isUsingCarrierAggregation = (ss.isUsingCarrierAggregation()) ? 1 : 0; final String operatorAlphaLongRaw = ss.getOperatorAlphaLongRaw(); final String operatorAlphaShortRaw = ss.getOperatorAlphaShortRaw(); final int dataNetworkType = ss.getDataNetworkType(); final int duplexMode = ss.getDuplexMode(); assertEquals(voiceRegState, cursor.getInt(0)); assertEquals(dataRegState, cursor.getInt(1)); 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)); } /** * Test that we don't notify for certain field changes. (e.g. we don't notify when the NetworkId * or SystemId change) This is an intentional behavior change from the broadcast. */ @Test @SmallTest public void testNoNotify() { int subId = 0; ServiceState oldSS = new ServiceState(); oldSS.setStateOutOfService(); oldSS.setCdmaSystemAndNetworkId(1, 1); ServiceState newSS = new ServiceState(); newSS.setStateOutOfService(); newSS.setCdmaSystemAndNetworkId(0, 0); // Test that notifyChange is not called for these fields assertFalse(notifyChangeCalledForSubIdAndField(oldSS, newSS, subId)); } @Test @SmallTest public void testNotifyChanged_noStateUpdated() { int subId = 0; ServiceState oldSS = new ServiceState(); oldSS.setStateOutOfService(); oldSS.setVoiceRegState(ServiceState.STATE_OUT_OF_SERVICE); ServiceState copyOfOldSS = new ServiceState(); copyOfOldSS.setStateOutOfService(); copyOfOldSS.setVoiceRegState(ServiceState.STATE_OUT_OF_SERVICE); // Test that notifyChange is not called with no change in notifyChangeForSubIdAndField assertFalse(notifyChangeCalledForSubId(oldSS, copyOfOldSS, subId)); // Test that notifyChange is not called with no change in notifyChangeForSubId assertFalse(notifyChangeCalledForSubIdAndField(oldSS, copyOfOldSS, subId)); } @Test @SmallTest public void testNotifyChanged_voiceRegStateUpdated() { int subId = 0; ServiceState oldSS = new ServiceState(); oldSS.setStateOutOfService(); oldSS.setVoiceRegState(ServiceState.STATE_OUT_OF_SERVICE); ServiceState newSS = new ServiceState(); newSS.setStateOutOfService(); newSS.setVoiceRegState(ServiceState.STATE_POWER_OFF); // Test that notifyChange is called by notifyChangeForSubIdAndField when the voice_reg_state // changes assertTrue(notifyChangeCalledForSubId(oldSS, newSS, subId)); // Test that notifyChange is called by notifyChangeForSubId when the voice_reg_state changes assertTrue(notifyChangeCalledForSubIdAndField(oldSS, newSS, subId)); } @Test @SmallTest public void testNotifyChanged_dataNetworkTypeUpdated() { int subId = 0; // While we don't have a method to directly set dataNetworkType, we emulate a ServiceState // change that will trigger the change of dataNetworkType, according to the logic in // ServiceState#getDataNetworkType ServiceState oldSS = new ServiceState(); oldSS.setStateOutOfService(); ServiceState newSS = new ServiceState(); newSS.setStateOutOfService(); NetworkRegistrationInfo nriWwan = new NetworkRegistrationInfo.Builder() .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WWAN) .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_LTE) .setDomain(NetworkRegistrationInfo.DOMAIN_PS) .setRegistrationState(REGISTRATION_STATE_HOME) .build(); newSS.addNetworkRegistrationInfo(nriWwan); // Test that notifyChange is called by notifyChangeForSubId when the // data_network_type changes assertTrue(notifyChangeCalledForSubId(oldSS, newSS, subId)); // Test that notifyChange is called by notifyChangeForSubIdAndField when the // data_network_type changes assertTrue(notifyChangeCalledForSubIdAndField(oldSS, newSS, subId)); } @Test @SmallTest public void testNotifyChanged_dataRegStateUpdated() { int subId = 0; ServiceState oldSS = new ServiceState(); oldSS.setStateOutOfService(); oldSS.setDataRegState(ServiceState.STATE_OUT_OF_SERVICE); ServiceState newSS = new ServiceState(); newSS.setStateOutOfService(); newSS.setDataRegState(ServiceState.STATE_POWER_OFF); // Test that notifyChange is called by notifyChangeForSubId // when the data_reg_state changes assertTrue(notifyChangeCalledForSubId(oldSS, newSS, subId)); // Test that notifyChange is called by notifyChangeForSubIdAndField // when the data_reg_state changes assertTrue(notifyChangeCalledForSubIdAndField(oldSS, newSS, subId)); } // Check if notifyChange was called by notifyChangeForSubId private boolean notifyChangeCalledForSubId(ServiceState oldSS, ServiceState newSS, int subId) { try { ServiceStateProvider.notifyChangeForSubId(mContext, oldSS, newSS, subId); } catch (TestNotifierException e) { return true; } return false; } // Check if notifyChange was called by notifyChangeForSubIdAndField private boolean notifyChangeCalledForSubIdAndField(ServiceState oldSS, ServiceState newSS, int subId) { try { ServiceStateProvider.notifyChangeForSubIdAndField(mContext, oldSS, newSS, subId); } catch (TestNotifierException e) { return true; } 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 void mockSystemService(Class 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); } }