Merge changes I51395a55,I63a86569 into main

* changes:
  Fix unable to erase eSIM
  SubscriptionRepository.activeSubscriptionIdListFlow
This commit is contained in:
Chaohui Wang
2024-06-26 08:59:07 +00:00
committed by Android (Google) Code Review
13 changed files with 371 additions and 690 deletions

View File

@@ -52,6 +52,7 @@ import com.android.settings.network.SubscriptionUtil;
import com.android.settings.network.telephony.EuiccRacConnectivityDialogActivity; import com.android.settings.network.telephony.EuiccRacConnectivityDialogActivity;
import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settings.password.ConfirmLockPattern; import com.android.settings.password.ConfirmLockPattern;
import com.android.settings.system.reset.ResetNetworkConfirm;
import com.android.settingslib.development.DevelopmentSettingsEnabler; import com.android.settingslib.development.DevelopmentSettingsEnabler;
import java.util.ArrayList; import java.util.ArrayList;

View File

@@ -1,247 +0,0 @@
/*
* Copyright (C) 2015 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;
import android.app.Activity;
import android.app.ProgressDialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Looper;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import com.android.settings.core.InstrumentedFragment;
import com.android.settings.network.ResetNetworkOperationBuilder;
import com.android.settings.network.ResetNetworkRestrictionViewBuilder;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 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.
*/
public class ResetNetworkConfirm extends InstrumentedFragment {
private static final String TAG = "ResetNetworkConfirm";
@VisibleForTesting View mContentView;
@VisibleForTesting ResetNetworkTask mResetNetworkTask;
@VisibleForTesting Activity mActivity;
@VisibleForTesting ResetNetworkRequest mResetNetworkRequest;
private ProgressDialog mProgressDialog;
private AlertDialog mAlertDialog;
@VisibleForTesting ResetSubscriptionContract mResetSubscriptionContract;
private OnSubscriptionsChangedListener mSubscriptionsChangedListener;
/**
* Async task used to do all reset task. If error happens during
* erasing eSIM profiles or timeout, an error msg is shown.
*/
private class ResetNetworkTask extends AsyncTask<Void, Void, Boolean> {
private static final String TAG = "ResetNetworkTask";
private final Context mContext;
ResetNetworkTask(Context context) {
mContext = context;
}
@Override
protected Boolean doInBackground(Void... params) {
final AtomicBoolean resetEsimSuccess = new AtomicBoolean(true);
String resetEsimPackageName = mResetNetworkRequest.getResetEsimPackageName();
ResetNetworkOperationBuilder builder = mResetNetworkRequest
.toResetNetworkOperationBuilder(mContext, Looper.getMainLooper());
if (resetEsimPackageName != null) {
// Override reset eSIM option for the result of reset operation
builder = builder.resetEsim(resetEsimPackageName,
success -> { resetEsimSuccess.set(success); }
);
}
builder.build().run();
boolean isResetSucceed = resetEsimSuccess.get();
Log.d(TAG, "network factoryReset complete. succeeded: "
+ String.valueOf(isResetSucceed));
return isResetSucceed;
}
@Override
protected void onPostExecute(Boolean succeeded) {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
if (succeeded) {
Toast.makeText(mContext, R.string.reset_network_complete_toast, Toast.LENGTH_SHORT)
.show();
} else {
mAlertDialog = new AlertDialog.Builder(mContext)
.setTitle(R.string.reset_esim_error_title)
.setMessage(R.string.reset_esim_error_msg)
.setPositiveButton(android.R.string.ok, null /* listener */)
.show();
}
}
}
/**
* The user has gone through the multiple confirmation, so now we go ahead
* and reset the network settings to its factory-default state.
*/
@VisibleForTesting
Button.OnClickListener mFinalClickListener = new Button.OnClickListener() {
@Override
public void onClick(View v) {
if (Utils.isMonkeyRunning()) {
return;
}
// abandon execution if subscription no longer active
Integer subId = mResetSubscriptionContract.getAnyMissingSubscriptionId();
if (subId != null) {
Log.w(TAG, "subId " + subId + " no longer active");
getActivity().onBackPressed();
return;
}
// Should dismiss the progress dialog firstly if it is showing
// Or not the progress dialog maybe not dismissed in fast clicking.
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
mProgressDialog = getProgressDialog(mActivity);
mProgressDialog.show();
mResetNetworkTask = new ResetNetworkTask(mActivity);
mResetNetworkTask.execute();
}
};
private ProgressDialog getProgressDialog(Context context) {
final ProgressDialog progressDialog = new ProgressDialog(context);
progressDialog.setIndeterminate(true);
progressDialog.setCancelable(false);
progressDialog.setMessage(
context.getString(R.string.main_clear_progress_text));
return progressDialog;
}
/**
* Configure the UI for the final confirmation interaction
*/
private void establishFinalConfirmationState() {
mContentView.findViewById(R.id.execute_reset_network)
.setOnClickListener(mFinalClickListener);
}
@VisibleForTesting
void setSubtitle() {
if (mResetNetworkRequest.getResetEsimPackageName() != null) {
((TextView) mContentView.findViewById(R.id.reset_network_confirm))
.setText(R.string.reset_network_final_desc_esim);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = (new ResetNetworkRestrictionViewBuilder(mActivity)).build();
if (view != null) {
mResetSubscriptionContract.close();
Log.w(TAG, "Access deny.");
return view;
}
mContentView = inflater.inflate(R.layout.reset_network_confirm, null);
establishFinalConfirmationState();
setSubtitle();
return mContentView;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args == null) {
args = savedInstanceState;
}
mResetNetworkRequest = new ResetNetworkRequest(args);
mActivity = getActivity();
mResetSubscriptionContract = new ResetSubscriptionContract(getContext(),
mResetNetworkRequest) {
@Override
public void onSubscriptionInactive(int subscriptionId) {
// close UI if subscription no longer active
Log.w(TAG, "subId " + subscriptionId + " no longer active.");
getActivity().onBackPressed();
}
};
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mResetNetworkRequest.writeIntoBundle(outState);
}
@Override
public void onDestroy() {
if (mResetNetworkTask != null) {
mResetNetworkTask.cancel(true /* mayInterruptIfRunning */);
mResetNetworkTask = null;
}
if (mResetSubscriptionContract != null) {
mResetSubscriptionContract.close();
mResetSubscriptionContract = null;
}
if (mProgressDialog != null) {
mProgressDialog.dismiss();
}
if (mAlertDialog != null) {
mAlertDialog.dismiss();
}
super.onDestroy();
}
@Override
public int getMetricsCategory() {
return SettingsEnums.RESET_NETWORK_CONFIRM;
}
}

