Initial setup for slot change receiver migration

Initial setup for migrating SlotChangehandler from LPA to Settings
Bug: 160819212
Bug: 153811431
Test: Manually tested pSIM insertion and removal

Change-Id: I3009182db4c6e1863dc9b619e21a17b1297743ed
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c9f4643..07c7eb0 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3635,6 +3635,14 @@
             </intent-filter>/>
         </receiver>
 
+        <receiver
+            android:name=".sim.receivers.SimSlotChangeReceiver"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.telephony.action.SIM_SLOT_STATUS_CHANGED" />
+            </intent-filter>
+        </receiver>
+
         <!-- This is the longest AndroidManifest.xml ever. -->
     </application>
 </manifest>
diff --git a/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java
new file mode 100644
index 0000000..814f1a4
--- /dev/null
+++ b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java
@@ -0,0 +1,229 @@
+/*
+ * 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.settings.sim.receivers;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Looper;
+import android.provider.Settings;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccSlotInfo;
+import android.util.Log;
+
+import com.android.settings.network.SubscriptionUtil;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+/** Perform actions after a slot change event is triggered. */
+public class SimSlotChangeHandler {
+    private static final String TAG = "SimSlotChangeHandler";
+
+    private static final String EUICC_PREFS = "euicc_prefs";
+    private static final String KEY_REMOVABLE_SLOT_STATE = "removable_slot_state";
+
+    private static volatile SimSlotChangeHandler sSlotChangeHandler;
+
+    /** Returns a SIM slot change handler singleton. */
+    public static SimSlotChangeHandler get() {
+        if (sSlotChangeHandler == null) {
+            synchronized (SimSlotChangeHandler.class) {
+                if (sSlotChangeHandler == null) {
+                    sSlotChangeHandler = new SimSlotChangeHandler();
+                }
+            }
+        }
+        return sSlotChangeHandler;
+    }
+
+    private SubscriptionManager mSubMgr;
+    private TelephonyManager mTelMgr;
+    private Context mContext;
+
+    void onSlotsStatusChange(Context context) {
+        init(context);
+
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            throw new IllegalStateException("Cannot be called from main thread.");
+        }
+
+        if (mTelMgr.getActiveModemCount() > 1) {
+            Log.i(TAG, "The device is already in DSDS mode. Do nothing.");
+            return;
+        }
+
+        UiccSlotInfo removableSlotInfo = getRemovableUiccSlotInfo();
+        if (removableSlotInfo == null) {
+            Log.e(TAG, "Unable to find the removable slot. Do nothing.");
+            return;
+        }
+
+        int lastRemovableSlotState = getLastRemovableSimSlotState(mContext);
+        int currentRemovableSlotState = removableSlotInfo.getCardStateInfo();
+
+        // Sets the current removable slot state.
+        setRemovableSimSlotState(mContext, currentRemovableSlotState);
+
+        if (lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT
+                && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT) {
+            handleSimInsert(removableSlotInfo);
+            return;
+        }
+        if (lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT
+                && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT) {
+            handleSimRemove(removableSlotInfo);
+            return;
+        }
+        Log.i(TAG, "Do nothing on slot status changes.");
+    }
+
+    private void init(Context context) {
+        mSubMgr =
+                (SubscriptionManager)
+                        context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        mTelMgr = context.getSystemService(TelephonyManager.class);
+        mContext = context;
+    }
+
+    private void handleSimInsert(UiccSlotInfo removableSlotInfo) {
+        Log.i(TAG, "Detect SIM inserted.");
+
+        if (!isSuwFinished(mContext)) {
+            // TODO(b/170508680): Store the action and handle it after SUW is finished.
+            Log.i(TAG, "Still in SUW. Handle SIM insertion after SUW is finished");
+            return;
+        }
+
+        if (removableSlotInfo.getIsActive()) {
+            Log.i(TAG, "The removable slot is already active. Do nothing.");
+            return;
+        }
+
+        if (!hasActiveEsimSubscription()) {
+            if (mTelMgr.isMultiSimEnabled()) {
+                Log.i(TAG, "Enabled profile exists. DSDS condition satisfied.");
+                // TODO(b/170508680): Display DSDS dialog to ask users whether to enable DSDS.
+            } else {
+                Log.i(TAG, "Enabled profile exists. DSDS condition not satisfied.");
+                // TODO(b/170508680): Display Choose a number to use screen for subscription
+                //  selection.
+            }
+            return;
+        }
+
+        Log.i(
+                TAG,
+                "No enabled eSIM profile. Ready to switch to removable slot and show"
+                        + " notification.");
+        // TODO(b/170508680): Switch the slot to the removebale slot and show the notification.
+    }
+
+    private void handleSimRemove(UiccSlotInfo removableSlotInfo) {
+        Log.i(TAG, "Detect SIM removed.");
+
+        if (!isSuwFinished(mContext)) {
+            // TODO(b/170508680): Store the action and handle it after SUW is finished.
+            Log.i(TAG, "Still in SUW. Handle SIM removal after SUW is finished");
+            return;
+        }
+
+        List<SubscriptionInfo> groupedEmbeddedSubscriptions = getGroupedEmbeddedSubscriptions();
+
+        if (groupedEmbeddedSubscriptions.size() == 0 || !removableSlotInfo.getIsActive()) {
+            Log.i(TAG, "eSIM slot is active or no subscriptions exist. Do nothing.");
+            return;
+        }
+
+        // If there is only 1 eSIM profile exists, we ask the user if they want to switch to that
+        // profile.
+        if (groupedEmbeddedSubscriptions.size() == 1) {
+            Log.i(TAG, "Only 1 eSIM profile found. Ask user's consent to switch.");
+            // TODO(b/170508680): Display a dialog to ask users to switch.
+            return;
+        }
+
+        // If there are more than 1 eSIM profiles installed, we show a screen to let users to choose
+        // the number they want to use.
+        Log.i(TAG, "Multiple eSIM profiles found. Ask user which subscription to use.");
+        // TODO(b/170508680): Display a dialog to ask user which SIM to switch.
+    }
+
+    private int getLastRemovableSimSlotState(Context context) {
+        final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE);
+        return prefs.getInt(KEY_REMOVABLE_SLOT_STATE, UiccSlotInfo.CARD_STATE_INFO_ABSENT);
+    }
+
+    private void setRemovableSimSlotState(Context context, int state) {
+        final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE);
+        prefs.edit().putInt(KEY_REMOVABLE_SLOT_STATE, state).apply();
+    }
+
+    @Nullable
+    private UiccSlotInfo getRemovableUiccSlotInfo() {
+        UiccSlotInfo[] slotInfos = mTelMgr.getUiccSlotsInfo();
+        if (slotInfos == null) {
+            Log.e(TAG, "slotInfos is null. Unable to get slot infos.");
+            return null;
+        }
+        for (UiccSlotInfo slotInfo : slotInfos) {
+            if (slotInfo != null && slotInfo.isRemovable()) {
+
+                return slotInfo;
+            }
+        }
+        return null;
+    }
+
+    private static boolean isSuwFinished(Context context) {
+        try {
+            // DEVICE_PROVISIONED is 0 if still in setup wizard. 1 if setup completed.
+            return Settings.Global.getInt(
+                            context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED)
+                    == 1;
+        } catch (Settings.SettingNotFoundException e) {
+            Log.e(TAG, "Cannot get DEVICE_PROVISIONED from the device.", e);
+            return false;
+        }
+    }
+
+    private boolean hasActiveEsimSubscription() {
+        List<SubscriptionInfo> activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr);
+        return activeSubs.stream().anyMatch(SubscriptionInfo::isEmbedded);
+    }
+
+    private List<SubscriptionInfo> getGroupedEmbeddedSubscriptions() {
+        List<SubscriptionInfo> groupedSubscriptions =
+                SubscriptionUtil.getSelectableSubscriptionInfoList(mContext);
+        if (groupedSubscriptions == null) {
+            return ImmutableList.of();
+        }
+        return ImmutableList.copyOf(
+                groupedSubscriptions.stream()
+                        .filter(sub -> sub.isEmbedded())
+                        .collect(Collectors.toList()));
+    }
+
+    private SimSlotChangeHandler() {}
+}
diff --git a/src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java b/src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java
new file mode 100644
index 0000000..17a1b8d
--- /dev/null
+++ b/src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java
@@ -0,0 +1,139 @@
+/*
+ * 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.settings.sim.receivers;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telephony.TelephonyManager;
+import android.telephony.UiccCardInfo;
+import android.telephony.UiccSlotInfo;
+import android.telephony.euicc.EuiccManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.List;
+
+/** The receiver when the slot status changes. */
+public class SimSlotChangeReceiver extends BroadcastReceiver {
+    private static final String TAG = "SlotChangeReceiver";
+
+    private final SimSlotChangeHandler mSlotChangeHandler = SimSlotChangeHandler.get();
+    private final Object mLock = new Object();
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+
+        String action = intent.getAction();
+        if (!TelephonyManager.ACTION_SIM_SLOT_STATUS_CHANGED.equals(action)) {
+            Log.e(TAG, "Ignore slot changes due to unexpected action: " + action);
+            return;
+        }
+
+        ThreadUtils.postOnBackgroundThread(
+                () -> {
+                    synchronized (mLock) {
+                        if (!shouldHandleSlotChange(context)) {
+                            return;
+                        }
+                        mSlotChangeHandler.onSlotsStatusChange(context);
+                    }
+                });
+    }
+
+    // Checks whether the slot event should be handled.
+    private boolean shouldHandleSlotChange(Context context) {
+        final EuiccManager euiccManager = context.getSystemService(EuiccManager.class);
+        if (euiccManager == null || !euiccManager.isEnabled()) {
+            Log.i(TAG, "Ignore slot changes because EuiccManager is disabled.");
+            return false;
+        }
+
+        if (euiccManager.getOtaStatus() == EuiccManager.EUICC_OTA_IN_PROGRESS) {
+            Log.i(TAG, "Ignore slot changes because eSIM OTA is in progress.");
+            return false;
+        }
+
+        if (!isSimSlotStateValid(context)) {
+            Log.i(TAG, "Ignore slot changes because SIM states are not valid.");
+            return false;
+        }
+
+        return true;
+    }
+
+    // Checks whether the SIM slot state is valid for slot change event.
+    private boolean isSimSlotStateValid(Context context) {
+        final TelephonyManager telMgr = context.getSystemService(TelephonyManager.class);
+        UiccSlotInfo[] slotInfos = telMgr.getUiccSlotsInfo();
+        if (slotInfos == null) {
+            Log.e(TAG, "slotInfos is null. Unable to get slot infos.");
+            return false;
+        }
+
+        boolean isAllCardStringsEmpty = true;
+        for (int i = 0; i < slotInfos.length; i++) {
+            UiccSlotInfo slotInfo = slotInfos[i];
+
+            if (slotInfo == null) {
+                return false;
+            }
+
+            // After pSIM is inserted, there might be a short period that the status of both slots
+            // are not accurate. We drop the event if any of sim presence state is ERROR or
+            // RESTRICTED.
+            if (slotInfo.getCardStateInfo() == UiccSlotInfo.CARD_STATE_INFO_ERROR
+                    || slotInfo.getCardStateInfo() == UiccSlotInfo.CARD_STATE_INFO_RESTRICTED) {
+                Log.i(TAG, "The SIM state is in an error. Drop the event. SIM info: " + slotInfo);
+                return false;
+            }
+
+            UiccCardInfo cardInfo = findUiccCardInfoBySlot(telMgr, i);
+            if (cardInfo == null) {
+                continue;
+            }
+            if (!TextUtils.isEmpty(slotInfo.getCardId())
+                    || !TextUtils.isEmpty(cardInfo.getIccId())) {
+                isAllCardStringsEmpty = false;
+            }
+        }
+
+        // We also drop the event if both the card strings are empty, which usually means it's
+        // between SIM slots switch the slot status is not stable at this moment.
+        if (isAllCardStringsEmpty) {
+            Log.i(TAG, "All UICC card strings are empty. Drop this event.");
+            return false;
+        }
+
+        return true;
+    }
+
+    @Nullable
+    private UiccCardInfo findUiccCardInfoBySlot(TelephonyManager telMgr, int physicalSlotIndex) {
+        List<UiccCardInfo> cardInfos = telMgr.getUiccCardsInfo();
+        if (cardInfos == null) {
+            return null;
+        }
+        return cardInfos.stream()
+                .filter(info -> info.getSlotIndex() == physicalSlotIndex)
+                .findFirst()
+                .orElse(null);
+    }
+}