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:
430
src/com/android/settings/network/SimOnboardingActivity.kt
Normal file
430
src/com/android/settings/network/SimOnboardingActivity.kt
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user