diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java index 90c74d105e8..0bd0aa9c588 100644 --- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java +++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java @@ -35,6 +35,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -103,6 +104,7 @@ public class ShadowTelephonyManagerTest { private TelephonyManager telephonyManager; private ShadowTelephonyManager shadowTelephonyManager; + private TelephonyManager tmForSub5; @Before public void setUp() throws Exception { @@ -110,6 +112,7 @@ public void setUp() throws Exception { shadowTelephonyManager = Shadow.extract(telephonyManager); shadowOf((Application) ApplicationProvider.getApplicationContext()) .grantPermissions(permission.READ_PRIVILEGED_PHONE_STATE); + tmForSub5 = ShadowTelephonyManager.shadowForSubId(5).realTelephonyManager; } @Test @@ -162,6 +165,15 @@ public void shouldGiveDeviceId() { assertEquals(testId, telephonyManager.getDeviceId()); } + @Test + @Config(minSdk = M) + public void setDeviceId_withSlot_doesNotAffectCallingInstance() { + String testId = "TESTING123"; + shadowOf(telephonyManager).setDeviceId(123, testId); + assertThat(telephonyManager.getDeviceId()).isNull(); + assertThat(telephonyManager.getDeviceId(123)).isEqualTo(testId); + } + @Test @Config(minSdk = M) public void shouldGiveDeviceIdForSlot() { @@ -197,6 +209,14 @@ public void getImeiForSlot() { assertEquals("imei1", telephonyManager.getImei(1)); } + @Test + @Config(minSdk = O) + public void setImei_withSlotId_acceptsNull() { + shadowOf(telephonyManager).setImei(0, "imei0"); + shadowOf(telephonyManager).setImei(0, null); + assertNull(null, telephonyManager.getImei(0)); + } + @Test @Config(minSdk = O) public void getMeid() { @@ -213,6 +233,7 @@ public void getMeidForSlot() { shadowOf(telephonyManager).setMeid(1, "meid1"); assertEquals("meid0", telephonyManager.getMeid(0)); assertEquals("meid1", telephonyManager.getMeid(1)); + assertEquals("defaultMeid", telephonyManager.getMeid()); } @Test @@ -221,12 +242,49 @@ public void shouldGiveNetworkOperatorName() { assertEquals("SomeOperatorName", telephonyManager.getNetworkOperatorName()); } + @Test + @Config(minSdk = M) + public void setNetworkOperatorNameForPhone_doesNotAffectDefault() { + shadowOf(telephonyManager).setNetworkOperatorNameForPhone(123, "SomeNetworkOperatorName"); + assertThat(telephonyManager.getNetworkOperatorName()).isEmpty(); + } + + @Test + @Config(minSdk = M) + public void setNetworkOperatorNameForPhone_affectsOtherInstance() { + shadowOf(telephonyManager).setNetworkOperatorNameForPhone(123, "SomeNetworkOperatorName"); + TelephonyManager other = ShadowTelephonyManager.tmOrNullForPhoneId(123); + assertEquals("SomeNetworkOperatorName", other.getNetworkOperatorName()); + } + @Test public void shouldGiveSimOperatorName() { shadowOf(telephonyManager).setSimOperatorName("SomeSimOperatorName"); assertEquals("SomeSimOperatorName", telephonyManager.getSimOperatorName()); } + @Test + @Config(minSdk = M) + public void setSimOperatorNameForPhone_byPhoneId_doesNotAffectDefault() { + shadowOf(telephonyManager).setSimOperatorNameForPhone(123, "SomeSimOperatorName"); + assertNull(telephonyManager.getSimOperatorName()); + } + + @Test + @Config(minSdk = M) + public void setSimOperatorNameForPhone_byPhoneId_canBeAccessedFromAnyInstance() { + shadowOf(telephonyManager).setSimOperatorNameForPhone(123, "SomeSimOperatorName"); + assertEquals("SomeSimOperatorName", tmForSub5.getSimOperatorNameForPhone(123)); + } + + @Test + @Config(minSdk = M) + public void setSimOperatorNameForPhone_byPhoneId_canBeAccessedFromOtherInstance() { + shadowOf(telephonyManager).setSimOperatorNameForPhone(123, "SomeSimOperatorName"); + TelephonyManager other = ShadowTelephonyManager.tmOrNullForPhoneId(123); + assertEquals("SomeSimOperatorName", other.getSimOperatorName()); + } + @Test(expected = SecurityException.class) public void getSimSerialNumber_shouldThrowSecurityExceptionWhenReadPhoneStatePermissionNotGranted() @@ -268,7 +326,7 @@ public void shouldGiveAllCellInfo() { PhoneStateListener listener = mock(PhoneStateListener.class); telephonyManager.listen(listener, LISTEN_CELL_INFO); - List allCellInfo = Collections.singletonList(mock(CellInfo.class)); + List allCellInfo = ImmutableList.of(mock(CellInfo.class)); shadowOf(telephonyManager).setAllCellInfo(allCellInfo); assertEquals(allCellInfo, telephonyManager.getAllCellInfo()); verify(listener).onCellInfoChanged(allCellInfo); @@ -281,7 +339,7 @@ public void shouldGiveAllCellInfo_toCallback() { mock(TelephonyCallback.class, withSettings().extraInterfaces(CellInfoListener.class)); telephonyManager.registerTelephonyCallback(directExecutor(), callback); - List allCellInfo = Collections.singletonList(mock(CellInfo.class)); + List allCellInfo = ImmutableList.of(mock(CellInfo.class)); shadowOf(telephonyManager).setAllCellInfo(allCellInfo); assertEquals(allCellInfo, telephonyManager.getAllCellInfo()); verify((CellInfoListener) callback).onCellInfoChanged(allCellInfo); @@ -290,7 +348,7 @@ public void shouldGiveAllCellInfo_toCallback() { @Test @Config(minSdk = Q) public void shouldGiveCellInfoUpdate() throws Exception { - List callbackCellInfo = Collections.singletonList(mock(CellInfo.class)); + List callbackCellInfo = ImmutableList.of(mock(CellInfo.class)); shadowOf(telephonyManager).setCallbackCellInfos(callbackCellInfo); assertNotEquals(callbackCellInfo, telephonyManager.getAllCellInfo()); @@ -385,6 +443,30 @@ public void shouldGivePhoneType() { assertEquals(TelephonyManager.PHONE_TYPE_GSM, telephonyManager.getPhoneType()); } + @Test + @Config(minSdk = M) + public void setCurrentPhoneType_fromPhoneId_canBeReadFromAnyInstance() { + shadowOf(telephonyManager).setCurrentPhoneType(123, TelephonyManager.PHONE_TYPE_CDMA); + + assertEquals(TelephonyManager.PHONE_TYPE_CDMA, tmForSub5.getCurrentPhoneType(123)); + } + + @Test + @Config(minSdk = M) + public void setPhoneType_canBeReadByFromOtherInstance() { + shadowOf(telephonyManager).setPhoneType(123, TelephonyManager.PHONE_TYPE_CDMA); + TelephonyManager other = ShadowTelephonyManager.tmOrNullForPhoneId(123); + assertEquals(TelephonyManager.PHONE_TYPE_CDMA, other.getPhoneType()); + } + + @Test + @Config(minSdk = M) + public void setPhoneType_doesNotModifyCallingInstance() { + shadowOf(telephonyManager).setPhoneType(123, TelephonyManager.PHONE_TYPE_CDMA); + + assertEquals(TelephonyManager.PHONE_TYPE_GSM, telephonyManager.getPhoneType()); + } + @Test public void shouldGiveCellLocation() { PhoneStateListener listener = mock(PhoneStateListener.class); @@ -461,6 +543,14 @@ public void isSmsCapable() { assertThat(telephonyManager.isSmsCapable()).isFalse(); } + @Test + @Config(minSdk = R) + public void setSmsCapable_modifiesAllInstances() { + shadowOf(telephonyManager).setIsSmsCapable(false); + ShadowTelephonyManager.shadowForSubId(123); + assertThat(telephonyManager.createForSubscriptionId(123).isSmsCapable()).isFalse(); + } + @Test @Config(minSdk = O) public void shouldGiveCarrierConfigIfSet() { @@ -541,6 +631,18 @@ public void shouldGiveVoiceVibrationEnabled() { assertTrue(telephonyManager.isVoicemailVibrationEnabled(phoneAccountHandle)); } + @Test + @Config(minSdk = N) + public void setVoicemailVibrationEnabled_accessibleFromAllTelephonyManagers() { + PhoneAccountHandle phoneAccountHandle = + new PhoneAccountHandle( + new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle"); + + shadowOf(telephonyManager).setVoicemailVibrationEnabled(phoneAccountHandle, true); + + assertTrue(telephonyManager.isVoicemailVibrationEnabled(phoneAccountHandle)); + } + @Test @Config(minSdk = N) public void shouldGiveVoicemailRingtoneUri() { @@ -568,6 +670,19 @@ public void shouldSetVoicemailRingtoneUri() { assertEquals(ringtoneUri, telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle)); } + @Test + @Config(minSdk = N) + public void setVoicemailRingtoneUri_accessibleFromAllTelephonyManagers() { + PhoneAccountHandle phoneAccountHandle = + new PhoneAccountHandle( + new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle"); + Uri ringtoneUri = Uri.fromParts("file", "ringtone.mp3", /* fragment= */ null); + + shadowOf(telephonyManager).setVoicemailRingtoneUri(phoneAccountHandle, ringtoneUri); + + assertEquals(ringtoneUri, telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle)); + } + @Test @Config(minSdk = O) public void shouldCreateForPhoneAccountHandle() { @@ -583,9 +698,23 @@ public void shouldCreateForPhoneAccountHandle() { mockTelephonyManager, telephonyManager.createForPhoneAccountHandle(phoneAccountHandle)); } + @Test + @Config(minSdk = O) + public void shouldCreateForPhoneAccountHandle_fromAllInstances() { + PhoneAccountHandle phoneAccountHandle = + new PhoneAccountHandle( + new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle"); + TelephonyManager mockTelephonyManager = mock(TelephonyManager.class); + + shadowOf(telephonyManager) + .setTelephonyManagerForHandle(phoneAccountHandle, mockTelephonyManager); + + assertEquals(mockTelephonyManager, tmForSub5.createForPhoneAccountHandle(phoneAccountHandle)); + } + @Test @Config(minSdk = N) - public void shouldCreateForSubscriptionId() { + public void shouldcreateForSubscriptionId() { int subscriptionId = 42; TelephonyManager mockTelephonyManager = mock(TelephonyManager.class); @@ -595,6 +724,65 @@ public void shouldCreateForSubscriptionId() { assertEquals(mockTelephonyManager, telephonyManager.createForSubscriptionId(subscriptionId)); } + @Test + @Config(minSdk = N) + public void createForSubscriptionId_returnsInstancesCreatedViaReflection() { + int subId = 4; + Class[] parameters = new Class[] {Context.class, int.class}; + Object[] arguments = new Object[] {ApplicationProvider.getApplicationContext(), subId}; + TelephonyManager newInstance = + Shadow.newInstance(TelephonyManager.class, parameters, arguments); + + assertThat(newInstance).isSameInstanceAs(telephonyManager.createForSubscriptionId(subId)); + } + + @Test + @Config(minSdk = N) + public void shouldcreateForSubscriptionId_fromAllInstances() { + int subscriptionId = 42; + TelephonyManager mockTelephonyManager = mock(TelephonyManager.class); + + shadowOf(telephonyManager) + .setTelephonyManagerForSubscriptionId(subscriptionId, mockTelephonyManager); + + assertEquals(mockTelephonyManager, tmForSub5.createForSubscriptionId(subscriptionId)); + } + + @Test + @Config(minSdk = N) + public void createForSubscriptionId_returnsLastInstanceAssociatedWithSubId() { + assertThat(tmForSub5).isSameInstanceAs(telephonyManager.createForSubscriptionId(5)); + + shadowOf(telephonyManager).setSubscriptionId(5); + assertThat(telephonyManager).isSameInstanceAs(telephonyManager.createForSubscriptionId(5)); + } + + @Test + @Config(minSdk = N) + public void setPhoneId_associatesInstanceWithPhoneId() { + shadowOf(tmForSub5).setPhoneId(6); + telephonyManager.setPhoneType(6, TelephonyManager.PHONE_TYPE_CDMA); + assertThat(tmForSub5.getPhoneType()).isEqualTo(TelephonyManager.PHONE_TYPE_CDMA); + } + + @Test + @Config(minSdk = N) + public void setSlotIndex_canBeReadFromAnyInstance() { + shadowOf(tmForSub5).setSlotIndex(6); + shadowOf(telephonyManager).setImei(6, "test_imei"); + assertThat(tmForSub5.getImei(6)).isEqualTo("test_imei"); + } + + @Test + @Config(minSdk = O) + public void setAccount_associatesInstanceWithPhoneId() { + PhoneAccountHandle handle = + new PhoneAccountHandle( + new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle"); + shadowOf(tmForSub5).setAccount(handle); + assertThat(tmForSub5).isSameInstanceAs(telephonyManager.createForPhoneAccountHandle(handle)); + } + @Test @Config(minSdk = O) public void shouldSetServiceState() { @@ -684,6 +872,13 @@ public void shouldGetSimState() { assertThat(telephonyManager.getSimState()).isEqualTo(TelephonyManager.SIM_STATE_READY); } + @Test + @Config(minSdk = O) + public void getSimState_defaultForZeroSpecial() { + assertThat(telephonyManager.getSimState(1)).isEqualTo(TelephonyManager.SIM_STATE_UNKNOWN); + assertThat(telephonyManager.getSimState(0)).isEqualTo(TelephonyManager.SIM_STATE_READY); + } + @Test @Config(minSdk = O) public void shouldGetSimStateUsingSlotNumber() { @@ -694,11 +889,54 @@ public void shouldGetSimStateUsingSlotNumber() { assertThat(telephonyManager.getSimState(slotNumber)).isEqualTo(expectedSimState); } + @Test + @Config(minSdk = O) + public void setSimState_withNoParameter_affectsOtherInstance() { + int expectedSimState = TelephonyManager.SIM_STATE_ABSENT; + int slotNumber = 3; + shadowOf(telephonyManager).setSlotIndex(slotNumber); + shadowOf(telephonyManager).setSimState(expectedSimState); + assertThat(tmForSub5.getSimState(slotNumber)).isEqualTo(expectedSimState); + } + + @Test + @Config(minSdk = O) + public void setSimState_withSlotParameter_affectsOtherInstance() { + int expectedSimState = TelephonyManager.SIM_STATE_ABSENT; + int slotNumber = 3; + shadowOf(tmForSub5).setSlotIndex(slotNumber); + shadowOf(telephonyManager).setSimState(slotNumber, expectedSimState); + assertThat(tmForSub5.getSimState()).isEqualTo(expectedSimState); + } + + @Test + @Config(minSdk = O) + public void setSimState_withSlotParameter_doesNotAffectCaller() { + int expectedSimState = TelephonyManager.SIM_STATE_ABSENT; + int slotNumber = 3; + shadowOf(telephonyManager).setSimState(slotNumber, expectedSimState); + + assertThat(telephonyManager.getSimState()).isEqualTo(TelephonyManager.SIM_STATE_READY); + } + @Test public void shouldGetSimIso() { assertThat(telephonyManager.getSimCountryIso()).isEmpty(); } + @Test + @Config(minSdk = N, maxSdk = Q) + public void shouldGetSimIso_resetsZeroSpecial() { + assertThat(callGetSimCountryIso(telephonyManager, 1)).isNull(); + assertThat(callGetSimCountryIso(telephonyManager, 0)).isEmpty(); + } + + private String callGetSimCountryIso(TelephonyManager telephonyManager, int subId) { + return (String) + ReflectionHelpers.callInstanceMethod( + telephonyManager, "getSimCountryIso", ClassParameter.from(int.class, subId)); + } + @Test @Config(minSdk = N, maxSdk = Q) public void shouldGetSimIsoWhenSetUsingSlotNumber() { @@ -706,11 +944,37 @@ public void shouldGetSimIsoWhenSetUsingSlotNumber() { int subId = 2; shadowOf(telephonyManager).setSimCountryIso(subId, expectedSimIso); - assertThat( - (String) - ReflectionHelpers.callInstanceMethod( - telephonyManager, "getSimCountryIso", ClassParameter.from(int.class, subId))) - .isEqualTo(expectedSimIso); + assertThat(callGetSimCountryIso(telephonyManager, subId)).isEqualTo(expectedSimIso); + } + + @Test + @Config(minSdk = N, maxSdk = Q) + public void setSimIso_withSlotParameter_doesNotAffectCaller() { + String expectedSimIso = "usa"; + int subId = 2; + shadowOf(telephonyManager).setSimCountryIso(subId, expectedSimIso); + + assertThat(telephonyManager.getSimCountryIso()).isEmpty(); + } + + @Test + @Config(minSdk = N, maxSdk = Q) + public void setSimIso_withSlotParameter_acceptsNull() { + String expectedSimIso = "usa"; + int subId = 2; + shadowOf(telephonyManager).setSimCountryIso(subId, expectedSimIso); + shadowOf(telephonyManager).setSimCountryIso(subId, null); + + assertThat(callGetSimCountryIso(telephonyManager, subId)).isEqualTo(null); + } + + @Test + @Config(minSdk = N, maxSdk = Q) + public void setSimIso_affectsRightInstance() { + String expectedSimIso = "usa"; + shadowOf(telephonyManager).setSimCountryIso(5, expectedSimIso); + + assertThat(tmForSub5.getSimCountryIso()).isEqualTo("usa"); } @Test @@ -741,10 +1005,38 @@ public void shouldGetCurrentPhoneTypeGivenSubId() { assertThat(telephonyManager.getCurrentPhoneType(subId)).isEqualTo(expectedPhoneType); } + @Test + @Config(minSdk = M) + public void shouldGetCurrentPhoneTypeGivenSubId_fromAllInstances() { + int subId = 1; + int expectedPhoneType = TelephonyManager.PHONE_TYPE_GSM; + shadowOf(telephonyManager).setCurrentPhoneType(subId, expectedPhoneType); + + assertThat(tmForSub5.getCurrentPhoneType(subId)).isEqualTo(expectedPhoneType); + } + + @Test + @Config(minSdk = M) + public void shouldGetCurrentPhoneTypeGivenSubId_affectsRightInstance() { + int subId = 5; + int expectedPhoneType = TelephonyManager.PHONE_TYPE_GSM; + shadowOf(telephonyManager).setCurrentPhoneType(subId, expectedPhoneType); + + assertThat(tmForSub5.getCurrentPhoneType()).isEqualTo(expectedPhoneType); + } + + @Test + @Config(minSdk = M) + public void clearPhoneTypes_setsStartingState() { + ShadowTelephonyManager.clearPhoneTypes(); + assertEquals(TelephonyManager.PHONE_TYPE_NONE, telephonyManager.getCurrentPhoneType(0)); + assertEquals(TelephonyManager.PHONE_TYPE_NONE, telephonyManager.getCurrentPhoneType(1)); + } + @Test @Config(minSdk = M) public void shouldGetCarrierPackageNamesForIntentAndPhone() { - List packages = Collections.singletonList("package1"); + ImmutableList packages = ImmutableList.of("package1"); int phoneId = 123; shadowOf(telephonyManager).setCarrierPackageNamesForPhone(phoneId, packages); @@ -752,10 +1044,43 @@ public void shouldGetCarrierPackageNamesForIntentAndPhone() { .isEqualTo(packages); } + @Test + @Config(minSdk = M) + public void setCarrierPackageNamesForPhone_acceptsNull() { + ImmutableList packages = ImmutableList.of("package1"); + int phoneId = 123; + shadowOf(telephonyManager).setCarrierPackageNamesForPhone(phoneId, packages); + shadowOf(telephonyManager).setCarrierPackageNamesForPhone(phoneId, null); + + assertThat(telephonyManager.getCarrierPackageNamesForIntentAndPhone(new Intent(), phoneId)) + .isEqualTo(null); + } + + @Test + @Config(minSdk = M) + public void shouldGetCarrierPackageNamesForIntentAndPhone_fromAllInstances() { + ImmutableList packages = ImmutableList.of("package1"); + int phoneId = 123; + shadowOf(telephonyManager).setCarrierPackageNamesForPhone(phoneId, packages); + + assertThat(tmForSub5.getCarrierPackageNamesForIntentAndPhone(new Intent(), phoneId)) + .isEqualTo(packages); + } + + @Test + @Config(minSdk = M) + public void shouldGetCarrierPackageNamesForIntentAndPhone_doesNotAffectCaller() { + ImmutableList packages = ImmutableList.of("package1"); + int phoneId = 123; + shadowOf(telephonyManager).setCarrierPackageNamesForPhone(phoneId, packages); + + assertThat(telephonyManager.getCarrierPackageNamesForIntent(new Intent())).isNull(); + } + @Test @Config(minSdk = M) public void shouldGetCarrierPackageNamesForIntent() { - List packages = Collections.singletonList("package1"); + ImmutableList packages = ImmutableList.of("package1"); shadowOf(telephonyManager) .setCarrierPackageNamesForPhone(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, packages); @@ -763,20 +1088,30 @@ public void shouldGetCarrierPackageNamesForIntent() { } @Test + @Config(minSdk = O) public void resetSimStates_shouldRetainDefaultState() { - shadowOf(telephonyManager).resetSimStates(); + ShadowTelephonyManager.resetAllSimStates(); assertThat(telephonyManager.getSimState()).isEqualTo(TelephonyManager.SIM_STATE_READY); + assertThat(telephonyManager.getSimState(1)).isEqualTo(TelephonyManager.SIM_STATE_UNKNOWN); + assertThat(telephonyManager.getSimState(0)).isEqualTo(TelephonyManager.SIM_STATE_READY); } @Test @Config(minSdk = N) public void resetSimCountryIsos_shouldRetainDefaultState() { - shadowOf(telephonyManager).resetSimCountryIsos(); - + ShadowTelephonyManager.resetAllSimStates(); assertThat(telephonyManager.getSimCountryIso()).isEmpty(); } + @Test + @Config(minSdk = N, maxSdk = Q) + public void resetSimCountryIsos_resetZeroSpecial() { + ShadowTelephonyManager.resetAllSimStates(); + assertThat(callGetSimCountryIso(telephonyManager, 1)).isNull(); + assertThat(callGetSimCountryIso(telephonyManager, 0)).isEmpty(); + } + @Test public void shouldSetSubscriberId() { String subscriberId = "123451234512345"; @@ -802,6 +1137,23 @@ public void getUiccSlotsInfo() { assertThat(shadowOf(telephonyManager).getUiccSlotsInfo()).isEqualTo(slotInfos); } + @Test + @Config(minSdk = Q) + public void getUiccCardsInfo() { + Object /*UiccCardInfo*/ cardsInfo1 = "new UiccCardInfo(true, true, null, 0, 0, true)"; + Object /*UiccCardInfo*/ cardsInfo2 = "new UiccCardInfo(true, true, null, 0, 1, true)"; + ImmutableList cardInfos = ImmutableList.of(cardsInfo1, cardsInfo2); + shadowOf(telephonyManager).setUiccCardsInfo(cardInfos); + + assertThat(telephonyManager.getUiccCardsInfo()).isEqualTo(cardInfos); + } + + @Test + @Config(minSdk = Q) + public void getUiccCardsInfo_returnsListAfterReset() { + assertThat(telephonyManager.getUiccCardsInfo()).isEmpty(); + } + @Test @Config(minSdk = O) public void shouldSetVisualVoicemailPackage() { @@ -925,6 +1277,14 @@ public void hasCarrierPrivilegesWithSubId() { assertThat(telephonyManager.hasCarrierPrivileges(subId)).isTrue(); } + @Test + @Config(minSdk = N) + public void hasCarrierPrivilegesWithSubId_canBeReadFromAllInstances() { + int subId = 3; + shadowOf(telephonyManager).setHasCarrierPrivileges(subId, true); + assertThat(tmForSub5.hasCarrierPrivileges(subId)).isTrue(); + } + @Test @Config(minSdk = O) public void sendDialerSpecialCode() { @@ -1227,4 +1587,39 @@ public void getSubscriptionIdForPhoneAccountHandle() { .setPhoneAccountHandleSubscriptionId(phoneAccountHandle, subscriptionId); assertEquals(subscriptionId, telephonyManager.getSubscriptionId(phoneAccountHandle)); } + + @Test + @Config(minSdk = R) + public void getSubscriptionIdForPhoneAccountHandle_affectsAllInstances() { + int subscriptionId = 123; + PhoneAccountHandle phoneAccountHandle = + new PhoneAccountHandle( + new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle"); + shadowOf(telephonyManager) + .setPhoneAccountHandleSubscriptionId(phoneAccountHandle, subscriptionId); + assertEquals(subscriptionId, tmForSub5.getSubscriptionId(phoneAccountHandle)); + } + + @Test + @Config(minSdk = R) + public void getSubscriptionIdForPhoneAccountHandle_doesNotModifyCaller() { + int subscriptionId = 123; + PhoneAccountHandle phoneAccountHandle = + new PhoneAccountHandle( + new ComponentName(ApplicationProvider.getApplicationContext(), Object.class), "handle"); + shadowOf(telephonyManager) + .setPhoneAccountHandleSubscriptionId(phoneAccountHandle, subscriptionId); + assertEquals(5, tmForSub5.getSubscriptionId()); + } + + @Test + @Config(minSdk = R) + public void newInstance_alreadyKnowsSubId() { + Context context = ApplicationProvider.getApplicationContext(); + Class[] parameters = new Class[] {Context.class, int.class}; + Object[] arguments = new Object[] {context, 123}; + TelephonyManager tm = Shadow.newInstance(TelephonyManager.class, parameters, arguments); + + assertThat(tm.getSubscriptionId()).isEqualTo(123); + } } diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java index 7b2a756b273..e1bcb3a7da3 100644 --- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java +++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java @@ -54,23 +54,23 @@ import android.telephony.VisualVoicemailSmsFilterSettings; import android.telephony.emergency.EmergencyNumber; import android.text.TextUtils; -import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.util.SparseIntArray; import com.google.common.base.Ascii; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.InlineMe; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.HiddenApi; @@ -80,25 +80,37 @@ import org.robolectric.annotation.Resetter; import org.robolectric.shadow.api.Shadow; import org.robolectric.util.ReflectionHelpers; +import org.robolectric.util.ReflectionHelpers.ClassParameter; import org.robolectric.versioning.AndroidVersions.U; import org.robolectric.versioning.AndroidVersions.V; @Implements(value = TelephonyManager.class, looseSignatures = true) public class ShadowTelephonyManager { + protected Context context; @RealObject protected TelephonyManager realTelephonyManager; - private final Map phoneStateRegistrations = new HashMap<>(); + private Integer subscriptionId; + private Integer phoneId; + private Integer slotIndex; + private PhoneAccountHandle handle; + + private final Map phoneStateRegistrations = + Collections.synchronizedMap(new LinkedHashMap<>()); private final /*List*/ List telephonyCallbackRegistrations = new ArrayList<>(); - private final Map slotIndexToDeviceId = new HashMap<>(); - private final Map slotIndexToImei = new HashMap<>(); - private final Map slotIndexToMeid = new HashMap<>(); - private final Map voicemailVibrationEnabledMap = new HashMap<>(); - private final Map voicemailRingtoneUriMap = new HashMap<>(); - private final Map phoneAccountToTelephonyManagers = - new HashMap<>(); - private final Map phoneAccountHandleSubscriptionId = new HashMap<>(); + private static final Map slotIndexToDeviceId = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map slotIndexToImei = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map slotIndexToMeid = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map voicemailVibrationEnabledMap = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map voicemailRingtoneUriMap = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map phoneAccountHandleSubscriptionId = + Collections.synchronizedMap(new LinkedHashMap<>()); private PhoneStateListener lastListener; private /*TelephonyCallback*/ Object lastTelephonyCallback; @@ -117,47 +129,51 @@ public class ShadowTelephonyManager { private String simOperator = ""; private String simOperatorName; private String simSerialNumber; - private boolean readPhoneStatePermission = true; + private static volatile boolean readPhoneStatePermission = true; private int phoneType = TelephonyManager.PHONE_TYPE_GSM; private String line1Number; private int networkType; private int dataNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; private int voiceNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; - private List allCellInfo = Collections.emptyList(); - private List callbackCellInfos = null; - private CellLocation cellLocation = null; + private static volatile List allCellInfo = Collections.emptyList(); + private static volatile List callbackCellInfos = null; + private static volatile CellLocation cellLocation = null; private int callState = CALL_STATE_IDLE; private int dataState = TelephonyManager.DATA_DISCONNECTED; private int dataActivity = TelephonyManager.DATA_ACTIVITY_NONE; private String incomingPhoneNumber = null; - private boolean isSmsCapable = true; - private boolean voiceCapable = true; + private static volatile boolean isSmsCapable = true; + private static volatile boolean voiceCapable = true; private String voiceMailNumber; private String voiceMailAlphaTag; - private int phoneCount = 1; - private int activeModemCount = 1; - private Map subscriptionIdsToTelephonyManagers = new HashMap<>(); + private static volatile int phoneCount = 1; + private static volatile int activeModemCount = 1; private PersistableBundle carrierConfig; private ServiceState serviceState; private boolean isNetworkRoaming; - private final SparseIntArray simStates = new SparseIntArray(); - private final SparseIntArray currentPhoneTypes = new SparseIntArray(); - private final SparseArray> carrierPackageNames = new SparseArray<>(); - private final Map simCountryIsoMap = new HashMap<>(); + private static final Map simStates = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map currentPhoneTypes = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map> carrierPackageNames = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map simCountryIsoMap = + Collections.synchronizedMap(new LinkedHashMap<>()); private int simCarrierId; private int carrierIdFromSimMccMnc; private String subscriberId; - private /*UiccSlotInfo[]*/ Object uiccSlotInfos; - private /*UiccCardInfo[]*/ Object uiccCardsInfo = new ArrayList<>(); + private static volatile /*UiccSlotInfo[]*/ Object uiccSlotInfos; + private static volatile /*List*/ Object uiccCardsInfo = new ArrayList<>(); private String visualVoicemailPackageName = null; private SignalStrength signalStrength; private boolean dataEnabled = false; private final Set dataDisabledReasons = new HashSet<>(); private boolean isRttSupported; - private boolean isTtyModeSupported; - private final SparseBooleanArray subIdToHasCarrierPrivileges = new SparseBooleanArray(); - private final List sentDialerSpecialCodes = new ArrayList<>(); - private boolean hearingAidCompatibilitySupported = false; + private static volatile boolean isTtyModeSupported; + private static final Map subIdToHasCarrierPrivileges = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final List sentDialerSpecialCodes = new ArrayList<>(); + private static volatile boolean hearingAidCompatibilitySupported = false; private int requestCellInfoUpdateErrorCode = 0; private Throwable requestCellInfoUpdateDetail = null; private Object telephonyDisplayInfo; @@ -165,7 +181,7 @@ public class ShadowTelephonyManager { private static int callComposerStatus = 0; private VisualVoicemailSmsParams lastVisualVoicemailSmsParams; private VisualVoicemailSmsFilterSettings visualVoicemailSmsFilterSettings; - private boolean emergencyCallbackMode; + private static volatile boolean emergencyCallbackMode; private static Map> emergencyNumbersList; /** @@ -176,17 +192,264 @@ public class ShadowTelephonyManager { */ private Object callback; - private /*PhoneCapability*/ Object phoneCapability; + private static volatile /*PhoneCapability*/ Object phoneCapability; + + private static final CopyOnWriteArrayList allShadowTelephonyManagers = + new CopyOnWriteArrayList<>(); + private static final Map subscriptionIdsToTelephonyManagers = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map phoneIdsToTelephonyManagers = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map slotIdsToTelephonyManagers = + Collections.synchronizedMap(new LinkedHashMap<>()); + private static final Map phoneAccountToTelephonyManagers = + Collections.synchronizedMap(new LinkedHashMap<>()); - { - resetSimStates(); - resetSimCountryIsos(); + static { + resetAllSimStates(); + resetAllSimCountryIsos(); } @Resetter public static void reset() { + allShadowTelephonyManagers.clear(); + subscriptionIdsToTelephonyManagers.clear(); + phoneIdsToTelephonyManagers.clear(); + slotIdsToTelephonyManagers.clear(); + phoneAccountToTelephonyManagers.clear(); + resetAllSimStates(); + currentPhoneTypes.clear(); + carrierPackageNames.clear(); + resetAllSimCountryIsos(); + slotIndexToDeviceId.clear(); + slotIndexToImei.clear(); + slotIndexToMeid.clear(); + voicemailVibrationEnabledMap.clear(); + voicemailRingtoneUriMap.clear(); + phoneAccountHandleSubscriptionId.clear(); + subIdToHasCarrierPrivileges.clear(); + allCellInfo = Collections.emptyList(); + cellLocation = null; + callbackCellInfos = null; + uiccSlotInfos = null; + uiccCardsInfo = new ArrayList<>(); callComposerStatus = 0; + emergencyCallbackMode = false; emergencyNumbersList = null; + phoneCapability = null; + isTtyModeSupported = false; + readPhoneStatePermission = true; + isSmsCapable = true; + voiceCapable = true; + phoneCount = 1; + activeModemCount = 1; + sentDialerSpecialCodes.clear(); + hearingAidCompatibilitySupported = false; + } + + private static ShadowTelephonyManager newTelephonyManager(Integer subId) { + Context context = RuntimeEnvironment.getApplication(); + Class[] parameters; + Object[] arguments; + if (subId == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + parameters = new Class[] {Context.class}; + arguments = new Object[] {context}; + } else { + parameters = new Class[] {Context.class, int.class}; + arguments = new Object[] {context, subId}; + } + TelephonyManager tm = Shadow.newInstance(TelephonyManager.class, parameters, arguments); + allShadowTelephonyManagers.addIfAbsent(tm); + ShadowTelephonyManager shadow = Shadow.extract(tm); + if (subId != null) { + subscriptionIdsToTelephonyManagers.put(subId, tm); + shadow.subscriptionId = subId; + } + return shadow; + } + + static TelephonyManager tmOrNullForSubId(int subId) { + // use map for consistency with other calls + TelephonyManager fromMap = subscriptionIdsToTelephonyManagers.get(subId); + if (fromMap != null) { + return fromMap; + } + // find any instance with that subId + for (TelephonyManager tm : allShadowTelephonyManagers) { + int otherSubId = getSubId(tm); + if (otherSubId == subId) { + subscriptionIdsToTelephonyManagers.put(subId, tm); + return tm; + } + } + return null; + } + + /** Get or create a TelephonyManager instance associated with a given subId */ + public static ShadowTelephonyManager shadowForSubId(int subId) { + TelephonyManager tm = tmOrNullForSubId(subId); + return tm != null ? Shadow.extract(tm) : newTelephonyManager(subId); + } + + static TelephonyManager tmOrNullForPhoneId(int phoneId) { + TelephonyManager fromMap = phoneIdsToTelephonyManagers.get(phoneId); + if (fromMap != null) { + return fromMap; + } + // find any instance with that phoneId + for (TelephonyManager tm : allShadowTelephonyManagers) { + ShadowTelephonyManager shadow = Shadow.extract(tm); + Integer otherPhoneId = shadow.phoneId; + if (otherPhoneId != null && otherPhoneId == phoneId) { + phoneIdsToTelephonyManagers.put(otherPhoneId, tm); + return tm; + } + } + return null; + } + + /** Get or create a TelephonyManager instance associated with a given phoneId */ + public static ShadowTelephonyManager shadowForPhoneId(int phoneId) { + TelephonyManager tm = tmOrNullForPhoneId(phoneId); + if (tm != null) { + return Shadow.extract(tm); + } else { + ShadowTelephonyManager shadow = newTelephonyManager(null); + shadow.setPhoneId(phoneId); + return shadow; + } + } + + static TelephonyManager tmOrNullForSlotIndex(int slotIndex) { + TelephonyManager fromMap = slotIdsToTelephonyManagers.get(slotIndex); + if (fromMap != null) { + return fromMap; + } + // find any instance with that slotIndex + for (TelephonyManager tm : allShadowTelephonyManagers) { + ShadowTelephonyManager shadow = Shadow.extract(tm); + int otherSlotIndex = shadow.getSlotIndex(); + if (otherSlotIndex == slotIndex) { + slotIdsToTelephonyManagers.put(otherSlotIndex, tm); + return tm; + } + } + return null; + } + + /** Get or create a TelephonyManager instance associated with a given slotIndex */ + public static ShadowTelephonyManager shadowForSlotIndex(int slotIndex) { + TelephonyManager tm = tmOrNullForSlotIndex(slotIndex); + if (tm != null) { + return Shadow.extract(tm); + } else { + ShadowTelephonyManager shadow = newTelephonyManager(null); + shadow.setSlotIndex(slotIndex); + return shadow; + } + } + + static TelephonyManager tmOrNullForHandle(PhoneAccountHandle handle) { + TelephonyManager fromMap = phoneAccountToTelephonyManagers.get(handle); + if (fromMap != null) { + return fromMap; + } + // find any instance with that handle + for (TelephonyManager tm : allShadowTelephonyManagers) { + ShadowTelephonyManager shadow = Shadow.extract(tm); + PhoneAccountHandle otherHandle = shadow.handle; + if (otherHandle != null && otherHandle.equals(handle)) { + phoneAccountToTelephonyManagers.put(otherHandle, tm); + return tm; + } + } + return null; + } + + /** Get or create a TelephonyManager instance associated with a given PhoneAccountHandle */ + public static ShadowTelephonyManager shadowForHandle(PhoneAccountHandle handle) { + TelephonyManager tm = tmOrNullForHandle(handle); + if (tm != null) { + return Shadow.extract(tm); + } else { + ShadowTelephonyManager shadow = newTelephonyManager(null); + shadow.setAccount(handle); + + return shadow; + } + } + + @Implementation + protected void __constructor__(Context context) { + Shadow.invokeConstructor( + TelephonyManager.class, realTelephonyManager, ClassParameter.from(Context.class, context)); + this.context = context; + allShadowTelephonyManagers.addIfAbsent(realTelephonyManager); + } + + @Implementation(minSdk = 24) + protected void __constructor__(Context context, int subId) { + Shadow.invokeConstructor( + TelephonyManager.class, + realTelephonyManager, + ClassParameter.from(Context.class, context), + ClassParameter.from(int.class, subId)); + this.context = context; + allShadowTelephonyManagers.addIfAbsent(realTelephonyManager); + } + + private Context context() { + if (this.context == null) { + this.context = ReflectionHelpers.getField(realTelephonyManager, "mContext"); + } + if (this.context == null) { + // TelephonyManager.getDefault() has no Context member, but is used by + // android.telephony.PhoneNumberUtils.isEmergencyNumber. Apparently it has a + // ShadowTelephonyManager, but __constructor__ is never invoked. The real isEmergencyNumber + // doesn't need a Context, but the shadow does, so we just use the ApplicationContext. + this.context = RuntimeEnvironment.getApplication(); + } + return this.context; + } + + /** + * Associates this TelephonyManager instance with a given subId + * + *

This replaces any prior TelephonyManager instances associated with this id + */ + public void setSubscriptionId(int subId) { + subscriptionIdsToTelephonyManagers.put(subId, realTelephonyManager); + this.subscriptionId = subId; + } + + /** + * Associates this TelephonyManager instance with a given phoneId + * + *

This replaces any prior TelephonyManager instances associated with this id + */ + public void setPhoneId(Integer phoneId) { + phoneIdsToTelephonyManagers.put(phoneId, realTelephonyManager); + this.phoneId = phoneId; + } + + /** + * Associates this TelephonyManager instance with a given slotIndex + * + *

This replaces any prior TelephonyManager instances associated with this id + */ + public void setSlotIndex(Integer slotIndex) { + slotIdsToTelephonyManagers.put(slotIndex, realTelephonyManager); + this.slotIndex = slotIndex; + } + + /** + * Associates this TelephonyManager instance with a given PhoneAccountHandle + * + *

This replaces any prior TelephonyManager instances associated with this id + */ + public void setAccount(PhoneAccountHandle handle) { + phoneAccountToTelephonyManagers.put(handle, realTelephonyManager); + this.handle = handle; } @Implementation(minSdk = S) @@ -217,7 +480,7 @@ public void bootstrapAuthenticationRequest( } public void setPhoneCapability(/*PhoneCapability*/ Object phoneCapability) { - this.phoneCapability = phoneCapability; + ShadowTelephonyManager.phoneCapability = phoneCapability; } @Implementation(minSdk = S) @@ -389,9 +652,8 @@ public void setNetworkOperatorName(String networkOperatorName) { } @Implementation(minSdk = V.SDK_INT) - public void setNetworkOperatorNameForPhone( - /* Ignored */ int phoneId, String networkOperatorName) { - setNetworkOperatorName(networkOperatorName); + public void setNetworkOperatorNameForPhone(int phoneId, String networkOperatorName) { + shadowForPhoneId(phoneId).setNetworkOperatorName(networkOperatorName); } @Implementation(minSdk = LOLLIPOP) @@ -506,8 +768,13 @@ public void setSimOperatorName(String simOperatorName) { } @Implementation(minSdk = V.SDK_INT) - public void setSimOperatorNameForPhone(/* Ignored */ int phoneId, String name) { - setSimOperatorName(name); + public void setSimOperatorNameForPhone(int phoneId, String name) { + shadowForPhoneId(phoneId).setSimOperatorName(name); + } + + @Implementation(minSdk = V.SDK_INT) + protected String getSimOperatorNameForPhone(int phoneId) { + return tmOrNullForPhoneId(phoneId).getSimOperatorName(); } @Implementation @@ -527,7 +794,7 @@ public void setSimSerialNumber(String simSerialNumber) { */ @Implementation protected String getSimCountryIso() { - String simCountryIso = simCountryIsoMap.get(/* subId= */ 0); + String simCountryIso = simCountryIsoMap.get(getSubId()); return simCountryIso == null ? simCountryIso : Ascii.toLowerCase(simCountryIso); } @@ -539,7 +806,7 @@ protected String getSimCountryIso(int subId) { @Implementation(minSdk = LOLLIPOP_MR1) public void setSimCountryIso(String simCountryIso) { - setSimCountryIso(/* subId= */ 0, simCountryIso); + setSimCountryIso(getSubId(), simCountryIso); } /** Sets the {@code simCountryIso} for the given {@code subId}. */ @@ -548,19 +815,32 @@ public void setSimCountryIso(int subId, String simCountryIso) { } /** Clears {@code subId} to simCountryIso mapping and resets to default state. */ - public void resetSimCountryIsos() { + public static void resetAllSimCountryIsos() { simCountryIsoMap.clear(); simCountryIsoMap.put(0, ""); } + /** + * Clears {@code subId} to simCountryIso mapping and resets to default state. + * + * @deprecated for resetAllSimCountryIsos + */ + @Deprecated + @InlineMe( + replacement = "ShadowTelephonyManager.resetAllSimCountryIsos()", + imports = "org.robolectric.shadows.ShadowTelephonyManager") + public final void resetSimCountryIsos() { + resetAllSimCountryIsos(); + } + @Implementation protected int getSimState() { - return getSimState(/* slotIndex= */ 0); + return getSimState(getSlotIndex()); } /** Sets the sim state of slot 0. */ public void setSimState(int simState) { - setSimState(/* slotIndex= */ 0, simState); + setSimState(getSlotIndex(), simState); } /** Set the sim state for the given {@code slotIndex}. */ @@ -570,12 +850,12 @@ public void setSimState(int slotIndex, int state) { @Implementation(minSdk = O) protected int getSimState(int slotIndex) { - return simStates.get(slotIndex, TelephonyManager.SIM_STATE_UNKNOWN); + return simStates.getOrDefault(slotIndex, TelephonyManager.SIM_STATE_UNKNOWN); } /** Sets the UICC slots information returned by {@link #getUiccSlotsInfo()}. */ public void setUiccSlotsInfo(/*UiccSlotInfo[]*/ Object uiccSlotsInfos) { - this.uiccSlotInfos = uiccSlotsInfos; + ShadowTelephonyManager.uiccSlotInfos = uiccSlotsInfos; } /** Returns the UICC slots information set by {@link #setUiccSlotsInfo}. */ @@ -586,25 +866,38 @@ public void setUiccSlotsInfo(/*UiccSlotInfo[]*/ Object uiccSlotsInfos) { } /** Sets the UICC cards information returned by {@link #getUiccCardsInfo()}. */ - public void setUiccCardsInfo(/*UiccCardsInfo[]*/ Object uiccCardsInfo) { - this.uiccCardsInfo = uiccCardsInfo; + public void setUiccCardsInfo(/*List*/ Object uiccCardsInfo) { + ShadowTelephonyManager.uiccCardsInfo = uiccCardsInfo; } /** Returns the UICC cards information set by {@link #setUiccCardsInfo}. */ @Implementation(minSdk = Q) @HiddenApi - protected /*UiccSlotInfo[]*/ Object getUiccCardsInfo() { + protected /*List*/ Object getUiccCardsInfo() { return uiccCardsInfo; } /** Clears {@code slotIndex} to state mapping and resets to default state. */ - public void resetSimStates() { + public static void resetAllSimStates() { simStates.clear(); - simStates.put(0, TelephonyManager.SIM_STATE_READY); + simStates.put(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, TelephonyManager.SIM_STATE_READY); + } + + /** + * Clears {@code slotIndex} to state mapping and resets to default state. + * + * @deprecated use resetAllSimStates() + */ + @Deprecated + @InlineMe( + replacement = "ShadowTelephonyManager.resetAllSimStates()", + imports = "org.robolectric.shadows.ShadowTelephonyManager") + public final void resetSimStates() { + resetAllSimStates(); } public void setReadPhoneStatePermission(boolean readPhoneStatePermission) { - this.readPhoneStatePermission = readPhoneStatePermission; + ShadowTelephonyManager.readPhoneStatePermission = readPhoneStatePermission; } private void checkReadPhoneStatePermission() { @@ -641,8 +934,8 @@ public void setPhoneType(int phoneType) { } @Implementation(minSdk = V.SDK_INT) - public void setPhoneType(/* Ignored */ int phoneId, int type) { - setPhoneType(type); + public void setPhoneType(int phoneId, int type) { + shadowForPhoneId(phoneId).setPhoneType(type); } @Implementation @@ -718,7 +1011,7 @@ protected List getAllCellInfo() { } public void setAllCellInfo(List allCellInfo) { - this.allCellInfo = allCellInfo; + ShadowTelephonyManager.allCellInfo = allCellInfo; if (VERSION.SDK_INT >= JELLY_BEAN_MR1) { for (PhoneStateListener listener : getListenersForFlags(LISTEN_CELL_INFO)) { @@ -739,6 +1032,7 @@ public void setAllCellInfo(List allCellInfo) { @Implementation(minSdk = Q) protected void requestCellInfoUpdate(Object cellInfoExecutor, Object cellInfoCallback) { Executor executor = (Executor) cellInfoExecutor; + List callbackCellInfos = ShadowTelephonyManager.callbackCellInfos; if (callbackCellInfos == null) { // ignore } else if (requestCellInfoUpdateErrorCode != 0 || requestCellInfoUpdateDetail != null) { @@ -768,7 +1062,7 @@ protected void requestCellInfoUpdate(Object cellInfoExecutor, Object cellInfoCal * setAllCellInfo}. */ public void setCallbackCellInfos(List callbackCellInfos) { - this.callbackCellInfos = callbackCellInfos; + ShadowTelephonyManager.callbackCellInfos = callbackCellInfos; } /** @@ -782,12 +1076,11 @@ public void setRequestCellInfoUpdateErrorValues(int errorCode, Throwable detail) @Implementation protected CellLocation getCellLocation() { - return this.cellLocation; + return ShadowTelephonyManager.cellLocation; } public void setCellLocation(CellLocation cellLocation) { - this.cellLocation = cellLocation; - + ShadowTelephonyManager.cellLocation = cellLocation; for (PhoneStateListener listener : getListenersForFlags(LISTEN_CELL_LOCATION)) { listener.onCellLocationChanged(cellLocation); } @@ -810,6 +1103,13 @@ public void setGroupIdLevel1(String groupIdLevel1) { @CallSuper protected void initListener(PhoneStateListener listener, int flags) { + // grab the state "atomically" before doing callbacks, in case they modify the state + String incomingPhoneNumber = this.incomingPhoneNumber; + List allCellInfo = ShadowTelephonyManager.allCellInfo; + CellLocation cellLocation = ShadowTelephonyManager.cellLocation; + Object telephonyDisplayInfo = this.telephonyDisplayInfo; + ServiceState serviceState = this.serviceState; + if ((flags & LISTEN_CALL_STATE) != 0) { listener.onCallStateChanged(callState, incomingPhoneNumber); } @@ -837,6 +1137,12 @@ protected void initTelephonyCallback(Object callback) { if (VERSION.SDK_INT < S) { return; } + // grab the state "atomically" before doing callbacks, in case they modify the state + int callState = this.callState; + List allCellInfo = ShadowTelephonyManager.allCellInfo; + CellLocation cellLocation = ShadowTelephonyManager.cellLocation; + Object telephonyDisplayInfo = this.telephonyDisplayInfo; + ServiceState serviceState = this.serviceState; if (callback instanceof CallStateListener) { ((CallStateListener) callback).onCallStateChanged(callState); @@ -858,7 +1164,7 @@ protected void initTelephonyCallback(Object callback) { protected Iterable getListenersForFlags(int flags) { return Iterables.filter( - phoneStateRegistrations.keySet(), + ImmutableSet.copyOf(phoneStateRegistrations.keySet()), new Predicate() { @Override public boolean apply(PhoneStateListener input) { @@ -874,7 +1180,7 @@ public boolean apply(PhoneStateListener input) { */ protected Iterable getCallbackForListener(Class clazz) { // Only selects TelephonyCallback with matching class. - return Iterables.filter(telephonyCallbackRegistrations, clazz); + return Iterables.filter(ImmutableList.copyOf(telephonyCallbackRegistrations), clazz); } /** @@ -887,7 +1193,7 @@ protected boolean isSmsCapable() { /** Sets the value returned by {@link TelephonyManager#isSmsCapable()}. */ public void setIsSmsCapable(boolean isSmsCapable) { - this.isSmsCapable = isSmsCapable; + ShadowTelephonyManager.isSmsCapable = isSmsCapable; } /** @@ -947,7 +1253,7 @@ protected int getPhoneCount() { /** Sets the value returned by {@link TelephonyManager#getPhoneCount()}. */ public void setPhoneCount(int phoneCount) { - this.phoneCount = phoneCount; + ShadowTelephonyManager.phoneCount = phoneCount; } /** Returns 1 by default or the value specified via {@link #setActiveModemCount(int)}. */ @@ -958,7 +1264,7 @@ protected int getActiveModemCount() { /** Sets the value returned by {@link TelephonyManager#getActiveModemCount()}. */ public void setActiveModemCount(int activeModemCount) { - this.activeModemCount = activeModemCount; + ShadowTelephonyManager.activeModemCount = activeModemCount; } /** @@ -985,7 +1291,7 @@ protected boolean isVoiceCapable() { /** Sets the value returned by {@link #isVoiceCapable()}. */ public void setVoiceCapable(boolean voiceCapable) { - this.voiceCapable = voiceCapable; + ShadowTelephonyManager.voiceCapable = voiceCapable; } /** @@ -1106,10 +1412,16 @@ public void setIsNetworkRoaming(boolean isNetworkRoaming) { this.isNetworkRoaming = isNetworkRoaming; } + @Implementation(minSdk = M) + @HiddenApi + protected int getCurrentPhoneType() { + return currentPhoneTypes.getOrDefault(getSubId(), TelephonyManager.PHONE_TYPE_NONE); + } + @Implementation(minSdk = M) @HiddenApi protected int getCurrentPhoneType(int subId) { - return currentPhoneTypes.get(subId, TelephonyManager.PHONE_TYPE_NONE); + return currentPhoneTypes.getOrDefault(subId, TelephonyManager.PHONE_TYPE_NONE); } /** Sets the phone type for the given {@code subId}. */ @@ -1118,7 +1430,7 @@ public void setCurrentPhoneType(int subId, int phoneType) { } /** Removes all {@code subId} to {@code phoneType} mappings. */ - public void clearPhoneTypes() { + public static void clearPhoneTypes() { currentPhoneTypes.clear(); } @@ -1170,12 +1482,58 @@ public void setSubscriberId(String subscriberId) { this.subscriberId = subscriberId; } + @Implementation(minSdk = N) + protected int getSubId() { + checkReadPhoneStatePermission(); + return getSubId(realTelephonyManager); + } + + private static int getSubId(TelephonyManager tm) { + ShadowTelephonyManager shadow = Shadow.extract(tm); + if (shadow != null) { + if (shadow.subscriptionId != null) { + return shadow.subscriptionId; + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + int subId = ReflectionHelpers.callInstanceMethod(tm, "getSubId"); + if (shadow != null) { + // The tester never set the subscriptionId, but is using this instance based on its subId + // and it has a real subId, so we'll just make the shadow value cache the real value. + shadow.subscriptionId = subId; + } + return subId; + } + return SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; + } + + @Implementation(minSdk = R) + protected int getSubscriptionId() { + return getSubId(realTelephonyManager); + } + @Implementation(minSdk = R) protected int getSubscriptionId(PhoneAccountHandle handle) { checkReadPhoneStatePermission(); return phoneAccountHandleSubscriptionId.get(handle); } + @Implementation(minSdk = O) + protected int getSlotIndex() { + if (slotIndex != null) { + return slotIndex; + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + slotIndex = 0; + } else { + slotIndex = SubscriptionManager.getSlotIndex(getSubId()); + if (slotIndex == SubscriptionManager.SIM_NOT_INSERTED) { + slotIndex = 0; + } + } + return slotIndex; + } + public void setPhoneAccountHandleSubscriptionId(PhoneAccountHandle handle, int subscriptionId) { phoneAccountHandleSubscriptionId.put(handle, subscriptionId); } @@ -1227,8 +1585,7 @@ protected boolean isEmergencyNumber(String number) { return false; } - Context context = ReflectionHelpers.getField(realTelephonyManager, "mContext"); - Locale locale = context == null ? null : context.getResources().getConfiguration().locale; + Locale locale = context().getResources().getConfiguration().locale; String defaultCountryIso = locale == null ? null : locale.getCountry(); int slotId = -1; @@ -1288,7 +1645,7 @@ protected boolean isEmergencyNumber(String number) { * @param emergencyCallbackMode whether the device is in ECBM or not. */ public void setEmergencyCallbackMode(boolean emergencyCallbackMode) { - this.emergencyCallbackMode = emergencyCallbackMode; + ShadowTelephonyManager.emergencyCallbackMode = emergencyCallbackMode; } @Implementation(minSdk = Build.VERSION_CODES.O) @@ -1377,7 +1734,7 @@ protected boolean isTtyModeSupported() { /** Sets the value to be returned by {@link #isTtyModeSupported()} */ public void setTtyModeSupported(boolean isTtyModeSupported) { - this.isTtyModeSupported = isTtyModeSupported; + ShadowTelephonyManager.isTtyModeSupported = isTtyModeSupported; } /** @@ -1386,12 +1743,11 @@ public void setTtyModeSupported(boolean isTtyModeSupported) { @Implementation(minSdk = Build.VERSION_CODES.N) @HiddenApi protected boolean hasCarrierPrivileges(int subId) { - return subIdToHasCarrierPrivileges.get(subId); + return subIdToHasCarrierPrivileges.getOrDefault(subId, false); } public void setHasCarrierPrivileges(boolean hasCarrierPrivileges) { - int subId = ReflectionHelpers.callInstanceMethod(realTelephonyManager, "getSubId"); - setHasCarrierPrivileges(subId, hasCarrierPrivileges); + setHasCarrierPrivileges(getSubId(), hasCarrierPrivileges); } /** Sets the {@code hasCarrierPrivileges} for the given {@code subId}. */