Files
app_Settings/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
Brian Lee 2eb8ed2488 Add RemoteLockscreenValidationFragment to help retain remote lockscreen
validation state.

Currently, if ConfirmDeviceCredentialBaseFragment is ever re-created due
to orientation change, screen getting turned off, etc., relevant state
gets lost. This led to the old ConfirmDeviceCredentialBaseFragment
handling results which led to issues such as lockscreen not getting set.
By addiing a retained RemoteLockscreenValidationFragment,
we're able to update the new ConfirmDeviceCredentialBaseFragment
that will handle results. We can also retain other important state like
the device credential guess to be set after successful validation.

Some smaller changes include:
* If the activity is finished for any reason other than "Back" getting
  pressed, RESULT_FIRST_USER is returned instead of RESULT_CANCELED.
* CheckBox, "Forgot [LSKF]?" button, and EditText/LockPatternView
  gets disabled during validation.
* The above also stay disabled if ConfirmDeviceCredentialBaseFragment
  gets re-created and remote lockscreen validation is still in progress.

Test: m RunSettingsRoboTests -j
ROBOTEST_FILTER=com.android.settings.password
Test: Manual
Bug: 274983372
Bug: 274991889
Bug: 274792310
Bug: 270395807

Change-Id: Ib6d47430e233a43e6985ab83abae45713c49771f
2023-04-05 16:35:25 +00:00

515 lines
22 KiB
Java

