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 android.annotation.Nullable; 21 import android.app.ActivityManager; 22 import android.app.ActivityOptions; 23 import android.app.AlertDialog; 24 import android.app.Dialog; 25 import android.app.DialogFragment; 26 import android.app.FragmentManager; 27 import android.app.IActivityManager; 28 import android.app.KeyguardManager; 29 import android.app.admin.DevicePolicyManager; 30 import android.app.trust.TrustManager; 31 import android.content.Context; 32 import android.content.DialogInterface; 33 import android.content.Intent; 34 import android.content.IntentSender; 35 import android.content.pm.UserInfo; 36 import android.graphics.Point; 37 import android.graphics.PorterDuff; 38 import android.graphics.drawable.ColorDrawable; 39 import android.graphics.drawable.Drawable; 40 import android.os.Bundle; 41 import android.os.Handler; 42 import android.os.RemoteException; 43 import android.os.UserManager; 44 import android.text.TextUtils; 45 import android.view.View; 46 import android.view.ViewGroup; 47 import android.widget.Button; 48 import android.widget.FrameLayout; 49 import android.widget.ImageView; 50 import android.widget.TextView; 51 52 import com.android.internal.widget.LockPatternUtils; 53 import com.android.settings.OptionsMenuFragment; 54 import com.android.settings.R; 55 import com.android.settings.Utils; 56 import com.android.settings.fingerprint.FingerprintUiHelper; 57 58 /** 59 * Base fragment to be shared for PIN/Pattern/Password confirmation fragments. 60 */ 61 public abstract class ConfirmDeviceCredentialBaseFragment extends OptionsMenuFragment 62 implements FingerprintUiHelper.Callback { 63 64 public static final String PACKAGE = "com.android.settings"; 65 public static final String TITLE_TEXT = PACKAGE + ".ConfirmCredentials.title"; 66 public static final String HEADER_TEXT = PACKAGE + ".ConfirmCredentials.header"; 67 public static final String DETAILS_TEXT = PACKAGE + ".ConfirmCredentials.details"; 68 public static final String ALLOW_FP_AUTHENTICATION = 69 PACKAGE + ".ConfirmCredentials.allowFpAuthentication"; 70 public static final String DARK_THEME = PACKAGE + ".ConfirmCredentials.darkTheme"; 71 public static final String SHOW_CANCEL_BUTTON = 72 PACKAGE + ".ConfirmCredentials.showCancelButton"; 73 public static final String SHOW_WHEN_LOCKED = 74 PACKAGE + ".ConfirmCredentials.showWhenLocked"; 75 76 protected static final int USER_TYPE_PRIMARY = 1; 77 protected static final int USER_TYPE_MANAGED_PROFILE = 2; 78 protected static final int USER_TYPE_SECONDARY = 3; 79 80 /** Time we wait before clearing a wrong input attempt (e.g. pattern) and the error message. */ 81 protected static final long CLEAR_WRONG_ATTEMPT_TIMEOUT_MS = 3000; 82 83 private FingerprintUiHelper mFingerprintHelper; 84 protected boolean mReturnCredentials = false; 85 protected Button mCancelButton; 86 protected ImageView mFingerprintIcon; 87 protected int mEffectiveUserId; 88 protected int mUserId; 89 protected UserManager mUserManager; 90 protected LockPatternUtils mLockPatternUtils; 91 protected DevicePolicyManager mDevicePolicyManager; 92 protected TextView mErrorTextView; 93 protected final Handler mHandler = new Handler(); 94 protected boolean mFrp; 95 private CharSequence mFrpAlternateButtonText; 96 97 @Override onCreate(@ullable Bundle savedInstanceState)98 public void onCreate(@Nullable Bundle savedInstanceState) { 99 super.onCreate(savedInstanceState); 100 mFrpAlternateButtonText = getActivity().getIntent().getCharSequenceExtra( 101 KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL); 102 mReturnCredentials = getActivity().getIntent().getBooleanExtra( 103 ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, false); 104 // Only take this argument into account if it belongs to the current profile. 105 Intent intent = getActivity().getIntent(); 106 mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras()); 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 } 114 115 @Override onViewCreated(View view, @Nullable Bundle savedInstanceState)116 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 117 super.onViewCreated(view, savedInstanceState); 118 mCancelButton = (Button) view.findViewById(R.id.cancelButton); 119 mFingerprintIcon = (ImageView) view.findViewById(R.id.fingerprintIcon); 120 mFingerprintHelper = new FingerprintUiHelper( 121 mFingerprintIcon, 122 (TextView) view.findViewById(R.id.errorText), this, mEffectiveUserId); 123 boolean showCancelButton = getActivity().getIntent().getBooleanExtra( 124 SHOW_CANCEL_BUTTON, false); 125 boolean hasAlternateButton = mFrp && !TextUtils.isEmpty(mFrpAlternateButtonText); 126 mCancelButton.setVisibility(showCancelButton || hasAlternateButton 127 ? View.VISIBLE : View.GONE); 128 if (hasAlternateButton) { 129 mCancelButton.setText(mFrpAlternateButtonText); 130 } 131 mCancelButton.setOnClickListener(new View.OnClickListener() { 132 @Override 133 public void onClick(View v) { 134 if (hasAlternateButton) { 135 getActivity().setResult(KeyguardManager.RESULT_ALTERNATE); 136 } 137 getActivity().finish(); 138 } 139 }); 140 int credentialOwnerUserId = Utils.getCredentialOwnerUserId( 141 getActivity(), 142 Utils.getUserIdFromBundle( 143 getActivity(), 144 getActivity().getIntent().getExtras())); 145 if (mUserManager.isManagedProfile(credentialOwnerUserId)) { 146 setWorkChallengeBackground(view, credentialOwnerUserId); 147 } 148 } 149 isFingerprintDisabledByAdmin()150 private boolean isFingerprintDisabledByAdmin() { 151 final int disabledFeatures = 152 mDevicePolicyManager.getKeyguardDisabledFeatures(null, mEffectiveUserId); 153 return (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0; 154 } 155 156 // User could be locked while Effective user is unlocked even though the effective owns the 157 // credential. Otherwise, fingerprint can't unlock fbe/keystore through 158 // verifyTiedProfileChallenge. In such case, we also wanna show the user message that 159 // fingerprint is disabled due to device restart. isStrongAuthRequired()160 protected boolean isStrongAuthRequired() { 161 return mFrp 162 || !mLockPatternUtils.isFingerprintAllowedForUser(mEffectiveUserId) 163 || !mUserManager.isUserUnlocked(mUserId); 164 } 165 isFingerprintAllowed()166 private boolean isFingerprintAllowed() { 167 return !mReturnCredentials 168 && getActivity().getIntent().getBooleanExtra(ALLOW_FP_AUTHENTICATION, false) 169 && !isStrongAuthRequired() 170 && !isFingerprintDisabledByAdmin(); 171 } 172 173 @Override onResume()174 public void onResume() { 175 super.onResume(); 176 refreshLockScreen(); 177 } 178 refreshLockScreen()179 protected void refreshLockScreen() { 180 if (isFingerprintAllowed()) { 181 mFingerprintHelper.startListening(); 182 } else { 183 if (mFingerprintHelper.isListening()) { 184 mFingerprintHelper.stopListening(); 185 } 186 } 187 updateErrorMessage(mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId)); 188 } 189 setAccessibilityTitle(CharSequence supplementalText)190 protected void setAccessibilityTitle(CharSequence supplementalText) { 191 Intent intent = getActivity().getIntent(); 192 if (intent != null) { 193 CharSequence titleText = intent.getCharSequenceExtra( 194 ConfirmDeviceCredentialBaseFragment.TITLE_TEXT); 195 if (supplementalText == null) { 196 return; 197 } 198 if (titleText == null) { 199 getActivity().setTitle(supplementalText); 200 } else { 201 String accessibilityTitle = 202 new StringBuilder(titleText).append(",").append(supplementalText).toString(); 203 getActivity().setTitle(Utils.createAccessibleSequence(titleText, accessibilityTitle)); 204 } 205 } 206 } 207 208 @Override onPause()209 public void onPause() { 210 super.onPause(); 211 if (mFingerprintHelper.isListening()) { 212 mFingerprintHelper.stopListening(); 213 } 214 } 215 216 @Override onAuthenticated()217 public void onAuthenticated() { 218 // Check whether we are still active. 219 if (getActivity() != null && getActivity().isResumed()) { 220 TrustManager trustManager = 221 (TrustManager) getActivity().getSystemService(Context.TRUST_SERVICE); 222 trustManager.setDeviceLockedForUser(mEffectiveUserId, false); 223 authenticationSucceeded(); 224 checkForPendingIntent(); 225 } 226 } 227 authenticationSucceeded()228 protected abstract void authenticationSucceeded(); 229 230 @Override onFingerprintIconVisibilityChanged(boolean visible)231 public void onFingerprintIconVisibilityChanged(boolean visible) { 232 } 233 prepareEnterAnimation()234 public void prepareEnterAnimation() { 235 } 236 startEnterAnimation()237 public void startEnterAnimation() { 238 } 239 checkForPendingIntent()240 protected void checkForPendingIntent() { 241 int taskId = getActivity().getIntent().getIntExtra(Intent.EXTRA_TASK_ID, -1); 242 if (taskId != -1) { 243 try { 244 IActivityManager activityManager = ActivityManager.getService(); 245 final ActivityOptions options = ActivityOptions.makeBasic(); 246 options.setLaunchStackId(ActivityManager.StackId.INVALID_STACK_ID); 247 activityManager.startActivityFromRecents(taskId, options.toBundle()); 248 return; 249 } catch (RemoteException e) { 250 // Do nothing. 251 } 252 } 253 IntentSender intentSender = getActivity().getIntent() 254 .getParcelableExtra(Intent.EXTRA_INTENT); 255 if (intentSender != null) { 256 try { 257 getActivity().startIntentSenderForResult(intentSender, -1, null, 0, 0, 0); 258 } catch (IntentSender.SendIntentException e) { 259 /* ignore */ 260 } 261 } 262 } 263 setWorkChallengeBackground(View baseView, int userId)264 private void setWorkChallengeBackground(View baseView, int userId) { 265 View mainContent = getActivity().findViewById(com.android.settings.R.id.main_content); 266 if (mainContent != null) { 267 // Remove the main content padding so that the background image is full screen. 268 mainContent.setPadding(0, 0, 0, 0); 269 } 270 271 baseView.setBackground( 272 new ColorDrawable(mDevicePolicyManager.getOrganizationColorForUser(userId))); 273 ImageView imageView = (ImageView) baseView.findViewById(R.id.background_image); 274 if (imageView != null) { 275 Drawable image = getResources().getDrawable(R.drawable.work_challenge_background); 276 image.setColorFilter( 277 getResources().getColor(R.color.confirm_device_credential_transparent_black), 278 PorterDuff.Mode.DARKEN); 279 imageView.setImageDrawable(image); 280 Point screenSize = new Point(); 281 getActivity().getWindowManager().getDefaultDisplay().getSize(screenSize); 282 imageView.setLayoutParams(new FrameLayout.LayoutParams( 283 ViewGroup.LayoutParams.MATCH_PARENT, 284 screenSize.y)); 285 } 286 } 287 reportSuccessfulAttempt()288 protected void reportSuccessfulAttempt() { 289 mLockPatternUtils.reportSuccessfulPasswordAttempt(mEffectiveUserId); 290 if (mUserManager.isManagedProfile(mEffectiveUserId)) { 291 // Keyguard is responsible to disable StrongAuth for primary user. Disable StrongAuth 292 // for work challenge only here. 293 mLockPatternUtils.userPresent(mEffectiveUserId); 294 } 295 } 296 reportFailedAttempt()297 protected void reportFailedAttempt() { 298 updateErrorMessage( 299 mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1); 300 mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId); 301 } 302 updateErrorMessage(int numAttempts)303 protected void updateErrorMessage(int numAttempts) { 304 final int maxAttempts = 305 mLockPatternUtils.getMaximumFailedPasswordsForWipe(mEffectiveUserId); 306 if (maxAttempts <= 0 || numAttempts <= 0) { 307 return; 308 } 309 310 // Update the on-screen error string 311 if (mErrorTextView != null) { 312 final String message = getActivity().getString( 313 R.string.lock_failed_attempts_before_wipe, numAttempts, maxAttempts); 314 showError(message, 0); 315 } 316 317 // Only show popup dialog before the last attempt and before wipe 318 final int remainingAttempts = maxAttempts - numAttempts; 319 if (remainingAttempts > 1) { 320 return; 321 } 322 final FragmentManager fragmentManager = getChildFragmentManager(); 323 final int userType = getUserTypeForWipe(); 324 if (remainingAttempts == 1) { 325 // Last try 326 final String title = getActivity().getString( 327 R.string.lock_last_attempt_before_wipe_warning_title); 328 final int messageId = getLastTryErrorMessage(userType); 329 LastTryDialog.show(fragmentManager, title, messageId, 330 android.R.string.ok, false /* dismiss */); 331 } else { 332 // Device, profile, or secondary user is wiped 333 final int messageId = getWipeMessage(userType); 334 LastTryDialog.show(fragmentManager, null /* title */, messageId, 335 R.string.lock_failed_attempts_now_wiping_dialog_dismiss, true /* dismiss */); 336 } 337 } 338 getUserTypeForWipe()339 private int getUserTypeForWipe() { 340 final UserInfo userToBeWiped = mUserManager.getUserInfo( 341 mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId)); 342 if (userToBeWiped == null || userToBeWiped.isPrimary()) { 343 return USER_TYPE_PRIMARY; 344 } else if (userToBeWiped.isManagedProfile()) { 345 return USER_TYPE_MANAGED_PROFILE; 346 } else { 347 return USER_TYPE_SECONDARY; 348 } 349 } 350 getLastTryErrorMessage(int userType)351 protected abstract int getLastTryErrorMessage(int userType); 352 getWipeMessage(int userType)353 private int getWipeMessage(int userType) { 354 switch (userType) { 355 case USER_TYPE_PRIMARY: 356 return R.string.lock_failed_attempts_now_wiping_device; 357 case USER_TYPE_MANAGED_PROFILE: 358 return R.string.lock_failed_attempts_now_wiping_profile; 359 case USER_TYPE_SECONDARY: 360 return R.string.lock_failed_attempts_now_wiping_user; 361 default: 362 throw new IllegalArgumentException("Unrecognized user type:" + userType); 363 } 364 } 365 366 private final Runnable mResetErrorRunnable = new Runnable() { 367 @Override 368 public void run() { 369 mErrorTextView.setText(""); 370 } 371 }; 372 showError(CharSequence msg, long timeout)373 protected void showError(CharSequence msg, long timeout) { 374 mErrorTextView.setText(msg); 375 onShowError(); 376 mHandler.removeCallbacks(mResetErrorRunnable); 377 if (timeout != 0) { 378 mHandler.postDelayed(mResetErrorRunnable, timeout); 379 } 380 } 381 onShowError()382 protected abstract void onShowError(); 383 showError(int msg, long timeout)384 protected void showError(int msg, long timeout) { 385 showError(getText(msg), timeout); 386 } 387 388 public static class LastTryDialog extends DialogFragment { 389 private static final String TAG = LastTryDialog.class.getSimpleName(); 390 391 private static final String ARG_TITLE = "title"; 392 private static final String ARG_MESSAGE = "message"; 393 private static final String ARG_BUTTON = "button"; 394 private static final String ARG_DISMISS = "dismiss"; 395 show(FragmentManager from, String title, int message, int button, boolean dismiss)396 static boolean show(FragmentManager from, String title, int message, int button, 397 boolean dismiss) { 398 LastTryDialog existent = (LastTryDialog) from.findFragmentByTag(TAG); 399 if (existent != null && !existent.isRemoving()) { 400 return false; 401 } 402 Bundle args = new Bundle(); 403 args.putString(ARG_TITLE, title); 404 args.putInt(ARG_MESSAGE, message); 405 args.putInt(ARG_BUTTON, button); 406 args.putBoolean(ARG_DISMISS, dismiss); 407 408 DialogFragment dialog = new LastTryDialog(); 409 dialog.setArguments(args); 410 dialog.show(from, TAG); 411 from.executePendingTransactions(); 412 return true; 413 } 414 hide(FragmentManager from)415 static void hide(FragmentManager from) { 416 LastTryDialog dialog = (LastTryDialog) from.findFragmentByTag(TAG); 417 if (dialog != null) { 418 dialog.dismissAllowingStateLoss(); 419 from.executePendingTransactions(); 420 } 421 } 422 423 /** 424 * Dialog setup. 425 * <p> 426 * To make it less likely that the dialog is dismissed accidentally, for example if the 427 * device is malfunctioning or if the device is in a pocket, we set 428 * {@code setCanceledOnTouchOutside(false)}. 429 */ 430 @Override onCreateDialog(Bundle savedInstanceState)431 public Dialog onCreateDialog(Bundle savedInstanceState) { 432 Dialog dialog = new AlertDialog.Builder(getActivity()) 433 .setTitle(getArguments().getString(ARG_TITLE)) 434 .setMessage(getArguments().getInt(ARG_MESSAGE)) 435 .setPositiveButton(getArguments().getInt(ARG_BUTTON), null) 436 .create(); 437 dialog.setCanceledOnTouchOutside(false); 438 return dialog; 439 } 440 441 @Override onDismiss(final DialogInterface dialog)442 public void onDismiss(final DialogInterface dialog) { 443 super.onDismiss(dialog); 444 if (getActivity() != null && getArguments().getBoolean(ARG_DISMISS)) { 445 getActivity().finish(); 446 } 447 } 448 } 449 } 450