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);
+ }
+}