1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.network 18 19 import android.content.Context 20 import android.content.Intent 21 import android.os.Bundle 22 import android.provider.Settings 23 import android.telephony.SubscriptionManager 24 import android.util.Log 25 import androidx.compose.foundation.layout.Column 26 import androidx.compose.foundation.layout.Row 27 import androidx.compose.foundation.layout.fillMaxWidth 28 import androidx.compose.foundation.layout.padding 29 import androidx.compose.foundation.layout.size 30 import androidx.compose.foundation.layout.width 31 import androidx.compose.material.icons.Icons 32 import androidx.compose.material.icons.outlined.SignalCellularAlt 33 import androidx.compose.material3.AlertDialogDefaults 34 import androidx.compose.material3.BasicAlertDialog 35 import androidx.compose.material3.CircularProgressIndicator 36 import androidx.compose.material3.ExperimentalMaterial3Api 37 import androidx.compose.material3.Icon 38 import androidx.compose.material3.MaterialTheme 39 import androidx.compose.material3.Surface 40 import androidx.compose.material3.Text 41 import androidx.compose.runtime.Composable 42 import androidx.compose.runtime.LaunchedEffect 43 import androidx.compose.runtime.MutableState 44 import androidx.compose.runtime.mutableStateOf 45 import androidx.compose.runtime.rememberCoroutineScope 46 import androidx.compose.runtime.saveable.rememberSaveable 47 import androidx.compose.ui.Alignment 48 import androidx.compose.ui.Modifier 49 import androidx.compose.ui.platform.LocalContext 50 import androidx.compose.ui.platform.LocalLifecycleOwner 51 import androidx.compose.ui.res.stringResource 52 import androidx.compose.ui.text.style.TextAlign 53 import androidx.lifecycle.LifecycleRegistry 54 import com.android.settings.R 55 import com.android.settings.SidecarFragment 56 import com.android.settings.network.telephony.SubscriptionActionDialogActivity 57 import com.android.settings.network.telephony.SubscriptionRepository 58 import com.android.settings.network.telephony.ToggleSubscriptionDialogActivity 59 import com.android.settings.network.telephony.requireSubscriptionManager 60 import com.android.settings.spa.SpaActivity.Companion.startSpaActivity 61 import com.android.settings.spa.network.SimOnboardingPageProvider.getRoute 62 import com.android.settings.wifi.WifiPickerTrackerHelper 63 import com.android.settingslib.spa.SpaBaseDialogActivity 64 import com.android.settingslib.spa.framework.theme.SettingsDimension 65 import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle 66 import com.android.settingslib.spa.widget.dialog.AlertDialogButton 67 import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogWithIcon 68 import com.android.settingslib.spa.widget.dialog.getDialogWidth 69 import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter 70 import com.android.settingslib.spa.widget.ui.SettingsTitle 71 import com.android.settingslib.spaprivileged.framework.common.userManager 72 import java.util.concurrent.CountDownLatch 73 import java.util.concurrent.TimeUnit 74 import kotlinx.coroutines.CoroutineScope 75 import kotlinx.coroutines.Dispatchers 76 import kotlinx.coroutines.channels.awaitClose 77 import kotlinx.coroutines.flow.Flow 78 import kotlinx.coroutines.flow.callbackFlow 79 import kotlinx.coroutines.flow.catch 80 import kotlinx.coroutines.flow.conflate 81 import kotlinx.coroutines.launch 82 import kotlinx.coroutines.withContext 83 84 class SimOnboardingActivity : SpaBaseDialogActivity() { 85 lateinit var scope: CoroutineScope 86 lateinit var wifiPickerTrackerHelper: WifiPickerTrackerHelper 87 lateinit var context: Context 88 lateinit var showStartingDialog: MutableState<Boolean> 89 lateinit var showError: MutableState<ErrorType> 90 lateinit var showProgressDialog: MutableState<Boolean> 91 lateinit var showDsdsProgressDialog: MutableState<Boolean> 92 lateinit var showRestartDialog: MutableState<Boolean> 93 94 private var switchToEuiccSubscriptionSidecar: SwitchToEuiccSubscriptionSidecar? = null 95 private var switchToRemovableSlotSidecar: SwitchToRemovableSlotSidecar? = null 96 private var enableMultiSimSidecar: EnableMultiSimSidecar? = null 97 98 override fun onCreate(savedInstanceState: Bundle?) { 99 super.onCreate(savedInstanceState) 100 101 if (!this.userManager.isAdminUser) { 102 Log.e(TAG, "It is not the admin user. Unable to toggle subscription.") 103 finish() 104 return 105 } 106 107 var targetSubId = intent.getIntExtra(SUB_ID,SubscriptionManager.INVALID_SUBSCRIPTION_ID) 108 initServiceData(this, targetSubId, callbackListener) 109 if (!onboardingService.isUsableTargetSubscriptionId) { 110 Log.e(TAG, "The subscription id is not usable.") 111 finish() 112 return 113 } 114 115 if (onboardingService.activeSubInfoList.isEmpty()) { 116 // TODO: refactor and replace the ToggleSubscriptionDialogActivity 117 Log.d(TAG, "onboardingService.activeSubInfoList is empty" + 118 ", start ToggleSubscriptionDialogActivity") 119 this.startActivity(ToggleSubscriptionDialogActivity 120 .getIntent(this.applicationContext, targetSubId, true)) 121 finish() 122 return 123 } 124 125 switchToEuiccSubscriptionSidecar = SwitchToEuiccSubscriptionSidecar.get(fragmentManager) 126 switchToRemovableSlotSidecar = SwitchToRemovableSlotSidecar.get(fragmentManager) 127 enableMultiSimSidecar = EnableMultiSimSidecar.get(fragmentManager) 128 } 129 130 override fun finish() { 131 setProgressDialog(false) 132 onboardingService.clear() 133 super.finish() 134 } 135 136 var callbackListener: (CallbackType) -> Unit = { 137 Log.d(TAG, "Receive the CALLBACK: $it") 138 when (it) { 139 CallbackType.CALLBACK_ERROR -> { 140 setProgressDialog(false) 141 } 142 143 CallbackType.CALLBACK_ENABLE_DSDS-> { 144 scope.launch { 145 onboardingService.startEnableDsds(this@SimOnboardingActivity) 146 } 147 } 148 149 CallbackType.CALLBACK_ONBOARDING_COMPLETE -> { 150 showStartingDialog.value = false 151 setProgressDialog(true) 152 scope.launch { 153 // TODO: refactor the Sidecar 154 // start to activate the sim 155 startSimSwitching() 156 } 157 } 158 159 CallbackType.CALLBACK_SETUP_NAME -> { 160 scope.launch { 161 onboardingService.startSetupName() 162 } 163 } 164 165 CallbackType.CALLBACK_SETUP_PRIMARY_SIM -> { 166 scope.launch { 167 onboardingService.startSetupPrimarySim( 168 this@SimOnboardingActivity, 169 wifiPickerTrackerHelper 170 ) 171 } 172 } 173 174 CallbackType.CALLBACK_FINISH -> { 175 finish() 176 } 177 } 178 } 179 180 fun setProgressDialog(enable: Boolean) { 181 if (!this::showProgressDialog.isInitialized) { 182 return 183 } 184 showProgressDialog.value = enable 185 val progressState = if (enable) { 186 SubscriptionActionDialogActivity.PROGRESS_IS_SHOWING 187 } else { 188 SubscriptionActionDialogActivity.PROGRESS_IS_NOT_SHOWING 189 } 190 setProgressState(progressState) 191 } 192 193 @OptIn(ExperimentalMaterial3Api::class) 194 @Composable 195 override fun Content() { 196 showStartingDialog = rememberSaveable { mutableStateOf(false) } 197 showError = rememberSaveable { mutableStateOf(ErrorType.ERROR_NONE) } 198 showProgressDialog = rememberSaveable { mutableStateOf(false) } 199 showDsdsProgressDialog = rememberSaveable { mutableStateOf(false) } 200 showRestartDialog = rememberSaveable { mutableStateOf(false) } 201 scope = rememberCoroutineScope() 202 context = LocalContext.current 203 val lifecycleOwner = LocalLifecycleOwner.current 204 wifiPickerTrackerHelper = WifiPickerTrackerHelper( 205 LifecycleRegistry(lifecycleOwner), context, 206 null /* WifiPickerTrackerCallback */ 207 ) 208 209 registerSidecarReceiverFlow() 210 211 ErrorDialogImpl() 212 RestartDialogImpl() 213 LaunchedEffect(Unit) { 214 if (showError.value != ErrorType.ERROR_NONE 215 || showProgressDialog.value 216 || showDsdsProgressDialog.value 217 || showRestartDialog.value) { 218 Log.d(TAG, "status: showError:${showError.value}, " + 219 "showProgressDialog:${showProgressDialog.value}, " + 220 "showDsdsProgressDialog:${showDsdsProgressDialog.value}, " + 221 "showRestartDialog:${showRestartDialog.value}") 222 showStartingDialog.value = false 223 } else if (onboardingService.activeSubInfoList.isNotEmpty()) { 224 showStartingDialog.value = true 225 } 226 } 227 228 if (showStartingDialog.value) { 229 StartingDialogImpl( 230 nextAction = { 231 if (onboardingService.isDsdsConditionSatisfied()) { 232 // TODO: if the phone is SS mode and the isDsdsConditionSatisfied is true, 233 // then enable the DSDS mode. 234 // case#1: the device need the reboot after enabling DSDS. Showing the 235 // confirm dialog to user whether reboot device or not. 236 // case#2: The device don't need the reboot. Enabling DSDS and then showing 237 // the SIM onboarding UI. 238 if (onboardingService.doesSwitchMultiSimConfigTriggerReboot) { 239 // case#1 240 Log.d(TAG, "Device does not support reboot free DSDS.") 241 showRestartDialog.value = true 242 } else { 243 // case#2 244 Log.d(TAG, "Enable DSDS mode") 245 showDsdsProgressDialog.value = true 246 enableMultiSimSidecar?.run(SimOnboardingService.NUM_OF_SIMS_FOR_DSDS) 247 } 248 } else { 249 startSimOnboardingProvider() 250 } 251 }, 252 cancelAction = { finish() }, 253 ) 254 } 255 256 if (showProgressDialog.value) { 257 ProgressDialogImpl( 258 stringResource( 259 R.string.sim_onboarding_progressbar_turning_sim_on, 260 onboardingService.targetSubInfo?.displayName ?: "" 261 ) 262 ) 263 } 264 if (showDsdsProgressDialog.value) { 265 ProgressDialogImpl( 266 stringResource(R.string.sim_action_enabling_sim_without_carrier_name) 267 ) 268 } 269 } 270 @Composable 271 private fun RestartDialogImpl() { 272 val restartDialogPresenter = rememberAlertDialogPresenter( 273 confirmButton = AlertDialogButton( 274 stringResource(R.string.sim_action_reboot) 275 ) { 276 callbackListener(CallbackType.CALLBACK_ENABLE_DSDS) 277 }, 278 dismissButton = AlertDialogButton( 279 stringResource( 280 R.string.sim_action_restart_dialog_cancel, 281 onboardingService.targetSubInfo?.displayName ?: "") 282 ) { 283 callbackListener(CallbackType.CALLBACK_ONBOARDING_COMPLETE) 284 }, 285 title = stringResource(R.string.sim_action_restart_dialog_title), 286 text = { 287 Text(stringResource(R.string.sim_action_restart_dialog_msg)) 288 }, 289 ) 290 291 if(showRestartDialog.value){ 292 LaunchedEffect(Unit) { 293 restartDialogPresenter.open() 294 } 295 } 296 } 297 298 @OptIn(ExperimentalMaterial3Api::class) 299 @Composable 300 fun ProgressDialogImpl(title: String) { 301 // TODO: Create the SPA's ProgressDialog and using SPA's widget 302 BasicAlertDialog( 303 onDismissRequest = {}, 304 modifier = Modifier.width( 305 getDialogWidth() 306 ), 307 ) { 308 Surface( 309 color = AlertDialogDefaults.containerColor, 310 shape = AlertDialogDefaults.shape 311 ) { 312 Row( 313 modifier = Modifier 314 .fillMaxWidth() 315 .padding(SettingsDimension.itemPaddingStart), 316 verticalAlignment = Alignment.CenterVertically 317 ) { 318 CircularProgressIndicator() 319 Column(modifier = Modifier 320 .padding(start = SettingsDimension.itemPaddingStart)) { 321 SettingsTitle(title) 322 } 323 } 324 } 325 } 326 } 327 328 @Composable 329 fun ErrorDialogImpl(){ 330 // EuiccSlotSidecar showErrorDialog 331 val errorDialogPresenterForEuiccSlotSidecar = rememberAlertDialogPresenter( 332 confirmButton = AlertDialogButton( 333 stringResource(android.R.string.ok) 334 ) { 335 finish() 336 }, 337 title = stringResource(R.string.privileged_action_disable_fail_title), 338 text = { 339 Text(stringResource(R.string.privileged_action_disable_fail_text)) 340 }, 341 ) 342 343 // RemovableSlotSidecar showErrorDialog 344 val errorDialogPresenterForRemovableSlotSidecar = rememberAlertDialogPresenter( 345 confirmButton = AlertDialogButton( 346 stringResource(android.R.string.ok) 347 ) { 348 finish() 349 }, 350 title = stringResource(R.string.sim_action_enable_sim_fail_title), 351 text = { 352 Text(stringResource(R.string.sim_action_enable_sim_fail_text)) 353 }, 354 ) 355 356 // enableDSDS showErrorDialog 357 val errorDialogPresenterForMultiSimSidecar = rememberAlertDialogPresenter( 358 confirmButton = AlertDialogButton( 359 stringResource(android.R.string.ok) 360 ) { 361 finish() 362 }, 363 title = stringResource(R.string.dsds_activation_failure_title), 364 text = { 365 Text(stringResource(R.string.dsds_activation_failure_body_msg2)) 366 }, 367 ) 368 369 // show error 370 when (showError.value) { 371 ErrorType.ERROR_EUICC_SLOT -> errorDialogPresenterForEuiccSlotSidecar.open() 372 ErrorType.ERROR_REMOVABLE_SLOT -> errorDialogPresenterForRemovableSlotSidecar.open() 373 ErrorType.ERROR_ENABLE_DSDS -> errorDialogPresenterForMultiSimSidecar.open() 374 else -> {} 375 } 376 } 377 378 @Composable 379 fun registerSidecarReceiverFlow(){ 380 switchToEuiccSubscriptionSidecar?.sidecarReceiverFlow() 381 ?.collectLatestWithLifecycle(LocalLifecycleOwner.current) { 382 onStateChange(it) 383 } 384 switchToRemovableSlotSidecar?.sidecarReceiverFlow() 385 ?.collectLatestWithLifecycle(LocalLifecycleOwner.current) { 386 onStateChange(it) 387 } 388 enableMultiSimSidecar?.sidecarReceiverFlow() 389 ?.collectLatestWithLifecycle(LocalLifecycleOwner.current) { 390 onStateChange(it) 391 } 392 } 393 394 fun SidecarFragment.sidecarReceiverFlow(): Flow<SidecarFragment> = callbackFlow { 395 val broadcastReceiver = SidecarFragment.Listener { 396 Log.d(TAG, "onReceive: $it") 397 trySend(it) 398 } 399 addListener(broadcastReceiver) 400 401 awaitClose { removeListener(broadcastReceiver) } 402 }.catch { e -> 403 Log.e(TAG, "Error while sidecarReceiverFlow", e) 404 }.conflate() 405 406 fun startSimSwitching() { 407 Log.d(TAG, "startSimSwitching:") 408 409 var targetSubInfo = onboardingService.targetSubInfo 410 if(onboardingService.doesTargetSimActive) { 411 Log.d(TAG, "target subInfo is already active") 412 callbackListener(CallbackType.CALLBACK_SETUP_NAME) 413 return 414 } 415 targetSubInfo?.let { 416 var removedSubInfo = onboardingService.getRemovedSim() 417 if (targetSubInfo.isEmbedded) { 418 switchToEuiccSubscriptionSidecar!!.run( 419 targetSubInfo.subscriptionId, 420 UiccSlotUtil.INVALID_PORT_ID, 421 removedSubInfo 422 ) 423 return@let 424 } 425 switchToRemovableSlotSidecar!!.run( 426 UiccSlotUtil.INVALID_PHYSICAL_SLOT_ID, 427 removedSubInfo 428 ) 429 } ?: run { 430 Log.e(TAG, "no target subInfo in onboardingService") 431 finish() 432 } 433 } 434 435 fun onStateChange(fragment: SidecarFragment?) { 436 if (fragment === switchToEuiccSubscriptionSidecar) { 437 handleSwitchToEuiccSubscriptionSidecarStateChange() 438 } else if (fragment === switchToRemovableSlotSidecar) { 439 handleSwitchToRemovableSlotSidecarStateChange() 440 } else if (fragment === enableMultiSimSidecar) { 441 handleEnableMultiSimSidecarStateChange() 442 } 443 } 444 445 fun handleSwitchToEuiccSubscriptionSidecarStateChange() { 446 when (switchToEuiccSubscriptionSidecar!!.state) { 447 SidecarFragment.State.SUCCESS -> { 448 Log.i(TAG, "Successfully enable the eSIM profile.") 449 switchToEuiccSubscriptionSidecar!!.reset() 450 scope.launch { 451 checkSimIsReadyAndGoNext() 452 } 453 } 454 455 SidecarFragment.State.ERROR -> { 456 Log.i(TAG, "Failed to enable the eSIM profile.") 457 switchToEuiccSubscriptionSidecar!!.reset() 458 showError.value = ErrorType.ERROR_EUICC_SLOT 459 callbackListener(CallbackType.CALLBACK_ERROR) 460 } 461 } 462 } 463 464 fun handleSwitchToRemovableSlotSidecarStateChange() { 465 when (switchToRemovableSlotSidecar!!.state) { 466 SidecarFragment.State.SUCCESS -> { 467 Log.i(TAG, "Successfully switched to removable slot.") 468 switchToRemovableSlotSidecar!!.reset() 469 onboardingService.handleTogglePsimAction() 470 scope.launch { 471 checkSimIsReadyAndGoNext() 472 } 473 } 474 475 SidecarFragment.State.ERROR -> { 476 Log.e(TAG, "Failed switching to removable slot.") 477 switchToRemovableSlotSidecar!!.reset() 478 showError.value = ErrorType.ERROR_REMOVABLE_SLOT 479 callbackListener(CallbackType.CALLBACK_ERROR) 480 } 481 } 482 } 483 484 fun handleEnableMultiSimSidecarStateChange() { 485 showDsdsProgressDialog.value = false 486 when (enableMultiSimSidecar!!.state) { 487 SidecarFragment.State.SUCCESS -> { 488 enableMultiSimSidecar!!.reset() 489 Log.i(TAG, "Successfully switched to DSDS without reboot.") 490 // refresh data 491 initServiceData(this, onboardingService.targetSubId, callbackListener) 492 startSimOnboardingProvider() 493 } 494 495 SidecarFragment.State.ERROR -> { 496 enableMultiSimSidecar!!.reset() 497 Log.i(TAG, "Failed to switch to DSDS without rebooting.") 498 showError.value = ErrorType.ERROR_ENABLE_DSDS 499 callbackListener(CallbackType.CALLBACK_ERROR) 500 } 501 } 502 } 503 504 suspend fun checkSimIsReadyAndGoNext() { 505 withContext(Dispatchers.Default) { 506 val isEnabled = context.requireSubscriptionManager() 507 .isSubscriptionEnabled(onboardingService.targetSubId) 508 if (!isEnabled) { 509 val latch = CountDownLatch(1) 510 val receiver = CarrierConfigChangedReceiver(latch) 511 try { 512 val waitingTimeMillis = 513 Settings.Global.getLong( 514 context.contentResolver, 515 Settings.Global.EUICC_SWITCH_SLOT_TIMEOUT_MILLIS, 516 UiccSlotUtil.DEFAULT_WAIT_AFTER_SWITCH_TIMEOUT_MILLIS 517 ) 518 receiver.registerOn(context) 519 Log.d(TAG, "Start waiting, waitingTime is $waitingTimeMillis") 520 latch.await(waitingTimeMillis, TimeUnit.MILLISECONDS) 521 } catch (e: InterruptedException) { 522 Thread.currentThread().interrupt() 523 Log.e(TAG, "Failed switching to physical slot.", e) 524 } finally { 525 context.unregisterReceiver(receiver) 526 } 527 } 528 Log.d(TAG, "Sim is ready then go to next") 529 callbackListener(CallbackType.CALLBACK_SETUP_NAME) 530 } 531 } 532 533 @Composable 534 fun StartingDialogImpl( 535 nextAction: () -> Unit, 536 cancelAction: () -> Unit, 537 ) { 538 SettingsAlertDialogWithIcon( 539 onDismissRequest = cancelAction, 540 confirmButton = AlertDialogButton( 541 text = getString(R.string.sim_onboarding_setup), 542 onClick = nextAction, 543 ), 544 dismissButton = AlertDialogButton( 545 text = getString(R.string.sim_onboarding_close), 546 onClick = cancelAction, 547 ), 548 title = stringResource(R.string.sim_onboarding_dialog_starting_title), 549 icon = { 550 Icon( 551 imageVector = Icons.Outlined.SignalCellularAlt, 552 contentDescription = null, 553 modifier = Modifier 554 .size(SettingsDimension.iconLarge), 555 tint = MaterialTheme.colorScheme.primary, 556 ) 557 }, 558 text = { 559 Text( 560 stringResource(R.string.sim_onboarding_dialog_starting_msg), 561 modifier = Modifier.fillMaxWidth(), 562 textAlign = TextAlign.Center 563 ) 564 }) 565 566 } 567 568 fun setProgressState(state: Int) { 569 val prefs = getSharedPreferences( 570 SubscriptionActionDialogActivity.SIM_ACTION_DIALOG_PREFS, 571 MODE_PRIVATE 572 ) 573 prefs.edit().putInt(SubscriptionActionDialogActivity.KEY_PROGRESS_STATE, state).apply() 574 Log.i(TAG, "setProgressState:$state") 575 } 576 577 fun initServiceData(context: Context,targetSubId: Int, callback:(CallbackType)->Unit) { 578 onboardingService.initData(targetSubId, context,callback) 579 } 580 581 private fun startSimOnboardingProvider() { 582 val route = getRoute(onboardingService.targetSubId) 583 startSpaActivity(route) 584 } 585 586 companion object { 587 @JvmStatic 588 fun startSimOnboardingActivity( 589 context: Context, 590 subId: Int, 591 ) { 592 val intent = Intent(context, SimOnboardingActivity::class.java).apply { 593 putExtra(SUB_ID, subId) 594 } 595 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 596 context.startActivity(intent) 597 } 598 599 var onboardingService:SimOnboardingService = SimOnboardingService() 600 const val TAG = "SimOnboardingActivity" 601 const val SUB_ID = "sub_id" 602 603 enum class ErrorType(val value:Int){ 604 ERROR_NONE(-1), 605 ERROR_EUICC_SLOT(1), 606 ERROR_REMOVABLE_SLOT(2), 607 ERROR_ENABLE_DSDS(3) 608 } 609 610 enum class CallbackType(val value:Int){ 611 CALLBACK_ERROR(-1), 612 CALLBACK_ONBOARDING_COMPLETE(1), 613 CALLBACK_ENABLE_DSDS(2), 614 CALLBACK_SETUP_NAME(3), 615 CALLBACK_SETUP_PRIMARY_SIM(4), 616 CALLBACK_FINISH(5) 617 } 618 } 619 }