Merge changes Ia5022310,Ia4529d08

* changes:
  Remove VCN Carrier Privilege grace period
  Update TelephonySubscriptionTracker to use CarrierPrivilegesListener
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 0c990ec..6a7afd9 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -24,7 +24,6 @@
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_INACTIVE;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_NOT_CONFIGURED;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_SAFE_MODE;
-import static android.telephony.SubscriptionManager.isValidSubscriptionId;
 
 import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback;
@@ -164,10 +163,6 @@
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     static final String VCN_CONFIG_FILE = "/data/system/vcn/configs.xml";
 
-    // TODO(b/176956496): Directly use CarrierServiceBindHelper.UNBIND_DELAY_MILLIS
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    static final long CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS = TimeUnit.SECONDS.toMillis(30);
-
     /* Binder context for this service */
     @NonNull private final Context mContext;
     @NonNull private final Dependencies mDeps;
@@ -372,12 +367,15 @@
 
     /** Notifies the VcnManagementService that external dependencies can be set up. */
     public void systemReady() {
-        mNetworkProvider.register();
-        mContext.getSystemService(ConnectivityManager.class)
-                .registerNetworkCallback(
-                        new NetworkRequest.Builder().clearCapabilities().build(),
-                        mTrackingNetworkCallback);
-        mTelephonySubscriptionTracker.register();
+        // Always run on the handler thread to ensure consistency.
+        mHandler.post(() -> {
+            mNetworkProvider.register();
+            mContext.getSystemService(ConnectivityManager.class)
+                    .registerNetworkCallback(
+                            new NetworkRequest.Builder().clearCapabilities().build(),
+                            mTrackingNetworkCallback);
+            mTelephonySubscriptionTracker.register();
+        });
     }
 
     private void enforcePrimaryUser() {
@@ -471,22 +469,15 @@
                         if (!mVcns.containsKey(subGrp)) {
                             startVcnLocked(subGrp, entry.getValue());
                         }
-
-                        // Cancel any scheduled teardowns for active subscriptions
-                        mHandler.removeCallbacksAndMessages(mVcns.get(subGrp));
                     }
                 }
 
