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