/* * 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.app.Dialog; import android.app.KeyguardManager; import android.app.RemoteLockscreenValidationSession; import android.app.admin.DevicePolicyManager; import android.app.admin.ManagedSubscriptionsPolicy; import android.app.admin.flags.Flags; 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.annotation.Nullable; 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 boolean mRequestWriteRepairModePassword; protected boolean mRepairMode; 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); mRequestWriteRepairModePassword = intent.getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_WRITE_REPAIR_MODE_PW, 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); mRepairMode = (mUserId == LockPatternUtils.USER_REPAIR_MODE); 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 || mRepairMode || !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)); UserHandle primaryUser = UserHandle.SYSTEM; if (Flags.headlessSingleUserFixes()) { UserHandle mainUser = mUserManager.getMainUser(); if (mainUser != null ) { primaryUser = mainUser; } } if (userToBeWiped == null || userToBeWiped.getUserHandle().equals(primaryUser)) { 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 clearResetErrorRunnable() { mHandler.removeCallbacks(mResetErrorRunnable); } 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. *
* 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(); } } } }