Minimize window during which default sub may be invalid.

Verified from logs that the window is reduced from ~475ms
to ~28ms.

Test: manually tested single sim<->multi sim transitions
Test: manually verified window duration from logs
Test: atest PhoneConfigurationManagerTest
Bug: 163582235
Change-Id: I75967e2099424997dbb32e4f4c96bd2bf961aac1
diff --git a/src/java/com/android/internal/telephony/MultiSimSettingController.java b/src/java/com/android/internal/telephony/MultiSimSettingController.java
index a90daf9..c69a115 100644
--- a/src/java/com/android/internal/telephony/MultiSimSettingController.java
+++ b/src/java/com/android/internal/telephony/MultiSimSettingController.java
@@ -37,6 +37,7 @@
 import android.content.IntentFilter;
 import android.os.AsyncResult;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelUuid;
 import android.provider.Settings;
@@ -226,6 +227,7 @@
      * Notify subscription info change.
      */
     public void notifySubscriptionInfoChanged() {
+        log("notifySubscriptionInfoChanged");
         obtainMessage(EVENT_SUBSCRIPTION_INFO_CHANGED).sendToTarget();
     }
 
@@ -332,6 +334,22 @@
     }
 
     /**
+     * This method is called when a phone object is removed (for example when going from multi-sim
+     * to single-sim).
+     * NOTE: This method does not post a message to self, instead it calls reEvaluateAll() directly.
+     * so it should only be called from the main thread. The reason is to update defaults asap
+     * after multi_sim_config property has been updated (see b/163582235).
+     */
+    public void onPhoneRemoved() {
+        if (DBG) log("onPhoneRemoved");
+        if (Looper.myLooper() != this.getLooper()) {
+            throw new RuntimeException("This method must be called from the same looper as "
+                    + "MultiSimSettingController.");
+        }
+        reEvaluateAll();
+    }
+
+    /**
      * Called when carrier config changes on any phone.
      */
     @VisibleForTesting
diff --git a/src/java/com/android/internal/telephony/PhoneConfigurationManager.java b/src/java/com/android/internal/telephony/PhoneConfigurationManager.java
index fba016c..03c6cbe 100644
--- a/src/java/com/android/internal/telephony/PhoneConfigurationManager.java
+++ b/src/java/com/android/internal/telephony/PhoneConfigurationManager.java
@@ -362,22 +362,35 @@
             log("onMultiSimConfigChanged: Rebooting is not required.");
             mMi.notifyPhoneFactoryOnMultiSimConfigChanged(mContext, numOfActiveModems);
             broadcastMultiSimConfigChange(numOfActiveModems);
-
+            boolean subInfoCleared = false;
             // if numOfActiveModems is decreasing, deregister old RILs
             // eg if we are going from 2 phones to 1 phone, we need to deregister RIL for the
             // second phone. This loop does nothing if numOfActiveModems is increasing.
-            for (int i = numOfActiveModems; i < oldNumOfActiveModems; i++) {
-                mPhones[i].mCi.onSlotActiveStatusChange(SubscriptionManager.isValidPhoneId(i));
+            for (int phoneId = numOfActiveModems; phoneId < oldNumOfActiveModems; phoneId++) {
+                SubscriptionController.getInstance().clearSubInfoRecord(phoneId);
+                subInfoCleared = true;
+                mPhones[phoneId].mCi.onSlotActiveStatusChange(
+                        SubscriptionManager.isValidPhoneId(phoneId));
+            }
+            if (subInfoCleared) {
+                // This triggers update of default subs. This should be done asap after
+                // setMultiSimProperties() to avoid (minimize) duration for which default sub can be
+                // invalid and can map to a non-existent phone.
+                // If forexample someone calls a TelephonyManager API on default sub after
+                // setMultiSimProperties() and before onSubscriptionsChanged() below -- they can be
+                // using an invalid sub, which can map to a non-existent phone and can cause an
+                // exception (see b/163582235).
+                MultiSimSettingController.getInstance().onPhoneRemoved();
             }
             // old phone objects are not needed now; mPhones can be updated
             mPhones = PhoneFactory.getPhones();
             // if numOfActiveModems is increasing, register new RILs
             // eg if we are going from 1 phone to 2 phones, we need to register RIL for the second
             // phone. This loop does nothing if numOfActiveModems is decreasing.
-            for (int i = oldNumOfActiveModems; i < numOfActiveModems; i++) {
-                Phone phone = mPhones[i];
+            for (int phoneId = oldNumOfActiveModems; phoneId < numOfActiveModems; phoneId++) {
+                Phone phone = mPhones[phoneId];
                 registerForRadioState(phone);
-                phone.mCi.onSlotActiveStatusChange(SubscriptionManager.isValidPhoneId(i));
+                phone.mCi.onSlotActiveStatusChange(SubscriptionManager.isValidPhoneId(phoneId));
             }
         }
     }
