SIM Onboarding flow completed

- Add the setup flow for switching sim and rename and setup primary sim
  items
- Add the bottom sheet and progress dialog.
Bug: 318310357
Bug: 298898436
Bug: 298891941
Test: build pass. Will upload another cl for testing

Change-Id: Ie9680f0a67afe453c1449c0f2b59e98fd627e215
This commit is contained in:
songferngwang
2024-02-05 23:46:13 +00:00
parent 5fa73d14ab
commit f1ea484425
14 changed files with 810 additions and 193 deletions

View File

@@ -0,0 +1,430 @@
/*
* 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.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.telephony.SubscriptionManager
import android.util.Log
import android.view.MotionEvent
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SignalCellularAlt
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import com.android.settings.R
import com.android.settings.SidecarFragment
import com.android.settings.network.telephony.SubscriptionActionDialogActivity
import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
import com.android.settings.spa.network.SimOnboardingPageProvider.getRoute
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.ui.SettingsTitle
import com.android.settingslib.spaprivileged.framework.common.userManager
import kotlinx.coroutines.CoroutineScope
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.launch
class SimOnboardingActivity : SpaBaseDialogActivity() {
lateinit var scope: CoroutineScope
lateinit var showBottomSheet: MutableState<Boolean>
lateinit var showError: MutableState<Boolean>
lateinit var showDialog: MutableState<Boolean>
private var switchToEuiccSubscriptionSidecar: SwitchToEuiccSubscriptionSidecar? = null
private var switchToRemovableSlotSidecar: SwitchToRemovableSlotSidecar? = null
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)
initServiceData(this, targetSubId, callbackListener)
if (!onboardingService.isUsableTargetSubscriptionId) {
Log.e(TAG, "The subscription id is not usable.")
finish()
return
}
switchToEuiccSubscriptionSidecar = SwitchToEuiccSubscriptionSidecar.get(fragmentManager)
switchToRemovableSlotSidecar = SwitchToRemovableSlotSidecar.get(fragmentManager)
enableMultiSimSidecar = EnableMultiSimSidecar.get(fragmentManager)
setContent {
Content()
}
}
override fun finish() {
setProgressDialog(false)
onboardingService.clear()
super.finish()
}
var callbackListener: (Int) -> Unit = {
Log.d(TAG, "Receive the CALLBACK: $it")
when (it) {
CALLBACK_ERROR -> {
setProgressDialog(false)
showError.value = true
}
CALLBACK_ONBOARDING_COMPLETE -> {
showBottomSheet.value = false
setProgressDialog(true)
scope.launch {
// TODO: refactor the Sidecar
// start to activate the sim
startSimSwitching()
}
}
CALLBACK_SETUP_NAME -> {
scope.launch {
onboardingService.startSetupName()
}
}
CALLBACK_SETUP_PRIMARY_SIM -> {
scope.launch {
onboardingService.startSetupPrimarySim(this@SimOnboardingActivity)
}
}
CALLBACK_FINISH -> {
finish()
}
}
}
fun setProgressDialog(enable: Boolean) {
showDialog.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() {
showBottomSheet = remember { mutableStateOf(true) }
showError = remember { mutableStateOf(false) }
showDialog = remember { mutableStateOf(false) }
scope = rememberCoroutineScope()
registerSidecarReceiverFlow()
if(showError.value){
// show error
return
}
if (showBottomSheet.value) {
var sheetState = rememberModalBottomSheetState()
BottomSheetImpl(
sheetState = sheetState,
nextAction = {
// 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.
// case#2
val route = getRoute(onboardingService.targetSubId)
startSpaActivity(route)
},
cancelAction = { finish() },
)
} else {
ProgressDialogImpl()
}
}
@Composable
fun ProgressDialogImpl() {
// TODO: 1. Create the SPA's ProgressDialog and using SPA's widget
val dialog: ProgressDialog = object : ProgressDialog(this) {
override fun onTouchEvent(event: MotionEvent): Boolean {
return true
}
}
dialog.setMessage(
stringResource(
R.string.sim_onboarding_progressbar_turning_sim_on,
onboardingService.targetSubInfo?.displayName ?: ""
)
)
dialog.setCancelable(false)
if(showDialog.value) {
dialog.show()
}
}
@Composable
fun registerSidecarReceiverFlow(){
switchToEuiccSubscriptionSidecar?.sidecarReceiverFlow()
?.collectLatestWithLifecycle(LocalLifecycleOwner.current) {
onStateChange(it)
}
switchToRemovableSlotSidecar?.sidecarReceiverFlow()
?.collectLatestWithLifecycle(LocalLifecycleOwner.current) {
onStateChange(it)
}
enableMultiSimSidecar?.sidecarReceiverFlow()
?.collectLatestWithLifecycle(LocalLifecycleOwner.current) {
onStateChange(it)
}
}
fun SidecarFragment.sidecarReceiverFlow(): Flow<SidecarFragment> = 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
targetSubInfo?.let {
var removedSubInfo = onboardingService.getRemovedSim()
if (targetSubInfo.isEmbedded) {
switchToEuiccSubscriptionSidecar!!.run(
targetSubInfo.subscriptionId,
UiccSlotUtil.INVALID_PORT_ID,
removedSubInfo
)
return@let
}
switchToRemovableSlotSidecar!!.run(
UiccSlotUtil.INVALID_PHYSICAL_SLOT_ID,
removedSubInfo
)
} ?: run {
Log.e(TAG, "no target subInfo in onboardingService")
finish()
}
}
fun onStateChange(fragment: SidecarFragment?) {
if (fragment === switchToEuiccSubscriptionSidecar) {
handleSwitchToEuiccSubscriptionSidecarStateChange()
} else if (fragment === switchToRemovableSlotSidecar) {
handleSwitchToRemovableSlotSidecarStateChange()
} else if (fragment === enableMultiSimSidecar) {
handleEnableMultiSimSidecarStateChange()
}
}
fun handleSwitchToEuiccSubscriptionSidecarStateChange() {
when (switchToEuiccSubscriptionSidecar!!.state) {
SidecarFragment.State.SUCCESS -> {
Log.i(TAG, "Successfully enable the eSIM profile.")
switchToEuiccSubscriptionSidecar!!.reset()
callbackListener(CALLBACK_SETUP_NAME)
}
SidecarFragment.State.ERROR -> {
Log.i(TAG, "Failed to enable the eSIM profile.")
switchToEuiccSubscriptionSidecar!!.reset()
callbackListener(CALLBACK_ERROR)
// TODO: showErrorDialog and using privileged_action_disable_fail_title and
// privileged_action_disable_fail_text
}
}
}
fun handleSwitchToRemovableSlotSidecarStateChange() {
when (switchToRemovableSlotSidecar!!.state) {
SidecarFragment.State.SUCCESS -> {
Log.i(TAG, "Successfully switched to removable slot.")
switchToRemovableSlotSidecar!!.reset()
onboardingService.handleTogglePsimAction()
callbackListener(CALLBACK_SETUP_NAME)
}
SidecarFragment.State.ERROR -> {
Log.e(TAG, "Failed switching to removable slot.")
switchToRemovableSlotSidecar!!.reset()
callbackListener(CALLBACK_ERROR)
// TODO: showErrorDialog and using sim_action_enable_sim_fail_title and
// sim_action_enable_sim_fail_text
}
}
}
fun handleEnableMultiSimSidecarStateChange() {
when (enableMultiSimSidecar!!.state) {
SidecarFragment.State.SUCCESS -> {
enableMultiSimSidecar!!.reset()
Log.i(TAG, "Successfully switched to DSDS without reboot.")
handleEnableSubscriptionAfterEnablingDsds()
}
SidecarFragment.State.ERROR -> {
enableMultiSimSidecar!!.reset()
Log.i(TAG, "Failed to switch to DSDS without rebooting.")
callbackListener(CALLBACK_ERROR)
// TODO: showErrorDialog and using dsds_activation_failure_title and
// dsds_activation_failure_body_msg2
}
}
}
fun handleEnableSubscriptionAfterEnablingDsds() {
var targetSubInfo = onboardingService.targetSubInfo
if (targetSubInfo?.isEmbedded == true) {
Log.i(TAG,
"DSDS enabled, start to enable profile: " + targetSubInfo.getSubscriptionId()
)
// For eSIM operations, we simply switch to the selected eSIM profile.
switchToEuiccSubscriptionSidecar!!.run(
targetSubInfo.subscriptionId,
UiccSlotUtil.INVALID_PORT_ID,
null
)
return
}
Log.i(TAG, "DSDS enabled, start to enable pSIM profile.")
onboardingService.handleTogglePsimAction()
callbackListener(CALLBACK_FINISH)
}
@Composable
fun BottomSheetBody(nextAction: () -> Unit) {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(bottom = SettingsDimension.itemPaddingVertical)) {
Icon(
imageVector = Icons.Outlined.SignalCellularAlt,
contentDescription = null,
modifier = Modifier
.size(SettingsDimension.iconLarge),
tint = MaterialTheme.colorScheme.primary,
)
SettingsTitle(stringResource(R.string.sim_onboarding_bottomsheets_title))
Column(Modifier.padding(SettingsDimension.itemPadding)) {
Text(
text = stringResource(R.string.sim_onboarding_bottomsheets_msg),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
)
}
Button(onClick = nextAction) {
Text(stringResource(R.string.sim_onboarding_setup))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomSheetImpl(
sheetState: SheetState,
nextAction: () -> Unit,
cancelAction: () -> Unit,
) {
ModalBottomSheet(
onDismissRequest = cancelAction,
sheetState = sheetState,
) {
BottomSheetBody(nextAction = nextAction)
}
LaunchedEffect(Unit) {
sheetState.show()
}
}
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:(Int)->Unit) {
onboardingService.initData(targetSubId, context,callback)
}
companion object {
@JvmStatic
fun startSimOnboardingActivity(
context: Context,
subId: Int,
) {
val intent = Intent(context, SimOnboardingActivity::class.java).apply {
putExtra(SUB_ID, subId)
}
context.startActivity(intent)
}
var onboardingService:SimOnboardingService = SimOnboardingService()
const val TAG = "SimOnboardingActivity"
const val SUB_ID = "sub_id"
const val CALLBACK_ERROR = -1
const val CALLBACK_ONBOARDING_COMPLETE = 1
const val CALLBACK_SETUP_NAME = 2
const val CALLBACK_SETUP_PRIMARY_SIM = 3
const val CALLBACK_FINISH = 4
}
}