diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt index 43bba0759a2..33c1e655040 100644 --- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt +++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn +import java.util.stream.Collectors private const val TAG = "SubscriptionRepository" @@ -154,6 +155,18 @@ class SubscriptionRepository(private val context: Context) { .conflate() .flowOn(Dispatchers.Default) + fun removableSubscriptionInfoListFlow(): Flow> { + return subscriptionsChangedFlow() + .map { + subscriptionManager.availableSubscriptionInfoList?.stream() + ?.filter { sub: SubscriptionInfo -> !sub.isEmbedded } + ?.collect(Collectors.toList()) ?: emptyList() + } + .conflate() + .onEach { Log.d(TAG, "getRemovableSubscriptionVisibleFlow: $it") } + .flowOn(Dispatchers.Default) + } + @OptIn(ExperimentalCoroutinesApi::class) fun phoneNumberFlow(subId: Int): Flow = activeSubscriptionInfoFlow(subId).flatMapLatest { subInfo -> diff --git a/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java deleted file mode 100644 index f808924fa41..00000000000 --- a/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * 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.Intent; -import android.content.SharedPreferences; -import android.os.Looper; -import android.os.UserHandle; -import android.provider.Settings; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; -import android.telephony.UiccCardInfo; -import android.telephony.UiccPortInfo; -import android.telephony.UiccSlotInfo; -import android.util.Log; - -import com.android.settings.flags.Flags; -import com.android.settings.network.SubscriptionUtil; -import com.android.settings.network.UiccSlotUtil; -import com.android.settings.network.UiccSlotsException; -import com.android.settings.sim.ChooseSimActivity; -import com.android.settings.sim.DsdsDialogActivity; -import com.android.settings.sim.SimActivationNotifier; -import com.android.settings.sim.SimNotificationService; -import com.android.settings.sim.SwitchToEsimConfirmDialogActivity; - -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"; - // Shared preference keys - private static final String KEY_REMOVABLE_SLOT_STATE = "removable_slot_state"; - private static final String KEY_SUW_PSIM_ACTION = "suw_psim_action"; - // User's last removable SIM insertion / removal action during SUW. - private static final int LAST_USER_ACTION_IN_SUW_NONE = 0; - private static final int LAST_USER_ACTION_IN_SUW_INSERT = 1; - private static final int LAST_USER_ACTION_IN_SUW_REMOVE = 2; - - 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."); - } - - UiccSlotInfo removableSlotInfo = getRemovableUiccSlotInfo(); - if (removableSlotInfo == null) { - Log.e(TAG, "Unable to find the removable slot. Do nothing."); - return; - } - Log.i(TAG, "The removableSlotInfo: " + removableSlotInfo); - int lastRemovableSlotState = getLastRemovableSimSlotState(mContext); - int currentRemovableSlotState = removableSlotInfo.getCardStateInfo(); - Log.d(TAG, - "lastRemovableSlotState: " + lastRemovableSlotState + ",currentRemovableSlotState: " - + currentRemovableSlotState); - boolean isRemovableSimInserted = - lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT - && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT; - boolean isRemovableSimRemoved = - lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT - && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT; - - // Sets the current removable slot state. - setRemovableSimSlotState(mContext, currentRemovableSlotState); - - if (mTelMgr.getActiveModemCount() > 1) { - if (!isRemovableSimInserted) { - Log.d(TAG, "Removable Sim is not inserted in DSDS mode. Do nothing."); - return; - } - - if (Flags.isDualSimOnboardingEnabled()) { - // ForNewUi, when the user inserts the psim, showing the sim onboarding for the user - // to setup the sim switching or the default data subscription in DSDS. - // Will show dialog for below case. - // 1. the psim slot is not active. - // 2. there are one or more active sim. - handleRemovableSimInsertWhenDsds(removableSlotInfo); - return; - } else if (!isMultipleEnabledProfilesSupported()) { - Log.d(TAG, "The device is already in DSDS mode and no MEP. Do nothing."); - return; - } else if (isMultipleEnabledProfilesSupported()) { - handleRemovableSimInsertUnderDsdsMep(removableSlotInfo); - return; - } - } - - if (isRemovableSimInserted) { - handleSimInsert(removableSlotInfo); - return; - } - if (isRemovableSimRemoved) { - handleSimRemove(removableSlotInfo); - return; - } - Log.i(TAG, "Do nothing on slot status changes."); - } - - void onSuwFinish(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; - } - - boolean embeddedSimExist = getGroupedEmbeddedSubscriptions().size() != 0; - int removableSlotAction = getSuwRemovableSlotAction(mContext); - setSuwRemovableSlotAction(mContext, LAST_USER_ACTION_IN_SUW_NONE); - - if (embeddedSimExist - && removableSlotInfo.getCardStateInfo() == UiccSlotInfo.CARD_STATE_INFO_PRESENT) { - if (mTelMgr.isMultiSimSupported() == TelephonyManager.MULTISIM_ALLOWED) { - Log.i(TAG, "DSDS condition satisfied. Show notification."); - SimNotificationService.scheduleSimNotification( - mContext, SimActivationNotifier.NotificationType.ENABLE_DSDS); - } else if (removableSlotAction == LAST_USER_ACTION_IN_SUW_INSERT) { - Log.i( - TAG, - "Both removable SIM and eSIM are present. DSDS condition doesn't" - + " satisfied. User inserted pSIM during SUW. Show choose SIM" - + " screen."); - startChooseSimActivity(true); - } - } else if (removableSlotAction == LAST_USER_ACTION_IN_SUW_REMOVE) { - handleSimRemove(removableSlotInfo); - } - } - - 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, "Handle SIM inserted."); - if (!isSuwFinished(mContext)) { - Log.i(TAG, "Still in SUW. Handle SIM insertion after SUW is finished"); - setSuwRemovableSlotAction(mContext, LAST_USER_ACTION_IN_SUW_INSERT); - return; - } - if (removableSlotInfo.getPorts().stream().findFirst().get().isActive()) { - Log.i(TAG, "The removable slot is already active. Do nothing."); - return; - } - - if (hasActiveEsimSubscription()) { - if (mTelMgr.isMultiSimSupported() == TelephonyManager.MULTISIM_ALLOWED) { - Log.i(TAG, "Enabled profile exists. DSDS condition satisfied."); - if (Flags.isDualSimOnboardingEnabled()) { - // enable dsds by sim onboarding flow - handleRemovableSimInsertWhenDsds(removableSlotInfo); - } else { - startDsdsDialogActivity(); - } - } else { - Log.i(TAG, "Enabled profile exists. DSDS condition not satisfied."); - startChooseSimActivity(true); - } - return; - } - - Log.i( - TAG, - "No enabled eSIM profile. Ready to switch to removable slot and show" - + " notification."); - try { - UiccSlotUtil.switchToRemovableSlot( - UiccSlotUtil.INVALID_PHYSICAL_SLOT_ID, mContext.getApplicationContext()); - } catch (UiccSlotsException e) { - Log.e(TAG, "Failed to switch to removable slot."); - return; - } - SimNotificationService.scheduleSimNotification( - mContext, SimActivationNotifier.NotificationType.SWITCH_TO_REMOVABLE_SLOT); - } - - private void handleSimRemove(UiccSlotInfo removableSlotInfo) { - Log.i(TAG, "Handle SIM removed."); - - if (!isSuwFinished(mContext)) { - Log.i(TAG, "Still in SUW. Handle SIM removal after SUW is finished"); - setSuwRemovableSlotAction(mContext, LAST_USER_ACTION_IN_SUW_REMOVE); - return; - } - - List groupedEmbeddedSubscriptions = getGroupedEmbeddedSubscriptions(); - if (groupedEmbeddedSubscriptions.size() == 0 || !removableSlotInfo.getPorts().stream() - .findFirst().get().isActive()) { - Log.i(TAG, "eSIM slot is active or no subscriptions exist. Do nothing." - + " The removableSlotInfo: " + removableSlotInfo - + ", groupedEmbeddedSubscriptions: " + groupedEmbeddedSubscriptions); - 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."); - startSwitchSlotConfirmDialogActivity(groupedEmbeddedSubscriptions.get(0)); - 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."); - startChooseSimActivity(false); - } - - private boolean hasOtherActiveSubInfo(int psimSubId) { - List activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr); - return activeSubs.stream() - .anyMatch(subscriptionInfo -> subscriptionInfo.getSubscriptionId() != psimSubId); - } - - private boolean hasAnyPortActiveInSlot(UiccSlotInfo removableSlotInfo) { - return removableSlotInfo.getPorts().stream().anyMatch(UiccPortInfo::isActive); - } - - private void handleRemovableSimInsertWhenDsds(UiccSlotInfo removableSlotInfo) { - Log.i(TAG, "ForNewUi: Handle Removable SIM inserted"); - List subscriptionInfos = getAvailableRemovableSubscription(); - if (subscriptionInfos.isEmpty()) { - Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing."); - return; - } - Log.d(TAG, "getAvailableRemovableSubscription:" + subscriptionInfos); - int psimSubId = subscriptionInfos.get(0).getSubscriptionId(); - if (!hasAnyPortActiveInSlot(removableSlotInfo) || hasOtherActiveSubInfo(psimSubId)) { - Log.d(TAG, "ForNewUi Start Setup flow"); - startSimConfirmDialogActivity(psimSubId); - } - } - - private void handleRemovableSimInsertUnderDsdsMep(UiccSlotInfo removableSlotInfo) { - Log.i(TAG, "Handle Removable SIM inserted under DSDS+Mep."); - - if (removableSlotInfo.getPorts().stream().findFirst().get().isActive()) { - Log.i(TAG, "The removable slot is already active. Do nothing. removableSlotInfo: " - + removableSlotInfo); - return; - } - - List subscriptionInfos = getAvailableRemovableSubscription(); - if (subscriptionInfos.isEmpty()) { - Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing."); - return; - } - Log.d(TAG, "getAvailableRemovableSubscription:" + subscriptionInfos); - startSimConfirmDialogActivity(subscriptionInfos.get(0).getSubscriptionId()); - } - - 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(); - Log.d(TAG, "setRemovableSimSlotState: " + state); - } - - private int getSuwRemovableSlotAction(Context context) { - final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE); - return prefs.getInt(KEY_SUW_PSIM_ACTION, LAST_USER_ACTION_IN_SUW_NONE); - } - - private void setSuwRemovableSlotAction(Context context, int action) { - final SharedPreferences prefs = context.getSharedPreferences(EUICC_PREFS, MODE_PRIVATE); - prefs.edit().putInt(KEY_SUW_PSIM_ACTION, action).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())); - } - - protected List getAvailableRemovableSubscription() { - List removableSubscriptions = - SubscriptionUtil.getAvailableSubscriptions(mContext); - return ImmutableList.copyOf( - removableSubscriptions.stream() - // ToDo: This condition is for psim only. If device supports removable - // esim, it needs an new condition. - .filter(sub -> !sub.isEmbedded()) - .collect(Collectors.toList())); - } - - private void startChooseSimActivity(boolean psimInserted) { - Intent intent = ChooseSimActivity.getIntent(mContext); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(ChooseSimActivity.KEY_HAS_PSIM, psimInserted); - mContext.startActivityAsUser(intent, UserHandle.SYSTEM); - } - - private void startSwitchSlotConfirmDialogActivity(SubscriptionInfo subscriptionInfo) { - Intent intent = new Intent(mContext, SwitchToEsimConfirmDialogActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(SwitchToEsimConfirmDialogActivity.KEY_SUB_TO_ENABLE, subscriptionInfo); - mContext.startActivityAsUser(intent, UserHandle.SYSTEM); - } - - private void startDsdsDialogActivity() { - Intent intent = new Intent(mContext, DsdsDialogActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivityAsUser(intent, UserHandle.SYSTEM); - } - - private void startSimConfirmDialogActivity(int subId) { - if (!isSuwFinished(mContext)) { - Log.d(TAG, "Still in SUW. Do nothing"); - return; - } - if (!SubscriptionManager.isUsableSubscriptionId(subId)) { - Log.i(TAG, "Unable to enable subscription due to invalid subscription ID."); - return; - } - Log.d(TAG, "Start ToggleSubscriptionDialogActivity with " + subId + " under DSDS+Mep."); - SubscriptionUtil.startToggleSubscriptionDialogActivity(mContext, subId, true, true); - } - - private boolean isMultipleEnabledProfilesSupported() { - List cardInfos = mTelMgr.getUiccCardsInfo(); - if (cardInfos == null) { - Log.d(TAG, "UICC cards info list is empty."); - return false; - } - return cardInfos.stream().anyMatch( - cardInfo -> cardInfo.isMultipleEnabledProfilesSupported()); - } - - private SimSlotChangeHandler() {} -} diff --git a/src/com/android/settings/sim/receivers/SimSlotChangeHandler.kt b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.kt new file mode 100644 index 00000000000..940184a926a --- /dev/null +++ b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.kt @@ -0,0 +1,480 @@ +/* + * 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.Context +import android.content.Intent +import android.os.Looper +import android.os.UserHandle +import android.provider.Settings +import android.provider.Settings.SettingNotFoundException +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.telephony.UiccCardInfo +import android.telephony.UiccSlotInfo +import android.util.Log +import com.android.settings.flags.Flags +import com.android.settings.network.SubscriptionUtil +import com.android.settings.network.UiccSlotUtil +import com.android.settings.network.UiccSlotsException +import com.android.settings.network.telephony.SubscriptionRepository +import com.android.settings.sim.ChooseSimActivity +import com.android.settings.sim.DsdsDialogActivity +import com.android.settings.sim.SimActivationNotifier +import com.android.settings.sim.SimNotificationService +import com.android.settings.sim.SwitchToEsimConfirmDialogActivity +import com.google.common.collect.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import java.util.stream.Collectors +import kotlin.concurrent.Volatile + +/** Perform actions after a slot change event is triggered. */ +class SimSlotChangeHandler private constructor() { + private var mSubMgr: SubscriptionManager? = null + private var mTelMgr: TelephonyManager? = null + private var mContext: Context? = null + + fun onSlotsStatusChange(context: Context) { + init(context) + + check(Looper.myLooper() != Looper.getMainLooper()) { "Cannot be called from main thread." } + + val removableSlotInfo = removableUiccSlotInfo + if (removableSlotInfo == null) { + Log.e(TAG, "Unable to find the removable slot. Do nothing.") + return + } + Log.i( + TAG, + "The removableSlotInfo: $removableSlotInfo" + ) + val lastRemovableSlotState = getLastRemovableSimSlotState(mContext!!) + val currentRemovableSlotState = removableSlotInfo.cardStateInfo + Log.d( + TAG, + ("lastRemovableSlotState: " + lastRemovableSlotState + ",currentRemovableSlotState: " + + currentRemovableSlotState) + ) + val isRemovableSimInserted = + lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT + && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT + val isRemovableSimRemoved = + lastRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_PRESENT + && currentRemovableSlotState == UiccSlotInfo.CARD_STATE_INFO_ABSENT + + // Sets the current removable slot state. + setRemovableSimSlotState(mContext!!, currentRemovableSlotState) + + if (mTelMgr!!.activeModemCount > 1) { + if (!isRemovableSimInserted) { + Log.d(TAG, "Removable Sim is not inserted in DSDS mode. Do nothing.") + return + } + + if (Flags.isDualSimOnboardingEnabled()) { + // ForNewUi, when the user inserts the psim, showing the sim onboarding for the user + // to setup the sim switching or the default data subscription in DSDS. + // Will show dialog for below case. + // 1. the psim slot is not active. + // 2. there are one or more active sim. + handleRemovableSimInsertWhenDsds(removableSlotInfo) + return + } else if (!isMultipleEnabledProfilesSupported) { + Log.d(TAG, "The device is already in DSDS mode and no MEP. Do nothing.") + return + } else if (isMultipleEnabledProfilesSupported) { + handleRemovableSimInsertUnderDsdsMep(removableSlotInfo) + return + } + } + + if (isRemovableSimInserted) { + handleSimInsert(removableSlotInfo) + return + } + if (isRemovableSimRemoved) { + handleSimRemove(removableSlotInfo) + return + } + Log.i(TAG, "Do nothing on slot status changes.") + } + + fun onSuwFinish(context: Context) { + init(context) + + check(Looper.myLooper() != Looper.getMainLooper()) { "Cannot be called from main thread." } + + if (mTelMgr!!.activeModemCount > 1) { + Log.i(TAG, "The device is already in DSDS mode. Do nothing.") + return + } + + val removableSlotInfo = removableUiccSlotInfo + if (removableSlotInfo == null) { + Log.e(TAG, "Unable to find the removable slot. Do nothing.") + return + } + + val embeddedSimExist = groupedEmbeddedSubscriptions.size != 0 + val removableSlotAction = getSuwRemovableSlotAction(mContext!!) + setSuwRemovableSlotAction(mContext!!, LAST_USER_ACTION_IN_SUW_NONE) + + if (embeddedSimExist + && removableSlotInfo.cardStateInfo == UiccSlotInfo.CARD_STATE_INFO_PRESENT + ) { + if (mTelMgr!!.isMultiSimSupported() == TelephonyManager.MULTISIM_ALLOWED) { + Log.i(TAG, "DSDS condition satisfied. Show notification.") + SimNotificationService.scheduleSimNotification( + mContext, SimActivationNotifier.NotificationType.ENABLE_DSDS + ) + } else if (removableSlotAction == LAST_USER_ACTION_IN_SUW_INSERT) { + Log.i( + TAG, + ("Both removable SIM and eSIM are present. DSDS condition doesn't" + + " satisfied. User inserted pSIM during SUW. Show choose SIM" + + " screen.") + ) + startChooseSimActivity(true) + } + } else if (removableSlotAction == LAST_USER_ACTION_IN_SUW_REMOVE) { + handleSimRemove(removableSlotInfo) + } + } + + private fun init(context: Context) { + mSubMgr = + context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager + mTelMgr = context.getSystemService(TelephonyManager::class.java) + mContext = context + } + + private fun handleSimInsert(removableSlotInfo: UiccSlotInfo) { + Log.i(TAG, "Handle SIM inserted.") + if (!isSuwFinished(mContext!!)) { + Log.i(TAG, "Still in SUW. Handle SIM insertion after SUW is finished") + setSuwRemovableSlotAction(mContext!!, LAST_USER_ACTION_IN_SUW_INSERT) + return + } + if (removableSlotInfo.ports.stream().findFirst().get().isActive) { + Log.i(TAG, "The removable slot is already active. Do nothing.") + return + } + + if (hasActiveEsimSubscription()) { + if (mTelMgr!!.isMultiSimSupported() == TelephonyManager.MULTISIM_ALLOWED) { + Log.i(TAG, "Enabled profile exists. DSDS condition satisfied.") + if (Flags.isDualSimOnboardingEnabled()) { + // enable dsds by sim onboarding flow + handleRemovableSimInsertWhenDsds(removableSlotInfo) + } else { + startDsdsDialogActivity() + } + } else { + Log.i(TAG, "Enabled profile exists. DSDS condition not satisfied.") + startChooseSimActivity(true) + } + return + } + + Log.i( + TAG, + "No enabled eSIM profile. Ready to switch to removable slot and show" + + " notification." + ) + try { + UiccSlotUtil.switchToRemovableSlot( + UiccSlotUtil.INVALID_PHYSICAL_SLOT_ID, mContext!!.applicationContext + ) + } catch (e: UiccSlotsException) { + Log.e(TAG, "Failed to switch to removable slot.") + return + } + SimNotificationService.scheduleSimNotification( + mContext, SimActivationNotifier.NotificationType.SWITCH_TO_REMOVABLE_SLOT + ) + } + + private fun handleSimRemove(removableSlotInfo: UiccSlotInfo) { + Log.i(TAG, "Handle SIM removed.") + + if (!isSuwFinished(mContext!!)) { + Log.i(TAG, "Still in SUW. Handle SIM removal after SUW is finished") + setSuwRemovableSlotAction(mContext!!, LAST_USER_ACTION_IN_SUW_REMOVE) + return + } + + val groupedEmbeddedSubscriptions = + groupedEmbeddedSubscriptions + if (groupedEmbeddedSubscriptions.isEmpty() || !removableSlotInfo.ports.stream() + .findFirst().get().isActive + ) { + Log.i( + TAG, ("eSIM slot is active or no subscriptions exist. Do nothing." + + " The removableSlotInfo: " + removableSlotInfo + + ", groupedEmbeddedSubscriptions: " + groupedEmbeddedSubscriptions) + ) + 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.") + startSwitchSlotConfirmDialogActivity(groupedEmbeddedSubscriptions[0]) + 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.") + startChooseSimActivity(false) + } + + private fun hasOtherActiveSubInfo(psimSubId: Int): Boolean { + val activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr) + return activeSubs.stream() + .anyMatch { subscriptionInfo -> subscriptionInfo.subscriptionId != psimSubId } + } + + private fun hasAnyPortActiveInSlot(removableSlotInfo: UiccSlotInfo): Boolean { + return removableSlotInfo.ports.stream().anyMatch { slot -> slot.isActive } + } + + private fun handleRemovableSimInsertWhenDsds(removableSlotInfo: UiccSlotInfo) { + Log.i(TAG, "ForNewUi: Handle Removable SIM inserted") + CoroutineScope(Dispatchers.Default + SupervisorJob()).launch { + withContext(Dispatchers.Default) { + val subscriptionInfos = + withTimeoutOrNull(DEFAULT_WAIT_AFTER_SIM_INSERTED_TIMEOUT_MILLIS) { + SubscriptionRepository(mContext!!) + .removableSubscriptionInfoListFlow() + .firstOrNull { it.isNotEmpty() } + } + + if (subscriptionInfos.isNullOrEmpty()) { + Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing.") + return@withContext + } + Log.d( + TAG, + "getAvailableRemovableSubscription:$subscriptionInfos" + ) + val psimSubId = subscriptionInfos[0].subscriptionId + if (!hasAnyPortActiveInSlot(removableSlotInfo) + || hasOtherActiveSubInfo(psimSubId)) { + Log.d(TAG, "ForNewUi Start Setup flow") + startSimConfirmDialogActivity(psimSubId) + } + } + } + } + + private fun handleRemovableSimInsertUnderDsdsMep(removableSlotInfo: UiccSlotInfo) { + Log.i(TAG, "Handle Removable SIM inserted under DSDS+Mep.") + + if (removableSlotInfo.ports.stream().findFirst().get().isActive) { + Log.i( + TAG, "The removable slot is already active. Do nothing. removableSlotInfo: " + + removableSlotInfo + ) + return + } + + val subscriptionInfos = + availableRemovableSubscription + if (subscriptionInfos.isEmpty()) { + Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing.") + return + } + Log.d( + TAG, + "getAvailableRemovableSubscription:$subscriptionInfos" + ) + startSimConfirmDialogActivity(subscriptionInfos[0].subscriptionId) + } + + private fun getLastRemovableSimSlotState(context: Context): Int { + val prefs = context.getSharedPreferences(EUICC_PREFS, Context.MODE_PRIVATE) + return prefs.getInt(KEY_REMOVABLE_SLOT_STATE, UiccSlotInfo.CARD_STATE_INFO_ABSENT) + } + + private fun setRemovableSimSlotState(context: Context, state: Int) { + val prefs = context.getSharedPreferences(EUICC_PREFS, Context.MODE_PRIVATE) + prefs.edit().putInt(KEY_REMOVABLE_SLOT_STATE, state).apply() + Log.d(TAG, "setRemovableSimSlotState: $state") + } + + private fun getSuwRemovableSlotAction(context: Context): Int { + val prefs = context.getSharedPreferences(EUICC_PREFS, Context.MODE_PRIVATE) + return prefs.getInt(KEY_SUW_PSIM_ACTION, LAST_USER_ACTION_IN_SUW_NONE) + } + + private fun setSuwRemovableSlotAction(context: Context, action: Int) { + val prefs = context.getSharedPreferences(EUICC_PREFS, Context.MODE_PRIVATE) + prefs.edit().putInt(KEY_SUW_PSIM_ACTION, action).apply() + } + + private val removableUiccSlotInfo: UiccSlotInfo? + get() { + val slotInfos = mTelMgr!!.uiccSlotsInfo + if (slotInfos == null) { + Log.e( + TAG, + "slotInfos is null. Unable to get slot infos." + ) + return null + } + for (slotInfo in slotInfos) { + if (slotInfo != null && slotInfo.isRemovable) { + return slotInfo + } + } + return null + } + + private fun hasActiveEsimSubscription(): Boolean { + val activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr) + return activeSubs.stream().anyMatch { subscriptionInfo -> subscriptionInfo.isEmbedded } + } + + private val groupedEmbeddedSubscriptions: List + get() { + val groupedSubscriptions = + SubscriptionUtil.getSelectableSubscriptionInfoList(mContext) + ?: return ImmutableList.of() + return ImmutableList.copyOf( + groupedSubscriptions.stream() + .filter { sub: SubscriptionInfo -> sub.isEmbedded } + .collect(Collectors.toList())) + } + + protected val availableRemovableSubscription: List + get() { + val removableSubscriptions = + SubscriptionUtil.getAvailableSubscriptions(mContext) + return ImmutableList.copyOf( + removableSubscriptions.stream() + // ToDo: This condition is for psim only. If device supports removable + // esim, it needs an new condition. + .filter { sub: SubscriptionInfo -> !sub.isEmbedded } + .collect(Collectors.toList())) + } + + private fun startChooseSimActivity(psimInserted: Boolean) { + val intent = ChooseSimActivity.getIntent(mContext) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(ChooseSimActivity.KEY_HAS_PSIM, psimInserted) + mContext!!.startActivityAsUser(intent, UserHandle.SYSTEM) + } + + private fun startSwitchSlotConfirmDialogActivity(subscriptionInfo: SubscriptionInfo) { + val intent = Intent( + mContext, + SwitchToEsimConfirmDialogActivity::class.java + ) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(SwitchToEsimConfirmDialogActivity.KEY_SUB_TO_ENABLE, subscriptionInfo) + mContext!!.startActivityAsUser(intent, UserHandle.SYSTEM) + } + + private fun startDsdsDialogActivity() { + val intent = Intent(mContext, DsdsDialogActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + mContext!!.startActivityAsUser(intent, UserHandle.SYSTEM) + } + + private fun startSimConfirmDialogActivity(subId: Int) { + if (!isSuwFinished(mContext!!)) { + Log.d(TAG, "Still in SUW. Do nothing") + return + } + if (!SubscriptionManager.isUsableSubscriptionId(subId)) { + Log.i(TAG, "Unable to enable subscription due to invalid subscription ID.") + return + } + Log.d( + TAG, + "Start ToggleSubscriptionDialogActivity with $subId under DSDS+Mep." + ) + SubscriptionUtil.startToggleSubscriptionDialogActivity(mContext, subId, true, true) + } + + private val isMultipleEnabledProfilesSupported: Boolean + get() { + val cardInfos = mTelMgr!!.uiccCardsInfo + if (cardInfos == null) { + Log.d( + TAG, + "UICC cards info list is empty." + ) + return false + } + return cardInfos.stream() + .anyMatch { cardInfo: UiccCardInfo -> cardInfo.isMultipleEnabledProfilesSupported } + } + + private fun isSuwFinished(context: Context): Boolean { + try { + // DEVICE_PROVISIONED is 0 if still in setup wizard. 1 if setup completed. + return (Settings.Global.getInt( + context.contentResolver, Settings.Global.DEVICE_PROVISIONED) + == 1) + } catch (e: SettingNotFoundException) { + Log.e(TAG, "Cannot get DEVICE_PROVISIONED from the device.", e) + return false + } + } + + companion object { + private const val TAG = "SimSlotChangeHandler" + + private const val EUICC_PREFS = "euicc_prefs" + + // Shared preference keys + private const val KEY_REMOVABLE_SLOT_STATE = "removable_slot_state" + private const val KEY_SUW_PSIM_ACTION = "suw_psim_action" + + // User's last removable SIM insertion / removal action during SUW. + private const val LAST_USER_ACTION_IN_SUW_NONE = 0 + private const val LAST_USER_ACTION_IN_SUW_INSERT = 1 + private const val LAST_USER_ACTION_IN_SUW_REMOVE = 2 + + private const val DEFAULT_WAIT_AFTER_SIM_INSERTED_TIMEOUT_MILLIS: Long = 25 * 1000L + + @Volatile + private var slotChangeHandler: SimSlotChangeHandler? = null + + /** Returns a SIM slot change handler singleton. */ + @JvmStatic + fun get(): SimSlotChangeHandler? { + if (slotChangeHandler == null) { + synchronized(SimSlotChangeHandler::class.java) { + if (slotChangeHandler == null) { + slotChangeHandler = SimSlotChangeHandler() + } + } + } + return slotChangeHandler + } + } +}