/* * Copyright (C) 2024 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.Context import android.content.Intent import android.os.Bundle import android.os.UserHandle import android.provider.Settings import android.telephony.SubscriptionManager import android.util.Log import androidx.activity.viewModels import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.SignalCellularAlt import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.lifecycle.LifecycleRegistry import com.android.settings.R import com.android.settings.SidecarFragment import com.android.settings.network.SimOnboardingViewModel.SwitchingState import com.android.settings.network.telephony.SimRepository import com.android.settings.network.telephony.SubscriptionActionDialogActivity import com.android.settings.network.telephony.SubscriptionRepository import com.android.settings.network.telephony.ToggleSubscriptionDialogActivity import com.android.settings.spa.SpaActivity.Companion.startSpaActivity import com.android.settings.spa.network.SimOnboardingPageProvider.getRoute import com.android.settings.wifi.WifiPickerTrackerHelper import com.android.settingslib.spa.SpaBaseDialogActivity import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spa.widget.dialog.AlertDialogButton import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogWithIcon import com.android.settingslib.spa.widget.dialog.getDialogWidth import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter import com.android.settingslib.spa.widget.ui.SettingsTitle import com.android.settingslib.spaprivileged.framework.common.userManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull class SimOnboardingActivity : SpaBaseDialogActivity() { lateinit var scope: CoroutineScope lateinit var wifiPickerTrackerHelper: WifiPickerTrackerHelper lateinit var context: Context lateinit var showStartingDialog: MutableState lateinit var showError: MutableState lateinit var showProgressDialog: MutableState lateinit var showDsdsProgressDialog: MutableState lateinit var showRestartDialog: MutableState private val viewModel: SimOnboardingViewModel by viewModels() private var enableMultiSimSidecar: EnableMultiSimSidecar? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!this.userManager.isAdminUser) { Log.e(TAG, "It is not the admin user. Unable to toggle subscription.") finish() return } var targetSubId = intent.getIntExtra(SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID) if (targetSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { targetSubId = intent.getIntExtra( Settings.EXTRA_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID, ) } initServiceData(this, targetSubId, callbackListener) if (!onboardingService.isUsableTargetSubscriptionId) { Log.e(TAG, "The subscription id is not usable.") finish() return } if ( onboardingService.activeSubInfoList.isEmpty() || (!onboardingService.isMultiSimEnabled && !onboardingService.isMultiSimSupported) ) { // TODO: refactor and replace the ToggleSubscriptionDialogActivity Log.d( TAG, "onboardingService.activeSubInfoList is empty or restricted ss mode " + ", start ToggleSubscriptionDialogActivity", ) this.startActivity( ToggleSubscriptionDialogActivity.getIntent( this.applicationContext, targetSubId, true, ) ) finish() return } enableMultiSimSidecar = EnableMultiSimSidecar.get(fragmentManager) } override fun finish() { setProgressDialog(false) onboardingService.clear() super.finish() } var callbackListener: (CallbackType) -> Unit = callbackListener@{ Log.d(TAG, "Receive the CALLBACK: $it") if (isFinishing || isDestroyed) { Log.w(TAG, "activity is finished or destroyed, abort the command") return@callbackListener } when (it) { CallbackType.CALLBACK_ERROR -> { setProgressDialog(false) } CallbackType.CALLBACK_ENABLE_DSDS -> { scope.launch { onboardingService.startEnableDsds(this@SimOnboardingActivity) } } CallbackType.CALLBACK_ONBOARDING_COMPLETE -> { showStartingDialog.value = false setProgressDialog(true) scope.launch { // TODO: refactor the Sidecar // start to activate the sim startSimSwitching() } } CallbackType.CALLBACK_SETUP_NAME -> { scope.launch { onboardingService.startSetupName() } } CallbackType.CALLBACK_SETUP_PRIMARY_SIM -> { scope.launch { onboardingService.startSetupPrimarySim( this@SimOnboardingActivity, wifiPickerTrackerHelper, ) } } CallbackType.CALLBACK_FINISH -> { finish() } } } fun setProgressDialog(enable: Boolean) { if (!this::showProgressDialog.isInitialized) { return } showProgressDialog.value = enable val progressState = if (enable) { SubscriptionActionDialogActivity.PROGRESS_IS_SHOWING } else { SubscriptionActionDialogActivity.PROGRESS_IS_NOT_SHOWING } setProgressState(progressState) } @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { showStartingDialog = rememberSaveable { mutableStateOf(false) } showError = rememberSaveable { mutableStateOf(ErrorType.ERROR_NONE) } showProgressDialog = rememberSaveable { mutableStateOf(false) } showDsdsProgressDialog = rememberSaveable { mutableStateOf(false) } showRestartDialog = rememberSaveable { mutableStateOf(false) } scope = rememberCoroutineScope() context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current wifiPickerTrackerHelper = WifiPickerTrackerHelper( LifecycleRegistry(lifecycleOwner), context, null, /* WifiPickerTrackerCallback */ ) registerSidecarReceiverFlow() ErrorDialogImpl() RestartDialogImpl() LaunchedEffect(Unit) { viewModelUiStateFlow() if ( showError.value != ErrorType.ERROR_NONE || showProgressDialog.value || showDsdsProgressDialog.value || showRestartDialog.value ) { Log.d( TAG, "status: showError:${showError.value}, " + "showProgressDialog:${showProgressDialog.value}, " + "showDsdsProgressDialog:${showDsdsProgressDialog.value}, " + "showRestartDialog:${showRestartDialog.value}", ) showStartingDialog.value = false } else if (onboardingService.activeSubInfoList.isNotEmpty()) { Log.d(TAG, "status: showStartingDialog.value:${showStartingDialog.value}") showStartingDialog.value = true } } if (showStartingDialog.value) { StartingDialogImpl( nextAction = { if (onboardingService.isDsdsConditionSatisfied()) { // TODO: if the phone is SS mode and the isDsdsConditionSatisfied is true, // then enable the DSDS mode. // case#1: the device need the reboot after enabling DSDS. Showing the // confirm dialog to user whether reboot device or not. // case#2: The device don't need the reboot. Enabling DSDS and then showing // the SIM onboarding UI. if (onboardingService.doesSwitchMultiSimConfigTriggerReboot) { // case#1 Log.d(TAG, "Device does not support reboot free DSDS.") showRestartDialog.value = true } else { // case#2 Log.d(TAG, "Enable DSDS mode") showDsdsProgressDialog.value = true enableMultiSimSidecar?.run(SimOnboardingService.NUM_OF_SIMS_FOR_DSDS) } } else { startSimOnboardingProvider() } }, cancelAction = { finish() }, ) } if (showProgressDialog.value) { ProgressDialogImpl( stringResource( R.string.sim_onboarding_progressbar_turning_sim_on, onboardingService.targetSubInfo?.displayName ?: "", ) ) } if (showDsdsProgressDialog.value) { ProgressDialogImpl( stringResource(R.string.sim_action_enabling_sim_without_carrier_name) ) } } @Composable private fun RestartDialogImpl() { val restartDialogPresenter = rememberAlertDialogPresenter( confirmButton = AlertDialogButton(stringResource(R.string.sim_action_reboot)) { callbackListener(CallbackType.CALLBACK_ENABLE_DSDS) }, dismissButton = AlertDialogButton( stringResource( R.string.sim_action_restart_dialog_cancel, onboardingService.targetSubInfo?.displayName ?: "", ) ) { callbackListener(CallbackType.CALLBACK_ONBOARDING_COMPLETE) }, title = stringResource(R.string.sim_action_restart_dialog_title), text = { Text(stringResource(R.string.sim_action_restart_dialog_msg)) }, ) if (showRestartDialog.value) { LaunchedEffect(Unit) { restartDialogPresenter.open() } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun ProgressDialogImpl(title: String) { // TODO: Create the SPA's ProgressDialog and using SPA's widget BasicAlertDialog(onDismissRequest = {}, modifier = Modifier.width(getDialogWidth())) { Surface(color = AlertDialogDefaults.containerColor, shape = AlertDialogDefaults.shape) { Row( modifier = Modifier.fillMaxWidth().padding(SettingsDimension.itemPaddingStart), verticalAlignment = Alignment.CenterVertically, ) { CircularProgressIndicator() Column( modifier = Modifier.padding(start = SettingsDimension.itemPaddingStart) ) { SettingsTitle(title) } } } } } @Composable fun ErrorDialogImpl() { // EuiccSlotSidecar showErrorDialog val errorDialogPresenterForSimSwitching = rememberAlertDialogPresenter( confirmButton = AlertDialogButton(stringResource(android.R.string.ok)) { finish() }, title = stringResource(R.string.sim_action_enable_sim_fail_title), text = { Text(stringResource(R.string.sim_action_enable_sim_fail_text)) }, ) // enableDSDS showErrorDialog val errorDialogPresenterForMultiSimSidecar = rememberAlertDialogPresenter( confirmButton = AlertDialogButton(stringResource(android.R.string.ok)) { finish() }, title = stringResource(R.string.dsds_activation_failure_title), text = { Text(stringResource(R.string.dsds_activation_failure_body_msg2)) }, ) // show error when (showError.value) { ErrorType.ERROR_SIM_SWITCHING -> errorDialogPresenterForSimSwitching.open() ErrorType.ERROR_ENABLE_DSDS -> errorDialogPresenterForMultiSimSidecar.open() else -> {} } } fun viewModelUiStateFlow() { scope.launch { viewModel.uiState.collect { when (it) { SwitchingState.COMPLETED -> { Log.d(TAG, "Successfully enable the SIM profile.") checkSimIsReadyAndGoNext() } SwitchingState.FAILED -> { Log.e(TAG, "Failed switching sim") showError.value = ErrorType.ERROR_SIM_SWITCHING callbackListener(CallbackType.CALLBACK_ERROR) } else -> { Log.d(TAG, "uiState: $it") } } } } } @Composable fun registerSidecarReceiverFlow() { enableMultiSimSidecar?.sidecarReceiverFlow()?.collectLatestWithLifecycle( LocalLifecycleOwner.current ) { onStateChange(it) } } fun SidecarFragment.sidecarReceiverFlow(): Flow = callbackFlow { val broadcastReceiver = SidecarFragment.Listener { Log.d(TAG, "onReceive: $it") trySend(it) } addListener(broadcastReceiver) awaitClose { removeListener(broadcastReceiver) } } .catch { e -> Log.e(TAG, "Error while sidecarReceiverFlow", e) } .conflate() fun startSimSwitching() { Log.d(TAG, "startSimSwitching:") var targetSubInfo = onboardingService.targetSubInfo if (onboardingService.doesTargetSimActive) { Log.d(TAG, "target subInfo is already active") callbackListener(CallbackType.CALLBACK_SETUP_NAME) return } targetSubInfo?.let { var removedSubInfo = onboardingService.getRemovedSim() viewModel.startSimSwitching(targetSubInfo, removedSubInfo) } ?: run { Log.e(TAG, "no target subInfo in onboardingService") finish() } } fun onStateChange(fragment: SidecarFragment?) { if (fragment === enableMultiSimSidecar) { handleEnableMultiSimSidecarStateChange() } } fun handleEnableMultiSimSidecarStateChange() { when (enableMultiSimSidecar!!.state) { SidecarFragment.State.SUCCESS -> { enableMultiSimSidecar!!.reset() Log.i(TAG, "Successfully switched to DSDS without reboot.") showDsdsProgressDialog.value = false // refresh data initServiceData(this, onboardingService.targetSubId, callbackListener) startSimOnboardingProvider() } SidecarFragment.State.ERROR -> { enableMultiSimSidecar!!.reset() showDsdsProgressDialog.value = false Log.i(TAG, "Failed to switch to DSDS without rebooting.") showError.value = ErrorType.ERROR_ENABLE_DSDS callbackListener(CallbackType.CALLBACK_ERROR) } } } private suspend fun checkSimIsReadyAndGoNext() { withContext(Dispatchers.Default) { val waitingTimeMillis = Settings.Global.getLong( context.contentResolver, Settings.Global.EUICC_SWITCH_SLOT_TIMEOUT_MILLIS, UiccSlotUtil.DEFAULT_WAIT_AFTER_SWITCH_TIMEOUT_MILLIS, ) Log.d(TAG, "Start waiting, waitingTime is $waitingTimeMillis") val isTimeout = withTimeoutOrNull(waitingTimeMillis) { SubscriptionRepository(context) .isSubscriptionEnabledFlow(onboardingService.targetSubId) .firstOrNull { it } } == null Log.d( TAG, if (isTimeout) "Sim is not ready after timeout" else "Sim is ready then go to next", ) callbackListener(CallbackType.CALLBACK_SETUP_NAME) } } @Composable fun StartingDialogImpl(nextAction: () -> Unit, cancelAction: () -> Unit) { SettingsAlertDialogWithIcon( onDismissRequest = cancelAction, confirmButton = AlertDialogButton( text = getString(R.string.sim_onboarding_setup), onClick = nextAction, ), dismissButton = AlertDialogButton( text = getString(R.string.sim_onboarding_close), onClick = cancelAction, ), title = stringResource(R.string.sim_onboarding_dialog_starting_title), icon = Icons.Outlined.SignalCellularAlt, text = { Text(stringResource(R.string.sim_onboarding_dialog_starting_msg)) }, ) } fun setProgressState(state: Int) { val prefs = getSharedPreferences( SubscriptionActionDialogActivity.SIM_ACTION_DIALOG_PREFS, MODE_PRIVATE, ) prefs.edit().putInt(SubscriptionActionDialogActivity.KEY_PROGRESS_STATE, state).apply() Log.i(TAG, "setProgressState:$state") } fun initServiceData(context: Context, targetSubId: Int, callback: (CallbackType) -> Unit) { onboardingService.initData(targetSubId, context, callback) } private fun startSimOnboardingProvider() { val route = getRoute(onboardingService.targetSubId) startSpaActivity(route) } companion object { @JvmStatic fun startSimOnboardingActivity(context: Context, subId: Int, isNewTask: Boolean = false) { if (!SimRepository(context).canEnterMobileNetworkPage()) { Log.i(TAG, "Unable to start SimOnboardingActivity due to missing permissions") return } val intent = Intent(context, SimOnboardingActivity::class.java).apply { putExtra(SUB_ID, subId) if (isNewTask) { setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } } context.startActivityAsUser(intent, UserHandle.CURRENT) } var onboardingService: SimOnboardingService = SimOnboardingService() const val TAG = "SimOnboardingActivity" const val SUB_ID = "sub_id" enum class ErrorType(val value: Int) { ERROR_NONE(-1), ERROR_SIM_SWITCHING(1), ERROR_ENABLE_DSDS(2), } enum class CallbackType(val value: Int) { CALLBACK_ERROR(-1), CALLBACK_ONBOARDING_COMPLETE(1), CALLBACK_ENABLE_DSDS(2), CALLBACK_SETUP_NAME(3), CALLBACK_SETUP_PRIMARY_SIM(4), CALLBACK_FINISH(5), } } }