View File

@@ -1,157 +0,0 @@
/*
* Copyright (C) 2022 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;
import android.content.Context;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.IntStream;
/**
* A Class monitoring the availability of subscription IDs provided within reset request.
*
* This is to detect the situation when user changing SIM card during the presenting of
* confirmation UI.
*/
public class ResetSubscriptionContract implements AutoCloseable {
private static final String TAG = "ResetSubscriptionContract";
private final Context mContext;
private ExecutorService mExecutorService;
private final int [] mResetSubscriptionIds;
@VisibleForTesting
protected OnSubscriptionsChangedListener mSubscriptionsChangedListener;
private AtomicBoolean mSubscriptionsUpdateNotify = new AtomicBoolean();
/**
* Constructor
* @param context Context
* @param resetRequest the request object for perform network reset operation.
*/
public ResetSubscriptionContract(Context context, ResetNetworkRequest resetRequest) {
mContext = context;
// Only keeps specific subscription ID required to perform reset operation
IntStream subIdStream = IntStream.of(
resetRequest.getResetTelephonyAndNetworkPolicyManager(),
resetRequest.getResetApnSubId(), resetRequest.getResetImsSubId());
mResetSubscriptionIds = subIdStream.sorted().distinct()
.filter(id -> SubscriptionManager.isUsableSubscriptionId(id))
.toArray();
if (mResetSubscriptionIds.length <= 0) {
return;
}
// Monitoring callback through background thread
mExecutorService = Executors.newSingleThreadExecutor();
startMonitorSubscriptionChange();
}
/**
* A method for detecting if there's any subscription under monitor no longer active.
* @return subscription ID which is no longer active.
*/
public Integer getAnyMissingSubscriptionId() {
if (mResetSubscriptionIds.length <= 0) {
return null;
}
SubscriptionManager mgr = getSubscriptionManager();
if (mgr == null) {
Log.w(TAG, "Fail to access subscription manager");
return mResetSubscriptionIds[0];
}
for (int idx = 0; idx < mResetSubscriptionIds.length; idx++) {
int subId = mResetSubscriptionIds[idx];
if (mgr.getActiveSubscriptionInfo(subId) == null) {
Log.w(TAG, "SubId " + subId + " no longer active.");
return subId;
}
}
return null;
}
/**
* Async callback when detecting if there's any subscription under monitor no longer active.
* @param subscriptionId subscription ID which is no longer active.
*/
public void onSubscriptionInactive(int subscriptionId) {}
@VisibleForTesting
protected SubscriptionManager getSubscriptionManager() {
return mContext.getSystemService(SubscriptionManager.class);
}
@VisibleForTesting
protected OnSubscriptionsChangedListener getChangeListener() {
return new OnSubscriptionsChangedListener() {
@Override
public void onSubscriptionsChanged() {
/**
* Reducing the processing time on main UI thread through a flag.
* Once flag get into false, which means latest callback has been
* processed.
*/
mSubscriptionsUpdateNotify.set(true);
// Back to main UI thread
mContext.getMainExecutor().execute(() -> {
// Remove notifications and perform checking.
if (mSubscriptionsUpdateNotify.getAndSet(false)) {
Integer subId = getAnyMissingSubscriptionId();
if (subId != null) {
onSubscriptionInactive(subId);
}
}
});
}
};
}
private void startMonitorSubscriptionChange() {
SubscriptionManager mgr = getSubscriptionManager();
if (mgr == null) {
return;
}
// update monitor listener
mSubscriptionsChangedListener = getChangeListener();
mgr.addOnSubscriptionsChangedListener(
mExecutorService, mSubscriptionsChangedListener);
}
// Implementation of AutoCloseable
public void close() {
if (mExecutorService == null) {
return;
}
// Stop monitoring subscription change
SubscriptionManager mgr = getSubscriptionManager();
if (mgr != null) {
mgr.removeOnSubscriptionsChangedListener(mSubscriptionsChangedListener);
}
// Release Executor
mExecutorService.shutdownNow();
mExecutorService = null;
}
}

