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