From 7840ae7569b67dbd05da6b993db1c16ae6a0419e Mon Sep 17 00:00:00 2001 From: Jiashen Wang Date: Thu, 29 Oct 2020 16:55:43 -0700 Subject: [PATCH] [SIM Dialog Migration] Migrate SIM enable dialog from LPA to Settings Migrates SIM enabling and DSDS dialog from LPA to Settings. Design: https://docs.google.com/document/d/1wb5_hoBkZVbkXGNWHbx4Jf61swjfxsJzkytiTzJosYo/edit?usp=sharing Bug: 160819390 Test: Manually tested eSIM profile enabling. Change-Id: I9ce690a1594adfc90b62840819237acd54418469 --- res/values/strings.xml | 48 ++- .../android/settings/AsyncTaskSidecar.java | 66 ++++ .../network/CarrierConfigChangedReceiver.java | 62 ++++ .../network/EnableMultiSimSidecar.java | 190 +++++++++++ .../settings/network/SwitchSlotSidecar.java | 115 +++++++ .../network/SwitchToRemovableSlotSidecar.java | 139 ++++++++ .../settings/network/UiccSlotUtil.java | 153 +++++++++ .../settings/network/UiccSlotsException.java | 35 ++ .../ToggleSubscriptionDialogActivity.java | 301 +++++++++++++++++- 9 files changed, 1095 insertions(+), 14 deletions(-) create mode 100644 src/com/android/settings/AsyncTaskSidecar.java create mode 100644 src/com/android/settings/network/CarrierConfigChangedReceiver.java create mode 100644 src/com/android/settings/network/EnableMultiSimSidecar.java create mode 100644 src/com/android/settings/network/SwitchSlotSidecar.java create mode 100644 src/com/android/settings/network/SwitchToRemovableSlotSidecar.java create mode 100644 src/com/android/settings/network/UiccSlotUtil.java create mode 100644 src/com/android/settings/network/UiccSlotsException.java diff --git a/res/values/strings.xml b/res/values/strings.xml index acc7f517fc4..2861d52ebe9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11975,6 +11975,30 @@ See less + + Turn on %1$s? + + Turn on SIM? + + Switch to %1$s? + + Switch to using SIM card? + + Only one SIM can be active at a time.\n\nSwitching to %1$s won\u2019t cancel your %2$s service. + + Only one downloaded SIM can be active at a time.\n\nSwitching to %1$s won\u2019t cancel your %2$s service. + + Only one SIM can be active at a time.\n\nSwitching won\u2019t cancel your %1$s service. + + Switch to %1$s + + Connecting to network… + + Switching to %1$s + + Can\u2019t switch carrier + + The carrier can\u2019t be switched due to an error. Turn off %1$s? @@ -11982,9 +12006,31 @@ Turning off SIM - Can\'t disable carrier + Can\u2019t disable carrier Something went wrong and your carrier could not be disabled. + + Use 2 SIMs? + + This device can have 2 SIMs active at once. To continue using 1 SIM at a time, tap \"No thanks\". + + Restart device? + + To get started, restart your device. Then you can add another SIM. + + Continue + + Restart + + No thanks + + Switch + + Can\u2019t activate SIM + + Remove the SIM and insert it again. If the problem continues, restart your device. + + Try turning on the SIM again. If the problem continues, restart your device. diff --git a/src/com/android/settings/AsyncTaskSidecar.java b/src/com/android/settings/AsyncTaskSidecar.java new file mode 100644 index 00000000000..31c8298e1f7 --- /dev/null +++ b/src/com/android/settings/AsyncTaskSidecar.java @@ -0,0 +1,66 @@ +/* + * 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; + +import android.os.AsyncTask; + +import androidx.annotation.Nullable; + +import com.android.settingslib.utils.ThreadUtils; + +import java.util.concurrent.Future; + +/** A {@link SidecarFragment} which uses an {@link AsyncTask} to perform background work. */ +public abstract class AsyncTaskSidecar extends SidecarFragment { + + private Future mAsyncTask; + + @Override + public void onDestroy() { + if (mAsyncTask != null) { + mAsyncTask.cancel(true /* mayInterruptIfRunning */); + } + + super.onDestroy(); + } + + /** + * Executes the background task. + * + * @param param parameters passed in from {@link #run} + */ + protected abstract Result doInBackground(@Nullable Param param); + + /** Handles the background task's result. */ + protected void onPostExecute(Result result) {} + + /** Runs the sidecar and sets the state to RUNNING. */ + public void run(@Nullable final Param param) { + setState(State.RUNNING, Substate.UNUSED); + + if (mAsyncTask != null) { + mAsyncTask.cancel(true /* mayInterruptIfRunning */); + } + + mAsyncTask = + ThreadUtils.postOnBackgroundThread( + () -> { + Result result = doInBackground(param); + ThreadUtils.postOnMainThread(() -> onPostExecute(result)); + }); + } +} diff --git a/src/com/android/settings/network/CarrierConfigChangedReceiver.java b/src/com/android/settings/network/CarrierConfigChangedReceiver.java new file mode 100644 index 00000000000..8a6d47d96fc --- /dev/null +++ b/src/com/android/settings/network/CarrierConfigChangedReceiver.java @@ -0,0 +1,62 @@ +/* + * 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.network; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.telephony.CarrierConfigManager; +import android.util.Log; + +import java.util.concurrent.CountDownLatch; + +/** A receiver listens to the carrier config changes. */ +public class CarrierConfigChangedReceiver extends BroadcastReceiver { + private static final String TAG = "CarrierConfigChangedReceiver"; + private static final String ACTION_CARRIER_CONFIG_CHANGED = + CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED; + + private final CountDownLatch mLatch; + + public CarrierConfigChangedReceiver(CountDownLatch latch) { + mLatch = latch; + } + + public void registerOn(Context context) { + context.registerReceiver(this, new IntentFilter(ACTION_CARRIER_CONFIG_CHANGED)); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (isInitialStickyBroadcast()) { + return; + } + + if (ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) { + checkSubscriptionIndex(intent); + } + } + + private void checkSubscriptionIndex(Intent intent) { + if (intent.hasExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX)) { + int subId = intent.getIntExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, -1); + Log.i(TAG, "subId from config changed: " + subId); + mLatch.countDown(); + } + } +} diff --git a/src/com/android/settings/network/EnableMultiSimSidecar.java b/src/com/android/settings/network/EnableMultiSimSidecar.java new file mode 100644 index 00000000000..c47e61a1929 --- /dev/null +++ b/src/com/android/settings/network/EnableMultiSimSidecar.java @@ -0,0 +1,190 @@ +/* + * 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.network; + +import android.app.FragmentManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.telephony.CarrierConfigManager; +import android.telephony.TelephonyManager; +import android.telephony.UiccSlotInfo; +import android.util.ArraySet; +import android.util.Log; + +import com.android.settings.AsyncTaskSidecar; +import com.android.settings.SidecarFragment; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * {@code EnableMultiSimSidecar} enables multi SIM on the device. It should only be called for + * Android R+. After {@code run} is called, it sets the configuration on modem side to enable + * multiple SIMs. Once the configuration is set successfully, it will listen to UICC card changes + * until {@code TelMan.EXTRA_ACTIVE_SIM_SUPPORTED_COUNT} matches {@code mNumOfActiveSim} or timeout. + */ +public class EnableMultiSimSidecar extends AsyncTaskSidecar { + + // Tags + private static final String TAG = "EnableMultiSimSidecar"; + + // TODO(b/171846124): Pass timeout value from LPA to Settings + private static final long ENABLE_MULTI_SIM_TIMEOUT_MILLS = 40 * 1000L; + + public static EnableMultiSimSidecar get(FragmentManager fm) { + return SidecarFragment.get(fm, TAG, EnableMultiSimSidecar.class, null /* args */); + } + + final CountDownLatch mSimCardStateChangedLatch = new CountDownLatch(1); + private TelephonyManager mTelephonyManager; + private int mNumOfActiveSim = 0; + + private final BroadcastReceiver mCarrierConfigChangeReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int readySimsCount = getReadySimsCount(); + int activeSlotsCount = getActiveSlotsCount(); + // If the number of ready SIM count and active slots equal to the number of SIMs + // need to be activated, the device is successfully switched to multiple active + // SIM mode. + if (readySimsCount == mNumOfActiveSim && activeSlotsCount == mNumOfActiveSim) { + Log.i( + TAG, + String.format("%d slots are active and ready.", mNumOfActiveSim)); + mSimCardStateChangedLatch.countDown(); + return; + } + Log.i( + TAG, + String.format( + "%d slots are active and %d SIMs are ready. Keep waiting until" + + " timeout.", + activeSlotsCount, readySimsCount)); + } + }; + + @Override + protected Boolean doInBackground(Void aVoid) { + return updateMultiSimConfig(); + } + + @Override + protected void onPostExecute(Boolean isDsdsEnabled) { + if (isDsdsEnabled) { + setState(State.SUCCESS, Substate.UNUSED); + } else { + setState(State.ERROR, Substate.UNUSED); + } + } + + public void run(int numberOfSimToActivate) { + mTelephonyManager = getContext().getSystemService(TelephonyManager.class); + mNumOfActiveSim = numberOfSimToActivate; + + if (mNumOfActiveSim > mTelephonyManager.getSupportedModemCount()) { + Log.e(TAG, "Requested number of active SIM is greater than supported modem count."); + setState(State.ERROR, Substate.UNUSED); + return; + } + if (mTelephonyManager.doesSwitchMultiSimConfigTriggerReboot()) { + Log.e(TAG, "The device does not support reboot free DSDS."); + setState(State.ERROR, Substate.UNUSED); + return; + } + super.run(null /* param */); + } + + // This method registers a ACTION_SIM_CARD_STATE_CHANGED broadcast receiver and wait for slot + // changes. If multi SIMs have been successfully enabled, it returns true. Otherwise, returns + // false. + private boolean updateMultiSimConfig() { + try { + getContext() + .registerReceiver( + mCarrierConfigChangeReceiver, + new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)); + mTelephonyManager.switchMultiSimConfig(mNumOfActiveSim); + if (mSimCardStateChangedLatch.await( + ENABLE_MULTI_SIM_TIMEOUT_MILLS, TimeUnit.MILLISECONDS)) { + Log.i(TAG, "Multi SIM were successfully enabled."); + return true; + } else { + Log.e(TAG, "Timeout for waiting SIM status."); + return false; + } + } catch (InterruptedException e) { + Log.e(TAG, "Failed to enable multiple SIM due to InterruptedException", e); + return false; + } finally { + getContext().unregisterReceiver(mCarrierConfigChangeReceiver); + } + } + + // Returns how many SIMs have SIM ready state, not ready state, or removable slot with absent + // SIM state. + private int getReadySimsCount() { + int readyCardsCount = 0; + int activeSlotCount = mTelephonyManager.getActiveModemCount(); + Set activeRemovableLogicalSlots = getActiveRemovableLogicalSlotIds(); + for (int logicalSlotId = 0; logicalSlotId < activeSlotCount; logicalSlotId++) { + int simState = mTelephonyManager.getSimState(logicalSlotId); + if (simState == TelephonyManager.SIM_STATE_READY + || simState == TelephonyManager.SIM_STATE_NOT_READY + || simState == TelephonyManager.SIM_STATE_LOADED + || (simState == TelephonyManager.SIM_STATE_ABSENT + && activeRemovableLogicalSlots.contains(logicalSlotId))) { + readyCardsCount++; + } + } + return readyCardsCount; + } + + // Get active slots count from {@code TelephonyManager#getUiccSlotsInfo}. + private int getActiveSlotsCount() { + UiccSlotInfo[] slotsInfo = mTelephonyManager.getUiccSlotsInfo(); + if (slotsInfo == null) { + return 0; + } + int activeSlots = 0; + for (UiccSlotInfo slotInfo : slotsInfo) { + if (slotInfo != null && slotInfo.getIsActive()) { + activeSlots++; + } + } + return activeSlots; + } + + /** Returns a list of active removable logical slot ids. */ + public Set getActiveRemovableLogicalSlotIds() { + UiccSlotInfo[] infos = mTelephonyManager.getUiccSlotsInfo(); + if (infos == null) { + return Collections.emptySet(); + } + Set activeRemovableLogicalSlotIds = new ArraySet<>(); + for (UiccSlotInfo info : infos) { + if (info != null && info.getIsActive() && info.isRemovable()) { + activeRemovableLogicalSlotIds.add(info.getLogicalSlotIdx()); + } + } + return activeRemovableLogicalSlotIds; + } +} diff --git a/src/com/android/settings/network/SwitchSlotSidecar.java b/src/com/android/settings/network/SwitchSlotSidecar.java new file mode 100644 index 00000000000..cffb23fb03f --- /dev/null +++ b/src/com/android/settings/network/SwitchSlotSidecar.java @@ -0,0 +1,115 @@ +/* + * 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.network; + +import android.annotation.IntDef; +import android.app.FragmentManager; +import android.os.Bundle; +import android.util.Log; + +import com.android.settings.AsyncTaskSidecar; +import com.android.settings.SidecarFragment; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.annotation.Nullable; + +/** {@link SidecarFragment} to switch SIM slot. */ +public class SwitchSlotSidecar + extends AsyncTaskSidecar { + private static final String TAG = "SwitchSlotSidecar"; + + /** Commands */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + Command.SWITCH_TO_REMOVABLE_SIM, + }) + private @interface Command { + int SWITCH_TO_REMOVABLE_SIM = 0; + } + + static class Param { + @Command int command; + int slotId; + } + + static class Result { + Exception exception; + } + + /** Returns a SwitchSlotSidecar sidecar instance. */ + public static SwitchSlotSidecar get(FragmentManager fm) { + return SidecarFragment.get(fm, TAG, SwitchSlotSidecar.class, null /* args */); + } + + @Nullable private Exception mException; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** Starts switching to the removable slot. */ + public void runSwitchToRemovableSlot(int id) { + Param param = new Param(); + param.command = Command.SWITCH_TO_REMOVABLE_SIM; + param.slotId = id; + super.run(param); + } + + /** + * Returns the exception thrown during the execution of a command. Will be null in any state + * other than {@link State#SUCCESS}, and may be null in that state if there was not an error. + */ + @Nullable + public Exception getException() { + return mException; + } + + @Override + protected Result doInBackground(@Nullable Param param) { + Result result = new Result(); + if (param == null) { + result.exception = new UiccSlotsException("Null param"); + return result; + } + try { + switch (param.command) { + case Command.SWITCH_TO_REMOVABLE_SIM: + UiccSlotUtil.switchToRemovableSlot(param.slotId, getContext()); + break; + default: + Log.e(TAG, "Wrong command."); + break; + } + } catch (UiccSlotsException e) { + result.exception = e; + } + return result; + } + + @Override + protected void onPostExecute(Result result) { + if (result.exception == null) { + setState(State.SUCCESS, Substate.UNUSED); + } else { + mException = result.exception; + setState(State.ERROR, Substate.UNUSED); + } + } +} diff --git a/src/com/android/settings/network/SwitchToRemovableSlotSidecar.java b/src/com/android/settings/network/SwitchToRemovableSlotSidecar.java new file mode 100644 index 00000000000..132a2fd2367 --- /dev/null +++ b/src/com/android/settings/network/SwitchToRemovableSlotSidecar.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.network; + +import android.app.FragmentManager; +import android.os.Bundle; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.util.Log; + +import com.android.settings.SidecarFragment; +import com.android.settings.network.telephony.EuiccOperationSidecar; + +/** + * This sidecar is responsible for switching to the removable slot. It disables the active eSIM + * profile before switching if there is one. + */ +public class SwitchToRemovableSlotSidecar extends EuiccOperationSidecar + implements SidecarFragment.Listener { + + private static final String TAG = "DisableSubscriptionAndSwitchSlotSidecar"; + private static final String ACTION_DISABLE_SUBSCRIPTION_AND_SWITCH_SLOT = + "disable_subscription_and_switch_slot_sidecar"; + + // Stateless members. + private SwitchToEuiccSubscriptionSidecar mSwitchToSubscriptionSidecar; + private SwitchSlotSidecar mSwitchSlotSidecar; + private int mPhysicalSlotId; + + /** Returns a SwitchToRemovableSlotSidecar sidecar instance. */ + public static SwitchToRemovableSlotSidecar get(FragmentManager fm) { + return SidecarFragment.get(fm, TAG, SwitchToRemovableSlotSidecar.class, null /* args */); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSwitchToSubscriptionSidecar = + SwitchToEuiccSubscriptionSidecar.get(getChildFragmentManager()); + mSwitchSlotSidecar = SwitchSlotSidecar.get(getChildFragmentManager()); + } + + @Override + public void onResume() { + super.onResume(); + mSwitchToSubscriptionSidecar.addListener(this); + mSwitchSlotSidecar.addListener(this); + } + + @Override + public void onPause() { + mSwitchToSubscriptionSidecar.removeListener(this); + mSwitchSlotSidecar.removeListener(this); + super.onPause(); + } + + @Override + protected String getReceiverAction() { + return ACTION_DISABLE_SUBSCRIPTION_AND_SWITCH_SLOT; + } + + @Override + public void onStateChange(SidecarFragment fragment) { + if (fragment == mSwitchToSubscriptionSidecar) { + onSwitchToSubscriptionSidecarStateChange(); + } else if (fragment == mSwitchSlotSidecar) { + onSwitchSlotSidecarStateChange(); + } else { + Log.wtf(TAG, "Received state change from a sidecar not expected."); + } + } + + /** + * Starts switching to the removable slot. It disables the active eSIM profile before switching + * if there is one. + * + * @param physicalSlotId removable physical SIM slot ID. + */ + public void run(int physicalSlotId) { + mPhysicalSlotId = physicalSlotId; + SubscriptionManager subscriptionManager = + getContext().getSystemService(SubscriptionManager.class); + if (SubscriptionUtil.getActiveSubscriptions(subscriptionManager).stream() + .anyMatch(SubscriptionInfo::isEmbedded)) { + Log.i(TAG, "There is an active eSIM profile. Disable the profile first."); + // Use INVALID_SUBSCRIPTION_ID to disable the only active profile. + mSwitchToSubscriptionSidecar.run(SubscriptionManager.INVALID_SUBSCRIPTION_ID); + } else { + Log.i(TAG, "There is no active eSIM profiles. Start to switch to removable slot."); + mSwitchSlotSidecar.runSwitchToRemovableSlot(mPhysicalSlotId); + } + } + + private void onSwitchToSubscriptionSidecarStateChange() { + switch (mSwitchToSubscriptionSidecar.getState()) { + case State.SUCCESS: + mSwitchToSubscriptionSidecar.reset(); + Log.i( + TAG, + "Successfully disabled eSIM profile. Start to switch to Removable slot."); + mSwitchSlotSidecar.runSwitchToRemovableSlot(mPhysicalSlotId); + break; + case State.ERROR: + mSwitchToSubscriptionSidecar.reset(); + Log.i(TAG, "Failed to disable the active eSIM profile."); + setState(State.ERROR, Substate.UNUSED); + break; + } + } + + private void onSwitchSlotSidecarStateChange() { + switch (mSwitchSlotSidecar.getState()) { + case State.SUCCESS: + mSwitchSlotSidecar.reset(); + Log.i(TAG, "Successfully switched to removable slot."); + setState(State.SUCCESS, Substate.UNUSED); + break; + case State.ERROR: + mSwitchSlotSidecar.reset(); + Log.i(TAG, "Failed to switch to removable slot."); + setState(State.ERROR, Substate.UNUSED); + break; + } + } +} diff --git a/src/com/android/settings/network/UiccSlotUtil.java b/src/com/android/settings/network/UiccSlotUtil.java new file mode 100644 index 00000000000..792f02a3c9c --- /dev/null +++ b/src/com/android/settings/network/UiccSlotUtil.java @@ -0,0 +1,153 @@ +/* + * 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.network; + +import android.annotation.IntDef; +import android.content.Context; +import android.telephony.TelephonyManager; +import android.telephony.UiccSlotInfo; +import android.util.Log; + +import com.android.settingslib.utils.ThreadUtils; + +import com.google.common.collect.ImmutableList; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class UiccSlotUtil { + + private static final String TAG = "UiccSlotUtil"; + + // TODO(b/171846124): Pass timeout value from LPA to Settings + private static final long WAIT_AFTER_SWITCH_TIMEOUT_MILLIS = 25000; + + public static final int INVALID_PHYSICAL_SLOT_ID = -1; + + /** + * Mode for switching to eSIM slot which decides whether there is cleanup process, e.g. + * disabling test profile, after eSIM slot is activated and whether we will wait it finished. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + SwitchingEsimMode.NO_CLEANUP, + SwitchingEsimMode.ASYNC_CLEANUP, + SwitchingEsimMode.SYNC_CLEANUP + }) + public @interface SwitchingEsimMode { + /** No cleanup process after switching to eSIM slot */ + int NO_CLEANUP = 0; + /** Has cleanup process, but we will not wait it finished. */ + int ASYNC_CLEANUP = 1; + /** Has cleanup process and we will wait until it's finished */ + int SYNC_CLEANUP = 2; + } + + /** + * Returns an immutable list of all UICC slots. If TelephonyManager#getUiccSlotsInfo returns, it + * returns an empty list instead. + */ + public static ImmutableList getSlotInfos(TelephonyManager telMgr) { + UiccSlotInfo[] slotInfos = telMgr.getUiccSlotsInfo(); + if (slotInfos == null) { + return ImmutableList.of(); + } + return ImmutableList.copyOf(slotInfos); + } + + /** + * Switches to the removable slot. It waits for SIM_STATE_LOADED after switch. If slotId is + * INVALID_PHYSICAL_SLOT_ID, the method will use the first detected inactive removable slot. + * + * @param slotId the physical removable slot id. + * @param context the application context. + * @throws UiccSlotsException if there is an error. + */ + public static synchronized void switchToRemovableSlot(int slotId, Context context) + throws UiccSlotsException { + if (ThreadUtils.isMainThread()) { + throw new IllegalThreadStateException( + "Do not call switchToRemovableSlot on the main thread."); + } + TelephonyManager telMgr = context.getSystemService(TelephonyManager.class); + if (telMgr.isMultiSimEnabled()) { + // If this device supports multiple active slots, don't mess with TelephonyManager. + Log.i(TAG, "Multiple active slots supported. Not calling switchSlots."); + return; + } + UiccSlotInfo[] slots = telMgr.getUiccSlotsInfo(); + if (slotId == INVALID_PHYSICAL_SLOT_ID) { + for (int i = 0; i < slots.length; i++) { + if (slots[i].isRemovable() + && !slots[i].getIsActive() + && slots[i].getCardStateInfo() != UiccSlotInfo.CARD_STATE_INFO_ERROR + && slots[i].getCardStateInfo() != UiccSlotInfo.CARD_STATE_INFO_RESTRICTED) { + performSwitchToRemovableSlot(i, context); + return; + } + } + } else { + if (slotId >= slots.length || !slots[slotId].isRemovable()) { + throw new UiccSlotsException("The given slotId is not a removable slot: " + slotId); + } + if (!slots[slotId].getIsActive()) { + performSwitchToRemovableSlot(slotId, context); + } + } + } + + private static void performSwitchToRemovableSlot(int slotId, Context context) + throws UiccSlotsException { + CarrierConfigChangedReceiver receiver = null; + try { + CountDownLatch latch = new CountDownLatch(1); + receiver = new CarrierConfigChangedReceiver(latch); + receiver.registerOn(context); + switchSlots(context, slotId); + latch.await(WAIT_AFTER_SWITCH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.e(TAG, "Failed switching to physical slot.", e); + } finally { + if (receiver != null) { + context.unregisterReceiver(receiver); + } + } + } + + /** + * Changes the logical slot to physical slot mapping. OEM should override this to provide + * device-specific implementation if the device supports switching slots. + * + * @param context the application context. + * @param physicalSlots List of physical slot ids in the order of logical slots. + */ + private static void switchSlots(Context context, int... physicalSlots) + throws UiccSlotsException { + TelephonyManager telMgr = context.getSystemService(TelephonyManager.class); + if (telMgr.isMultiSimEnabled()) { + // If this device supports multiple active slots, don't mess with TelephonyManager. + Log.i(TAG, "Multiple active slots supported. Not calling switchSlots."); + return; + } + if (!telMgr.switchSlots(physicalSlots)) { + throw new UiccSlotsException("Failed to switch slots"); + } + } +} diff --git a/src/com/android/settings/network/UiccSlotsException.java b/src/com/android/settings/network/UiccSlotsException.java new file mode 100644 index 00000000000..2a2edc5d4e4 --- /dev/null +++ b/src/com/android/settings/network/UiccSlotsException.java @@ -0,0 +1,35 @@ +/* + * 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.network; + +/** The exception that is thrown when an error happens in a call to {@link UiccSlotUtil}. */ +public class UiccSlotsException extends Exception { + + public UiccSlotsException() {} + + public UiccSlotsException(String message) { + super(message); + } + + public UiccSlotsException(String message, Throwable cause) { + super(message, cause); + } + + public UiccSlotsException(Throwable cause) { + super(cause); + } +} diff --git a/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java b/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java index 905ead0f92c..919415b53e0 100644 --- a/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java +++ b/src/com/android/settings/network/telephony/ToggleSubscriptionDialogActivity.java @@ -22,15 +22,22 @@ import android.os.Bundle; import android.os.UserManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.telephony.UiccSlotInfo; import android.text.TextUtils; import android.util.Log; -import androidx.appcompat.app.AlertDialog; - import com.android.settings.R; import com.android.settings.SidecarFragment; +import com.android.settings.network.EnableMultiSimSidecar; import com.android.settings.network.SubscriptionUtil; import com.android.settings.network.SwitchToEuiccSubscriptionSidecar; +import com.android.settings.network.SwitchToRemovableSlotSidecar; +import com.android.settings.network.UiccSlotUtil; + +import com.google.common.collect.ImmutableList; + +import java.util.List; /** This dialog activity handles both eSIM and pSIM subscriptions enabling and disabling. */ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogActivity @@ -41,6 +48,11 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc private static final String ARG_enable = "enable"; // Dialog tags private static final int DIALOG_TAG_DISABLE_SIM_CONFIRMATION = 1; + private static final int DIALOG_TAG_ENABLE_SIM_CONFIRMATION = 2; + private static final int DIALOG_TAG_ENABLE_DSDS_CONFIRMATION = 3; + private static final int DIALOG_TAG_ENABLE_DSDS_REBOOT_CONFIRMATION = 4; + // Number of SIMs for DSDS + private static final int NUM_OF_SIMS_FOR_DSDS = 2; /** * Returns an intent of ToggleSubscriptionDialogActivity. @@ -58,8 +70,11 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc private SubscriptionInfo mSubInfo; private SwitchToEuiccSubscriptionSidecar mSwitchToEuiccSubscriptionSidecar; - private AlertDialog mToggleSimConfirmDialog; + private SwitchToRemovableSlotSidecar mSwitchToRemovableSlotSidecar; + private EnableMultiSimSidecar mEnableMultiSimSidecar; private boolean mEnable; + private boolean mIsEsimOperation; + private TelephonyManager mTelMgr; @Override protected void onCreate(Bundle savedInstanceState) { @@ -67,6 +82,7 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc Intent intent = getIntent(); int subId = intent.getIntExtra(ARG_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID); + mTelMgr = getSystemService(TelephonyManager.class); UserManager userManager = getSystemService(UserManager.class); if (!userManager.isAdminUser()) { @@ -82,13 +98,16 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc } mSubInfo = SubscriptionUtil.getSubById(mSubscriptionManager, subId); + mIsEsimOperation = mSubInfo != null && mSubInfo.isEmbedded(); mSwitchToEuiccSubscriptionSidecar = SwitchToEuiccSubscriptionSidecar.get(getFragmentManager()); + mSwitchToRemovableSlotSidecar = SwitchToRemovableSlotSidecar.get(getFragmentManager()); + mEnableMultiSimSidecar = EnableMultiSimSidecar.get(getFragmentManager()); mEnable = intent.getBooleanExtra(ARG_enable, true); if (savedInstanceState == null) { if (mEnable) { - handleEnablingSubAction(); + showEnableSubDialog(); } else { showDisableSimConfirmDialog(); } @@ -99,10 +118,14 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc protected void onResume() { super.onResume(); mSwitchToEuiccSubscriptionSidecar.addListener(this); + mSwitchToRemovableSlotSidecar.addListener(this); + mEnableMultiSimSidecar.addListener(this); } @Override protected void onPause() { + mEnableMultiSimSidecar.removeListener(this); + mSwitchToRemovableSlotSidecar.removeListener(this); mSwitchToEuiccSubscriptionSidecar.removeListener(this); super.onPause(); } @@ -111,19 +134,25 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc public void onStateChange(SidecarFragment fragment) { if (fragment == mSwitchToEuiccSubscriptionSidecar) { handleSwitchToEuiccSubscriptionSidecarStateChange(); + } else if (fragment == mSwitchToRemovableSlotSidecar) { + handleSwitchToRemovableSlotSidecarStateChange(); + } else if (fragment == mEnableMultiSimSidecar) { + handleEnableMultiSimSidecarStateChange(); } } @Override public void onConfirm(int tag, boolean confirmed) { - if (!confirmed) { + if (!confirmed + && tag != DIALOG_TAG_ENABLE_DSDS_CONFIRMATION + && tag != DIALOG_TAG_ENABLE_DSDS_REBOOT_CONFIRMATION) { finish(); return; } switch (tag) { case DIALOG_TAG_DISABLE_SIM_CONFIRMATION: - if (mSubInfo.isEmbedded()) { + if (mIsEsimOperation) { Log.i(TAG, "Disabling the eSIM profile."); showProgressDialog( getString(R.string.privileged_action_disable_sub_dialog_progress)); @@ -132,6 +161,51 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc return; } Log.i(TAG, "Disabling the pSIM profile."); + handleTogglePsimAction(); + break; + case DIALOG_TAG_ENABLE_DSDS_CONFIRMATION: + if (!confirmed) { + Log.i(TAG, "User cancel the dialog to enable DSDS."); + showEnableSimConfirmDialog(); + return; + } + if (mTelMgr.doesSwitchMultiSimConfigTriggerReboot()) { + Log.i(TAG, "Device does not support reboot free DSDS."); + showRebootConfirmDialog(); + return; + } + Log.i( + TAG, + "Enabling DSDS without rebooting. " + + getString(R.string.sim_action_enabling_sim_without_carrier_name)); + showProgressDialog( + getString(R.string.sim_action_enabling_sim_without_carrier_name)); + mEnableMultiSimSidecar.run(NUM_OF_SIMS_FOR_DSDS); + break; + case DIALOG_TAG_ENABLE_DSDS_REBOOT_CONFIRMATION: + if (!confirmed) { + Log.i(TAG, "User cancel the dialog to reboot to enable DSDS."); + showEnableSimConfirmDialog(); + return; + } + Log.i(TAG, "User confirmed reboot to enable DSDS."); + mTelMgr.switchMultiSimConfig(NUM_OF_SIMS_FOR_DSDS); + // TODO(b/170507290): Store a bit in preferences for displaying the notification + // after the reboot. + break; + case DIALOG_TAG_ENABLE_SIM_CONFIRMATION: + Log.i(TAG, "User confirmed to enable the subscription."); + if (mIsEsimOperation) { + showProgressDialog( + getString( + R.string.sim_action_switch_sub_dialog_progress, + mSubInfo.getDisplayName())); + mSwitchToEuiccSubscriptionSidecar.run(mSubInfo.getSubscriptionId()); + return; + } + showProgressDialog( + getString(R.string.sim_action_enabling_sim_without_carrier_name)); + mSwitchToRemovableSlotSidecar.run(UiccSlotUtil.INVALID_PHYSICAL_SLOT_ID); break; default: Log.e(TAG, "Unrecognized confirmation dialog tag: " + tag); @@ -165,10 +239,105 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc } } + private void handleSwitchToRemovableSlotSidecarStateChange() { + switch (mSwitchToRemovableSlotSidecar.getState()) { + case SidecarFragment.State.SUCCESS: + Log.i(TAG, "Successfully switched to removable slot."); + mSwitchToRemovableSlotSidecar.reset(); + handleTogglePsimAction(); + dismissProgressDialog(); + finish(); + break; + case SidecarFragment.State.ERROR: + Log.e(TAG, "Failed switching to removable slot."); + mSwitchToRemovableSlotSidecar.reset(); + dismissProgressDialog(); + showErrorDialog( + getString(R.string.sim_action_enable_sim_fail_title), + getString(R.string.sim_action_enable_sim_fail_text)); + break; + } + } + + private void handleEnableMultiSimSidecarStateChange() { + switch (mEnableMultiSimSidecar.getState()) { + case SidecarFragment.State.SUCCESS: + mEnableMultiSimSidecar.reset(); + Log.i(TAG, "Successfully switched to DSDS without reboot."); + handleEnableSubscriptionAfterEnablingDsds(); + break; + case SidecarFragment.State.ERROR: + mEnableMultiSimSidecar.reset(); + Log.i(TAG, "Failed to switch to DSDS without rebooting."); + ProgressDialogFragment.dismiss(getFragmentManager()); + showErrorDialog( + getString(R.string.dsds_activation_failure_title), + getString(R.string.dsds_activation_failure_body_msg2)); + break; + } + } + + private void handleEnableSubscriptionAfterEnablingDsds() { + if (mIsEsimOperation) { + Log.i(TAG, "DSDS enabled, start to enable profile: " + mSubInfo.getSubscriptionId()); + // For eSIM operations, we simply switch to the selected eSIM profile. + mSwitchToEuiccSubscriptionSidecar.run(mSubInfo.getSubscriptionId()); + return; + } + + Log.i(TAG, "DSDS enabled, start to enable pSIM profile."); + handleTogglePsimAction(); + ProgressDialogFragment.dismiss(getFragmentManager()); + finish(); + } + + private void handleTogglePsimAction() { + if (mSubscriptionManager.canDisablePhysicalSubscription() && mSubInfo != null) { + mSubscriptionManager.setUiccApplicationsEnabled(mSubInfo.getSubscriptionId(), mEnable); + } else { + Log.i( + TAG, + "The device does not support toggling pSIM. It is enough to just " + + "enable the removable slot."); + } + } + /* Handles the enabling SIM action. */ - private void handleEnablingSubAction() { - Log.i(TAG, "handleEnableSub"); - // TODO(b/160819390): Implement enabling eSIM/pSIM profile. + private void showEnableSubDialog() { + Log.i(TAG, "Handle subscription enabling."); + if (isDsdsConditionSatisfied()) { + showEnableDsdsConfirmDialog(); + return; + } + if (!mIsEsimOperation && mTelMgr.isMultiSimEnabled()) { + Log.i(TAG, "Toggle on pSIM, no dialog displayed."); + handleTogglePsimAction(); + finish(); + return; + } + showEnableSimConfirmDialog(); + } + + private void showEnableDsdsConfirmDialog() { + ConfirmDialogFragment.show( + this, + ConfirmDialogFragment.OnConfirmListener.class, + DIALOG_TAG_ENABLE_DSDS_CONFIRMATION, + getString(R.string.sim_action_enable_dsds_title), + getString(R.string.sim_action_enable_dsds_text), + getString(R.string.sim_action_continue), + getString(R.string.sim_action_no_thanks)); + } + + private void showRebootConfirmDialog() { + ConfirmDialogFragment.show( + this, + ConfirmDialogFragment.OnConfirmListener.class, + DIALOG_TAG_ENABLE_DSDS_REBOOT_CONFIRMATION, + getString(R.string.sim_action_restart_title), + getString(R.string.sim_action_enable_dsds_text), + getString(R.string.sim_action_reboot), + getString(R.string.cancel)); } /* Displays the SIM toggling confirmation dialog. */ @@ -191,10 +360,116 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc getString(R.string.cancel)); } - /* Dismisses the SIM toggling confirmation dialog. */ - private void dismissToggleSimConfirmDialog() { - if (mToggleSimConfirmDialog != null) { - mToggleSimConfirmDialog.dismiss(); + private void showEnableSimConfirmDialog() { + List activeSubs = + SubscriptionUtil.getActiveSubscriptions(mSubscriptionManager); + SubscriptionInfo activeSub = activeSubs.isEmpty() ? null : activeSubs.get(0); + if (activeSub == null) { + Log.i(TAG, "No active subscriptions available."); + showNonSwitchSimConfirmDialog(); + return; + } + Log.i(TAG, "Found active subscription."); + boolean isBetweenEsim = mIsEsimOperation && activeSub.isEmbedded(); + if (mTelMgr.isMultiSimEnabled() && !isBetweenEsim) { + showNonSwitchSimConfirmDialog(); + return; + } + ConfirmDialogFragment.show( + this, + ConfirmDialogFragment.OnConfirmListener.class, + DIALOG_TAG_ENABLE_SIM_CONFIRMATION, + getSwitchSubscriptionTitle(), + getSwitchDialogBodyMsg(activeSub, isBetweenEsim), + getSwitchDialogPosBtnText(), + getString(android.R.string.cancel)); + } + + private void showNonSwitchSimConfirmDialog() { + ConfirmDialogFragment.show( + this, + ConfirmDialogFragment.OnConfirmListener.class, + DIALOG_TAG_ENABLE_SIM_CONFIRMATION, + getEnableSubscriptionTitle(), + null /* msg */, + getString(R.string.yes), + getString(android.R.string.cancel)); + } + + private String getSwitchDialogPosBtnText() { + return mIsEsimOperation + ? getString( + R.string.sim_action_switch_sub_dialog_confirm, mSubInfo.getDisplayName()) + : getString(R.string.sim_switch_button); + } + + private String getEnableSubscriptionTitle() { + if (mSubInfo == null || TextUtils.isEmpty(mSubInfo.getDisplayName())) { + return getString(R.string.sim_action_enable_sub_dialog_title_without_carrier_name); + } + return getString(R.string.sim_action_enable_sub_dialog_title, mSubInfo.getDisplayName()); + } + + private String getSwitchSubscriptionTitle() { + if (mIsEsimOperation) { + return getString( + R.string.sim_action_switch_sub_dialog_title, mSubInfo.getDisplayName()); + } + return getString(R.string.sim_action_switch_psim_dialog_title); + } + + private String getSwitchDialogBodyMsg(SubscriptionInfo activeSub, boolean betweenEsim) { + if (betweenEsim && mIsEsimOperation) { + return getString( + R.string.sim_action_switch_sub_dialog_text_downloaded, + mSubInfo.getDisplayName(), + activeSub.getDisplayName()); + } else if (mIsEsimOperation) { + return getString( + R.string.sim_action_switch_sub_dialog_text, + mSubInfo.getDisplayName(), + activeSub.getDisplayName()); + } else { + return getString( + R.string.sim_action_switch_sub_dialog_text_single_sim, + activeSub.getDisplayName()); } } + + private boolean isDsdsConditionSatisfied() { + if (mTelMgr.isMultiSimEnabled()) { + Log.i(TAG, "DSDS is already enabled. Condition not satisfied."); + return false; + } + if (mTelMgr.isMultiSimSupported() != TelephonyManager.MULTISIM_ALLOWED) { + Log.i(TAG, "Hardware does not support DSDS."); + return false; + } + ImmutableList slotInfos = UiccSlotUtil.getSlotInfos(mTelMgr); + boolean isRemovableSimEnabled = + slotInfos.stream() + .anyMatch( + slot -> + slot != null + && slot.isRemovable() + && slot.getIsActive() + && slot.getCardStateInfo() + == UiccSlotInfo.CARD_STATE_INFO_PRESENT); + if (mIsEsimOperation && isRemovableSimEnabled) { + Log.i(TAG, "eSIM operation and removable SIM is enabled. DSDS condition satisfied."); + return true; + } + boolean isEsimProfileEnabled = + SubscriptionUtil.getActiveSubscriptions(mSubscriptionManager).stream() + .anyMatch(SubscriptionInfo::isEmbedded); + if (!mIsEsimOperation && isEsimProfileEnabled) { + Log.i( + TAG, + "Removable SIM operation and eSIM profile is enabled. DSDS condition" + + " satisfied."); + return true; + } + Log.i(TAG, "DSDS condition not satisfied."); + return false; + } }