View File

@@ -65,6 +65,8 @@ public class ResetNetworkOperationBuilder {
private Context mContext; private Context mContext;
private List<Runnable> mResetSequence = new ArrayList<Runnable>(); private List<Runnable> mResetSequence = new ArrayList<Runnable>();
@Nullable
private Consumer<Boolean> mResetEsimResultCallback = null;
/** /**
* Constructor of builder. * Constructor of builder.
@@ -129,31 +131,32 @@ public class ResetNetworkOperationBuilder {
} }
/** /**
* Append a step of resetting E-SIM. * Append a result callback of resetting E-SIM.
* @param callerPackage package name of caller * @param resultCallback a callback dealing with result of resetting eSIM
* @return this * @return this
*/ */
public ResetNetworkOperationBuilder resetEsim(String callerPackage) { public ResetNetworkOperationBuilder resetEsimResultCallback(Consumer<Boolean> resultCallback) {
resetEsim(callerPackage, null); mResetEsimResultCallback = resultCallback;
return this; return this;
} }
/** /**
* Append a step of resetting E-SIM. * Append a step of resetting E-SIM.
* @param callerPackage package name of caller * @param callerPackage package name of caller
* @param resultCallback a Consumer<Boolean> dealing with result of resetting eSIM
* @return this * @return this
*/ */
public ResetNetworkOperationBuilder resetEsim(String callerPackage, public ResetNetworkOperationBuilder resetEsim(String callerPackage) {
Consumer<Boolean> resultCallback) {
Runnable runnable = () -> { Runnable runnable = () -> {
long startTime = SystemClock.elapsedRealtime(); long startTime = SystemClock.elapsedRealtime();
if (!DRY_RUN) { boolean wipped;
Boolean wipped = RecoverySystem.wipeEuiccData(mContext, callerPackage); if (DRY_RUN) {
if (resultCallback != null) { wipped = true;
resultCallback.accept(wipped); } else {
wipped = RecoverySystem.wipeEuiccData(mContext, callerPackage);
} }
if (mResetEsimResultCallback != null) {
mResetEsimResultCallback.accept(wipped);
} }
long endTime = SystemClock.elapsedRealtime(); long endTime = SystemClock.elapsedRealtime();

View File

@@ -25,14 +25,17 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class CallStateRepository(private val context: Context) { class CallStateRepository(
private val subscriptionManager = context.requireSubscriptionManager() private val context: Context,
private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context),
) {
/** Flow for call state of given [subId]. */ /** Flow for call state of given [subId]. */
fun callStateFlow(subId: Int): Flow<Int> = context.telephonyCallbackFlow(subId) { fun callStateFlow(subId: Int): Flow<Int> = context.telephonyCallbackFlow(subId) {
@@ -48,9 +51,8 @@ class CallStateRepository(private val context: Context) {
* *
* @return true if any active subscription's call state is not idle. * @return true if any active subscription's call state is not idle.
*/ */
fun isInCallFlow(): Flow<Boolean> = context.subscriptionsChangedFlow() fun isInCallFlow(): Flow<Boolean> = subscriptionRepository.activeSubscriptionIdListFlow()
.flatMapLatest { .flatMapLatest { subIds ->
val subIds = subscriptionManager.activeSubscriptionIdList
if (subIds.isEmpty()) { if (subIds.isEmpty()) {
flowOf(false) flowOf(false)
} else { } else {
@@ -59,9 +61,10 @@ class CallStateRepository(private val context: Context) {
} }
} }
} }
.distinctUntilChanged()
.conflate() .conflate()
.flowOn(Dispatchers.Default)
.onEach { Log.d(TAG, "isInCallFlow: $it") } .onEach { Log.d(TAG, "isInCallFlow: $it") }
.flowOn(Dispatchers.Default)
private companion object { private companion object {
private const val TAG = "CallStateRepository" private const val TAG = "CallStateRepository"

View File

@@ -29,6 +29,7 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -68,20 +69,9 @@ class SubscriptionRepository(private val context: Context) {
} }
fun canDisablePhysicalSubscription() = subscriptionManager.canDisablePhysicalSubscription() fun canDisablePhysicalSubscription() = subscriptionManager.canDisablePhysicalSubscription()
}
val Context.subscriptionManager: SubscriptionManager?
get() = getSystemService(SubscriptionManager::class.java)
fun Context.requireSubscriptionManager(): SubscriptionManager = subscriptionManager!!
fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo) = subscriptionsChangedFlow().map {
SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo)
}.filterNot { it.isNullOrEmpty() }.flowOn(Dispatchers.Default)
fun Context.subscriptionsChangedFlow() = callbackFlow {
val subscriptionManager = requireSubscriptionManager()
/** Flow for subscriptions changes. */
fun subscriptionsChangedFlow() = callbackFlow {
val listener = object : SubscriptionManager.OnSubscriptionsChangedListener() { val listener = object : SubscriptionManager.OnSubscriptionsChangedListener() {
override fun onSubscriptionsChanged() { override fun onSubscriptionsChanged() {
trySend(Unit) trySend(Unit)
@@ -94,7 +84,28 @@ fun Context.subscriptionsChangedFlow() = callbackFlow {
) )
awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(listener) } awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(listener) }
}.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default) }.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default)
/** Flow of active subscription ids. */
fun activeSubscriptionIdListFlow(): Flow<List<Int>> = context.subscriptionsChangedFlow()
.map { subscriptionManager.activeSubscriptionIdList.sorted() }
.distinctUntilChanged()
.conflate()
.onEach { Log.d(TAG, "activeSubscriptionIdList: $it") }
.flowOn(Dispatchers.Default)
}
val Context.subscriptionManager: SubscriptionManager?
get() = getSystemService(SubscriptionManager::class.java)
fun Context.requireSubscriptionManager(): SubscriptionManager = subscriptionManager!!
fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo) = subscriptionsChangedFlow().map {
SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo)
}.filterNot { it.isNullOrEmpty() }.flowOn(Dispatchers.Default)
fun Context.subscriptionsChangedFlow(): Flow<Unit> =
SubscriptionRepository(this).subscriptionsChangedFlow()
/** /**
* Return a list of subscriptions that are available and visible to the user. * Return a list of subscriptions that are available and visible to the user.

View File

@@ -25,10 +25,9 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.settings.R import com.android.settings.R
import com.android.settings.network.telephony.MobileDataRepository import com.android.settings.network.telephony.MobileDataRepository
import com.android.settings.network.telephony.SubscriptionRepository
import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
import com.android.settings.network.telephony.requireSubscriptionManager
import com.android.settings.network.telephony.safeGetConfig import com.android.settings.network.telephony.safeGetConfig
import com.android.settings.network.telephony.subscriptionsChangedFlow
import com.android.settings.network.telephony.telephonyManager import com.android.settings.network.telephony.telephonyManager
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -48,7 +47,7 @@ class CrossSimCallingViewModel(
private val application: Application, private val application: Application,
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {
private val subscriptionManager = application.requireSubscriptionManager() private val subscriptionRepository = SubscriptionRepository(application)
private val carrierConfigManager = private val carrierConfigManager =
application.getSystemService(CarrierConfigManager::class.java)!! application.getSystemService(CarrierConfigManager::class.java)!!
private val scope = viewModelScope + Dispatchers.Default private val scope = viewModelScope + Dispatchers.Default
@@ -59,9 +58,8 @@ class CrossSimCallingViewModel(
init { init {
val resources = application.resources val resources = application.resources
if (resources.getBoolean(R.bool.config_auto_data_switch_enables_cross_sim_calling)) { if (resources.getBoolean(R.bool.config_auto_data_switch_enables_cross_sim_calling)) {
application.subscriptionsChangedFlow() subscriptionRepository.activeSubscriptionIdListFlow()
.flatMapLatest { .flatMapLatest { activeSubIds ->
val activeSubIds = subscriptionManager.activeSubscriptionIdList.toList()
merge( merge(
activeSubIds.anyMobileDataEnableChangedFlow(), activeSubIds.anyMobileDataEnableChangedFlow(),
updateChannel.receiveAsFlow(), updateChannel.receiveAsFlow(),

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"
}
}

View File

@@ -1,124 +0,0 @@
/*
* Copyright (C) 2018 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;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.spy;
import android.view.LayoutInflater;
import android.widget.TextView;
import androidx.fragment.app.FragmentActivity;
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
import com.android.settings.testutils.shadow.ShadowRecoverySystem;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.util.concurrent.PausedExecutorService;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowPausedAsyncTask;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowRecoverySystem.class, ShadowBluetoothAdapter.class})
public class ResetNetworkConfirmTest {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
private static final String TEST_PACKAGE = "com.android.settings";
private FragmentActivity mActivity;
@Mock
private ResetNetworkConfirm mResetNetworkConfirm;
private PausedExecutorService mExecutorService;
@Before
public void setUp() {
mExecutorService = new PausedExecutorService();
ShadowPausedAsyncTask.overrideExecutor(mExecutorService);
mResetNetworkConfirm = new ResetNetworkConfirm();
mActivity = spy(Robolectric.setupActivity(FragmentActivity.class));
mResetNetworkConfirm.mActivity = mActivity;
}
@After
public void tearDown() {
ShadowRecoverySystem.reset();
}
@Test
public void testResetNetworkData_notResetEsim() {
mResetNetworkConfirm.mResetNetworkRequest =
new ResetNetworkRequest(ResetNetworkRequest.RESET_NONE);
mResetNetworkConfirm.mResetSubscriptionContract =
new ResetSubscriptionContract(mActivity,
mResetNetworkConfirm.mResetNetworkRequest) {
@Override
public void onSubscriptionInactive(int subscriptionId) {
mActivity.onBackPressed();
}
};
mResetNetworkConfirm.mFinalClickListener.onClick(null /* View */);
mExecutorService.runAll();
ShadowLooper.idleMainLooper();
assertThat(ShadowRecoverySystem.getWipeEuiccCalledCount()).isEqualTo(0);
}
@Test
public void setSubtitle_eraseEsim() {
mResetNetworkConfirm.mResetNetworkRequest =
new ResetNetworkRequest(ResetNetworkRequest.RESET_NONE);
mResetNetworkConfirm.mResetNetworkRequest.setResetEsim(TEST_PACKAGE);
mResetNetworkConfirm.mContentView =
LayoutInflater.from(mActivity).inflate(R.layout.reset_network_confirm, null);
mResetNetworkConfirm.setSubtitle();
assertThat(((TextView) mResetNetworkConfirm.mContentView
.findViewById(R.id.reset_network_confirm)).getText())
.isEqualTo(mActivity.getString(R.string.reset_network_final_desc_esim));
}
@Test
public void setSubtitle_notEraseEsim() {
mResetNetworkConfirm.mResetNetworkRequest =
new ResetNetworkRequest(ResetNetworkRequest.RESET_NONE);
mResetNetworkConfirm.mContentView =
LayoutInflater.from(mActivity).inflate(R.layout.reset_network_confirm, null);
mResetNetworkConfirm.setSubtitle();
assertThat(((TextView) mResetNetworkConfirm.mContentView
.findViewById(R.id.reset_network_confirm)).getText())
.isEqualTo(mActivity.getString(R.string.reset_network_final_desc));
}
}