-                // Schedule teardown of any VCN instances that have lost carrier privileges (after a
-                // delay)
+                // Schedule teardown of any VCN instances that have lost carrier privileges
                 for (Entry<ParcelUuid, Vcn> entry : mVcns.entrySet()) {
                     final ParcelUuid subGrp = entry.getKey();
                     final VcnConfig config = mConfigs.get(subGrp);
 
                     final boolean isActiveSubGrp = isActiveSubGroup(subGrp, snapshot);
-                    final boolean isValidActiveDataSubIdNotInVcnSubGrp =
-                            isValidSubscriptionId(snapshot.getActiveDataSubscriptionId())
-                                    && !isActiveSubGroup(subGrp, snapshot);
 
                     // TODO(b/193687515): Support multiple VCNs active at the same time
                     if (config == null
@@ -496,31 +487,12 @@
                         final ParcelUuid uuidToTeardown = subGrp;
                         final Vcn instanceToTeardown = entry.getValue();
 
-                        // TODO(b/193687515): Support multiple VCNs active at the same time
-                        // If directly switching to a subscription not in the current group,
-                        // teardown immediately to prevent other subscription's network from being
-                        // outscored by the VCN. Otherwise, teardown after a delay to ensure that
-                        // SIM profile switches do not trigger the VCN to cycle.
-                        final long teardownDelayMs =
-                                isValidActiveDataSubIdNotInVcnSubGrp
-                                        ? 0
-                                        : CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS;
-                        mHandler.postDelayed(() -> {
-                            synchronized (mLock) {
-                                // Guard against case where this is run after a old instance was
-                                // torn down, and a new instance was started. Verify to ensure
-                                // correct instance is torn down. This could happen as a result of a
-                                // Carrier App manually removing/adding a VcnConfig.
-                                if (mVcns.get(uuidToTeardown) == instanceToTeardown) {
-                                    stopVcnLocked(uuidToTeardown);
+                        stopVcnLocked(uuidToTeardown);
 
-                                    // TODO(b/181789060): invoke asynchronously after Vcn notifies
-                                    // through VcnCallback
-                                    notifyAllPermissionedStatusCallbacksLocked(
-                                            uuidToTeardown, VCN_STATUS_CODE_INACTIVE);
-                                }
-                            }
-                        }, instanceToTeardown, teardownDelayMs);
+                        // TODO(b/181789060): invoke asynchronously after Vcn notifies
+                        // through VcnCallback
+                        notifyAllPermissionedStatusCallbacksLocked(
+                                uuidToTeardown, VCN_STATUS_CODE_INACTIVE);
                     } else {
                         // If this VCN's status has not changed, update it with the new snapshot
                         entry.getValue().updateSubscriptionSnapshot(mLastSnapshot);
diff --git a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
index a31c56a..a17e792 100644
--- a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
+++ b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
@@ -21,6 +21,7 @@
 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 android.telephony.TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -38,15 +39,19 @@
 import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
+import android.telephony.TelephonyManager.CarrierPrivilegesListener;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -92,6 +97,10 @@
     @NonNull private final Map<Integer, Integer> mReadySubIdsBySlotId = new HashMap<>();
     @NonNull private final OnSubscriptionsChangedListener mSubscriptionChangedListener;
 
+    @NonNull
+    private final List<CarrierPrivilegesListener> mCarrierPrivilegesChangedListeners =
+            new ArrayList<>();
+
     @NonNull private TelephonySubscriptionSnapshot mCurrentSnapshot;
 
     public TelephonySubscriptionTracker(
@@ -126,22 +135,71 @@
                 };
     }
 
-    /** Registers the receivers, and starts tracking subscriptions. */
+    /**
+     * Registers the receivers, and starts tracking subscriptions.
+     *
+     * <p>Must always be run on the VcnManagementService thread.
+     */
     public void register() {
         final HandlerExecutor executor = new HandlerExecutor(mHandler);
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
+        filter.addAction(ACTION_MULTI_SIM_CONFIG_CHANGED);
 
-        mContext.registerReceiver(
-                this, new IntentFilter(ACTION_CARRIER_CONFIG_CHANGED), null, mHandler);
+        mContext.registerReceiver(this, filter, null, mHandler);
         mSubscriptionManager.addOnSubscriptionsChangedListener(
                 executor, mSubscriptionChangedListener);
         mTelephonyManager.registerTelephonyCallback(executor, mActiveDataSubIdListener);
+
+        registerCarrierPrivilegesListeners();
     }
 
-    /** Unregisters the receivers, and stops tracking subscriptions. */
+    private void registerCarrierPrivilegesListeners() {
+        final HandlerExecutor executor = new HandlerExecutor(mHandler);
+        final int modemCount = mTelephonyManager.getActiveModemCount();
+        try {
+            for (int i = 0; i < modemCount; i++) {
+                CarrierPrivilegesListener carrierPrivilegesListener =
+                        new CarrierPrivilegesListener() {
+                            @Override
+                            public void onCarrierPrivilegesChanged(
+                                    @NonNull List<String> privilegedPackageNames,
+                                    @NonNull int[] privilegedUids) {
+                                // Re-trigger the synchronous check (which is also very cheap due
+                                // to caching in CarrierPrivilegesTracker). This allows consistency
+                                // with the onSubscriptionsChangedListener and broadcasts.
+                                handleSubscriptionsChanged();
+                            }
+                        };
+
+                mTelephonyManager.addCarrierPrivilegesListener(
+                        i, executor, carrierPrivilegesListener);
+                mCarrierPrivilegesChangedListeners.add(carrierPrivilegesListener);
+            }
+        } catch (IllegalArgumentException e) {
+            Slog.wtf(TAG, "Encounted exception registering carrier privileges listeners", e);
+        }
+    }
+
+    /**
+     * Unregisters the receivers, and stops tracking subscriptions.
+     *
+     * <p>Must always be run on the VcnManagementService thread.
+     */
     public void unregister() {
         mContext.unregisterReceiver(this);
         mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionChangedListener);
         mTelephonyManager.unregisterTelephonyCallback(mActiveDataSubIdListener);
+
+        unregisterCarrierPrivilegesListeners();
+    }
+
+    private void unregisterCarrierPrivilegesListeners() {
+        for (CarrierPrivilegesListener carrierPrivilegesListener :
+                mCarrierPrivilegesChangedListeners) {
+            mTelephonyManager.removeCarrierPrivilegesListener(carrierPrivilegesListener);
+        }
+        mCarrierPrivilegesChangedListeners.clear();
     }
 
     /**
@@ -178,8 +236,6 @@
             // group.
             if (subInfo.getSimSlotIndex() != INVALID_SIM_SLOT_INDEX
                     && mReadySubIdsBySlotId.values().contains(subInfo.getSubscriptionId())) {
-                // TODO (b/172619301): Cache based on callbacks from CarrierPrivilegesTracker
-
                 final TelephonyManager subIdSpecificTelephonyManager =
                         mTelephonyManager.createForSubscriptionId(subInfo.getSubscriptionId());
 
@@ -214,12 +270,39 @@
      */
     @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;
