Fix unable to erase eSIM

Before this change,
- eSIM will be erased twice, one with result callback and one without
  result callback.
- During reset, ResetNetworkConfirm could interrupted by subscription
  invalid event, which happens during reset.

After this change,
- eSIM will be erased only once, result callback is registered
  separately.
- Explicit exit the page when reset finish, and ignore the subscription
  invalid event after reset started.

Bug: 328293508
Flag: EXEMPT bug fix
Test: manual - dry run the reset
Test: ResetNetworkConfirmTest
Change-Id: I51395a556b1c8775192d5897a87f13046c042578
This commit is contained in:
Chaohui Wang
2024-06-24 16:52:01 +08:00
parent edc72a9b0f
commit b70c805717
8 changed files with 312 additions and 649 deletions

View File

@@ -0,0 +1,217 @@
/*
* 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.system.reset
import android.app.ProgressDialog
import android.app.settings.SettingsEnums
import android.os.Bundle
import android.os.Looper
import android.telephony.SubscriptionManager
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.android.settings.R
import com.android.settings.ResetNetworkRequest
import com.android.settings.Utils
import com.android.settings.core.InstrumentedFragment
import com.android.settings.network.ResetNetworkRestrictionViewBuilder
import com.android.settings.network.telephony.SubscriptionRepository
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Confirm and execute a reset of the network settings to a clean "just out of the box" state.
* Multiple confirmations are required: first, a general "are you sure you want to do this?" prompt,
* followed by a keyguard pattern trace if the user has defined one, followed by a final
* strongly-worded "THIS WILL RESET EVERYTHING" prompt. If at any time the phone is allowed to go to
* sleep, is locked, et cetera, then the confirmation sequence is abandoned.
*
* This is the confirmation screen.
*/
class ResetNetworkConfirm : InstrumentedFragment() {
@VisibleForTesting lateinit var resetNetworkRequest: ResetNetworkRequest
private var progressDialog: ProgressDialog? = null
private var alertDialog: AlertDialog? = null
private var resetStarted = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: $arguments")
resetNetworkRequest = ResetNetworkRequest(arguments)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = ResetNetworkRestrictionViewBuilder(requireActivity()).build()
if (view != null) {
Log.w(TAG, "Access deny.")
return view
}
return inflater.inflate(R.layout.reset_network_confirm, null).apply {
establishFinalConfirmationState()
setSubtitle()
}
}
/** Configure the UI for the final confirmation interaction */
private fun View.establishFinalConfirmationState() {
requireViewById<View>(R.id.execute_reset_network).setOnClickListener {
if (!Utils.isMonkeyRunning() && !resetStarted) {
resetStarted = true
viewLifecycleOwner.lifecycleScope.launch { onResetClicked() }
}
}
}
private fun View.setSubtitle() {
if (resetNetworkRequest.resetEsimPackageName != null) {
requireViewById<TextView>(R.id.reset_network_confirm)
.setText(R.string.reset_network_final_desc_esim)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
invalidSubIdFlow().collectLatestWithLifecycle(viewLifecycleOwner) { invalidSubId ->
// Reset process could triage this callback, so if reset has started, ignore the event.
if (!resetStarted) {
Log.w(TAG, "subId $invalidSubId no longer active.")
requireActivity().finish()
}
}
}
/**
* Monitor the sub ids in the request, if any sub id becomes inactive, the request is abandoned.
*/
private fun invalidSubIdFlow(): Flow<Int> {
val subIdsInRequest =
listOf(
resetNetworkRequest.resetTelephonyAndNetworkPolicyManager,
resetNetworkRequest.resetApnSubId,
resetNetworkRequest.resetImsSubId,
)
.distinct()
.filter(SubscriptionManager::isUsableSubscriptionId)
if (subIdsInRequest.isEmpty()) return emptyFlow()
return SubscriptionRepository(requireContext())
.activeSubscriptionIdListFlow()
.mapNotNull { activeSubIds -> subIdsInRequest.firstOrNull { it !in activeSubIds } }
.conflate()
.flowOn(Dispatchers.Default)
}
/**
* The user has gone through the multiple confirmation, so now we go ahead and reset the network
* settings to its factory-default state.
*/
@VisibleForTesting
suspend fun onResetClicked() {
showProgressDialog()
resetNetwork()
}
private fun showProgressDialog() {
progressDialog =
ProgressDialog(requireContext()).apply {
isIndeterminate = true
setCancelable(false)
setMessage(requireContext().getString(R.string.main_clear_progress_text))
show()
}
}
private fun dismissProgressDialog() {
progressDialog?.let { progressDialog ->
if (progressDialog.isShowing) {
progressDialog.dismiss()
}
}
}
/**
* Do all reset task.
*
* If error happens during erasing eSIM profiles or timeout, an error msg is shown.
*/
private suspend fun resetNetwork() {
var resetEsimSuccess = true
withContext(Dispatchers.Default) {
val builder =
resetNetworkRequest.toResetNetworkOperationBuilder(
requireContext(), Looper.getMainLooper())
resetNetworkRequest.resetEsimPackageName?.let { resetEsimPackageName ->
builder.resetEsim(resetEsimPackageName)
builder.resetEsimResultCallback { resetEsimSuccess = it }
}
builder.build().run()
}
Log.d(TAG, "network factoryReset complete. succeeded: $resetEsimSuccess")
onResetFinished(resetEsimSuccess)
}
private fun onResetFinished(resetEsimSuccess: Boolean) {
dismissProgressDialog()
val activity = requireActivity()
if (!resetEsimSuccess) {
alertDialog =
AlertDialog.Builder(activity)
.setTitle(R.string.reset_esim_error_title)
.setMessage(R.string.reset_esim_error_msg)
.setPositiveButton(android.R.string.ok, /* listener= */ null)
.show()
} else {
Toast.makeText(activity, R.string.reset_network_complete_toast, Toast.LENGTH_SHORT)
.show()
}
activity.finish()
}
override fun onDestroy() {
progressDialog?.dismiss()
alertDialog?.dismiss()
super.onDestroy()
}
override fun getMetricsCategory() = SettingsEnums.RESET_NETWORK_CONFIRM
private companion object {
const val TAG = "ResetNetworkConfirm"
}
}