/*
* 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
*/
// TODO (b/35202196): move this class out of the root of the package.
package com.android.settings.password;
import static android.app.Activity.RESULT_FIRST_USER;
import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_LOCK_ATTEMPTS_FAILED;
import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME;
import android.annotation.Nullable;
import android.app.Dialog;
import android.app.KeyguardManager;
import android.app.RemoteLockscreenValidationSession;
import android.app.admin.DevicePolicyManager;
import android.app.admin.ManagedSubscriptionsPolicy;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.UserInfo;
import android.hardware.biometrics.BiometricManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
import android.service.remotelockscreenvalidation.RemoteLockscreenValidationClient;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.FeatureFlagUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockscreenCredential;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.InstrumentedFragment;
import com.google.android.setupdesign.GlifLayout;
/**
* Base fragment to be shared for PIN/Pattern/Password confirmation fragments.
*/
public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFragment {
public static final String TAG = ConfirmDeviceCredentialBaseFragment.class.getSimpleName();
public static final String TITLE_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.title";
public static final String HEADER_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.header";
public static final String DETAILS_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.details";
public static final String DARK_THEME = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.darkTheme";
public static final String SHOW_CANCEL_BUTTON =
SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showCancelButton";
public static final String SHOW_WHEN_LOCKED =
SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showWhenLocked";
public static final String USE_FADE_ANIMATION =
SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.useFadeAnimation";
public static final String IS_REMOTE_LOCKSCREEN_VALIDATION =
SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.isRemoteLockscreenValidation";
protected static final int USER_TYPE_PRIMARY = 1;
protected static final int USER_TYPE_MANAGED_PROFILE = 2;
protected static final int USER_TYPE_SECONDARY = 3;
/** Time we wait before clearing a wrong input attempt (e.g. pattern) and the error message. */
protected static final long CLEAR_WRONG_ATTEMPT_TIMEOUT_MS = 3000;
protected static final String FRAGMENT_TAG_REMOTE_LOCKSCREEN_VALIDATION =
"remote_lockscreen_validation";
protected boolean mReturnCredentials = false;
protected boolean mReturnGatekeeperPassword = false;
protected boolean mForceVerifyPath = false;
protected GlifLayout mGlifLayout;
protected CheckBox mCheckBox;
protected Button mCancelButton;
/** Button allowing managed profile password reset, null when is not shown. */
@Nullable protected Button mForgotButton;
protected int mEffectiveUserId;
protected int mUserId;
protected UserManager mUserManager;
protected LockPatternUtils mLockPatternUtils;
protected DevicePolicyManager mDevicePolicyManager;
protected TextView mErrorTextView;
protected final Handler mHandler = new Handler();
protected boolean mFrp;
protected boolean mRemoteValidation;
protected CharSequence mAlternateButtonText;
protected BiometricManager mBiometricManager;
@Nullable protected RemoteLockscreenValidationSession mRemoteLockscreenValidationSession;
/** Credential saved so the credential can be set for device if remote validation passes */
@Nullable protected RemoteLockscreenValidationClient mRemoteLockscreenValidationClient;
protected RemoteLockscreenValidationFragment mRemoteLockscreenValidationFragment;
private boolean isInternalActivity() {
return (getActivity() instanceof ConfirmLockPassword.InternalActivity)
|| (getActivity() instanceof ConfirmLockPattern.InternalActivity);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getActivity().getIntent();
mAlternateButtonText = intent.getCharSequenceExtra(
KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL);
mReturnCredentials = intent.getBooleanExtra(
ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, false);
mReturnGatekeeperPassword = intent.getBooleanExtra(
ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, false);
mForceVerifyPath = intent.getBooleanExtra(
ChooseLockSettingsHelper.EXTRA_KEY_FORCE_VERIFY, false);
if (intent.getBooleanExtra(IS_REMOTE_LOCKSCREEN_VALIDATION, false)) {
if (FeatureFlagUtils.isEnabled(getContext(),
FeatureFlagUtils.SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION)) {
mRemoteValidation = true;
} else {
onRemoteLockscreenValidationFailure(
"Remote lockscreen validation not enabled.");
}
}
if (mRemoteValidation) {
mRemoteLockscreenValidationSession = intent.getParcelableExtra(
KeyguardManager.EXTRA_REMOTE_LOCKSCREEN_VALIDATION_SESSION,
RemoteLockscreenValidationSession.class);
if (mRemoteLockscreenValidationSession == null
|| mRemoteLockscreenValidationSession.getRemainingAttempts() == 0) {
onRemoteLockscreenValidationFailure("RemoteLockscreenValidationSession is null or "
+ "no more attempts for remote lockscreen validation.");
}
ComponentName remoteLockscreenValidationServiceComponent =
intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName.class);
if (remoteLockscreenValidationServiceComponent == null) {
onRemoteLockscreenValidationFailure(
"RemoteLockscreenValidationService ComponentName is null");
}
mRemoteLockscreenValidationClient = RemoteLockscreenValidationClient
.create(getContext(), remoteLockscreenValidationServiceComponent);
if (!mRemoteLockscreenValidationClient.isServiceAvailable()) {
onRemoteLockscreenValidationFailure(String.format(
"RemoteLockscreenValidationService at %s is not available",
remoteLockscreenValidationServiceComponent.getClassName()));
}
mRemoteLockscreenValidationFragment =
(RemoteLockscreenValidationFragment) getFragmentManager()
.findFragmentByTag(FRAGMENT_TAG_REMOTE_LOCKSCREEN_VALIDATION);
if (mRemoteLockscreenValidationFragment == null) {
mRemoteLockscreenValidationFragment = new RemoteLockscreenValidationFragment();
getFragmentManager().beginTransaction().add(mRemoteLockscreenValidationFragment,
FRAGMENT_TAG_REMOTE_LOCKSCREEN_VALIDATION).commit();
}
}
// Only take this argument into account if it belongs to the current profile.
mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras(),
isInternalActivity());
mFrp = (mUserId == LockPatternUtils.USER_FRP);
mUserManager = UserManager.get(getActivity());
mEffectiveUserId = mUserManager.getCredentialOwnerProfile(mUserId);
mLockPatternUtils = new LockPatternUtils(getActivity());
mDevicePolicyManager = (DevicePolicyManager) getActivity().getSystemService(
Context.DEVICE_POLICY_SERVICE);
mBiometricManager = getActivity().getSystemService(BiometricManager.class);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mCancelButton = view.findViewById(R.id.cancelButton);
boolean showCancelButton = mRemoteValidation || getActivity().getIntent().getBooleanExtra(
SHOW_CANCEL_BUTTON, false);
boolean hasAlternateButton = (mFrp || mRemoteValidation) && !TextUtils.isEmpty(
mAlternateButtonText);
mCancelButton.setVisibility(showCancelButton || hasAlternateButton
? View.VISIBLE : View.GONE);
if (hasAlternateButton) {
mCancelButton.setText(mAlternateButtonText);
}
mCancelButton.setOnClickListener(v -> {
if (hasAlternateButton) {
getActivity().setResult(KeyguardManager.RESULT_ALTERNATE);
getActivity().finish();
} else if (mRemoteValidation) {
onRemoteLockscreenValidationFailure("Forgot lockscreen credential button pressed.");
}
});
setupForgotButtonIfManagedProfile(view);
mCheckBox = view.findViewById(R.id.checkbox);
if (mCheckBox != null && mRemoteValidation) {
mCheckBox.setVisibility(View.VISIBLE);
}
setupEmergencyCallButtonIfManagedSubscription(view);
}
private void setupEmergencyCallButtonIfManagedSubscription(View view) {
int policyType = getContext().getSystemService(
DevicePolicyManager.class).getManagedSubscriptionsPolicy().getPolicyType();
if (policyType == ManagedSubscriptionsPolicy.TYPE_ALL_MANAGED_SUBSCRIPTIONS) {
Button emergencyCallButton = view.findViewById(R.id.emergencyCallButton);
if (emergencyCallButton == null) {
Log.wtf(TAG,
"Emergency call button not found in managed profile credential dialog");
return;
}
emergencyCallButton.setVisibility(View.VISIBLE);
emergencyCallButton.setOnClickListener(v -> {
final Intent intent = getActivity()
.getSystemService(TelecomManager.class)
.createLaunchEmergencyDialerIntent(null)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
getActivity().startActivity(intent);
getActivity().finish();
});
}
}
private void setupForgotButtonIfManagedProfile(View view) {
if (mUserManager.isManagedProfile(mUserId)
&& mUserManager.isQuietModeEnabled(UserHandle.of(mUserId))
&& mDevicePolicyManager.canProfileOwnerResetPasswordWhenLocked(mUserId)) {
mForgotButton = view.findViewById(R.id.forgotButton);
if (mForgotButton == null) {
Log.wtf(TAG, "Forgot button not found in managed profile credential dialog");
return;
}
mForgotButton.setVisibility(View.VISIBLE);
mForgotButton.setOnClickListener(v -> {
final Intent intent = new Intent();
intent.setClassName(SETTINGS_PACKAGE_NAME, ForgotPasswordActivity.class.getName());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
getActivity().startActivity(intent);
getActivity().finish();
});
}
}
// User could be locked while Effective user is unlocked even though the effective owns the
// credential. Otherwise, fingerprint can't unlock fbe/keystore through
// verifyTiedProfileChallenge. In such case, we also wanna show the user message that
// fingerprint is disabled due to device restart.
protected boolean isStrongAuthRequired() {
return mFrp
|| !mLockPatternUtils.isBiometricAllowedForUser(mEffectiveUserId)
|| !mUserManager.isUserUnlocked(mUserId);
}
@Override
public void onResume() {
super.onResume();
refreshLockScreen();
}
protected void refreshLockScreen() {
updateErrorMessage(mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId));
}
protected void setAccessibilityTitle(CharSequence supplementalText) {
Intent intent = getActivity().getIntent();
if (intent != null) {
CharSequence titleText = intent.getCharSequenceExtra(
ConfirmDeviceCredentialBaseFragment.TITLE_TEXT);
if (supplementalText == null) {
return;
}
if (titleText == null) {
getActivity().setTitle(supplementalText);
} else {
String accessibilityTitle =
new StringBuilder(titleText).append(",").append(supplementalText).toString();
getActivity().setTitle(Utils.createAccessibleSequence(titleText, accessibilityTitle));
}
}
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onDestroy() {
if (mRemoteLockscreenValidationClient != null) {
mRemoteLockscreenValidationClient.disconnect();
}
super.onDestroy();
}
protected abstract void authenticationSucceeded();
public void prepareEnterAnimation() {
}
public void startEnterAnimation() {
}
protected void reportFailedAttempt() {
updateErrorMessage(
mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1);
mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId);
}
protected void updateErrorMessage(int numAttempts) {
final int maxAttempts =
mLockPatternUtils.getMaximumFailedPasswordsForWipe(mEffectiveUserId);
if (maxAttempts <= 0 || numAttempts <= 0) {
return;
}
// Update the on-screen error string
if (mErrorTextView != null) {
final String message = getActivity().getString(
R.string.lock_failed_attempts_before_wipe, numAttempts, maxAttempts);
showError(message, 0);
}
// Only show popup dialog before the last attempt and before wipe
final int remainingAttempts = maxAttempts - numAttempts;
if (remainingAttempts > 1) {
return;
}
final FragmentManager fragmentManager = getChildFragmentManager();
final int userType = getUserTypeForWipe();
if (remainingAttempts == 1) {
// Last try
final String title = getActivity().getString(
R.string.lock_last_attempt_before_wipe_warning_title);
final String overrideMessageId = getLastTryOverrideErrorMessageId(userType);
final int defaultMessageId = getLastTryDefaultErrorMessage(userType);
final String message = mDevicePolicyManager.getResources().getString(
overrideMessageId, () -> getString(defaultMessageId));
LastTryDialog.show(fragmentManager, title, message,
android.R.string.ok, false /* dismiss */);
} else {
// Device, profile, or secondary user is wiped
final String message = getWipeMessage(userType);
LastTryDialog.show(fragmentManager, null /* title */, message,
com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss,
true /* dismiss */);
}
}
private int getUserTypeForWipe() {
final UserInfo userToBeWiped = mUserManager.getUserInfo(
mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId));
if (userToBeWiped == null || userToBeWiped.isPrimary()) {
return USER_TYPE_PRIMARY;
} else if (userToBeWiped.isManagedProfile()) {
return USER_TYPE_MANAGED_PROFILE;
} else {
return USER_TYPE_SECONDARY;
}
}
protected abstract String getLastTryOverrideErrorMessageId(int userType);
protected abstract int getLastTryDefaultErrorMessage(int userType);
private String getWipeMessage(int userType) {
switch (userType) {
case USER_TYPE_PRIMARY:
return getString(com.android.settingslib
.R.string.failed_attempts_now_wiping_device);
case USER_TYPE_MANAGED_PROFILE:
return mDevicePolicyManager.getResources().getString(
WORK_PROFILE_LOCK_ATTEMPTS_FAILED,
() -> getString(
com.android.settingslib.R.string.failed_attempts_now_wiping_profile));
case USER_TYPE_SECONDARY:
return getString(com.android.settingslib.R.string.failed_attempts_now_wiping_user);
default:
throw new IllegalArgumentException("Unrecognized user type:" + userType);
}
}
private final Runnable mResetErrorRunnable = new Runnable() {
@Override
public void run() {
mErrorTextView.setText("");
}
};
protected void showError(CharSequence msg, long timeout) {
mErrorTextView.setText(msg);
onShowError();
mHandler.removeCallbacks(mResetErrorRunnable);
if (timeout != 0) {
mHandler.postDelayed(mResetErrorRunnable, timeout);
}
}
protected void validateGuess(LockscreenCredential credentialGuess) {
mRemoteLockscreenValidationFragment.validateLockscreenGuess(
mRemoteLockscreenValidationClient, credentialGuess,
mRemoteLockscreenValidationSession.getSourcePublicKey(), mCheckBox.isChecked());
}
protected void updateRemoteLockscreenValidationViews() {
if (!mRemoteValidation || mRemoteLockscreenValidationFragment == null) {
return;
}
boolean enable = mRemoteLockscreenValidationFragment.isRemoteValidationInProgress();
mGlifLayout.setProgressBarShown(enable);
mCheckBox.setEnabled(!enable);
mCancelButton.setEnabled(!enable);
}
/**
* Finishes the activity with result code {@link android.app.Activity#RESULT_FIRST_USER}
* after logging the error message.
* @param message Optional message to log.
*/
public void onRemoteLockscreenValidationFailure(String message) {
if (!TextUtils.isEmpty(message)) {
Log.w(TAG, message);
}
getActivity().setResult(RESULT_FIRST_USER);
getActivity().finish();
}
protected abstract void onShowError();
protected void showError(int msg, long timeout) {
showError(getText(msg), timeout);
}
public static class LastTryDialog extends DialogFragment {
private static final String TAG = LastTryDialog.class.getSimpleName();
private static final String ARG_TITLE = "title";
private static final String ARG_MESSAGE = "message";
private static final String ARG_BUTTON = "button";
private static final String ARG_DISMISS = "dismiss";
static boolean show(FragmentManager from, String title, String message, int button,
boolean dismiss) {
LastTryDialog existent = (LastTryDialog) from.findFragmentByTag(TAG);
if (existent != null && !existent.isRemoving()) {
return false;
}
Bundle args = new Bundle();
args.putString(ARG_TITLE, title);
args.putString(ARG_MESSAGE, message);
args.putInt(ARG_BUTTON, button);
args.putBoolean(ARG_DISMISS, dismiss);
DialogFragment dialog = new LastTryDialog();
dialog.setArguments(args);
dialog.show(from, TAG);
from.executePendingTransactions();
return true;
}
static void hide(FragmentManager from) {
LastTryDialog dialog = (LastTryDialog) from.findFragmentByTag(TAG);
if (dialog != null) {
dialog.dismissAllowingStateLoss();
from.executePendingTransactions();
}
}
/**
* Dialog setup.
* <p>
* To make it less likely that the dialog is dismissed accidentally, for example if the
* device is malfunctioning or if the device is in a pocket, we set
* {@code setCanceledOnTouchOutside(false)}.
*/
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = new AlertDialog.Builder(getActivity())
.setTitle(getArguments().getString(ARG_TITLE))
.setMessage(getArguments().getString(ARG_MESSAGE))
.setPositiveButton(getArguments().getInt(ARG_BUTTON), null)
.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
@Override
public void onDismiss(final DialogInterface dialog) {
super.onDismiss(dialog);
if (getActivity() != null && getArguments().getBoolean(ARG_DISMISS)) {
getActivity().finish();
}
}
}
}