diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 13d9296659c..826ce5b9987 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3571,6 +3571,15 @@
android:value="com.android.settings.sound.MediaControlsSettings" />
+
+
+
+
+
+
+
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 00000000000..ea659e1baf8
--- /dev/null
+++ b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java
@@ -0,0 +1,224 @@
+/*
+ * 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.os.SystemProperties;
+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 SYSTEM_PROPERTY_DEVICE_PROVISIONED =
+ "persist.sys.device_provisioned";
+
+ 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;
+ private boolean mNotificationEnabled = true;
+
+ 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()) {
+ // TODO: 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 Display DSDS dialog to ask users whether to enable DSDS.
+ } else {
+ Log.i(TAG, "Enabled profile exists. DSDS condition not satisfied.");
+ // TODO 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 Switch the slot to the removebale slot and show the notification.
+ }
+
+ private void handleSimRemove(UiccSlotInfo removableSlotInfo) {
+ Log.i(TAG, "Detect SIM removed.");
+
+ if (!isSuwFinished()) {
+ // TODO: 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 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 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 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 boolean isSuwFinished() {
+ return "1".equals(SystemProperties.get(SYSTEM_PROPERTY_DEVICE_PROVISIONED, "0"));
+ }
+
+ private boolean hasActiveEsimSubscription() {
+ List activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr);
+ return activeSubs.stream().anyMatch(SubscriptionInfo::isEmbedded);
+ }
+
+ private List getGroupedEmbeddedSubscriptions() {
+ List 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 00000000000..440d3a4f8ac
--- /dev/null
+++ b/src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java
@@ -0,0 +1,143 @@
+/*
+ * 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 static final String SLOT_CHANGE_ACTION_QC =
+ "qualcomm.intent.action.ACTION_SLOT_STATUS_CHANGED_IND";
+
+ 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)
+ && !SLOT_CHANGE_ACTION_QC.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 cardInfos = telMgr.getUiccCardsInfo();
+ if (cardInfos == null) {
+ return null;
+ }
+ return cardInfos.stream()
+ .filter(info -> info.getSlotIndex() == physicalSlotIndex)
+ .findFirst()
+ .orElse(null);
+ }
+}