diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e917b5cb9c2..3cf759470b8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3644,6 +3644,14 @@ /> + + + + + + 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..814f1a49146 --- /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 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 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..17a1b8d56cf --- /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 cardInfos = telMgr.getUiccCardsInfo(); + if (cardInfos == null) { + return null; + } + return cardInfos.stream() + .filter(info -> info.getSlotIndex() == physicalSlotIndex) + .findFirst() + .orElse(null); + } +}