View File

@@ -17,7 +17,6 @@
package com.android.settings.network.telephony package com.android.settings.network.telephony
import android.content.Context import android.content.Context
import android.telephony.SubscriptionManager
import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
@@ -27,6 +26,7 @@ import com.android.settingslib.spa.testutils.toListWithTimeout
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -49,20 +49,15 @@ class CallStateRepositoryTest {
} }
} }
private val mockSubscriptionManager = mock<SubscriptionManager> { private val mockSubscriptionRepository = mock<SubscriptionRepository> {
on { activeSubscriptionIdList } doReturn intArrayOf(SUB_ID) on { activeSubscriptionIdListFlow() } doReturn flowOf(listOf(SUB_ID))
on { addOnSubscriptionsChangedListener(any(), any()) } doAnswer {
val listener = it.arguments[1] as SubscriptionManager.OnSubscriptionsChangedListener
listener.onSubscriptionsChanged()
}
} }
private val context: Context = spy(ApplicationProvider.getApplicationContext()) { private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager
on { subscriptionManager } doReturn mockSubscriptionManager
} }
private val repository = CallStateRepository(context) private val repository = CallStateRepository(context, mockSubscriptionRepository)
@Test @Test
fun callStateFlow_initial_sendInitialState() = runBlocking { fun callStateFlow_initial_sendInitialState() = runBlocking {
@@ -89,8 +84,8 @@ class CallStateRepositoryTest {
@Test @Test
fun isInCallFlow_noActiveSubscription() = runBlocking { fun isInCallFlow_noActiveSubscription() = runBlocking {
mockSubscriptionManager.stub { mockSubscriptionRepository.stub {
on { activeSubscriptionIdList } doReturn intArrayOf() on { activeSubscriptionIdListFlow() } doReturn flowOf(emptyList())
} }
val isInCall = repository.isInCallFlow().firstWithTimeoutOrNull() val isInCall = repository.isInCallFlow().firstWithTimeoutOrNull()

View File

@@ -77,7 +77,7 @@ class SubscriptionRepositoryTest {
@Test @Test
fun subscriptionsChangedFlow_hasInitialValue() = runBlocking { fun subscriptionsChangedFlow_hasInitialValue() = runBlocking {
val initialValue = context.subscriptionsChangedFlow().firstWithTimeoutOrNull() val initialValue = repository.subscriptionsChangedFlow().firstWithTimeoutOrNull()
assertThat(initialValue).isSameInstanceAs(Unit) assertThat(initialValue).isSameInstanceAs(Unit)
} }
@@ -85,7 +85,7 @@ class SubscriptionRepositoryTest {
@Test @Test
fun subscriptionsChangedFlow_changed() = runBlocking { fun subscriptionsChangedFlow_changed() = runBlocking {
val listDeferred = async { val listDeferred = async {
context.subscriptionsChangedFlow().toListWithTimeout() repository.subscriptionsChangedFlow().toListWithTimeout()
} }
delay(100) delay(100)
@@ -94,6 +94,17 @@ class SubscriptionRepositoryTest {
assertThat(listDeferred.await()).hasSize(2) assertThat(listDeferred.await()).hasSize(2)
} }
@Test
fun activeSubscriptionIdListFlow(): Unit = runBlocking {
mockSubscriptionManager.stub {
on { activeSubscriptionIdList } doReturn intArrayOf(SUB_ID_IN_SLOT_0)
}
val activeSubIds = repository.activeSubscriptionIdListFlow().firstWithTimeoutOrNull()
assertThat(activeSubIds).containsExactly(SUB_ID_IN_SLOT_0)
}
@Test @Test
fun getSelectableSubscriptionInfoList_sortedBySimSlotIndex() { fun getSelectableSubscriptionInfoList_sortedBySimSlotIndex() {
mockSubscriptionManager.stub { mockSubscriptionManager.stub {

View File

@@ -0,0 +1,79 @@
/*
* 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.content.Context
import android.view.LayoutInflater
import android.widget.TextView
import androidx.fragment.app.testing.launchFragment
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.ResetNetworkRequest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.never
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class ResetNetworkConfirmTest {
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {}
private val scenario = launchFragment<ResetNetworkConfirm>()
@Test
fun resetNetworkData_notResetEsim() {
scenario.recreate().onFragment { fragment ->
fragment.resetNetworkRequest = ResetNetworkRequest(ResetNetworkRequest.RESET_NONE)
runBlocking { fragment.onResetClicked() }
verify(context, never()).getSystemService(any())
}
}
@Test
fun setSubtitle_eraseEsim() {
scenario.onFragment { fragment ->
fragment.resetNetworkRequest =
ResetNetworkRequest(ResetNetworkRequest.RESET_NONE).apply {
setResetEsim(context.packageName)
}
val view = fragment.onCreateView(LayoutInflater.from(context), null, null)
assertThat(view.requireViewById<TextView>(R.id.reset_network_confirm).text)
.isEqualTo(context.getString(R.string.reset_network_final_desc_esim))
}
}
@Test
fun setSubtitle_notEraseEsim() {
scenario.onFragment { fragment ->
fragment.resetNetworkRequest = ResetNetworkRequest(ResetNetworkRequest.RESET_NONE)
val view = fragment.onCreateView(LayoutInflater.from(context), null, null)
assertThat(view.requireViewById<TextView>(R.id.reset_network_confirm).text)
.isEqualTo(context.getString(R.string.reset_network_final_desc))
}
}
}

View File

@@ -1,109 +0,0 @@
/*
* Copyright (C) 2022 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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.os.Bundle;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
public class ResetSubscriptionContractTest {
private static final int SUB_ID_1 = 3;
private static final int SUB_ID_2 = 8;
@Mock
private SubscriptionManager mSubscriptionManager;
@Mock
private OnSubscriptionsChangedListener mOnSubscriptionsChangedListener;
@Mock
private SubscriptionInfo mSubscriptionInfo1;
@Mock
private SubscriptionInfo mSubscriptionInfo2;
private Context mContext;
private ResetNetworkRequest mRequestArgs;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(ApplicationProvider.getApplicationContext());
mRequestArgs = new ResetNetworkRequest(new Bundle());
}
private ResetSubscriptionContract createTestObject() {
return new ResetSubscriptionContract(mContext, mRequestArgs) {
@Override
protected SubscriptionManager getSubscriptionManager() {
return mSubscriptionManager;
}
@Override
protected OnSubscriptionsChangedListener getChangeListener() {
return mOnSubscriptionsChangedListener;
}
};
}
@Test
public void getAnyMissingSubscriptionId_returnNull_whenNoSubscriptionChange() {
mRequestArgs.setResetTelephonyAndNetworkPolicyManager(SUB_ID_1);
doReturn(mSubscriptionInfo1).when(mSubscriptionManager)
.getActiveSubscriptionInfo(SUB_ID_1);
mRequestArgs.setResetApn(SUB_ID_2);
doReturn(mSubscriptionInfo2).when(mSubscriptionManager)
.getActiveSubscriptionInfo(SUB_ID_2);
ResetSubscriptionContract target = createTestObject();
verify(mSubscriptionManager).addOnSubscriptionsChangedListener(any(), any());
assertNull(target.getAnyMissingSubscriptionId());
}
@Test
public void getAnyMissingSubscriptionId_returnSubId_whenSubscriptionNotActive() {
mRequestArgs.setResetTelephonyAndNetworkPolicyManager(SUB_ID_1);
doReturn(mSubscriptionInfo1).when(mSubscriptionManager)
.getActiveSubscriptionInfo(SUB_ID_1);
mRequestArgs.setResetApn(SUB_ID_2);
doReturn(null).when(mSubscriptionManager)
.getActiveSubscriptionInfo(SUB_ID_2);
ResetSubscriptionContract target = createTestObject();
verify(mSubscriptionManager).addOnSubscriptionsChangedListener(any(), any());
assertEquals(target.getAnyMissingSubscriptionId(), new Integer(SUB_ID_2));
}
}