diff --git a/src/java/com/android/internal/telephony/SubscriptionInfoUpdater.java b/src/java/com/android/internal/telephony/SubscriptionInfoUpdater.java
index dc842d2..5d7ad9a 100644
--- a/src/java/com/android/internal/telephony/SubscriptionInfoUpdater.java
+++ b/src/java/com/android/internal/telephony/SubscriptionInfoUpdater.java
@@ -340,7 +340,6 @@
                 Context.TELEPHONY_SERVICE)).getActiveModemCount();
         // For inactive modems, reset its states.
         for (int phoneId = activeModemCount; phoneId < SUPPORTED_MODEM_COUNT; phoneId++) {
-            SubscriptionController.getInstance().clearSubInfoRecord(phoneId);
             sIccId[phoneId] = null;
             sSimCardState[phoneId] = TelephonyManager.SIM_STATE_UNKNOWN;
             sSimApplicationState[phoneId] = TelephonyManager.SIM_STATE_UNKNOWN;
diff --git a/tests/telephonytests/src/com/android/internal/telephony/MultiSimSettingControllerTest.java b/tests/telephonytests/src/com/android/internal/telephony/MultiSimSettingControllerTest.java
index af72c70..2033af5 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/MultiSimSettingControllerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/MultiSimSettingControllerTest.java
@@ -37,6 +37,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.content.Intent;
+import android.os.HandlerThread;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
 import android.provider.Settings;
@@ -51,6 +52,7 @@
 import com.android.internal.telephony.dataconnection.DataEnabledSettings;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -60,6 +62,9 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
@@ -653,4 +658,41 @@
         // This time user data should be disabled on phone1.
         verify(mDataEnabledSettingsMock2).setUserDataEnabled(false);
     }
+
+    @Test
+    @SmallTest
+    public void testOnPhoneRemoved() {
+        try {
+            mMultiSimSettingControllerUT.onPhoneRemoved();
+        } catch (RuntimeException re) {
+            Assert.fail("Exception not expected when calling from the same thread");
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testOnPhoneRemoved_DifferentThread() {
+        AtomicBoolean result = new AtomicBoolean(false);
+        CountDownLatch latch = new CountDownLatch(1);
+        HandlerThread handlerThread = new HandlerThread("MultiSimSettingControllerTest") {
+            public void onLooperPrepared() {
+                try {
+                    mMultiSimSettingControllerUT.onPhoneRemoved();
+                } catch (RuntimeException re) {
+                    result.set(true); // true to indicate that the test passed
+                }
+                latch.countDown();
+            }
+        };
+        handlerThread.start();
+        try {
+            if (!latch.await(5, TimeUnit.SECONDS)) {
+                Assert.fail("CountDownLatch did not reach 0");
+            } else if (!result.get()) {
+                Assert.fail("Exception expected when not calling from the same thread");
+            }
+        } catch (InterruptedException ie) {
+            Assert.fail("InterruptedException during latch.await");
+        }
+    }
 }
diff --git a/tests/telephonytests/src/com/android/internal/telephony/PhoneConfigurationManagerTest.java b/tests/telephonytests/src/com/android/internal/telephony/PhoneConfigurationManagerTest.java
index d8094c0..a702aac 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/PhoneConfigurationManagerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/PhoneConfigurationManagerTest.java
@@ -203,4 +203,58 @@
         verify(mMockCi1, times(1)).registerForAvailable(any(), anyInt(), any());
         verify(mMockCi1, times(1)).onSlotActiveStatusChange(anyBoolean());
     }
+
+    @Test
+    @SmallTest
+    public void testSwitchMultiSimConfig_multiSimToSingleSim() throws Exception {
+        mPhones = new Phone[]{mPhone, mPhone1};
+        replaceInstance(PhoneFactory.class, "sPhones", null, mPhones);
+        init(2);
+        verify(mMockCi0, times(1)).registerForAvailable(any(), anyInt(), any());
+        verify(mMockCi1, times(1)).registerForAvailable(any(), anyInt(), any());
+
+        // Register for multi SIM config change.
+        mPcm.registerForMultiSimConfigChange(mHandler, EVENT_MULTI_SIM_CONFIG_CHANGED, null);
+        verify(mHandler, never()).sendMessageAtTime(any(), anyLong());
+
+        // Switch to single sim.
+        setRebootRequiredForConfigSwitch(false);
+        mPcm.switchMultiSimConfig(1);
+        ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
+        verify(mMockRadioConfig).setModemsConfig(eq(1), captor.capture());
+
+        // Send message back to indicate switch success.
+        Message message = captor.getValue();
+        AsyncResult.forMessage(message, null, null);
+        message.sendToTarget();
+        processAllMessages();
+
+        // Verify set system property being called.
+        verify(mMi).setMultiSimProperties(1);
+        verify(mMi).notifyPhoneFactoryOnMultiSimConfigChanged(any(), eq(1));
+
+        // Capture and verify registration notification.
+        verify(mHandler).sendMessageAtTime(captor.capture(), anyLong());
+        message = captor.getValue();
+        assertEquals(EVENT_MULTI_SIM_CONFIG_CHANGED, message.what);
+        assertEquals(1, ((AsyncResult) message.obj).result);
+
+        // Capture and verify broadcast.
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).sendBroadcast(intentCaptor.capture());
+        Intent intent = intentCaptor.getValue();
+        assertEquals(ACTION_MULTI_SIM_CONFIG_CHANGED, intent.getAction());
+        assertEquals(1, intent.getIntExtra(
+                EXTRA_ACTIVE_SIM_SUPPORTED_COUNT, 0));
+
+        // Verify clearSubInfoRecord() and onSlotActiveStatusChange() are called for second phone,
+        // and not for the first one
+        verify(mSubscriptionController).clearSubInfoRecord(1);
+        verify(mMockCi1).onSlotActiveStatusChange(anyBoolean());
+        verify(mSubscriptionController, never()).clearSubInfoRecord(0);
+        verify(mMockCi0, never()).onSlotActiveStatusChange(anyBoolean());
+
+        // Verify onPhoneRemoved() gets called on MultiSimSettingController phone
+        verify(mMultiSimSettingController).onPhoneRemoved();
+    }
 }