+        switch (intent.getAction()) {
+            case ACTION_CARRIER_CONFIG_CHANGED:
+                handleActionCarrierConfigChanged(context, intent);
+                break;
+            case ACTION_MULTI_SIM_CONFIG_CHANGED:
+                handleActionMultiSimConfigChanged(context, intent);
+                break;
+            default:
+                Slog.v(TAG, "Unknown intent received with action: " + intent.getAction());
+        }
+    }
+
+    private void handleActionMultiSimConfigChanged(Context context, Intent intent) {
+        unregisterCarrierPrivilegesListeners();
+
+        // Clear invalid slotIds from the mReadySubIdsBySlotId map.
+        final int modemCount = mTelephonyManager.getActiveModemCount();
+        final Iterator<Integer> slotIdIterator = mReadySubIdsBySlotId.keySet().iterator();
+        while (slotIdIterator.hasNext()) {
+            final int slotId = slotIdIterator.next();
+
+            if (slotId >= modemCount) {
+                slotIdIterator.remove();
+            }
         }
 
+        registerCarrierPrivilegesListeners();
+        handleSubscriptionsChanged();
+    }
+
+    private void handleActionCarrierConfigChanged(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
         final int subId = intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, INVALID_SUBSCRIPTION_ID);
         final int slotId = intent.getIntExtra(EXTRA_SLOT_INDEX, INVALID_SIM_SLOT_INDEX);
 
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 7c7dc4d..bb98bc0 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -23,7 +23,6 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_SAFE_MODE;
-import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 import static android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
 import static android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS;
 
@@ -264,6 +263,7 @@
     @Test
     public void testSystemReady() throws Exception {
         mVcnMgmtSvc.systemReady();
+        mTestLooper.dispatchAll();
 
         verify(mConnMgr).registerNetworkProvider(any(VcnNetworkProvider.class));
         verify(mSubscriptionTracker).register();
@@ -475,10 +475,8 @@
         mVcnMgmtSvc.addVcnUnderlyingNetworkPolicyListener(mMockPolicyListener);
 
         triggerSubscriptionTrackerCbAndGetSnapshot(null, Collections.emptySet());
-
-        // Verify teardown after delay
-        mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
         mTestLooper.dispatchAll();
+
         verify(vcn).teardownAsynchronously();
         verify(mMockPolicyListener).onPolicyChanged();
     }
@@ -504,92 +502,6 @@
         assertEquals(0, mVcnMgmtSvc.getAllVcns().size());
     }
 
