1 /* 2 * Copyright (C) 2019 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 package com.android.systemui.biometrics; 18 19 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS; 20 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT; 21 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT; 22 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT; 23 import static android.app.admin.DevicePolicyResources.UNDEFINED; 24 25 import android.annotation.IntDef; 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.app.AlertDialog; 29 import android.app.admin.DevicePolicyManager; 30 import android.content.Context; 31 import android.content.pm.UserInfo; 32 import android.graphics.drawable.Drawable; 33 import android.hardware.biometrics.PromptInfo; 34 import android.os.AsyncTask; 35 import android.os.CountDownTimer; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.os.SystemClock; 39 import android.os.UserManager; 40 import android.text.TextUtils; 41 import android.util.AttributeSet; 42 import android.view.View; 43 import android.view.WindowManager; 44 import android.view.accessibility.AccessibilityManager; 45 import android.widget.ImageView; 46 import android.widget.LinearLayout; 47 import android.widget.TextView; 48 49 import androidx.annotation.StringRes; 50 import androidx.annotation.VisibleForTesting; 51 52 import com.android.internal.widget.LockPatternUtils; 53 import com.android.internal.widget.VerifyCredentialResponse; 54 import com.android.systemui.R; 55 import com.android.systemui.animation.Interpolators; 56 import com.android.systemui.dagger.qualifiers.Background; 57 import com.android.systemui.util.concurrency.DelayableExecutor; 58 59 import java.lang.annotation.Retention; 60 import java.lang.annotation.RetentionPolicy; 61 62 /** 63 * Abstract base class for Pin, Pattern, or Password authentication, for 64 * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}} 65 */ 66 public abstract class AuthCredentialView extends LinearLayout { 67 private static final String TAG = "BiometricPrompt/AuthCredentialView"; 68 private static final int ERROR_DURATION_MS = 3000; 69 70 static final int USER_TYPE_PRIMARY = 1; 71 static final int USER_TYPE_MANAGED_PROFILE = 2; 72 static final int USER_TYPE_SECONDARY = 3; 73 @Retention(RetentionPolicy.SOURCE) 74 @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY}) 75 private @interface UserType {} 76 77 protected final Handler mHandler; 78 protected final LockPatternUtils mLockPatternUtils; 79 80 protected final AccessibilityManager mAccessibilityManager; 81 private final UserManager mUserManager; 82 private final DevicePolicyManager mDevicePolicyManager; 83 84 private PromptInfo mPromptInfo; 85 private AuthPanelController mPanelController; 86 private boolean mShouldAnimatePanel; 87 private boolean mShouldAnimateContents; 88 89 protected TextView mTitleView; 90 protected TextView mSubtitleView; 91 protected TextView mDescriptionView; 92 protected ImageView mIconView; 93 protected TextView mErrorView; 94 95 protected @Utils.CredentialType int mCredentialType; 96 protected AuthContainerView mContainerView; 97 protected Callback mCallback; 98 protected AsyncTask<?, ?, ?> mPendingLockCheck; 99 protected int mUserId; 100 protected long mOperationId; 101 protected int mEffectiveUserId; 102 @VisibleForTesting ErrorTimer mErrorTimer; 103 104 protected @Background DelayableExecutor mBackgroundExecutor; 105 106 interface Callback { onCredentialMatched(byte[] attestation)107 void onCredentialMatched(byte[] attestation); 108 } 109 110 protected static class ErrorTimer extends CountDownTimer { 111 private final TextView mErrorView; 112 private final Context mContext; 113 114 /** 115 * @param millisInFuture The number of millis in the future from the call 116 * to {@link #start()} until the countdown is done and {@link 117 * #onFinish()} 118 * is called. 119 * @param countDownInterval The interval along the way to receive 120 * {@link #onTick(long)} callbacks. 121 */ ErrorTimer(Context context, long millisInFuture, long countDownInterval, TextView errorView)122 public ErrorTimer(Context context, long millisInFuture, long countDownInterval, 123 TextView errorView) { 124 super(millisInFuture, countDownInterval); 125 mErrorView = errorView; 126 mContext = context; 127 } 128 129 @Override onTick(long millisUntilFinished)130 public void onTick(long millisUntilFinished) { 131 final int secondsCountdown = (int) (millisUntilFinished / 1000); 132 mErrorView.setText(mContext.getString( 133 R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown)); 134 } 135 136 @Override onFinish()137 public void onFinish() { 138 if (mErrorView != null) { 139 mErrorView.setText(""); 140 } 141 } 142 } 143 144 protected final Runnable mClearErrorRunnable = new Runnable() { 145 @Override 146 public void run() { 147 if (mErrorView != null) { 148 mErrorView.setText(""); 149 } 150 } 151 }; 152 AuthCredentialView(Context context, AttributeSet attrs)153 public AuthCredentialView(Context context, AttributeSet attrs) { 154 super(context, attrs); 155 156 mLockPatternUtils = new LockPatternUtils(mContext); 157 mHandler = new Handler(Looper.getMainLooper()); 158 mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); 159 mUserManager = mContext.getSystemService(UserManager.class); 160 mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); 161 } 162 showError(String error)163 protected void showError(String error) { 164 if (mHandler != null) { 165 mHandler.removeCallbacks(mClearErrorRunnable); 166 mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS); 167 } 168 if (mErrorView != null) { 169 mErrorView.setText(error); 170 } 171 } 172 setTextOrHide(TextView view, CharSequence text)173 private void setTextOrHide(TextView view, CharSequence text) { 174 if (TextUtils.isEmpty(text)) { 175 view.setVisibility(View.GONE); 176 } else { 177 view.setText(text); 178 } 179 180 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 181 } 182 setText(TextView view, CharSequence text)183 private void setText(TextView view, CharSequence text) { 184 view.setText(text); 185 } 186 setUserId(int userId)187 void setUserId(int userId) { 188 mUserId = userId; 189 } 190 setOperationId(long operationId)191 void setOperationId(long operationId) { 192 mOperationId = operationId; 193 } 194 setEffectiveUserId(int effectiveUserId)195 void setEffectiveUserId(int effectiveUserId) { 196 mEffectiveUserId = effectiveUserId; 197 } 198 setCredentialType(@tils.CredentialType int credentialType)199 void setCredentialType(@Utils.CredentialType int credentialType) { 200 mCredentialType = credentialType; 201 } 202 setCallback(Callback callback)203 void setCallback(Callback callback) { 204 mCallback = callback; 205 } 206 setPromptInfo(PromptInfo promptInfo)207 void setPromptInfo(PromptInfo promptInfo) { 208 mPromptInfo = promptInfo; 209 } 210 setPanelController(AuthPanelController panelController, boolean animatePanel)211 void setPanelController(AuthPanelController panelController, boolean animatePanel) { 212 mPanelController = panelController; 213 mShouldAnimatePanel = animatePanel; 214 } 215 setShouldAnimateContents(boolean animateContents)216 void setShouldAnimateContents(boolean animateContents) { 217 mShouldAnimateContents = animateContents; 218 } 219 setContainerView(AuthContainerView containerView)220 void setContainerView(AuthContainerView containerView) { 221 mContainerView = containerView; 222 } 223 setBackgroundExecutor(@ackground DelayableExecutor bgExecutor)224 void setBackgroundExecutor(@Background DelayableExecutor bgExecutor) { 225 mBackgroundExecutor = bgExecutor; 226 } 227 228 @Override onAttachedToWindow()229 protected void onAttachedToWindow() { 230 super.onAttachedToWindow(); 231 232 final CharSequence title = getTitle(mPromptInfo); 233 setText(mTitleView, title); 234 setTextOrHide(mSubtitleView, getSubtitle(mPromptInfo)); 235 setTextOrHide(mDescriptionView, getDescription(mPromptInfo)); 236 announceForAccessibility(title); 237 238 if (mIconView != null) { 239 final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId); 240 final Drawable image; 241 if (isManagedProfile) { 242 image = getResources().getDrawable(R.drawable.auth_dialog_enterprise, 243 mContext.getTheme()); 244 } else { 245 image = getResources().getDrawable(R.drawable.auth_dialog_lock, 246 mContext.getTheme()); 247 } 248 mIconView.setImageDrawable(image); 249 } 250 251 // Only animate this if we're transitioning from a biometric view. 252 if (mShouldAnimateContents) { 253 setTranslationY(getResources() 254 .getDimension(R.dimen.biometric_dialog_credential_translation_offset)); 255 setAlpha(0); 256 257 postOnAnimation(() -> { 258 animate().translationY(0) 259 .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS) 260 .alpha(1.f) 261 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) 262 .withLayer() 263 .start(); 264 }); 265 } 266 } 267 268 @Override onDetachedFromWindow()269 protected void onDetachedFromWindow() { 270 super.onDetachedFromWindow(); 271 if (mErrorTimer != null) { 272 mErrorTimer.cancel(); 273 } 274 } 275 276 @Override onFinishInflate()277 protected void onFinishInflate() { 278 super.onFinishInflate(); 279 mTitleView = findViewById(R.id.title); 280 mSubtitleView = findViewById(R.id.subtitle); 281 mDescriptionView = findViewById(R.id.description); 282 mIconView = findViewById(R.id.icon); 283 mErrorView = findViewById(R.id.error); 284 } 285 286 @Override onLayout(boolean changed, int left, int top, int right, int bottom)287 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 288 super.onLayout(changed, left, top, right, bottom); 289 290 if (mShouldAnimatePanel) { 291 // Credential view is always full screen. 292 mPanelController.setUseFullScreen(true); 293 mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(), 294 mPanelController.getContainerHeight(), 0 /* animateDurationMs */); 295 mShouldAnimatePanel = false; 296 } 297 } 298 onErrorTimeoutFinish()299 protected void onErrorTimeoutFinish() {} 300 onCredentialVerified(@onNull VerifyCredentialResponse response, int timeoutMs)301 protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) { 302 if (response.isMatched()) { 303 mClearErrorRunnable.run(); 304 mLockPatternUtils.userPresent(mEffectiveUserId); 305 306 // The response passed into this method contains the Gatekeeper Password. We still 307 // have to request Gatekeeper to create a Hardware Auth Token with the 308 // Gatekeeper Password and Challenge (keystore operationId in this case) 309 final long pwHandle = response.getGatekeeperPasswordHandle(); 310 final VerifyCredentialResponse gkResponse = mLockPatternUtils 311 .verifyGatekeeperPasswordHandle(pwHandle, mOperationId, mEffectiveUserId); 312 313 mCallback.onCredentialMatched(gkResponse.getGatekeeperHAT()); 314 mLockPatternUtils.removeGatekeeperPasswordHandle(pwHandle); 315 } else { 316 if (timeoutMs > 0) { 317 mHandler.removeCallbacks(mClearErrorRunnable); 318 long deadline = mLockPatternUtils.setLockoutAttemptDeadline( 319 mEffectiveUserId, timeoutMs); 320 mErrorTimer = new ErrorTimer(mContext, 321 deadline - SystemClock.elapsedRealtime(), 322 LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS, 323 mErrorView) { 324 @Override 325 public void onFinish() { 326 onErrorTimeoutFinish(); 327 mClearErrorRunnable.run(); 328 } 329 }; 330 mErrorTimer.start(); 331 } else { 332 final boolean didUpdateErrorText = reportFailedAttempt(); 333 if (!didUpdateErrorText) { 334 final @StringRes int errorRes; 335 switch (mCredentialType) { 336 case Utils.CREDENTIAL_PIN: 337 errorRes = R.string.biometric_dialog_wrong_pin; 338 break; 339 case Utils.CREDENTIAL_PATTERN: 340 errorRes = R.string.biometric_dialog_wrong_pattern; 341 break; 342 case Utils.CREDENTIAL_PASSWORD: 343 default: 344 errorRes = R.string.biometric_dialog_wrong_password; 345 break; 346 } 347 showError(getResources().getString(errorRes)); 348 } 349 } 350 } 351 } 352 reportFailedAttempt()353 private boolean reportFailedAttempt() { 354 boolean result = updateErrorMessage( 355 mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1); 356 mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId); 357 return result; 358 } 359 updateErrorMessage(int numAttempts)360 private boolean updateErrorMessage(int numAttempts) { 361 // Don't show any message if there's no maximum number of attempts. 362 final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe( 363 mEffectiveUserId); 364 if (maxAttempts <= 0 || numAttempts <= 0) { 365 return false; 366 } 367 368 // Update the on-screen error string. 369 if (mErrorView != null) { 370 final String message = getResources().getString( 371 R.string.biometric_dialog_credential_attempts_before_wipe, 372 numAttempts, 373 maxAttempts); 374 showError(message); 375 } 376 377 // Only show dialog if <=1 attempts are left before wiping. 378 final int remainingAttempts = maxAttempts - numAttempts; 379 if (remainingAttempts == 1) { 380 showLastAttemptBeforeWipeDialog(); 381 } else if (remainingAttempts <= 0) { 382 showNowWipingDialog(); 383 } 384 return true; 385 } 386 showLastAttemptBeforeWipeDialog()387 private void showLastAttemptBeforeWipeDialog() { 388 mBackgroundExecutor.execute(() -> { 389 final AlertDialog alertDialog = new AlertDialog.Builder(mContext) 390 .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title) 391 .setMessage( 392 getLastAttemptBeforeWipeMessage(getUserTypeForWipe(), mCredentialType)) 393 .setPositiveButton(android.R.string.ok, null) 394 .create(); 395 alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); 396 mHandler.post(alertDialog::show); 397 }); 398 } 399 showNowWipingDialog()400 private void showNowWipingDialog() { 401 mBackgroundExecutor.execute(() -> { 402 String nowWipingMessage = getNowWipingMessage(getUserTypeForWipe()); 403 final AlertDialog alertDialog = new AlertDialog.Builder(mContext) 404 .setMessage(nowWipingMessage) 405 .setPositiveButton( 406 com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss, 407 null /* OnClickListener */) 408 .setOnDismissListener( 409 dialog -> mContainerView.animateAway( 410 AuthDialogCallback.DISMISSED_ERROR)) 411 .create(); 412 alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); 413 mHandler.post(alertDialog::show); 414 }); 415 } 416 getUserTypeForWipe()417 private @UserType int getUserTypeForWipe() { 418 final UserInfo userToBeWiped = mUserManager.getUserInfo( 419 mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId)); 420 if (userToBeWiped == null || userToBeWiped.isPrimary()) { 421 return USER_TYPE_PRIMARY; 422 } else if (userToBeWiped.isManagedProfile()) { 423 return USER_TYPE_MANAGED_PROFILE; 424 } else { 425 return USER_TYPE_SECONDARY; 426 } 427 } 428 429 // This should not be called on the main thread to avoid making an IPC. getLastAttemptBeforeWipeMessage( @serType int userType, @Utils.CredentialType int credentialType)430 private String getLastAttemptBeforeWipeMessage( 431 @UserType int userType, @Utils.CredentialType int credentialType) { 432 switch (userType) { 433 case USER_TYPE_PRIMARY: 434 return getLastAttemptBeforeWipeDeviceMessage(credentialType); 435 case USER_TYPE_MANAGED_PROFILE: 436 return getLastAttemptBeforeWipeProfileMessage(credentialType); 437 case USER_TYPE_SECONDARY: 438 return getLastAttemptBeforeWipeUserMessage(credentialType); 439 default: 440 throw new IllegalArgumentException("Unrecognized user type:" + userType); 441 } 442 } 443 getLastAttemptBeforeWipeDeviceMessage( @tils.CredentialType int credentialType)444 private String getLastAttemptBeforeWipeDeviceMessage( 445 @Utils.CredentialType int credentialType) { 446 switch (credentialType) { 447 case Utils.CREDENTIAL_PIN: 448 return mContext.getString( 449 R.string.biometric_dialog_last_pin_attempt_before_wipe_device); 450 case Utils.CREDENTIAL_PATTERN: 451 return mContext.getString( 452 R.string.biometric_dialog_last_pattern_attempt_before_wipe_device); 453 case Utils.CREDENTIAL_PASSWORD: 454 default: 455 return mContext.getString( 456 R.string.biometric_dialog_last_password_attempt_before_wipe_device); 457 } 458 } 459 460 // This should not be called on the main thread to avoid making an IPC. getLastAttemptBeforeWipeProfileMessage( @tils.CredentialType int credentialType)461 private String getLastAttemptBeforeWipeProfileMessage( 462 @Utils.CredentialType int credentialType) { 463 return mDevicePolicyManager.getResources().getString( 464 getLastAttemptBeforeWipeProfileUpdatableStringId(credentialType), 465 () -> getLastAttemptBeforeWipeProfileDefaultMessage(credentialType)); 466 } 467 getLastAttemptBeforeWipeProfileUpdatableStringId( @tils.CredentialType int credentialType)468 private static String getLastAttemptBeforeWipeProfileUpdatableStringId( 469 @Utils.CredentialType int credentialType) { 470 switch (credentialType) { 471 case Utils.CREDENTIAL_PIN: 472 return BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT; 473 case Utils.CREDENTIAL_PATTERN: 474 return BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT; 475 case Utils.CREDENTIAL_PASSWORD: 476 default: 477 return BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT; 478 } 479 } 480 getLastAttemptBeforeWipeProfileDefaultMessage( @tils.CredentialType int credentialType)481 private String getLastAttemptBeforeWipeProfileDefaultMessage( 482 @Utils.CredentialType int credentialType) { 483 int resId; 484 switch (credentialType) { 485 case Utils.CREDENTIAL_PIN: 486 resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_profile; 487 break; 488 case Utils.CREDENTIAL_PATTERN: 489 resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile; 490 break; 491 case Utils.CREDENTIAL_PASSWORD: 492 default: 493 resId = R.string.biometric_dialog_last_password_attempt_before_wipe_profile; 494 } 495 return mContext.getString(resId); 496 } 497 getLastAttemptBeforeWipeUserMessage( @tils.CredentialType int credentialType)498 private String getLastAttemptBeforeWipeUserMessage( 499 @Utils.CredentialType int credentialType) { 500 int resId; 501 switch (credentialType) { 502 case Utils.CREDENTIAL_PIN: 503 resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_user; 504 break; 505 case Utils.CREDENTIAL_PATTERN: 506 resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_user; 507 break; 508 case Utils.CREDENTIAL_PASSWORD: 509 default: 510 resId = R.string.biometric_dialog_last_password_attempt_before_wipe_user; 511 } 512 return mContext.getString(resId); 513 } 514 getNowWipingMessage(@serType int userType)515 private String getNowWipingMessage(@UserType int userType) { 516 return mDevicePolicyManager.getResources().getString( 517 getNowWipingUpdatableStringId(userType), 518 () -> getNowWipingDefaultMessage(userType)); 519 } 520 getNowWipingUpdatableStringId(@serType int userType)521 private String getNowWipingUpdatableStringId(@UserType int userType) { 522 switch (userType) { 523 case USER_TYPE_MANAGED_PROFILE: 524 return BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS; 525 default: 526 return UNDEFINED; 527 } 528 } 529 getNowWipingDefaultMessage(@serType int userType)530 private String getNowWipingDefaultMessage(@UserType int userType) { 531 int resId; 532 switch (userType) { 533 case USER_TYPE_PRIMARY: 534 resId = com.android.settingslib.R.string.failed_attempts_now_wiping_device; 535 break; 536 case USER_TYPE_MANAGED_PROFILE: 537 resId = com.android.settingslib.R.string.failed_attempts_now_wiping_profile; 538 break; 539 case USER_TYPE_SECONDARY: 540 resId = com.android.settingslib.R.string.failed_attempts_now_wiping_user; 541 break; 542 default: 543 throw new IllegalArgumentException("Unrecognized user type:" + userType); 544 } 545 return mContext.getString(resId); 546 } 547 548 @Nullable getTitle(@onNull PromptInfo promptInfo)549 private static CharSequence getTitle(@NonNull PromptInfo promptInfo) { 550 final CharSequence credentialTitle = promptInfo.getDeviceCredentialTitle(); 551 return credentialTitle != null ? credentialTitle : promptInfo.getTitle(); 552 } 553 554 @Nullable getSubtitle(@onNull PromptInfo promptInfo)555 private static CharSequence getSubtitle(@NonNull PromptInfo promptInfo) { 556 final CharSequence credentialSubtitle = promptInfo.getDeviceCredentialSubtitle(); 557 return credentialSubtitle != null ? credentialSubtitle : promptInfo.getSubtitle(); 558 } 559 560 @Nullable getDescription(@onNull PromptInfo promptInfo)561 private static CharSequence getDescription(@NonNull PromptInfo promptInfo) { 562 final CharSequence credentialDescription = promptInfo.getDeviceCredentialDescription(); 563 return credentialDescription != null ? credentialDescription : promptInfo.getDescription(); 564 } 565 } 566