summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java314
-rw-r--r--tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java333
2 files changed, 647 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
new file mode 100644
index 000000000000..c0608072df9d
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2020 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.server.vcn;
+
+import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
+import static android.telephony.CarrierConfigManager.EXTRA_SLOT_INDEX;
+import static android.telephony.CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX;
+import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.ParcelUuid;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * TelephonySubscriptionTracker provides a caching layer for tracking active subscription groups.
+ *
+ * <p>This class performs two roles:
+ *
+ * <ol>
+ * <li>De-noises subscription changes by ensuring that only changes in active and ready
+ * subscription groups are acted upon
+ * <li>Caches mapping between subIds and subscription groups
+ * </ol>
+ *
+ * <p>An subscription group is active and ready if any of its contained subIds has had BOTH the
+ * {@link CarrierConfigManager#isConfigForIdentifiedCarrier()} return true, AND the subscription is
+ * listed as active per SubscriptionManager#getAllSubscriptionInfoList().
+ *
+ * <p>Note that due to the asynchronous nature of callbacks and broadcasts, the output of this class
+ * is (only) eventually consistent.
+ *
+ * @hide
+ */
+public class TelephonySubscriptionTracker extends BroadcastReceiver {
+ @NonNull private static final String TAG = TelephonySubscriptionTracker.class.getSimpleName();
+ private static final boolean LOG_DBG = false; // STOPSHIP if true
+
+ @NonNull private final Context mContext;
+ @NonNull private final Handler mHandler;
+ @NonNull private final TelephonySubscriptionTrackerCallback mCallback;
+ @NonNull private final Dependencies mDeps;
+
+ @NonNull private final SubscriptionManager mSubscriptionManager;
+ @NonNull private final CarrierConfigManager mCarrierConfigManager;
+
+ // TODO (Android T+): Add ability to handle multiple subIds per slot.
+ @NonNull private final Map<Integer, Integer> mReadySubIdsBySlotId = new HashMap<>();
+ @NonNull private final OnSubscriptionsChangedListener mSubscriptionChangedListener;
+
+ @NonNull private TelephonySubscriptionSnapshot mCurrentSnapshot;
+
+ public TelephonySubscriptionTracker(
+ @NonNull Context context,
+ @NonNull Handler handler,
+ @NonNull TelephonySubscriptionTrackerCallback callback) {
+ this(context, handler, callback, new Dependencies());
+ }
+
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ TelephonySubscriptionTracker(
+ @NonNull Context context,
+ @NonNull Handler handler,
+ @NonNull TelephonySubscriptionTrackerCallback callback,
+ @NonNull Dependencies deps) {
+ mContext = Objects.requireNonNull(context, "Missing context");
+ mHandler = Objects.requireNonNull(handler, "Missing handler");
+ mCallback = Objects.requireNonNull(callback, "Missing callback");
+ mDeps = Objects.requireNonNull(deps, "Missing deps");
+
+ mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
+ mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+
+ mSubscriptionChangedListener =
+ new OnSubscriptionsChangedListener() {
+ @Override
+ public void onSubscriptionsChanged() {
+ handleSubscriptionsChanged();
+ }
+ };
+ }
+
+ /** Registers the receivers, and starts tracking subscriptions. */
+ public void register() {
+ mContext.registerReceiver(
+ this, new IntentFilter(ACTION_CARRIER_CONFIG_CHANGED), null, mHandler);
+ mSubscriptionManager.addOnSubscriptionsChangedListener(
+ new HandlerExecutor(mHandler), mSubscriptionChangedListener);
+ }
+
+ /** Unregisters the receivers, and stops tracking subscriptions. */
+ public void unregister() {
+ mContext.unregisterReceiver(this);
+ mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionChangedListener);
+ }
+
+ /**
+ * Handles subscription changes, correlating available subscriptions and loaded carrier configs
+ *
+ * <p>The subscription change listener is registered with a HandlerExecutor backed by mHandler,
+ * so callbacks & broadcasts are all serialized on mHandler, avoiding the need for locking.
+ */
+ public void handleSubscriptionsChanged() {
+ final Set<ParcelUuid> activeSubGroups = new ArraySet<>();
+ final Map<Integer, ParcelUuid> newSubIdToGroupMap = new HashMap<>();
+
+ final List<SubscriptionInfo> allSubs = mSubscriptionManager.getAllSubscriptionInfoList();
+ if (allSubs == null) {
+ return; // Telephony crashed; no way to verify subscriptions.
+ }
+
+ // If allSubs is empty, no subscriptions exist. Cache will be cleared by virtue of no active
+ // subscriptions
+ for (SubscriptionInfo subInfo : allSubs) {
+ if (subInfo.getGroupUuid() == null) {
+ continue;
+ }
+
+ // Build subId -> subGrp cache
+ newSubIdToGroupMap.put(subInfo.getSubscriptionId(), subInfo.getGroupUuid());
+
+ // Update subscription groups that are both ready, and active. For a group to be
+ // considered active, both of the following must be true:
+ //
+ // 1. A final CARRIER_CONFIG_CHANGED (where config is for an identified carrier)
+ // broadcast must have been received for the subId
+ // 2. A active subscription (is loaded into a SIM slot) must be part of the subscription
+ // group.
+ if (subInfo.getSimSlotIndex() != INVALID_SIM_SLOT_INDEX
+ && mReadySubIdsBySlotId.values().contains(subInfo.getSubscriptionId())) {
+ activeSubGroups.add(subInfo.getGroupUuid());
+ }
+ }
+
+ final TelephonySubscriptionSnapshot newSnapshot =
+ new TelephonySubscriptionSnapshot(newSubIdToGroupMap, activeSubGroups);
+
+ // If snapshot was meaningfully updated, fire the callback
+ if (!newSnapshot.equals(mCurrentSnapshot)) {
+ mCurrentSnapshot = newSnapshot;
+ mHandler.post(
+ () -> {
+ mCallback.onNewSnapshot(newSnapshot);
+ });
+ }
+ }
+
+ /**
+ * Broadcast receiver for ACTION_CARRIER_CONFIG_CHANGED
+ *
+ * <p>The broadcast receiver is registered with mHandler, so callbacks & broadcasts are all
+ * serialized on mHandler, avoiding the need for locking.
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Accept sticky broadcasts; if CARRIER_CONFIG_CHANGED was previously broadcast and it
+ // already was for an identified carrier, we can stop waiting for initial load to complete
+ if (!ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) {
+ return;
+ }
+
+ final int subId = intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, INVALID_SUBSCRIPTION_ID);
+ final int slotId = intent.getIntExtra(EXTRA_SLOT_INDEX, INVALID_SIM_SLOT_INDEX);
+
+ if (slotId == INVALID_SIM_SLOT_INDEX) {
+ return;
+ }
+
+ if (SubscriptionManager.isValidSubscriptionId(subId)) {
+ final PersistableBundle carrierConfigs = mCarrierConfigManager.getConfigForSubId(subId);
+ if (mDeps.isConfigForIdentifiedCarrier(carrierConfigs)) {
+ Slog.v(TAG, String.format("SubId %s ready for SlotId %s", subId, slotId));
+ mReadySubIdsBySlotId.put(slotId, subId);
+ handleSubscriptionsChanged();
+ }
+ } else {
+ Slog.v(TAG, "Slot unloaded: " + slotId);
+ mReadySubIdsBySlotId.remove(slotId);
+ handleSubscriptionsChanged();
+ }
+ }
+
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ void setReadySubIdsBySlotId(Map<Integer, Integer> readySubIdsBySlotId) {
+ mReadySubIdsBySlotId.putAll(readySubIdsBySlotId);
+ }
+
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ Map<Integer, Integer> getReadySubIdsBySlotId() {
+ return Collections.unmodifiableMap(mReadySubIdsBySlotId);
+ }
+
+ /** TelephonySubscriptionSnapshot is a class containing info about active subscriptions */
+ public static class TelephonySubscriptionSnapshot {
+ private final Map<Integer, ParcelUuid> mSubIdToGroupMap;
+ private final Set<ParcelUuid> mActiveGroups;
+
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ TelephonySubscriptionSnapshot(
+ @NonNull Map<Integer, ParcelUuid> subIdToGroupMap,
+ @NonNull Set<ParcelUuid> activeGroups) {
+ mSubIdToGroupMap = Collections.unmodifiableMap(
+ Objects.requireNonNull(subIdToGroupMap, "subIdToGroupMap was null"));
+ mActiveGroups = Collections.unmodifiableSet(
+ Objects.requireNonNull(activeGroups, "activeGroups was null"));
+ }
+
+ /** Returns the active subscription groups */
+ @NonNull
+ public Set<ParcelUuid> getActiveSubscriptionGroups() {
+ return mActiveGroups;
+ }
+
+ /** Returns the Subscription Group for a given subId. */
+ @Nullable
+ public ParcelUuid getGroupForSubId(int subId) {
+ return mSubIdToGroupMap.get(subId);
+ }
+
+ /**
+ * Returns all the subIds in a given group, including available, but inactive subscriptions.
+ */
+ @NonNull
+ public Set<Integer> getAllSubIdsInGroup(ParcelUuid subGrp) {
+ final Set<Integer> subIds = new ArraySet<>();
+
+ for (Entry<Integer, ParcelUuid> entry : mSubIdToGroupMap.entrySet()) {
+ if (subGrp.equals(entry.getValue())) {
+ subIds.add(entry.getKey());
+ }
+ }
+
+ return subIds;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSubIdToGroupMap, mActiveGroups);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof TelephonySubscriptionSnapshot)) {
+ return false;
+ }
+
+ final TelephonySubscriptionSnapshot other = (TelephonySubscriptionSnapshot) obj;
+
+ return mSubIdToGroupMap.equals(other.mSubIdToGroupMap)
+ && mActiveGroups.equals(other.mActiveGroups);
+ }
+ }
+
+ /**
+ * Interface for listening to changes in subscriptions
+ *
+ * @see TelephonySubscriptionTracker
+ */
+ public interface TelephonySubscriptionTrackerCallback {
+ /**
+ * Called when subscription information changes, and a new subscription snapshot was taken
+ *
+ * @param snapshot the snapshot of subscription information.
+ */
+ void onNewSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot);
+ }
+
+ /** External static dependencies for test injection */
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ public static class Dependencies {
+ /** Checks if the given bundle is for an identified carrier */
+ public boolean isConfigForIdentifiedCarrier(PersistableBundle bundle) {
+ return CarrierConfigManager.isConfigForIdentifiedCarrier(bundle);
+ }
+ }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
new file mode 100644
index 000000000000..17b8f64a13fa
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2020 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.server.vcn;
+
+import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
+import static android.telephony.CarrierConfigManager.EXTRA_SLOT_INDEX;
+import static android.telephony.CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX;
+import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.ParcelUuid;
+import android.os.test.TestLooper;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+/** Tests for TelephonySubscriptionTracker */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TelephonySubscriptionTrackerTest {
+ private static final ParcelUuid TEST_PARCEL_UUID = new ParcelUuid(UUID.randomUUID());
+ private static final int TEST_SIM_SLOT_INDEX = 1;
+ private static final int TEST_SUBSCRIPTION_ID_1 = 2;
+ private static final SubscriptionInfo TEST_SUBINFO_1 = mock(SubscriptionInfo.class);
+ private static final int TEST_SUBSCRIPTION_ID_2 = 3;
+ private static final SubscriptionInfo TEST_SUBINFO_2 = mock(SubscriptionInfo.class);
+ private static final Map<Integer, ParcelUuid> TEST_SUBID_TO_GROUP_MAP;
+
+ static {
+ final Map<Integer, ParcelUuid> subIdToGroupMap = new HashMap<>();
+ subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_1, TEST_PARCEL_UUID);
+ subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_2, TEST_PARCEL_UUID);
+ TEST_SUBID_TO_GROUP_MAP = Collections.unmodifiableMap(subIdToGroupMap);
+ }
+
+ @NonNull private final Context mContext;
+ @NonNull private final TestLooper mTestLooper;
+ @NonNull private final Handler mHandler;
+ @NonNull private final TelephonySubscriptionTracker.Dependencies mDeps;
+
+ @NonNull private final SubscriptionManager mSubscriptionManager;
+ @NonNull private final CarrierConfigManager mCarrierConfigManager;
+
+ @NonNull private TelephonySubscriptionTrackerCallback mCallback;
+ @NonNull private TelephonySubscriptionTracker mTelephonySubscriptionTracker;
+
+ public TelephonySubscriptionTrackerTest() {
+ mContext = mock(Context.class);
+ mTestLooper = new TestLooper();
+ mHandler = new Handler(mTestLooper.getLooper());
+ mDeps = mock(TelephonySubscriptionTracker.Dependencies.class);
+
+ mSubscriptionManager = mock(SubscriptionManager.class);
+ mCarrierConfigManager = mock(CarrierConfigManager.class);
+
+ doReturn(Context.TELEPHONY_SUBSCRIPTION_SERVICE)
+ .when(mContext)
+ .getSystemServiceName(SubscriptionManager.class);
+ doReturn(mSubscriptionManager)
+ .when(mContext)
+ .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+
+ doReturn(Context.CARRIER_CONFIG_SERVICE)
+ .when(mContext)
+ .getSystemServiceName(CarrierConfigManager.class);
+ doReturn(mCarrierConfigManager)
+ .when(mContext)
+ .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+
+ // subId 1, 2 are in same subGrp, only subId 1 is active
+ doReturn(TEST_PARCEL_UUID).when(TEST_SUBINFO_1).getGroupUuid();
+ doReturn(TEST_PARCEL_UUID).when(TEST_SUBINFO_2).getGroupUuid();
+ doReturn(TEST_SIM_SLOT_INDEX).when(TEST_SUBINFO_1).getSimSlotIndex();
+ doReturn(INVALID_SIM_SLOT_INDEX).when(TEST_SUBINFO_2).getSimSlotIndex();
+ doReturn(TEST_SUBSCRIPTION_ID_1).when(TEST_SUBINFO_1).getSubscriptionId();
+ doReturn(TEST_SUBSCRIPTION_ID_2).when(TEST_SUBINFO_2).getSubscriptionId();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mCallback = mock(TelephonySubscriptionTrackerCallback.class);
+ mTelephonySubscriptionTracker =
+ new TelephonySubscriptionTracker(mContext, mHandler, mCallback, mDeps);
+ mTelephonySubscriptionTracker.register();
+
+ doReturn(true).when(mDeps).isConfigForIdentifiedCarrier(any());
+ doReturn(Arrays.asList(TEST_SUBINFO_1, TEST_SUBINFO_2))
+ .when(mSubscriptionManager)
+ .getAllSubscriptionInfoList();
+ }
+
+ private IntentFilter getIntentFilter() {
+ final ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class);
+ verify(mContext).registerReceiver(any(), captor.capture(), any(), any());
+
+ return captor.getValue();
+ }
+
+ private OnSubscriptionsChangedListener getOnSubscriptionsChangedListener() {
+ final ArgumentCaptor<OnSubscriptionsChangedListener> captor =
+ ArgumentCaptor.forClass(OnSubscriptionsChangedListener.class);
+ verify(mSubscriptionManager).addOnSubscriptionsChangedListener(any(), captor.capture());
+
+ return captor.getValue();
+ }
+
+ private Intent buildTestBroadcastIntent(boolean hasValidSubscription) {
+ Intent intent = new Intent(ACTION_CARRIER_CONFIG_CHANGED);
+ intent.putExtra(EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX);
+ intent.putExtra(
+ EXTRA_SUBSCRIPTION_INDEX,
+ hasValidSubscription ? TEST_SUBSCRIPTION_ID_1 : INVALID_SUBSCRIPTION_ID);
+
+ return intent;
+ }
+
+ private TelephonySubscriptionSnapshot buildExpectedSnapshot(Set<ParcelUuid> activeSubGroups) {
+ return buildExpectedSnapshot(TEST_SUBID_TO_GROUP_MAP, activeSubGroups);
+ }
+
+ private TelephonySubscriptionSnapshot buildExpectedSnapshot(
+ Map<Integer, ParcelUuid> subIdToGroupMap, Set<ParcelUuid> activeSubGroups) {
+ return new TelephonySubscriptionSnapshot(subIdToGroupMap, activeSubGroups);
+ }
+
+ private void verifyNoActiveSubscriptions() {
+ verify(mCallback).onNewSnapshot(
+ argThat(snapshot -> snapshot.getActiveSubscriptionGroups().isEmpty()));
+ }
+
+ private void setupReadySubIds() {
+ mTelephonySubscriptionTracker.setReadySubIdsBySlotId(
+ Collections.singletonMap(TEST_SIM_SLOT_INDEX, TEST_SUBSCRIPTION_ID_1));
+ }
+
+ @Test
+ public void testRegister() throws Exception {
+ verify(mContext)
+ .registerReceiver(
+ eq(mTelephonySubscriptionTracker),
+ any(IntentFilter.class),
+ any(),
+ eq(mHandler));
+ final IntentFilter filter = getIntentFilter();
+ assertEquals(1, filter.countActions());
+ assertTrue(filter.hasAction(ACTION_CARRIER_CONFIG_CHANGED));
+
+ verify(mSubscriptionManager)
+ .addOnSubscriptionsChangedListener(any(HandlerExecutor.class), any());
+ assertNotNull(getOnSubscriptionsChangedListener());
+ }
+
+ @Test
+ public void testUnregister() throws Exception {
+ mTelephonySubscriptionTracker.unregister();
+
+ verify(mContext).unregisterReceiver(eq(mTelephonySubscriptionTracker));
+
+ final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
+ verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(eq(listener));
+ }
+
+ @Test
+ public void testOnSubscriptionsChangedFired_NoReadySubIds() throws Exception {
+ final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
+ listener.onSubscriptionsChanged();
+ mTestLooper.dispatchAll();
+
+ verifyNoActiveSubscriptions();
+ }
+
+ @Test
+ public void testOnSubscriptionsChangedFired_WithReadySubIds() throws Exception {
+ setupReadySubIds();
+
+ final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
+ listener.onSubscriptionsChanged();
+ mTestLooper.dispatchAll();
+
+ final Set<ParcelUuid> activeSubGroups = Collections.singleton(TEST_PARCEL_UUID);
+ verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups)));
+ }
+
+ @Test
+ public void testReceiveBroadcast_ConfigReadyWithSubscriptions() throws Exception {
+ mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
+ mTestLooper.dispatchAll();
+
+ final Set<ParcelUuid> activeSubGroups = Collections.singleton(TEST_PARCEL_UUID);
+ verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups)));
+ }
+
+ @Test
+ public void testReceiveBroadcast_ConfigReadyNoSubscriptions() throws Exception {
+ doReturn(new ArrayList<SubscriptionInfo>())
+ .when(mSubscriptionManager)
+ .getAllSubscriptionInfoList();
+
+ mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
+ mTestLooper.dispatchAll();
+
+ // Expect an empty snapshot
+ verify(mCallback).onNewSnapshot(
+ eq(buildExpectedSnapshot(Collections.emptyMap(), Collections.emptySet())));
+ }
+
+ @Test
+ public void testReceiveBroadcast_SlotCleared() throws Exception {
+ setupReadySubIds();
+
+ mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(false));
+ mTestLooper.dispatchAll();
+
+ verifyNoActiveSubscriptions();
+ assertTrue(mTelephonySubscriptionTracker.getReadySubIdsBySlotId().isEmpty());
+ }
+
+ @Test
+ public void testReceiveBroadcast_ConfigNotReady() throws Exception {
+ doReturn(false).when(mDeps).isConfigForIdentifiedCarrier(any());
+
+ mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
+ mTestLooper.dispatchAll();
+
+ // No interactions expected; config was not loaded
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void testSubscriptionsClearedAfterValidTriggersCallbacks() throws Exception {
+ final Set<ParcelUuid> activeSubGroups = Collections.singleton(TEST_PARCEL_UUID);
+
+ mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
+ mTestLooper.dispatchAll();
+ verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups)));
+ assertNotNull(
+ mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX));
+
+ doReturn(Collections.emptyList()).when(mSubscriptionManager).getAllSubscriptionInfoList();
+ mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
+ mTestLooper.dispatchAll();
+ verify(mCallback).onNewSnapshot(
+ eq(buildExpectedSnapshot(Collections.emptyMap(), Collections.emptySet())));
+ }
+
+ @Test
+ public void testSlotClearedAfterValidTriggersCallbacks() throws Exception {
+ final Set<ParcelUuid> activeSubGroups = Collections.singleton(TEST_PARCEL_UUID);
+
+ mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
+ mTestLooper.dispatchAll();
+ verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups)));
+ assertNotNull(
+ mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX));
+
+ mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(false));
+ mTestLooper.dispatchAll();
+ verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(Collections.emptySet())));
+ assertNull(mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX));
+ }
+
+ @Test
+ public void testTelephonySubscriptionSnapshotGetGroupForSubId() throws Exception {
+ final TelephonySubscriptionSnapshot snapshot =
+ new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, Collections.emptySet());
+
+ assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_1));
+ assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_2));
+ }
+
+ @Test
+ public void testTelephonySubscriptionSnapshotGetAllSubIdsInGroup() throws Exception {
+ final TelephonySubscriptionSnapshot snapshot =
+ new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, Collections.emptySet());
+
+ assertEquals(
+ new ArraySet<>(Arrays.asList(TEST_SUBSCRIPTION_ID_1, TEST_SUBSCRIPTION_ID_2)),
+ snapshot.getAllSubIdsInGroup(TEST_PARCEL_UUID));
+ }
+}