Refactor CarrierTextController

The list of carriers is now created on the go, so it will always have
the same number of items as there are subscriptions.

Fixes code that reused same slotID into carrierName order.

Test: atest
Test: manual using DSDS

Change-Id: I2533738429b8f0f94c84110641e8c162d19f094a
diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextController.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextController.java
index 1539582..64517ba 100644
--- a/packages/SystemUI/src/com/android/keyguard/CarrierTextController.java
+++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextController.java
@@ -52,9 +52,11 @@
     private boolean mTelephonyCapable;
     private boolean mShowMissingSim;
     private boolean mShowAirplaneMode;
-    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @VisibleForTesting
+    protected KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private WifiManager mWifiManager;
-    private boolean[] mSimErrorState = new boolean[TelephonyManager.getDefault().getPhoneCount()];
+    private boolean[] mSimErrorState;
+    private final int mSimSlotsNumber;
     private CarrierTextCallback mCarrierTextCallback;
     private Context mContext;
     private CharSequence mSeparator;
@@ -72,7 +74,8 @@
                 }
             };
 
-    private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() {
+    @VisibleForTesting
+    protected final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() {
         @Override
         public void onRefreshCarrierInfo() {
             if (DEBUG) {
@@ -93,7 +96,7 @@
         }
 
         public void onSimStateChanged(int subId, int slotId, IccCardConstants.State simState) {
-            if (slotId < 0) {
+            if (slotId < 0 || slotId >= mSimSlotsNumber) {
                 Log.d(TAG, "onSimStateChanged() - slotId invalid: " + slotId
                         + " mTelephonyCapable: " + Boolean.toString(mTelephonyCapable));
                 return;
@@ -144,36 +147,48 @@
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         mSeparator = separator;
         mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
+        mSimSlotsNumber = ((TelephonyManager) context.getSystemService(
+                Context.TELEPHONY_SERVICE)).getPhoneCount();
+        mSimErrorState = new boolean[mSimSlotsNumber];
     }
 
     /**
      * Checks if there are faulty cards. Adds the text depending on the slot of the card
      *
      * @param text:   current carrier text based on the sim state
+     * @param carrierNames names order by subscription order
+     * @param subOrderBySlot array containing the sub index for each slot ID
      * @param noSims: whether a valid sim card is inserted
      * @return text
      */
-    private CharSequence updateCarrierTextWithSimIoError(CharSequence text, boolean noSims) {
+    private CharSequence updateCarrierTextWithSimIoError(CharSequence text,
+            CharSequence[] carrierNames, int[] subOrderBySlot, boolean noSims) {
         final CharSequence carrier = "";
         CharSequence carrierTextForSimIOError = getCarrierTextForSimState(
                 IccCardConstants.State.CARD_IO_ERROR, carrier);
+        // mSimErrorState has the state of each sim indexed by slotID.
         for (int index = 0; index < mSimErrorState.length; index++) {
-            if (mSimErrorState[index]) {
-                // In the case when no sim cards are detected but a faulty card is inserted
-                // overwrite the text and only show "Invalid card"
-                if (noSims) {
-                    return concatenate(carrierTextForSimIOError,
-                            getContext().getText(
-                                    com.android.internal.R.string.emergency_calls_only),
-                            mSeparator);
-                } else if (index == 0) {
-                    // prepend "Invalid card" when faulty card is inserted in slot 0
-                    text = concatenate(carrierTextForSimIOError, text, mSeparator);
-                } else {
-                    // concatenate "Invalid card" when faulty card is inserted in slot 1
-                    text = concatenate(text, carrierTextForSimIOError, mSeparator);
-                }
+            if (!mSimErrorState[index]) {
+                continue;
             }
+            // In the case when no sim cards are detected but a faulty card is inserted
+            // overwrite the text and only show "Invalid card"
+            if (noSims) {
+                return concatenate(carrierTextForSimIOError,
+                        getContext().getText(
+                                com.android.internal.R.string.emergency_calls_only),
+                        mSeparator);
+            } else if (subOrderBySlot[index] != -1) {
+                int subIndex = subOrderBySlot[index];
+                // prepend "Invalid card" when faulty card is inserted in slot 0 or 1
+                carrierNames[subIndex] = concatenate(carrierTextForSimIOError,
+                        carrierNames[subIndex],
+                        mSeparator);
+            } else {
+                // concatenate "Invalid card" when faulty card is inserted in other slot
+                text = concatenate(text, carrierTextForSimIOError, mSeparator);
+            }
+
         }
         return text;
     }
@@ -209,16 +224,25 @@
     protected void updateCarrierText() {
         boolean allSimsMissing = true;
         boolean anySimReadyAndInService = false;
-        boolean missingSimsWithSubs = false;
         CharSequence displayText = null;
 
         List<SubscriptionInfo> subs = mKeyguardUpdateMonitor.getSubscriptionInfo(false);
         final int numSubs = subs.size();
         final int[] subsIds = new int[numSubs];
+        // This array will contain in position i, the index of subscription in slot ID i.
+        // -1 if no subscription in that slot
+        final int[] subOrderBySlot = new int[mSimSlotsNumber];
+        for (int i = 0; i < mSimSlotsNumber; i++) {
+            subOrderBySlot[i] = -1;
+        }
+        final CharSequence[] carrierNames = new CharSequence[numSubs];
         if (DEBUG) Log.d(TAG, "updateCarrierText(): " + numSubs);
+
         for (int i = 0; i < numSubs; i++) {
             int subId = subs.get(i).getSubscriptionId();
+            carrierNames[i] = "";
             subsIds[i] = subId;
+            subOrderBySlot[subs.get(i).getSimSlotIndex()] = i;
             IccCardConstants.State simState = mKeyguardUpdateMonitor.getSimState(subId);
             CharSequence carrierName = subs.get(i).getCarrierName();
             CharSequence carrierTextForSimState = getCarrierTextForSimState(simState, carrierName);
@@ -227,7 +251,7 @@
             }
             if (carrierTextForSimState != null) {
                 allSimsMissing = false;
-                displayText = concatenate(displayText, carrierTextForSimState, mSeparator);
+                carrierNames[i] = carrierTextForSimState;
             }
             if (simState == IccCardConstants.State.READY) {
                 ServiceState ss = mKeyguardUpdateMonitor.mServiceStates.get(subId);
@@ -256,7 +280,6 @@
                 // described above.
                 displayText = makeCarrierStringOnEmergencyCapable(
                         getMissingSimMessage(), subs.get(0).getCarrierName());
-                missingSimsWithSubs = true;
             } else {
                 // We don't have a SubscriptionInfo to get the emergency calls only from.
                 // Grab it from the old sticky broadcast if possible instead. We can use it
@@ -286,24 +309,32 @@
             }
         }
 
-        displayText = updateCarrierTextWithSimIoError(displayText, allSimsMissing);
+        displayText = updateCarrierTextWithSimIoError(displayText, carrierNames, subOrderBySlot,
+                allSimsMissing);
         // APM (airplane mode) != no carrier state. There are carrier services
         // (e.g. WFC = Wi-Fi calling) which may operate in APM.
         if (!anySimReadyAndInService && WirelessUtils.isAirplaneModeOn(mContext)) {
             displayText = getAirplaneModeMessage();
         }
 
-        Handler handler = Dependency.get(Dependency.MAIN_HANDLER);
+        if (TextUtils.isEmpty(displayText)) {
+            displayText = TextUtils.join(mSeparator, carrierNames);
+        }
         final CarrierTextCallbackInfo info = new CarrierTextCallbackInfo(
                 displayText,
-                displayText.toString().split(mSeparator.toString()),
-                anySimReadyAndInService && !missingSimsWithSubs,
+                carrierNames,
+                !allSimsMissing,
                 subsIds);
+        postToCallback(info);
+    }
+
+    @VisibleForTesting
+    protected void postToCallback(CarrierTextCallbackInfo info) {
+        Handler handler = Dependency.get(Dependency.MAIN_HANDLER);
         final CarrierTextCallback callback = mCarrierTextCallback;
         if (callback != null) {
             handler.post(() -> callback.updateCarrierInfo(info));
         }
-
     }
 
     private Context getContext() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java
index f91c9d9..bbd8de3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java
@@ -521,16 +521,7 @@
                     }
                 }
             } else {
-                // If there are sims ready but there are not the same number of carrier names as
-                // subscription ids, just show the full text in the first slot
-                mInfos[0].visible = true;
-                mCarrierTexts[0].setText(info.carrierText);
-                mCarrierGroups[0].setVisibility(View.VISIBLE);
-                for (int i = 1; i < SIM_SLOTS; i++) {
-                    mInfos[i].visible = false;
-                    mCarrierTexts[i].setText("");
-                    mCarrierGroups[i].setVisibility(View.GONE);
-                }
+                Log.e(TAG, "Carrier information arrays not of same length");
             }
         } else {
             mInfos[0].visible = false;
@@ -612,8 +603,10 @@
             // Only show marquee when visible
             if (visibility == VISIBLE) {
                 setEllipsize(TextUtils.TruncateAt.MARQUEE);
+                setSelected(true);
             } else {
                 setEllipsize(TextUtils.TruncateAt.END);
+                setSelected(false);
             }
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextControllerTest.java
new file mode 100644
index 0000000..8e061cc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextControllerTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard;
+
+
+import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE;
+import static android.telephony.SubscriptionManager.DATA_ROAMING_ENABLE;
+import static android.telephony.SubscriptionManager.NAME_SOURCE_DEFAULT_SOURCE;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.telephony.SubscriptionInfo;
+import android.telephony.TelephonyManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.internal.telephony.IccCardConstants;
+import com.android.systemui.Dependency;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class CarrierTextControllerTest extends SysuiTestCase {
+
+    private static final CharSequence SEPARATOR = " \u2014 ";
+    private static final String TEST_CARRIER = "TEST_CARRIER";
+    private static final SubscriptionInfo TEST_SUBSCRIPTION = new SubscriptionInfo(0, "", 0,
+            TEST_CARRIER, TEST_CARRIER, NAME_SOURCE_DEFAULT_SOURCE, 0xFFFFFF, "",
+            DATA_ROAMING_DISABLE, null, null, null, null, false, null, "");
+    private static final SubscriptionInfo TEST_SUBSCRIPTION_ROAMING = new SubscriptionInfo(0, "", 0,
+            TEST_CARRIER, TEST_CARRIER, NAME_SOURCE_DEFAULT_SOURCE, 0xFFFFFF, "",
+            DATA_ROAMING_ENABLE, null, null, null, null, false, null, "");
+    @Mock
+    private WifiManager mWifiManager;
+    @Mock
+    private CarrierTextController.CarrierTextCallback mCarrierTextCallback;
+    @Mock
+    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock
+    private ConnectivityManager mConnectivityManager;
+    @Mock
+    private TelephonyManager mTelephonyManager;
+    private CarrierTextController.CarrierTextCallbackInfo mCarrierTextCallbackInfo;
+
+    private CarrierTextController mCarrierTextController;
+    private TestableLooper mTestableLooper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mTestableLooper = TestableLooper.get(this);
+
+        mContext.addMockSystemService(WifiManager.class, mWifiManager);
+        mContext.addMockSystemService(ConnectivityManager.class, mConnectivityManager);
+        mContext.addMockSystemService(TelephonyManager.class, mTelephonyManager);
+        mDependency.injectMockDependency(WakefulnessLifecycle.class);
+        mDependency.injectTestDependency(Dependency.MAIN_HANDLER,
+                new Handler(mTestableLooper.getLooper()));
+
+        mCarrierTextCallbackInfo = new CarrierTextController.CarrierTextCallbackInfo("",
+                new CharSequence[]{}, false, new int[]{});
+        when(mTelephonyManager.getPhoneCount()).thenReturn(2);
+        mCarrierTextController = new TestCarrierTextController(mContext, SEPARATOR, true, true,
+                mKeyguardUpdateMonitor);
+        // This should not start listening on any of the real dependencies
+        mCarrierTextController.setListening(mCarrierTextCallback);
+    }
+
+    @Test
+    public void testWrongSlots() {
+        reset(mCarrierTextCallback);
+        when(mKeyguardUpdateMonitor.getSubscriptionInfo(anyBoolean())).thenReturn(
+                new ArrayList<>());
+        when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+                IccCardConstants.State.CARD_IO_ERROR);
+        // This should not produce an out of bounds error, even though there are no subscriptions
+        mCarrierTextController.mCallback.onSimStateChanged(0, -3,
+                IccCardConstants.State.CARD_IO_ERROR);
+        mCarrierTextController.mCallback.onSimStateChanged(0, 3, IccCardConstants.State.READY);
+        verify(mCarrierTextCallback, never()).updateCarrierInfo(any());
+    }
+
+    @Test
+    public void testMoreSlotsThanSubs() {
+        reset(mCarrierTextCallback);
+        when(mKeyguardUpdateMonitor.getSubscriptionInfo(anyBoolean())).thenReturn(
+                new ArrayList<>());
+        when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+                IccCardConstants.State.CARD_IO_ERROR);
+        // This should not produce an out of bounds error, even though there are no subscriptions
+        mCarrierTextController.mCallback.onSimStateChanged(0, 1,
+                IccCardConstants.State.CARD_IO_ERROR);
+
+        mTestableLooper.processAllMessages();
+        verify(mCarrierTextCallback).updateCarrierInfo(
+                any(CarrierTextController.CarrierTextCallbackInfo.class));
+    }
+
+    @Test
+    public void testCallback() {
+        reset(mCarrierTextCallback);
+        mCarrierTextController.postToCallback(mCarrierTextCallbackInfo);
+        mTestableLooper.processAllMessages();
+
+        ArgumentCaptor<CarrierTextController.CarrierTextCallbackInfo> captor =
+                ArgumentCaptor.forClass(
+                        CarrierTextController.CarrierTextCallbackInfo.class);
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        assertEquals(mCarrierTextCallbackInfo, captor.getValue());
+    }
+
+    @Test
+    public void testNullingCallback() {
+        reset(mCarrierTextCallback);
+
+        mCarrierTextController.postToCallback(mCarrierTextCallbackInfo);
+        mCarrierTextController.setListening(null);
+
+        // This shouldn't produce NPE
+        mTestableLooper.processAllMessages();
+        verify(mCarrierTextCallback).updateCarrierInfo(any());
+    }
+
+    @Test
+    public void testCreateInfo_OneValidSubscription() {
+        reset(mCarrierTextCallback);
+        List<SubscriptionInfo> list = new ArrayList<>();
+        list.add(TEST_SUBSCRIPTION);
+        when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(IccCardConstants.State.READY);
+        when(mKeyguardUpdateMonitor.getSubscriptionInfo(anyBoolean())).thenReturn(list);
+        mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+        ArgumentCaptor<CarrierTextController.CarrierTextCallbackInfo> captor =
+                ArgumentCaptor.forClass(
+                        CarrierTextController.CarrierTextCallbackInfo.class);
+
+        mCarrierTextController.updateCarrierText();
+        mTestableLooper.processAllMessages();
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+
+        CarrierTextController.CarrierTextCallbackInfo info = captor.getValue();
+        assertEquals(1, info.listOfCarriers.length);
+        assertEquals(TEST_CARRIER, info.listOfCarriers[0]);
+        assertEquals(1, info.subscriptionIds.length);
+    }
+
+    @Test
+    public void testCreateInfo_OneValidSubscriptionWithRoaming() {
+        reset(mCarrierTextCallback);
+        List<SubscriptionInfo> list = new ArrayList<>();
+        list.add(TEST_SUBSCRIPTION_ROAMING);
+        when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(IccCardConstants.State.READY);
+        when(mKeyguardUpdateMonitor.getSubscriptionInfo(anyBoolean())).thenReturn(list);
+        mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+        ArgumentCaptor<CarrierTextController.CarrierTextCallbackInfo> captor =
+                ArgumentCaptor.forClass(
+                        CarrierTextController.CarrierTextCallbackInfo.class);
+
+        mCarrierTextController.updateCarrierText();
+        mTestableLooper.processAllMessages();
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+
+        CarrierTextController.CarrierTextCallbackInfo info = captor.getValue();
+        assertEquals(1, info.listOfCarriers.length);
+        assertTrue(info.listOfCarriers[0].toString().contains(TEST_CARRIER));
+        assertEquals(1, info.subscriptionIds.length);
+    }
+
+    @Test
+    public void testCreateInfo_noSubscriptions() {
+        reset(mCarrierTextCallback);
+        when(mKeyguardUpdateMonitor.getSubscriptionInfo(anyBoolean())).thenReturn(
+                new ArrayList<>());
+        ArgumentCaptor<CarrierTextController.CarrierTextCallbackInfo> captor =
+                ArgumentCaptor.forClass(
+                        CarrierTextController.CarrierTextCallbackInfo.class);
+
+        mCarrierTextController.updateCarrierText();
+        mTestableLooper.processAllMessages();
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+
+        CarrierTextController.CarrierTextCallbackInfo info = captor.getValue();
+        assertEquals(0, info.listOfCarriers.length);
+        assertEquals(0, info.subscriptionIds.length);
+
+    }
+
+    public static class TestCarrierTextController extends CarrierTextController {
+        private KeyguardUpdateMonitor mKUM;
+
+        public TestCarrierTextController(Context context, CharSequence separator,
+                boolean showAirplaneMode, boolean showMissingSim, KeyguardUpdateMonitor kum) {
+            super(context, separator, showAirplaneMode, showMissingSim);
+            mKUM = kum;
+        }
+
+        @Override
+        public void setListening(CarrierTextCallback callback) {
+            super.setListening(callback);
+            mKeyguardUpdateMonitor = mKUM;
+        }
+    }
+}