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