| /* | |
| * Copyright (C) 2026 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.security; | |
| import android.app.KeyguardManager; | |
| import android.content.Context; | |
| import android.hardware.biometrics.BiometricPrompt; | |
| import android.os.Bundle; | |
| import android.os.OutcomeReceiver; | |
| import android.security.Flags; | |
| import android.telephony.PinResult; | |
| import android.telephony.SubscriptionManager; | |
| import android.telephony.TelephonyManager; | |
| import android.util.Log; | |
| import androidx.annotation.NonNull; | |
| import androidx.annotation.Nullable; | |
| import androidx.annotation.VisibleForTesting; | |
| import androidx.preference.PreferenceScreen; | |
| import androidx.preference.SwitchPreferenceCompat; | |
| import com.android.settings.R; | |
| import com.android.settings.core.TogglePreferenceController; | |
| import com.android.settingslib.PrimarySwitchPreference; | |
| import com.google.common.util.concurrent.FutureCallback; | |
| import com.google.common.util.concurrent.Futures; | |
| import com.google.common.util.concurrent.ListenableFuture; | |
| /** | |
| * Controller for the SIM card protection preference (automatic and manual PIN management). | |
| * | |
| * It is responsible for enabling/disabling the preference (whether the SIM PIN is manually | |
| * or automatically managed), triggering the authentication dialog for enrollment/unenrollment and | |
| * reporting the correct status for the toggle. | |
| */ | |
| public class SimPinProtectionToggleController extends TogglePreferenceController implements | |
| EnterSimPinDialogFragment.SimPinEntryListener { | |
| private static final String TAG = "AutoManagedSimPin"; | |
| private static final String MANUAL_PIN_ONLY_KEY = "sim_pin_manual_management_only_toggle"; | |
| private static final String AUTO_PIN_KEY = "sim_pin_auto_management_toggle"; | |
| private KeyguardManager mKeyguardManager; | |
| public enum EnrollmentState { | |
| ENROLL_TO_MANUAL_PIN_MANAGEMENT(0), | |
| ENROLL_TO_AUTOMATIC_PIN_MANAGEMENT(1), | |
| UNENROLL_FROM_MANUAL_PIN_MANAGEMENT(2); | |
| /** Bundle extra key for the enum value. */ | |
| public static final String EXTRA_ENROLLMENT_STATE_VALUE = "enrollment_state_value"; | |
| private final int mIntValue; | |
| EnrollmentState(int intValue) { | |
| this.mIntValue = intValue; | |
| } | |
| public int getIntValue() { | |
| return mIntValue; | |
| } | |
| /** | |
| * Creates an enrollment state from a bundle. | |
| * @param args bundle with state. | |
| * @return EnrollmentState created from the int stored in the bundle. | |
| */ | |
| @NonNull | |
| public static EnrollmentState fromBundle(@Nullable Bundle args) { | |
| if (args == null) { | |
| return ENROLL_TO_MANUAL_PIN_MANAGEMENT; | |
| } | |
| int value = args.getInt(EXTRA_ENROLLMENT_STATE_VALUE, | |
| ENROLL_TO_MANUAL_PIN_MANAGEMENT.mIntValue); | |
| return fromIntValue(value); | |
| } | |
| private static EnrollmentState fromIntValue(int value) { | |
| for (EnrollmentState entry : EnrollmentState.values()) { | |
| if (entry.mIntValue == value) { | |
| return entry; | |
| } | |
| } | |
| return ENROLL_TO_MANUAL_PIN_MANAGEMENT; | |
| } | |
| /** | |
| * Stores the enum in the bundle. | |
| * @param bundle to store the enum in. | |
| */ | |
| public void storeState(@NonNull Bundle bundle) { | |
| bundle.putInt(EXTRA_ENROLLMENT_STATE_VALUE, this.mIntValue); | |
| } | |
| } | |
| private TelephonyManager mTelephonyManager; | |
| private SubscriptionManager mSubscriptionManager; | |
| private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; | |
| private EnrollmentState mEnrollmentState; | |
| // Used when android.security.Flags.enableAutoSimPinUi() is false. | |
| @Nullable | |
| private SwitchPreferenceCompat mSwitchPreference = null; | |
| // Used when android.security.Flags.enableAutoSimPinUi() is true. | |
| @Nullable | |
| private PrimarySwitchPreference mPrimarySwitchPreference = null; | |
| @Nullable | |
| private BaseSimPinFragment mFragment; | |
| private AutoManagedSimPinHelper mAutoManagedSimPinHelper; | |
| public SimPinProtectionToggleController(@NonNull Context context, | |
| @NonNull String preferenceKey) { | |
| this(context, preferenceKey, new AutoManagedSimPinHelper(context)); | |
| } | |
| @VisibleForTesting | |
| public SimPinProtectionToggleController(@NonNull Context context, | |
| @NonNull String preferenceKey, AutoManagedSimPinHelper autoManagedSimPinHelper) { | |
| super(context, preferenceKey); | |
| mTelephonyManager = context.getSystemService(TelephonyManager.class); | |
| mSubscriptionManager = context.getSystemService(SubscriptionManager.class); | |
| mAutoManagedSimPinHelper = autoManagedSimPinHelper; | |
| mKeyguardManager = mContext.getSystemService(KeyguardManager.class); | |
| mEnrollmentState = EnrollmentState.ENROLL_TO_AUTOMATIC_PIN_MANAGEMENT; | |
| } | |
| @Override | |
| public boolean isChecked() { | |
| boolean isPlatformManaged = mAutoManagedSimPinHelper.isPinAutoManagedForSubscription( | |
| mSubId); | |
| boolean isIccLockEnabled = mAutoManagedSimPinHelper.isIccLockEnabled(mSubId); | |
| return isPlatformManaged || isIccLockEnabled; | |
| } | |
| @Override | |
| public boolean setChecked(boolean isChecked) { | |
| if (isChecked) { | |
| enableManualOrAutomaticSimPin(); | |
| } else { | |
| disableManualOrAutomaticSimPin(); | |
| } | |
| // The check state changes by default, so indicate that it was changed in case the user | |
| // cancels later and it has to be un-checked. | |
| return true; | |
| } | |
| private void disableManualOrAutomaticSimPin() { | |
| if (mAutoManagedSimPinHelper.isPinAutoManagedForSubscription(mSubId)) { | |
| showAuthenticationDialogSimUnenrollment( | |
| mTelephonyManager.createForSubscriptionId(mSubId)); | |
| } else if (mAutoManagedSimPinHelper.isIccLockEnabled(mSubId)) { | |
| mEnrollmentState = EnrollmentState.UNENROLL_FROM_MANUAL_PIN_MANAGEMENT; | |
| showPinEntryDialog(); | |
| } else { | |
| Log.e(TAG, "Error: SIM PIN not automatically or manually managed."); | |
| } | |
| } | |
| private void enableManualOrAutomaticSimPin() { | |
| boolean isValidSubscription = mSubscriptionManager.isValidSubscriptionId(mSubId); | |
| boolean isEmbeddedSim = | |
| isValidSubscription && mSubscriptionManager.getActiveSubscriptionInfo( | |
| mSubId).isEmbedded(); | |
| if (isDeviceSecure() && !isEmbeddedSim && Flags.enableAutoSimPinUi()) { | |
| mEnrollmentState = EnrollmentState.ENROLL_TO_AUTOMATIC_PIN_MANAGEMENT; | |
| Log.i(TAG, "Enrolling into automatic PIN management."); | |
| showAuthenticationDialogSimEnrollment(); | |
| } else { | |
| Log.i(TAG, "Enrolling into manual PIN management."); | |
| mEnrollmentState = EnrollmentState.ENROLL_TO_MANUAL_PIN_MANAGEMENT; | |
| showPinEntryDialog(); | |
| } | |
| } | |
| @Override | |
| public int getAvailabilityStatus() { | |
| boolean isFlagEnabled = Flags.autoSimPinManagement(); | |
| if (!isFlagEnabled) { | |
| return CONDITIONALLY_UNAVAILABLE; | |
| } | |
| if (!mSubscriptionManager.isValidSubscriptionId(mSubId)) { | |
| return CONDITIONALLY_UNAVAILABLE; | |
| } | |
| // Manual PIN management only available: Show this preference as available but not | |
| // the other one. | |
| if (getPreferenceKey().startsWith(MANUAL_PIN_ONLY_KEY) && !Flags.enableAutoSimPinUi()) { | |
| return AVAILABLE; | |
| } | |
| // Automatic PIN management as well as manual PIN management: Show this preference as | |
| // available but not the other one. | |
| if (getPreferenceKey().startsWith(AUTO_PIN_KEY) && Flags.enableAutoSimPinUi()) { | |
| return AVAILABLE; | |
| } | |
| return CONDITIONALLY_UNAVAILABLE; | |
| } | |
| @Override | |
| public CharSequence getSummary() { | |
| return mContext.getResources().getString(getSummaryResId()); | |
| } | |
| private int getSummaryResId() { | |
| if (mAutoManagedSimPinHelper.isPinAutoManagedForSubscription(mSubId)) { | |
| return R.string.sim_protection_mode_protected_by_platform; | |
| } else if (mAutoManagedSimPinHelper.isIccLockEnabled(mSubId)) { | |
| return R.string.sim_protection_mode_manually_managed; | |
| } else if (!isDeviceSecure() && Flags.enableAutoSimPinUi()) { | |
| return R.string.sim_protection_mode_lskf_required; | |
| } | |
| return R.string.sim_choose_protection_mode_title; | |
| } | |
| @Override | |
| public int getSliceHighlightMenuRes() { | |
| return R.string.menu_key_security; | |
| } | |
| @Override | |
| public void displayPreference(PreferenceScreen screen) { | |
| super.displayPreference(screen); | |
| if (getPreferenceKey().startsWith(AUTO_PIN_KEY)) { | |
| mPrimarySwitchPreference = screen.findPreference(getPreferenceKey()); | |
| } else if (getPreferenceKey().startsWith(MANUAL_PIN_ONLY_KEY)) { | |
| mSwitchPreference = screen.findPreference(getPreferenceKey()); | |
| } else { | |
| Log.e(TAG, "Ambiguous preference key: " + getPreferenceKey()); | |
| return; | |
| } | |
| if (!mSubscriptionManager.isValidSubscriptionId(mSubId)) { | |
| Log.w(TAG, "invalid subscription, returning."); | |
| return; | |
| } | |
| setPreferenceSummary(getSummary()); | |
| } | |
| /** | |
| * Stores the current enrollment state in the bundle. | |
| */ | |
| void storeEnrollmentState(@NonNull Bundle bundle) { | |
| mEnrollmentState.storeState(bundle); | |
| } | |
| /** | |
| * Sets the enrollment state (in case of instance restore). | |
| * @param bundle the bundle to load the state from. | |
| */ | |
| void loadEnrollmentState(@Nullable Bundle bundle) { | |
| mEnrollmentState = EnrollmentState.fromBundle(bundle); | |
| } | |
| /** | |
| * Sets the index of the SIM card slot that this controller is responsible for. | |
| * @param slotIndex index in the array of active slots. | |
| */ | |
| public void setSlotIndex(int slotIndex) { | |
| mSubId = mAutoManagedSimPinHelper.getSubscriptionIdForSlot(slotIndex); | |
| Log.d(TAG, "Preference " + getPreferenceKey() + ": Subscription for slot index " + slotIndex | |
| + ": " + mSubId); | |
| } | |
| private boolean isDeviceSecure() { | |
| return mKeyguardManager != null ? mKeyguardManager.isDeviceSecure() : false; | |
| } | |
| public void setFragment(BaseSimPinFragment fragment) { | |
| mFragment = fragment; | |
| } | |
| private class EnrollmentResultReceiver implements | |
| OutcomeReceiver<String, TelephonyManager.SimAutoPinManagementException> { | |
| @Override | |
| public void onResult(String generatedPin) { | |
| setPreferenceState(true); | |
| DisplaySimPinDialogFragment df = DisplaySimPinDialogFragment.newInstance(true, | |
| generatedPin); | |
| if (mFragment != null) { | |
| df.showNow(mFragment.getChildFragmentManager(), "PinShow"); | |
| } | |
| } | |
| @Override | |
| public void onError( | |
| @NonNull TelephonyManager.SimAutoPinManagementException error) { | |
| OutcomeReceiver.super.onError(error); | |
| Log.w(TAG, "Error enrolling: " + error.getErrorCode()); | |
| setPreferenceState(false, mContext.getString(R.string.sim_enrollment_failed)); | |
| } | |
| } | |
| private void setPreferenceSummary(CharSequence summary) { | |
| if (mPrimarySwitchPreference != null) { | |
| mPrimarySwitchPreference.setSummary(summary); | |
| } else if (mSwitchPreference != null) { | |
| mSwitchPreference.setSummary(summary); | |
| } | |
| } | |
| private void setPreferenceState(boolean isChecked) { | |
| setPreferenceState(isChecked, null); | |
| } | |
| private void setPreferenceState(boolean isChecked, @Nullable CharSequence summary) { | |
| if (mPrimarySwitchPreference != null) { | |
| mPrimarySwitchPreference.setChecked(isChecked); | |
| mPrimarySwitchPreference.setSummary(summary); | |
| } else if (mSwitchPreference != null) { | |
| mSwitchPreference.setChecked(isChecked); | |
| mSwitchPreference.setSummary(summary); | |
| } | |
| // Required to ensure that the checked state of the preference is updated. | |
| if (mFragment != null) { | |
| mFragment.forceUpdatePreferences(); | |
| } | |
| } | |
| private class UnenrollmentResultReceiver implements | |
| OutcomeReceiver<Void, TelephonyManager.SimAutoPinManagementException> { | |
| @Override | |
| public void onResult(Void result) { | |
| setPreferenceState(false); | |
| Log.d(TAG, "Unenrollment successful."); | |
| } | |
| @Override | |
| public void onError( | |
| @NonNull TelephonyManager.SimAutoPinManagementException error) { | |
| OutcomeReceiver.super.onError(error); | |
| setPreferenceState(true); | |
| Log.w(TAG, "Error unenrolling: " + error.getErrorCode()); | |
| } | |
| } | |
| private void showPinEntryDialog(int attemptsRemaining) { | |
| showPinEntryDialog(EnterSimPinDialogFragment.newEnterCurrentPin(attemptsRemaining)); | |
| } | |
| private void showPinEntryDialog() { | |
| showPinEntryDialog(EnterSimPinDialogFragment.newEnterCurrentPin()); | |
| } | |
| private void showPinEntryDialog(EnterSimPinDialogFragment df) { | |
| if (mFragment != null) { | |
| mFragment.setCurrentListener(this); | |
| df.showNow(mFragment.getChildFragmentManager(), "CurrentPin"); | |
| } | |
| } | |
| /** | |
| * To be called by the SIM PIN entry dialog to proceed with enrollment after the user | |
| * has provided the PIN. | |
| * | |
| * @param pin The current SIM PIN as provided by the user. | |
| */ | |
| @Override | |
| public void onPinEntered(String pin) { | |
| switch (mEnrollmentState) { | |
| case ENROLL_TO_MANUAL_PIN_MANAGEMENT -> { | |
| Log.d(TAG, "Enrolling into manual PIN management mode."); | |
| ListenableFuture<PinResult> future = mAutoManagedSimPinHelper.setIccLockState( | |
| pin, /* enabled= */ true, mSubId); | |
| Futures.addCallback(future, new FutureCallback<PinResult>() { | |
| @Override | |
| public void onSuccess(PinResult result) { | |
| boolean success = result.getResult() == PinResult.PIN_RESULT_TYPE_SUCCESS; | |
| int summaryResId; | |
| if (success) { | |
| summaryResId = R.string.sim_protection_mode_manually_managed; | |
| } else { | |
| summaryResId = R.string.sim_pin_enable_failed; | |
| } | |
| setPreferenceState(success, | |
| mContext.getResources().getString(summaryResId)); | |
| if (result.getResult() == PinResult.PIN_RESULT_TYPE_INCORRECT | |
| && result.getAttemptsRemaining() > 0) { | |
| showPinEntryDialog(result.getAttemptsRemaining()); | |
| } | |
| } | |
| @Override | |
| public void onFailure(Throwable t) { | |
| setPreferenceState(false, | |
| mContext.getResources().getString(R.string.sim_pin_enable_failed)); | |
| } | |
| }, mContext.getMainExecutor()); | |
| } | |
| case ENROLL_TO_AUTOMATIC_PIN_MANAGEMENT -> { | |
| Log.d(TAG, "Enrolling into automatic PIN management mode."); | |
| TelephonyManager tm = mTelephonyManager.createForSubscriptionId(mSubId); | |
| tm.enrollSimInAutoPinManagement(pin, mContext.getMainExecutor(), | |
| new EnrollmentResultReceiver()); | |
| } | |
| case UNENROLL_FROM_MANUAL_PIN_MANAGEMENT -> { | |
| Log.d(TAG, "Unenrolling from manual PIN management mode."); | |
| ListenableFuture<PinResult> future = mAutoManagedSimPinHelper.setIccLockState( | |
| pin, /* enabled= */ false, mSubId); | |
| Futures.addCallback(future, new FutureCallback<PinResult>() { | |
| @Override | |
| public void onSuccess(PinResult result) { | |
| boolean success = result.getResult() == PinResult.PIN_RESULT_TYPE_SUCCESS; | |
| int summaryResId; | |
| if (success) { | |
| summaryResId = R.string.sim_choose_protection_mode_title; | |
| } else { | |
| summaryResId = R.string.sim_pin_disable_failed; | |
| } | |
| setPreferenceState(!success, | |
| mContext.getResources().getString(summaryResId)); | |
| if (result.getResult() == PinResult.PIN_RESULT_TYPE_INCORRECT | |
| && result.getAttemptsRemaining() > 0) { | |
| showPinEntryDialog(result.getAttemptsRemaining()); | |
| } | |
| } | |
| @Override | |
| public void onFailure(Throwable t) { | |
| setPreferenceState(true, | |
| mContext.getResources().getString(R.string.sim_pin_disable_failed)); | |
| Log.w(TAG, "Failure to set ICC lock state: " + t.getMessage()); | |
| } | |
| }, mContext.getMainExecutor()); | |
| } | |
| } | |
| } | |
| /** | |
| * To be called by the SIM PIN entry dialog when the user cancels the enrollment. | |
| */ | |
| @Override | |
| public void onEntryCancelled() { | |
| setPreferenceState(isChecked()); | |
| } | |
| private void showAuthenticationDialogSimEnrollment() { | |
| final BiometricPrompt.AuthenticationCallback authenticationCallback = | |
| new BiometricPrompt.AuthenticationCallback() { | |
| @Override | |
| public void onAuthenticationSucceeded( | |
| BiometricPrompt.AuthenticationResult result) { | |
| showPinEntryDialog(); | |
| } | |
| @Override | |
| public void onAuthenticationError(int errorCode, CharSequence errString) { | |
| Log.w(TAG, "Authentication error for enrolling, error code " + errorCode | |
| + " message: " + errString); | |
| setPreferenceState(false); | |
| } | |
| }; | |
| mAutoManagedSimPinHelper.showAuthenticationPromptForCallback(authenticationCallback); | |
| } | |
| private void showAuthenticationDialogSimUnenrollment(TelephonyManager telephonyManager) { | |
| final BiometricPrompt.AuthenticationCallback authenticationCallback = | |
| new BiometricPrompt.AuthenticationCallback() { | |
| @Override | |
| public void onAuthenticationSucceeded( | |
| BiometricPrompt.AuthenticationResult result) { | |
| telephonyManager.unenrollSimFromAutoPinManagement( | |
| mContext.getMainExecutor(), | |
| new UnenrollmentResultReceiver()); | |
| } | |
| @Override | |
| public void onAuthenticationError(int errorCode, CharSequence errString) { | |
| Log.w(TAG, "Authentication error for unenrollment, error code " | |
| + errorCode + " message: " + errString); | |
| setPreferenceState(false, getSummary()); | |
| } | |
| }; | |
| mAutoManagedSimPinHelper.showAuthenticationPromptForCallback(authenticationCallback); | |
| } | |
| } |