/* * 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.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; } int lastRemovableSlotState = getLastRemovableSimSlotState(mContext); int currentRemovableSlotState = removableSlotInfo.getCardStateInfo(); Log.i(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. handleRemovableSimInsertWhenDsds(); } 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; } Log.d(TAG, "the device is already in DSDS mode and have the DDS. Do nothing."); 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."); 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 void handleRemovableSimInsertWhenDsds() { List subscriptionInfos = getAvailableRemovableSubscription(); if (subscriptionInfos.isEmpty()) { Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing."); return; } Log.d(TAG, "ForNewUi and getAvailableRemovableSubscription:" + subscriptionInfos); startSimConfirmDialogActivity(subscriptionInfos.get(0).getSubscriptionId()); } 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); } 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() {} }