1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 // TODO (b/35202196): move this class out of the root of the package. 18 package com.android.settings.password; 19 20 import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; 21 22 import android.annotation.Nullable; 23 import android.app.Dialog; 24 import android.app.KeyguardManager; 25 import android.app.admin.DevicePolicyManager; 26 import android.content.Context; 27 import android.content.DialogInterface; 28 import android.content.Intent; 29 import android.content.pm.UserInfo; 30 import android.hardware.biometrics.BiometricManager; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.UserHandle; 34 import android.os.UserManager; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.view.View; 38 import android.widget.Button; 39 import android.widget.TextView; 40 41 import androidx.appcompat.app.AlertDialog; 42 import androidx.fragment.app.DialogFragment; 43 import androidx.fragment.app.FragmentManager; 44 45 import com.android.internal.widget.LockPatternUtils; 46 import com.android.settings.R; 47 import com.android.settings.Utils; 48 import com.android.settings.core.InstrumentedFragment; 49 50 /** 51 * Base fragment to be shared for PIN/Pattern/Password confirmation fragments. 52 */ 53 public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFragment { 54 public static final String TAG = ConfirmDeviceCredentialBaseFragment.class.getSimpleName(); 55 public static final String TITLE_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.title"; 56 public static final String HEADER_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.header"; 57 public static final String DETAILS_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.details"; 58 public static final String DARK_THEME = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.darkTheme"; 59 public static final String SHOW_CANCEL_BUTTON = 60 SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showCancelButton"; 61 public static final String SHOW_WHEN_LOCKED = 62 SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showWhenLocked"; 63 public static final String USE_FADE_ANIMATION = 64 SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.useFadeAnimation"; 65 66 protected static final int USER_TYPE_PRIMARY = 1; 67 protected static final int USER_TYPE_MANAGED_PROFILE = 2; 68 protected static final int USER_TYPE_SECONDARY = 3; 69 70 /** Time we wait before clearing a wrong input attempt (e.g. pattern) and the error message. */ 71 protected static final long CLEAR_WRONG_ATTEMPT_TIMEOUT_MS = 3000; 72 73 protected boolean mReturnCredentials = false; 74 protected boolean mReturnGatekeeperPassword = false; 75 protected boolean mForceVerifyPath = false; 76 protected Button mCancelButton; 77 /** Button allowing managed profile password reset, null when is not shown. */ 78 @Nullable protected Button mForgotButton; 79 protected int mEffectiveUserId; 80 protected int mUserId; 81 protected UserManager mUserManager; 82 protected LockPatternUtils mLockPatternUtils; 83 protected DevicePolicyManager mDevicePolicyManager; 84 protected TextView mErrorTextView; 85 protected final Handler mHandler = new Handler(); 86 protected boolean mFrp; 87 private CharSequence mFrpAlternateButtonText; 88 protected BiometricManager mBiometricManager; 89 isInternalActivity()90 private boolean isInternalActivity() { 91 return (getActivity() instanceof ConfirmLockPassword.InternalActivity) 92 || (getActivity() instanceof ConfirmLockPattern.InternalActivity); 93 } 94 95 @Override onCreate(@ullable Bundle savedInstanceState)96 public void onCreate(@Nullable Bundle savedInstanceState) { 97 super.onCreate(savedInstanceState); 98 final Intent intent = getActivity().getIntent(); 99 mFrpAlternateButtonText = intent.getCharSequenceExtra( 100 KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL); 101 mReturnCredentials = intent.getBooleanExtra( 102 ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, false); 103 104 mReturnGatekeeperPassword = intent.getBooleanExtra( 105 ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, false); 106 mForceVerifyPath = intent.getBooleanExtra( 107 ChooseLockSettingsHelper.EXTRA_KEY_FORCE_VERIFY, false); 108 109 // Only take this argument into account if it belongs to the current profile. 110 mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras(), 111 isInternalActivity()); 112 mFrp = (mUserId == LockPatternUtils.USER_FRP); 113 mUserManager = UserManager.get(getActivity()); 114 mEffectiveUserId = mUserManager.getCredentialOwnerProfile(mUserId); 115 mLockPatternUtils = new LockPatternUtils(getActivity()); 116 mDevicePolicyManager = (DevicePolicyManager) getActivity().getSystemService( 117 Context.DEVICE_POLICY_SERVICE); 118 mBiometricManager = getActivity().getSystemService(BiometricManager.class); 119 } 120 121 @Override onViewCreated(View view, @Nullable Bundle savedInstanceState)122 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 123 super.onViewCreated(view, savedInstanceState); 124 mCancelButton = view.findViewById(R.id.cancelButton); 125 boolean showCancelButton = getActivity().getIntent().getBooleanExtra( 126 SHOW_CANCEL_BUTTON, false); 127 boolean hasAlternateButton = mFrp && !TextUtils.isEmpty(mFrpAlternateButtonText); 128 mCancelButton.setVisibility(showCancelButton || hasAlternateButton 129 ? View.VISIBLE : View.GONE); 130 if (hasAlternateButton) { 131 mCancelButton.setText(mFrpAlternateButtonText); 132 } 133 mCancelButton.setOnClickListener(v -> { 134 if (hasAlternateButton) { 135 getActivity().setResult(KeyguardManager.RESULT_ALTERNATE); 136 } 137 getActivity().finish(); 138 }); 139 setupForgotButtonIfManagedProfile(view); 140 } 141 setupForgotButtonIfManagedProfile(View view)142 private void setupForgotButtonIfManagedProfile(View view) { 143 if (mUserManager.isManagedProfile(mUserId) 144 && mUserManager.isQuietModeEnabled(UserHandle.of(mUserId)) 145 && mDevicePolicyManager.canProfileOwnerResetPasswordWhenLocked(mUserId)) { 146 mForgotButton = view.findViewById(R.id.forgotButton); 147 if (mForgotButton == null) { 148 Log.wtf(TAG, "Forgot button not found in managed profile credential dialog"); 149 return; 150 } 151 mForgotButton.setVisibility(View.VISIBLE); 152 mForgotButton.setOnClickListener(v -> { 153 final Intent intent = new Intent(); 154 intent.setClassName(SETTINGS_PACKAGE_NAME, ForgotPasswordActivity.class.getName()); 155 intent.putExtra(Intent.EXTRA_USER_ID, mUserId); 156 getActivity().startActivity(intent); 157 getActivity().finish(); 158 }); 159 } 160 } 161 162 // User could be locked while Effective user is unlocked even though the effective owns the 163 // credential. Otherwise, fingerprint can't unlock fbe/keystore through 164 // verifyTiedProfileChallenge. In such case, we also wanna show the user message that 165 // fingerprint is disabled due to device restart. isStrongAuthRequired()166 protected boolean isStrongAuthRequired() { 167 return mFrp 168 || !mLockPatternUtils.isBiometricAllowedForUser(mEffectiveUserId) 169 || !mUserManager.isUserUnlocked(mUserId); 170 } 171 172 @Override onResume()173 public void onResume() { 174 super.onResume(); 175 refreshLockScreen(); 176 } 177 refreshLockScreen()178 protected void refreshLockScreen() { 179 updateErrorMessage(mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId)); 180 } 181 setAccessibilityTitle(CharSequence supplementalText)182 protected void setAccessibilityTitle(CharSequence supplementalText) { 183 Intent intent = getActivity().getIntent(); 184 if (intent != null) { 185 CharSequence titleText = intent.getCharSequenceExtra( 186 ConfirmDeviceCredentialBaseFragment.TITLE_TEXT); 187 if (supplementalText == null) { 188 return; 189 } 190 if (titleText == null) { 191 getActivity().setTitle(supplementalText); 192 } else { 193 String accessibilityTitle = 194 new StringBuilder(titleText).append(",").append(supplementalText).toString(); 195 getActivity().setTitle(Utils.createAccessibleSequence(titleText, accessibilityTitle)); 196 } 197 } 198 } 199 200 @Override onPause()201 public void onPause() { 202 super.onPause(); 203 } 204 authenticationSucceeded()205 protected abstract void authenticationSucceeded(); 206 207 prepareEnterAnimation()208 public void prepareEnterAnimation() { 209 } 210 startEnterAnimation()211 public void startEnterAnimation() { 212 } 213 reportFailedAttempt()214 protected void reportFailedAttempt() { 215 updateErrorMessage( 216 mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1); 217 mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId); 218 } 219 updateErrorMessage(int numAttempts)220 protected void updateErrorMessage(int numAttempts) { 221 final int maxAttempts = 222 mLockPatternUtils.getMaximumFailedPasswordsForWipe(mEffectiveUserId); 223 if (maxAttempts <= 0 || numAttempts <= 0) { 224 return; 225 } 226 227 // Update the on-screen error string 228 if (mErrorTextView != null) { 229 final String message = getActivity().getString( 230 R.string.lock_failed_attempts_before_wipe, numAttempts, maxAttempts); 231 showError(message, 0); 232 } 233 234 // Only show popup dialog before the last attempt and before wipe 235 final int remainingAttempts = maxAttempts - numAttempts; 236 if (remainingAttempts > 1) { 237 return; 238 } 239 final FragmentManager fragmentManager = getChildFragmentManager(); 240 final int userType = getUserTypeForWipe(); 241 if (remainingAttempts == 1) { 242 // Last try 243 final String title = getActivity().getString( 244 R.string.lock_last_attempt_before_wipe_warning_title); 245 final int messageId = getLastTryErrorMessage(userType); 246 LastTryDialog.show(fragmentManager, title, messageId, 247 android.R.string.ok, false /* dismiss */); 248 } else { 249 // Device, profile, or secondary user is wiped 250 final int messageId = getWipeMessage(userType); 251 LastTryDialog.show(fragmentManager, null /* title */, messageId, 252 R.string.lock_failed_attempts_now_wiping_dialog_dismiss, true /* dismiss */); 253 } 254 } 255 getUserTypeForWipe()256 private int getUserTypeForWipe() { 257 final UserInfo userToBeWiped = mUserManager.getUserInfo( 258 mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId)); 259 if (userToBeWiped == null || userToBeWiped.isPrimary()) { 260 return USER_TYPE_PRIMARY; 261 } else if (userToBeWiped.isManagedProfile()) { 262 return USER_TYPE_MANAGED_PROFILE; 263 } else { 264 return USER_TYPE_SECONDARY; 265 } 266 } 267 getLastTryErrorMessage(int userType)268 protected abstract int getLastTryErrorMessage(int userType); 269 getWipeMessage(int userType)270 private int getWipeMessage(int userType) { 271 switch (userType) { 272 case USER_TYPE_PRIMARY: 273 return R.string.lock_failed_attempts_now_wiping_device; 274 case USER_TYPE_MANAGED_PROFILE: 275 return R.string.lock_failed_attempts_now_wiping_profile; 276 case USER_TYPE_SECONDARY: 277 return R.string.lock_failed_attempts_now_wiping_user; 278 default: 279 throw new IllegalArgumentException("Unrecognized user type:" + userType); 280 } 281 } 282 283 private final Runnable mResetErrorRunnable = new Runnable() { 284 @Override 285 public void run() { 286 mErrorTextView.setText(""); 287 } 288 }; 289 showError(CharSequence msg, long timeout)290 protected void showError(CharSequence msg, long timeout) { 291 mErrorTextView.setText(msg); 292 onShowError(); 293 mHandler.removeCallbacks(mResetErrorRunnable); 294 if (timeout != 0) { 295 mHandler.postDelayed(mResetErrorRunnable, timeout); 296 } 297 } 298 onShowError()299 protected abstract void onShowError(); 300 showError(int msg, long timeout)301 protected void showError(int msg, long timeout) { 302 showError(getText(msg), timeout); 303 } 304 305 public static class LastTryDialog extends DialogFragment { 306 private static final String TAG = LastTryDialog.class.getSimpleName(); 307 308 private static final String ARG_TITLE = "title"; 309 private static final String ARG_MESSAGE = "message"; 310 private static final String ARG_BUTTON = "button"; 311 private static final String ARG_DISMISS = "dismiss"; 312 show(FragmentManager from, String title, int message, int button, boolean dismiss)313 static boolean show(FragmentManager from, String title, int message, int button, 314 boolean dismiss) { 315 LastTryDialog existent = (LastTryDialog) from.findFragmentByTag(TAG); 316 if (existent != null && !existent.isRemoving()) { 317 return false; 318 } 319 Bundle args = new Bundle(); 320 args.putString(ARG_TITLE, title); 321 args.putInt(ARG_MESSAGE, message); 322 args.putInt(ARG_BUTTON, button); 323 args.putBoolean(ARG_DISMISS, dismiss); 324 325 DialogFragment dialog = new LastTryDialog(); 326 dialog.setArguments(args); 327 dialog.show(from, TAG); 328 from.executePendingTransactions(); 329 return true; 330 } 331 hide(FragmentManager from)332 static void hide(FragmentManager from) { 333 LastTryDialog dialog = (LastTryDialog) from.findFragmentByTag(TAG); 334 if (dialog != null) { 335 dialog.dismissAllowingStateLoss(); 336 from.executePendingTransactions(); 337 } 338 } 339 340 /** 341 * Dialog setup. 342 * <p> 343 * To make it less likely that the dialog is dismissed accidentally, for example if the 344 * device is malfunctioning or if the device is in a pocket, we set 345 * {@code setCanceledOnTouchOutside(false)}. 346 */ 347 @Override onCreateDialog(Bundle savedInstanceState)348 public Dialog onCreateDialog(Bundle savedInstanceState) { 349 Dialog dialog = new AlertDialog.Builder(getActivity()) 350 .setTitle(getArguments().getString(ARG_TITLE)) 351 .setMessage(getArguments().getInt(ARG_MESSAGE)) 352 .setPositiveButton(getArguments().getInt(ARG_BUTTON), null) 353 .create(); 354 dialog.setCanceledOnTouchOutside(false); 355 return dialog; 356 } 357 358 @Override onDismiss(final DialogInterface dialog)359 public void onDismiss(final DialogInterface dialog) { 360 super.onDismiss(dialog); 361 if (getActivity() != null && getArguments().getBoolean(ARG_DISMISS)) { 362 getActivity().finish(); 363 } 364 } 365 } 366 } 367