-    /**
-     * Tests an intermediate state where carrier privileges are marked as lost before active data
-     * subId changes during a SIM ejection.
-     *
-     * <p>The expected outcome is that the VCN is torn down after a delay, as opposed to
-     * immediately.
-     */
-    @Test
-    public void testTelephonyNetworkTrackerCallbackLostCarrierPrivilegesBeforeActiveDataSubChanges()
-            throws Exception {
-        setupActiveSubscription(TEST_UUID_2);
-
-        final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback();
-        final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2);
-
-        // Simulate privileges lost
-        triggerSubscriptionTrackerCbAndGetSnapshot(
-                TEST_SUBSCRIPTION_ID,
-                TEST_UUID_2,
-                Collections.emptySet(),
-                Collections.emptyMap(),
-                false /* hasCarrierPrivileges */);
-
-        // Verify teardown after delay
-        mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
-        mTestLooper.dispatchAll();
-        verify(vcn).teardownAsynchronously();
-    }
-
-    @Test
-    public void testTelephonyNetworkTrackerCallbackSimSwitchesDoNotKillVcnInstances()
-            throws Exception {
-        setupActiveSubscription(TEST_UUID_2);
-
-        final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback();
-        final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2);
-
-        // Simulate SIM unloaded
-        triggerSubscriptionTrackerCbAndGetSnapshot(
-                INVALID_SUBSCRIPTION_ID,
-                null /* activeDataSubscriptionGroup */,
-                Collections.emptySet(),
-                Collections.emptyMap(),
-                false /* hasCarrierPrivileges */);
-
-        // Simulate new SIM loaded right during teardown delay.
-        mTestLooper.moveTimeForward(
-                VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2);
-        mTestLooper.dispatchAll();
-        triggerSubscriptionTrackerCbAndGetSnapshot(TEST_UUID_2, Collections.singleton(TEST_UUID_2));
-
-        // Verify that even after the full timeout duration, the VCN instance is not torn down
-        mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
-        mTestLooper.dispatchAll();
-        verify(vcn, never()).teardownAsynchronously();
-    }
-
-    @Test
-    public void testTelephonyNetworkTrackerCallbackDoesNotKillNewVcnInstances() throws Exception {
-        setupActiveSubscription(TEST_UUID_2);
-
-        final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback();
-        final Vcn oldInstance = startAndGetVcnInstance(TEST_UUID_2);
-
-        // Simulate SIM unloaded
-        triggerSubscriptionTrackerCbAndGetSnapshot(null, Collections.emptySet());
-
-        // Config cleared, SIM reloaded & config re-added right before teardown delay, staring new
-        // vcnInstance.
-        mTestLooper.moveTimeForward(
-                VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2);
-        mTestLooper.dispatchAll();
-        mVcnMgmtSvc.clearVcnConfig(TEST_UUID_2, TEST_PACKAGE_NAME);
-        triggerSubscriptionTrackerCbAndGetSnapshot(TEST_UUID_2, Collections.singleton(TEST_UUID_2));
-        final Vcn newInstance = startAndGetVcnInstance(TEST_UUID_2);
-
-        // Verify that new instance was different, and the old one was torn down
-        assertTrue(oldInstance != newInstance);
-        verify(oldInstance).teardownAsynchronously();
-
-        // Verify that even after the full timeout duration, the new VCN instance is not torn down
-        mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
-        mTestLooper.dispatchAll();
-        verify(newInstance, never()).teardownAsynchronously();
-    }
-
     @Test
     public void testPackageChangeListenerRegistered() throws Exception {
         verify(mMockContext).registerReceiver(any(BroadcastReceiver.class), argThat(filter -> {
@@ -925,6 +837,8 @@
     private void setupSubscriptionAndStartVcn(
             int subId, ParcelUuid subGrp, boolean isVcnActive, boolean hasCarrierPrivileges) {
         mVcnMgmtSvc.systemReady();
+        mTestLooper.dispatchAll();
+
         triggerSubscriptionTrackerCbAndGetSnapshot(
                 subGrp,
                 Collections.singleton(subGrp),
@@ -1020,6 +934,7 @@
 
     private void setupTrackedCarrierWifiNetwork(NetworkCapabilities caps) {
         mVcnMgmtSvc.systemReady();
+        mTestLooper.dispatchAll();
 
         final ArgumentCaptor<NetworkCallback> captor =
                 ArgumentCaptor.forClass(NetworkCallback.class);
@@ -1264,15 +1179,14 @@
                 true /* isActive */,
                 true /* hasCarrierPrivileges */);
 
-        // VCN is currently active. Lose carrier privileges for TEST_PACKAGE and hit teardown
-        // timeout so the VCN goes inactive.
+        // VCN is currently active. Lose carrier privileges for TEST_PACKAGE so the VCN goes
+        // inactive.
         final TelephonySubscriptionSnapshot snapshot =
                 triggerSubscriptionTrackerCbAndGetSnapshot(
                         TEST_UUID_1,
                         Collections.singleton(TEST_UUID_1),
                         Collections.singletonMap(TEST_SUBSCRIPTION_ID, TEST_UUID_1),
                         false /* hasCarrierPrivileges */);
-        mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
         mTestLooper.dispatchAll();
 
         // Giving TEST_PACKAGE privileges again will restart the VCN (which will indicate ACTIVE
diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
index 1f0df62..978bf3e 100644
--- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
@@ -22,6 +22,7 @@
 import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 import static android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener;
+import static android.telephony.TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED;
 
 import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback;
@@ -34,8 +35,10 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
@@ -57,6 +60,8 @@
 import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
+import android.telephony.TelephonyManager.CarrierPrivilegesListener;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 
 import androidx.test.filters.SmallTest;
@@ -83,7 +88,7 @@
     private static final String PACKAGE_NAME =
             TelephonySubscriptionTrackerTest.class.getPackage().getName();
     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_SIM_SLOT_INDEX = 0;
     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;
@@ -151,6 +156,8 @@
 
     @Before
     public void setUp() throws Exception {
+        doReturn(2).when(mTelephonyManager).getActiveModemCount();
+
         mCallback = mock(TelephonySubscriptionTrackerCallback.class);
         mTelephonySubscriptionTracker =
                 new TelephonySubscriptionTracker(mContext, mHandler, mCallback, mDeps);
@@ -180,6 +187,15 @@
         return captor.getValue();
     }
 
+    private List<CarrierPrivilegesListener> getCarrierPrivilegesListeners() {
+        final ArgumentCaptor<CarrierPrivilegesListener> captor =
+                ArgumentCaptor.forClass(CarrierPrivilegesListener.class);
+        verify(mTelephonyManager, atLeastOnce())
+                .addCarrierPrivilegesListener(anyInt(), any(), captor.capture());
+
+        return captor.getAllValues();
+    }
+
     private ActiveDataSubscriptionIdListener getActiveDataSubscriptionIdListener() {
         final ArgumentCaptor<TelephonyCallback> captor =
                 ArgumentCaptor.forClass(TelephonyCallback.class);
@@ -188,6 +204,11 @@
         return (ActiveDataSubscriptionIdListener) captor.getValue();
     }
 
+    private Intent buildTestMultiSimConfigBroadcastIntent() {
+        Intent intent = new Intent(ACTION_MULTI_SIM_CONFIG_CHANGED);
+        return intent;
+    }
+
     private Intent buildTestBroadcastIntent(boolean hasValidSubscription) {
         Intent intent = new Intent(ACTION_CARRIER_CONFIG_CHANGED);
         intent.putExtra(EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX);
@@ -239,12 +260,21 @@
                         any(),
                         eq(mHandler));
         final IntentFilter filter = getIntentFilter();
-        assertEquals(1, filter.countActions());
+        assertEquals(2, filter.countActions());
         assertTrue(filter.hasAction(ACTION_CARRIER_CONFIG_CHANGED));
+        assertTrue(filter.hasAction(ACTION_MULTI_SIM_CONFIG_CHANGED));
 
         verify(mSubscriptionManager)
                 .addOnSubscriptionsChangedListener(any(HandlerExecutor.class), any());
         assertNotNull(getOnSubscriptionsChangedListener());
+
+        verify(mTelephonyManager, times(2))
+                .addCarrierPrivilegesListener(anyInt(), any(HandlerExecutor.class), any());
+        verify(mTelephonyManager)
+                .addCarrierPrivilegesListener(eq(0), any(HandlerExecutor.class), any());
+        verify(mTelephonyManager)
+                .addCarrierPrivilegesListener(eq(1), any(HandlerExecutor.class), any());
+        assertEquals(2, getCarrierPrivilegesListeners().size());
     }
 
     @Test
@@ -255,6 +285,49 @@
 
         final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
         verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(eq(listener));
+
+        for (CarrierPrivilegesListener carrierPrivilegesListener :
+                getCarrierPrivilegesListeners()) {
+            verify(mTelephonyManager)
+                    .removeCarrierPrivilegesListener(eq(carrierPrivilegesListener));
+        }
+    }
+
+    @Test
+    public void testMultiSimConfigChanged() throws Exception {
+        final ArrayMap<Integer, Integer> readySubIdsBySlotId = new ArrayMap<>();
+        readySubIdsBySlotId.put(TEST_SIM_SLOT_INDEX, TEST_SUBSCRIPTION_ID_1);
+        readySubIdsBySlotId.put(TEST_SIM_SLOT_INDEX + 1, TEST_SUBSCRIPTION_ID_1);
+
+        mTelephonySubscriptionTracker.setReadySubIdsBySlotId(readySubIdsBySlotId);
+        doReturn(1).when(mTelephonyManager).getActiveModemCount();
+
+        List<CarrierPrivilegesListener> carrierPrivilegesListeners =
+                getCarrierPrivilegesListeners();
+
+        mTelephonySubscriptionTracker.onReceive(mContext, buildTestMultiSimConfigBroadcastIntent());
+        mTestLooper.dispatchAll();
+
+        for (CarrierPrivilegesListener carrierPrivilegesListener : carrierPrivilegesListeners) {
+            verify(mTelephonyManager)
+                    .removeCarrierPrivilegesListener(eq(carrierPrivilegesListener));
+        }
+
+        // Expect cache cleared for inactive slots.
+        assertNull(
+                mTelephonySubscriptionTracker
+                        .getReadySubIdsBySlotId()
+                        .get(TEST_SIM_SLOT_INDEX + 1));
+
+        // Expect a new CarrierPrivilegesListener to have been registered for slot 0, and none other
+        // (2 previously registered during startup, for slots 0 & 1)
+        verify(mTelephonyManager, times(3))
+                .addCarrierPrivilegesListener(anyInt(), any(HandlerExecutor.class), any());
+        verify(mTelephonyManager, times(2))
+                .addCarrierPrivilegesListener(eq(0), any(HandlerExecutor.class), any());
+
+        // Verify that this triggers a re-evaluation
+        verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES)));
     }
 
     @Test
@@ -314,6 +387,17 @@
     }
 
     @Test
+    public void testOnCarrierPrivilegesChanged() throws Exception {
+        setupReadySubIds();
+
+        final CarrierPrivilegesListener listener = getCarrierPrivilegesListeners().get(0);
+        listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[] {});
+        mTestLooper.dispatchAll();
+
+        verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES)));
+    }
+
+    @Test
     public void testReceiveBroadcast_ConfigReadyWithSubscriptions() throws Exception {
         mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
         mTestLooper.dispatchAll();