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.hardware.biometrics.BiometricAuthenticator.TYPE_NONE; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.AnimatorSet; 24 import android.animation.ValueAnimator; 25 import android.annotation.IntDef; 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.annotation.StringRes; 29 import android.content.Context; 30 import android.content.res.Configuration; 31 import android.graphics.Insets; 32 import android.hardware.biometrics.BiometricAuthenticator.Modality; 33 import android.hardware.biometrics.BiometricPrompt; 34 import android.hardware.biometrics.PromptInfo; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.text.TextUtils; 39 import android.text.method.ScrollingMovementMethod; 40 import android.util.AttributeSet; 41 import android.util.Log; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.view.accessibility.AccessibilityManager; 45 import android.widget.Button; 46 import android.widget.LinearLayout; 47 import android.widget.TextView; 48 49 import com.android.internal.annotations.VisibleForTesting; 50 import com.android.internal.widget.LockPatternUtils; 51 import com.android.systemui.R; 52 53 import com.airbnb.lottie.LottieAnimationView; 54 55 import java.lang.annotation.Retention; 56 import java.lang.annotation.RetentionPolicy; 57 import java.util.ArrayList; 58 import java.util.List; 59 60 /** 61 * Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers. 62 */ 63 public abstract class AuthBiometricView extends LinearLayout implements AuthBiometricViewAdapter { 64 65 private static final String TAG = "AuthBiometricView"; 66 67 /** 68 * Authentication hardware idle. 69 */ 70 public static final int STATE_IDLE = 0; 71 /** 72 * UI animating in, authentication hardware active. 73 */ 74 public static final int STATE_AUTHENTICATING_ANIMATING_IN = 1; 75 /** 76 * UI animated in, authentication hardware active. 77 */ 78 public static final int STATE_AUTHENTICATING = 2; 79 /** 80 * UI animated in, authentication hardware active. 81 */ 82 public static final int STATE_HELP = 3; 83 /** 84 * Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle. 85 */ 86 public static final int STATE_ERROR = 4; 87 /** 88 * Authenticated, waiting for user confirmation. Authentication hardware idle. 89 */ 90 public static final int STATE_PENDING_CONFIRMATION = 5; 91 /** 92 * Authenticated, dialog animating away soon. 93 */ 94 public static final int STATE_AUTHENTICATED = 6; 95 96 @Retention(RetentionPolicy.SOURCE) 97 @IntDef({STATE_IDLE, STATE_AUTHENTICATING_ANIMATING_IN, STATE_AUTHENTICATING, STATE_HELP, 98 STATE_ERROR, STATE_PENDING_CONFIRMATION, STATE_AUTHENTICATED}) 99 @interface BiometricState {} 100 101 /** 102 * Callback to the parent when a user action has occurred. 103 */ 104 public interface Callback { 105 int ACTION_AUTHENTICATED = 1; 106 int ACTION_USER_CANCELED = 2; 107 int ACTION_BUTTON_NEGATIVE = 3; 108 int ACTION_BUTTON_TRY_AGAIN = 4; 109 int ACTION_ERROR = 5; 110 int ACTION_USE_DEVICE_CREDENTIAL = 6; 111 int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7; 112 int ACTION_AUTHENTICATED_AND_CONFIRMED = 8; 113 114 /** 115 * When an action has occurred. The caller will only invoke this when the callback should 116 * be propagated. e.g. the caller will handle any necessary delay. 117 * @param action 118 */ onAction(int action)119 void onAction(int action); 120 } 121 122 private final Handler mHandler; 123 private final AccessibilityManager mAccessibilityManager; 124 private final LockPatternUtils mLockPatternUtils; 125 protected final int mTextColorError; 126 protected final int mTextColorHint; 127 128 private AuthPanelController mPanelController; 129 130 private PromptInfo mPromptInfo; 131 private boolean mRequireConfirmation; 132 private int mUserId; 133 private int mEffectiveUserId; 134 private @AuthDialog.DialogSize int mSize = AuthDialog.SIZE_UNKNOWN; 135 136 private TextView mTitleView; 137 private TextView mSubtitleView; 138 private TextView mDescriptionView; 139 private View mIconHolderView; 140 protected LottieAnimationView mIconViewOverlay; 141 protected LottieAnimationView mIconView; 142 protected TextView mIndicatorView; 143 144 @VisibleForTesting @NonNull AuthIconController mIconController; 145 @VisibleForTesting int mAnimationDurationShort = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS; 146 @VisibleForTesting int mAnimationDurationLong = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS; 147 @VisibleForTesting int mAnimationDurationHideDialog = BiometricPrompt.HIDE_DIALOG_DELAY; 148 149 // Negative button position, exclusively for the app-specified behavior 150 @VisibleForTesting Button mNegativeButton; 151 // Negative button position, exclusively for cancelling auth after passive auth success 152 @VisibleForTesting Button mCancelButton; 153 // Negative button position, shown if device credentials are allowed 154 @VisibleForTesting Button mUseCredentialButton; 155 156 // Positive button position, 157 @VisibleForTesting Button mConfirmButton; 158 @VisibleForTesting Button mTryAgainButton; 159 160 // Measurements when biometric view is showing text, buttons, etc. 161 @Nullable @VisibleForTesting AuthDialog.LayoutParams mLayoutParams; 162 163 private Callback mCallback; 164 @BiometricState private int mState; 165 166 private float mIconOriginalY; 167 168 protected boolean mDialogSizeAnimating; 169 protected Bundle mSavedState; 170 171 private final Runnable mResetErrorRunnable; 172 private final Runnable mResetHelpRunnable; 173 174 private Animator.AnimatorListener mJankListener; 175 176 private final boolean mUseCustomBpSize; 177 private final int mCustomBpWidth; 178 private final int mCustomBpHeight; 179 180 private final OnClickListener mBackgroundClickListener = (view) -> { 181 if (mState == STATE_AUTHENTICATED) { 182 Log.w(TAG, "Ignoring background click after authenticated"); 183 return; 184 } else if (mSize == AuthDialog.SIZE_SMALL) { 185 Log.w(TAG, "Ignoring background click during small dialog"); 186 return; 187 } else if (mSize == AuthDialog.SIZE_LARGE) { 188 Log.w(TAG, "Ignoring background click during large dialog"); 189 return; 190 } 191 mCallback.onAction(Callback.ACTION_USER_CANCELED); 192 }; 193 AuthBiometricView(Context context)194 public AuthBiometricView(Context context) { 195 this(context, null); 196 } 197 AuthBiometricView(Context context, AttributeSet attrs)198 public AuthBiometricView(Context context, AttributeSet attrs) { 199 super(context, attrs); 200 mHandler = new Handler(Looper.getMainLooper()); 201 mTextColorError = getResources().getColor( 202 R.color.biometric_dialog_error, context.getTheme()); 203 mTextColorHint = getResources().getColor( 204 R.color.biometric_dialog_gray, context.getTheme()); 205 206 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 207 mLockPatternUtils = new LockPatternUtils(context); 208 209 mResetErrorRunnable = () -> { 210 updateState(getStateForAfterError()); 211 handleResetAfterError(); 212 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 213 }; 214 215 mResetHelpRunnable = () -> { 216 updateState(STATE_AUTHENTICATING); 217 handleResetAfterHelp(); 218 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 219 }; 220 221 mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size); 222 mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width); 223 mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height); 224 } 225 226 /** Delay after authentication is confirmed, before the dialog should be animated away. */ getDelayAfterAuthenticatedDurationMs()227 protected int getDelayAfterAuthenticatedDurationMs() { 228 return 0; 229 } 230 231 /** State that the dialog/icon should be in after showing a help message. */ getStateForAfterError()232 protected int getStateForAfterError() { 233 return STATE_IDLE; 234 } 235 236 /** Invoked when the error message is being cleared. */ handleResetAfterError()237 protected void handleResetAfterError() {} 238 239 /** Invoked when the help message is being cleared. */ handleResetAfterHelp()240 protected void handleResetAfterHelp() {} 241 242 /** True if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}. */ supportsSmallDialog()243 protected boolean supportsSmallDialog() { 244 return false; 245 } 246 247 /** The string to show when the user must tap to confirm via the button or icon. */ 248 @StringRes getConfirmationPrompt()249 protected int getConfirmationPrompt() { 250 return R.string.biometric_dialog_tap_confirm; 251 } 252 253 /** True if require confirmation will be honored when set via the API. */ supportsRequireConfirmation()254 protected boolean supportsRequireConfirmation() { 255 return false; 256 } 257 258 /** True if confirmation will be required even if it was not supported/requested. */ forceRequireConfirmation(@odality int modality)259 protected boolean forceRequireConfirmation(@Modality int modality) { 260 return false; 261 } 262 263 /** Ignore all events from this (secondary) modality except successful authentication. */ ignoreUnsuccessfulEventsFrom(@odality int modality, String unsuccessfulReason)264 protected boolean ignoreUnsuccessfulEventsFrom(@Modality int modality, 265 String unsuccessfulReason) { 266 return false; 267 } 268 269 /** Create the controller for managing the icons transitions during the prompt.*/ 270 @NonNull createIconController()271 protected abstract AuthIconController createIconController(); 272 273 @Override getLegacyIconController()274 public AuthIconController getLegacyIconController() { 275 return mIconController; 276 } 277 278 @Override cancelAnimation()279 public void cancelAnimation() { 280 animate().cancel(); 281 } 282 283 @Override asView()284 public View asView() { 285 return this; 286 } 287 288 @Override isCoex()289 public boolean isCoex() { 290 return false; 291 } 292 setPanelController(AuthPanelController panelController)293 void setPanelController(AuthPanelController panelController) { 294 mPanelController = panelController; 295 } setPromptInfo(PromptInfo promptInfo)296 void setPromptInfo(PromptInfo promptInfo) { 297 mPromptInfo = promptInfo; 298 } 299 setCallback(Callback callback)300 void setCallback(Callback callback) { 301 mCallback = callback; 302 } 303 setBackgroundView(View backgroundView)304 void setBackgroundView(View backgroundView) { 305 backgroundView.setOnClickListener(mBackgroundClickListener); 306 } 307 setUserId(int userId)308 void setUserId(int userId) { 309 mUserId = userId; 310 } 311 setEffectiveUserId(int effectiveUserId)312 void setEffectiveUserId(int effectiveUserId) { 313 mEffectiveUserId = effectiveUserId; 314 } 315 setRequireConfirmation(boolean requireConfirmation)316 void setRequireConfirmation(boolean requireConfirmation) { 317 mRequireConfirmation = requireConfirmation && supportsRequireConfirmation(); 318 } 319 setJankListener(Animator.AnimatorListener jankListener)320 void setJankListener(Animator.AnimatorListener jankListener) { 321 mJankListener = jankListener; 322 } 323 updatePaddings(int size)324 private void updatePaddings(int size) { 325 final Insets navBarInsets = Utils.getNavbarInsets(mContext); 326 if (size != AuthDialog.SIZE_LARGE) { 327 if (mPanelController.getPosition() == AuthPanelController.POSITION_LEFT) { 328 setPadding(navBarInsets.left, 0, 0, 0); 329 } else if (mPanelController.getPosition() == AuthPanelController.POSITION_RIGHT) { 330 setPadding(0, 0, navBarInsets.right, 0); 331 } else { 332 setPadding(0, 0, 0, navBarInsets.bottom); 333 } 334 } else { 335 setPadding(0, 0, 0, 0); 336 } 337 } 338 339 @VisibleForTesting updateSize(@uthDialog.DialogSize int newSize)340 final void updateSize(@AuthDialog.DialogSize int newSize) { 341 Log.v(TAG, "Current size: " + mSize + " New size: " + newSize); 342 updatePaddings(newSize); 343 if (newSize == AuthDialog.SIZE_SMALL) { 344 mTitleView.setVisibility(View.GONE); 345 mSubtitleView.setVisibility(View.GONE); 346 mDescriptionView.setVisibility(View.GONE); 347 mIndicatorView.setVisibility(View.GONE); 348 mNegativeButton.setVisibility(View.GONE); 349 mUseCredentialButton.setVisibility(View.GONE); 350 351 final float iconPadding = getResources() 352 .getDimension(R.dimen.biometric_dialog_icon_padding); 353 mIconHolderView.setY(getHeight() - mIconHolderView.getHeight() - iconPadding); 354 355 // Subtract the vertical padding from the new height since it's only used to create 356 // extra space between the other elements, and not part of the actual icon. 357 final int newHeight = mIconHolderView.getHeight() + 2 * (int) iconPadding 358 - mIconHolderView.getPaddingTop() - mIconHolderView.getPaddingBottom(); 359 mPanelController.updateForContentDimensions(mLayoutParams.mMediumWidth, newHeight, 360 0 /* animateDurationMs */); 361 362 mSize = newSize; 363 } else if (mSize == AuthDialog.SIZE_SMALL && newSize == AuthDialog.SIZE_MEDIUM) { 364 if (mDialogSizeAnimating) { 365 return; 366 } 367 mDialogSizeAnimating = true; 368 369 // Animate the icon back to original position 370 final ValueAnimator iconAnimator = 371 ValueAnimator.ofFloat(mIconHolderView.getY(), mIconOriginalY); 372 iconAnimator.addUpdateListener((animation) -> { 373 mIconHolderView.setY((float) animation.getAnimatedValue()); 374 }); 375 376 // Animate the text 377 final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1); 378 opacityAnimator.addUpdateListener((animation) -> { 379 final float opacity = (float) animation.getAnimatedValue(); 380 mTitleView.setAlpha(opacity); 381 mIndicatorView.setAlpha(opacity); 382 mNegativeButton.setAlpha(opacity); 383 mCancelButton.setAlpha(opacity); 384 mTryAgainButton.setAlpha(opacity); 385 386 if (!TextUtils.isEmpty(mSubtitleView.getText())) { 387 mSubtitleView.setAlpha(opacity); 388 } 389 if (!TextUtils.isEmpty(mDescriptionView.getText())) { 390 mDescriptionView.setAlpha(opacity); 391 } 392 }); 393 394 // Choreograph together 395 final AnimatorSet as = new AnimatorSet(); 396 as.setDuration(mAnimationDurationShort); 397 as.addListener(new AnimatorListenerAdapter() { 398 @Override 399 public void onAnimationStart(Animator animation) { 400 super.onAnimationStart(animation); 401 mTitleView.setVisibility(View.VISIBLE); 402 mIndicatorView.setVisibility(View.VISIBLE); 403 404 if (isDeviceCredentialAllowed()) { 405 mUseCredentialButton.setVisibility(View.VISIBLE); 406 } else { 407 mNegativeButton.setVisibility(View.VISIBLE); 408 } 409 if (supportsManualRetry()) { 410 mTryAgainButton.setVisibility(View.VISIBLE); 411 } 412 413 if (!TextUtils.isEmpty(mSubtitleView.getText())) { 414 mSubtitleView.setVisibility(View.VISIBLE); 415 } 416 if (!TextUtils.isEmpty(mDescriptionView.getText())) { 417 mDescriptionView.setVisibility(View.VISIBLE); 418 } 419 } 420 @Override 421 public void onAnimationEnd(Animator animation) { 422 super.onAnimationEnd(animation); 423 mSize = newSize; 424 mDialogSizeAnimating = false; 425 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, 426 AuthBiometricView.this); 427 } 428 }); 429 430 if (mJankListener != null) { 431 as.addListener(mJankListener); 432 } 433 as.play(iconAnimator).with(opacityAnimator); 434 as.start(); 435 // Animate the panel 436 mPanelController.updateForContentDimensions(mLayoutParams.mMediumWidth, 437 mLayoutParams.mMediumHeight, 438 AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS); 439 } else if (newSize == AuthDialog.SIZE_MEDIUM) { 440 mPanelController.updateForContentDimensions(mLayoutParams.mMediumWidth, 441 mLayoutParams.mMediumHeight, 442 0 /* animateDurationMs */); 443 mSize = newSize; 444 } else if (newSize == AuthDialog.SIZE_LARGE) { 445 final float translationY = getResources().getDimension( 446 R.dimen.biometric_dialog_medium_to_large_translation_offset); 447 final AuthBiometricView biometricView = this; 448 449 // Translate at full duration 450 final ValueAnimator translationAnimator = ValueAnimator.ofFloat( 451 biometricView.getY(), biometricView.getY() - translationY); 452 translationAnimator.setDuration(mAnimationDurationLong); 453 translationAnimator.addUpdateListener((animation) -> { 454 final float translation = (float) animation.getAnimatedValue(); 455 biometricView.setTranslationY(translation); 456 }); 457 translationAnimator.addListener(new AnimatorListenerAdapter() { 458 @Override 459 public void onAnimationEnd(Animator animation) { 460 super.onAnimationEnd(animation); 461 if (biometricView.getParent() instanceof ViewGroup) { 462 ((ViewGroup) biometricView.getParent()).removeView(biometricView); 463 } 464 mSize = newSize; 465 } 466 }); 467 468 // Opacity to 0 in half duration 469 final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0); 470 opacityAnimator.setDuration(mAnimationDurationLong / 2); 471 opacityAnimator.addUpdateListener((animation) -> { 472 final float opacity = (float) animation.getAnimatedValue(); 473 biometricView.setAlpha(opacity); 474 }); 475 476 mPanelController.setUseFullScreen(true); 477 mPanelController.updateForContentDimensions( 478 mPanelController.getContainerWidth(), 479 mPanelController.getContainerHeight(), 480 mAnimationDurationLong); 481 482 // Start the animations together 483 AnimatorSet as = new AnimatorSet(); 484 List<Animator> animators = new ArrayList<>(); 485 animators.add(translationAnimator); 486 animators.add(opacityAnimator); 487 488 if (mJankListener != null) { 489 as.addListener(mJankListener); 490 } 491 as.playTogether(animators); 492 as.setDuration(mAnimationDurationLong * 2 / 3); 493 as.start(); 494 } else { 495 Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize); 496 } 497 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 498 } 499 supportsManualRetry()500 protected boolean supportsManualRetry() { 501 return false; 502 } 503 504 /** 505 * Updates mIconView animation on updates to fold state, device rotation, or rear display mode 506 * @param animation new asset to use for iconw 507 */ updateIconViewAnimation(int animation)508 public void updateIconViewAnimation(int animation) { 509 mIconView.setAnimation(animation); 510 } 511 updateState(@iometricState int newState)512 public void updateState(@BiometricState int newState) { 513 Log.d(TAG, "newState: " + newState); 514 515 mIconController.updateState(mState, newState); 516 517 switch (newState) { 518 case STATE_AUTHENTICATING_ANIMATING_IN: 519 case STATE_AUTHENTICATING: 520 removePendingAnimations(); 521 if (mRequireConfirmation) { 522 mConfirmButton.setEnabled(false); 523 mConfirmButton.setVisibility(View.VISIBLE); 524 } 525 break; 526 527 case STATE_AUTHENTICATED: 528 removePendingAnimations(); 529 if (mSize != AuthDialog.SIZE_SMALL) { 530 mConfirmButton.setVisibility(View.GONE); 531 mNegativeButton.setVisibility(View.GONE); 532 mUseCredentialButton.setVisibility(View.GONE); 533 mCancelButton.setVisibility(View.GONE); 534 mIndicatorView.setVisibility(View.INVISIBLE); 535 } 536 announceForAccessibility(getResources() 537 .getString(R.string.biometric_dialog_authenticated)); 538 if (mState == STATE_PENDING_CONFIRMATION) { 539 mHandler.postDelayed(() -> mCallback.onAction( 540 Callback.ACTION_AUTHENTICATED_AND_CONFIRMED), 541 getDelayAfterAuthenticatedDurationMs()); 542 } else { 543 mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_AUTHENTICATED), 544 getDelayAfterAuthenticatedDurationMs()); 545 } 546 break; 547 548 case STATE_PENDING_CONFIRMATION: 549 removePendingAnimations(); 550 mNegativeButton.setVisibility(View.GONE); 551 mCancelButton.setVisibility(View.VISIBLE); 552 mUseCredentialButton.setVisibility(View.GONE); 553 // forced confirmations (multi-sensor) use the icon view as the confirm button 554 mConfirmButton.setEnabled(mRequireConfirmation); 555 mConfirmButton.setVisibility(mRequireConfirmation ? View.VISIBLE : View.GONE); 556 mIndicatorView.setTextColor(mTextColorHint); 557 mIndicatorView.setText(getConfirmationPrompt()); 558 mIndicatorView.setVisibility(View.VISIBLE); 559 break; 560 561 case STATE_ERROR: 562 if (mSize == AuthDialog.SIZE_SMALL) { 563 updateSize(AuthDialog.SIZE_MEDIUM); 564 } 565 break; 566 567 default: 568 Log.w(TAG, "Unhandled state: " + newState); 569 break; 570 } 571 572 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 573 mState = newState; 574 } 575 onOrientationChanged()576 public void onOrientationChanged() { 577 // Update padding and AuthPanel outline by calling updateSize when the orientation changed. 578 updateSize(mSize); 579 } 580 onDialogAnimatedIn(boolean fingerprintWasStarted)581 public void onDialogAnimatedIn(boolean fingerprintWasStarted) { 582 updateState(STATE_AUTHENTICATING); 583 } 584 onAuthenticationSucceeded(@odality int modality)585 public void onAuthenticationSucceeded(@Modality int modality) { 586 removePendingAnimations(); 587 if (mRequireConfirmation || forceRequireConfirmation(modality)) { 588 updateState(STATE_PENDING_CONFIRMATION); 589 } else { 590 updateState(STATE_AUTHENTICATED); 591 } 592 } 593 594 /** 595 * Notify the view that auth has failed. 596 * 597 * @param modality sensor modality that failed 598 * @param failureReason message 599 */ onAuthenticationFailed( @odality int modality, @Nullable String failureReason)600 public void onAuthenticationFailed( 601 @Modality int modality, @Nullable String failureReason) { 602 if (ignoreUnsuccessfulEventsFrom(modality, failureReason)) { 603 return; 604 } 605 606 showTemporaryMessage(failureReason, mResetErrorRunnable); 607 updateState(STATE_ERROR); 608 } 609 610 /** 611 * Notify the view that an error occurred. 612 * 613 * @param modality sensor modality that failed 614 * @param error message 615 */ onError(@odality int modality, String error)616 public void onError(@Modality int modality, String error) { 617 if (ignoreUnsuccessfulEventsFrom(modality, error)) { 618 return; 619 } 620 621 showTemporaryMessage(error, mResetErrorRunnable); 622 updateState(STATE_ERROR); 623 624 mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_ERROR), 625 mAnimationDurationHideDialog); 626 } 627 628 /** 629 * Show a help message to the user. 630 * 631 * @param modality sensor modality 632 * @param help message 633 */ onHelp(@odality int modality, String help)634 public void onHelp(@Modality int modality, String help) { 635 if (ignoreUnsuccessfulEventsFrom(modality, help)) { 636 return; 637 } 638 if (mSize != AuthDialog.SIZE_MEDIUM) { 639 Log.w(TAG, "Help received in size: " + mSize); 640 return; 641 } 642 if (TextUtils.isEmpty(help)) { 643 Log.w(TAG, "Ignoring blank help message"); 644 return; 645 } 646 647 showTemporaryMessage(help, mResetHelpRunnable); 648 updateState(STATE_HELP); 649 } 650 onSaveState(@onNull Bundle outState)651 public void onSaveState(@NonNull Bundle outState) { 652 outState.putInt(AuthDialog.KEY_BIOMETRIC_CONFIRM_VISIBILITY, 653 mConfirmButton.getVisibility()); 654 outState.putInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY, 655 mTryAgainButton.getVisibility()); 656 outState.putInt(AuthDialog.KEY_BIOMETRIC_STATE, mState); 657 outState.putString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING, 658 mIndicatorView.getText() != null ? mIndicatorView.getText().toString() : ""); 659 outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING, 660 mHandler.hasCallbacks(mResetErrorRunnable)); 661 outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING, 662 mHandler.hasCallbacks(mResetHelpRunnable)); 663 outState.putInt(AuthDialog.KEY_BIOMETRIC_DIALOG_SIZE, mSize); 664 } 665 666 /** 667 * Invoked after inflation but before being attached to window. 668 * @param savedState 669 */ restoreState(@ullable Bundle savedState)670 public void restoreState(@Nullable Bundle savedState) { 671 mSavedState = savedState; 672 } setTextOrHide(TextView view, CharSequence charSequence)673 private void setTextOrHide(TextView view, CharSequence charSequence) { 674 if (TextUtils.isEmpty(charSequence)) { 675 view.setVisibility(View.GONE); 676 } else { 677 view.setText(charSequence); 678 } 679 680 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 681 } 682 683 // Remove all pending icon and text animations removePendingAnimations()684 private void removePendingAnimations() { 685 mHandler.removeCallbacks(mResetHelpRunnable); 686 mHandler.removeCallbacks(mResetErrorRunnable); 687 } 688 showTemporaryMessage(String message, Runnable resetMessageRunnable)689 private void showTemporaryMessage(String message, Runnable resetMessageRunnable) { 690 removePendingAnimations(); 691 mIndicatorView.setText(message); 692 mIndicatorView.setTextColor(mTextColorError); 693 mIndicatorView.setVisibility(View.VISIBLE); 694 // select to enable marquee unless a screen reader is enabled 695 mIndicatorView.setSelected(!mAccessibilityManager.isEnabled() 696 || !mAccessibilityManager.isTouchExplorationEnabled()); 697 mHandler.postDelayed(resetMessageRunnable, mAnimationDurationHideDialog); 698 699 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 700 } 701 702 @Override onConfigurationChanged(Configuration newConfig)703 protected void onConfigurationChanged(Configuration newConfig) { 704 super.onConfigurationChanged(newConfig); 705 if (mSavedState != null) { 706 updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); 707 } 708 } 709 710 @Override onFinishInflate()711 protected void onFinishInflate() { 712 super.onFinishInflate(); 713 714 mTitleView = findViewById(R.id.title); 715 mSubtitleView = findViewById(R.id.subtitle); 716 mDescriptionView = findViewById(R.id.description); 717 mIconViewOverlay = findViewById(R.id.biometric_icon_overlay); 718 mIconView = findViewById(R.id.biometric_icon); 719 mIconHolderView = findViewById(R.id.biometric_icon_frame); 720 mIndicatorView = findViewById(R.id.indicator); 721 722 // Negative-side (left) buttons 723 mNegativeButton = findViewById(R.id.button_negative); 724 mCancelButton = findViewById(R.id.button_cancel); 725 mUseCredentialButton = findViewById(R.id.button_use_credential); 726 727 // Positive-side (right) buttons 728 mConfirmButton = findViewById(R.id.button_confirm); 729 mTryAgainButton = findViewById(R.id.button_try_again); 730 731 mNegativeButton.setOnClickListener((view) -> { 732 mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE); 733 }); 734 735 mCancelButton.setOnClickListener((view) -> { 736 mCallback.onAction(Callback.ACTION_USER_CANCELED); 737 }); 738 739 mUseCredentialButton.setOnClickListener((view) -> { 740 startTransitionToCredentialUI(false /* isError */); 741 }); 742 743 mConfirmButton.setOnClickListener((view) -> { 744 updateState(STATE_AUTHENTICATED); 745 }); 746 747 mTryAgainButton.setOnClickListener((view) -> { 748 updateState(STATE_AUTHENTICATING); 749 mCallback.onAction(Callback.ACTION_BUTTON_TRY_AGAIN); 750 mTryAgainButton.setVisibility(View.GONE); 751 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 752 }); 753 754 mIconController = createIconController(); 755 if (mIconController.getActsAsConfirmButton()) { 756 mIconViewOverlay.setOnClickListener((view)->{ 757 if (mState == STATE_PENDING_CONFIRMATION) { 758 updateState(STATE_AUTHENTICATED); 759 } 760 }); 761 mIconView.setOnClickListener((view) -> { 762 if (mState == STATE_PENDING_CONFIRMATION) { 763 updateState(STATE_AUTHENTICATED); 764 } 765 }); 766 } 767 } 768 769 /** 770 * Kicks off the animation process and invokes the callback. 771 * 772 * @param isError if this was triggered due to an error and not a user action (unused, 773 * previously for haptics). 774 */ 775 @Override startTransitionToCredentialUI(boolean isError)776 public void startTransitionToCredentialUI(boolean isError) { 777 updateSize(AuthDialog.SIZE_LARGE); 778 mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL); 779 } 780 781 @Override onAttachedToWindow()782 protected void onAttachedToWindow() { 783 super.onAttachedToWindow(); 784 785 mTitleView.setText(mPromptInfo.getTitle()); 786 787 // setSelected could make marquee work 788 mTitleView.setSelected(true); 789 mSubtitleView.setSelected(true); 790 // make description view become scrollable 791 mDescriptionView.setMovementMethod(new ScrollingMovementMethod()); 792 793 if (isDeviceCredentialAllowed()) { 794 final CharSequence credentialButtonText; 795 @Utils.CredentialType final int credentialType = 796 Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId); 797 switch (credentialType) { 798 case Utils.CREDENTIAL_PIN: 799 credentialButtonText = 800 getResources().getString(R.string.biometric_dialog_use_pin); 801 break; 802 case Utils.CREDENTIAL_PATTERN: 803 credentialButtonText = 804 getResources().getString(R.string.biometric_dialog_use_pattern); 805 break; 806 case Utils.CREDENTIAL_PASSWORD: 807 default: 808 credentialButtonText = 809 getResources().getString(R.string.biometric_dialog_use_password); 810 break; 811 } 812 813 mNegativeButton.setVisibility(View.GONE); 814 815 mUseCredentialButton.setText(credentialButtonText); 816 mUseCredentialButton.setVisibility(View.VISIBLE); 817 } else { 818 mNegativeButton.setText(mPromptInfo.getNegativeButtonText()); 819 } 820 821 setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle()); 822 setTextOrHide(mDescriptionView, mPromptInfo.getDescription()); 823 824 if (mSavedState == null) { 825 updateState(STATE_AUTHENTICATING_ANIMATING_IN); 826 } else { 827 // Restore as much state as possible first 828 updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); 829 830 // Restore positive button(s) state 831 mConfirmButton.setVisibility( 832 mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_CONFIRM_VISIBILITY)); 833 if (mConfirmButton.getVisibility() == View.GONE) { 834 setRequireConfirmation(false); 835 } 836 mTryAgainButton.setVisibility( 837 mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)); 838 839 } 840 } 841 842 @Override onDetachedFromWindow()843 protected void onDetachedFromWindow() { 844 super.onDetachedFromWindow(); 845 846 mIconController.setDeactivated(true); 847 848 // Empty the handler, otherwise things like ACTION_AUTHENTICATED may be duplicated once 849 // the new dialog is restored. 850 mHandler.removeCallbacksAndMessages(null /* all */); 851 } 852 853 /** 854 * Contains all of the testable logic that should be invoked when {@link #onMeasure(int, int)} 855 * is invoked. In addition, this allows subclasses to implement custom measuring logic while 856 * allowing the base class to have common code to apply the custom measurements. 857 * 858 * @param width Width to constrain the measurements to. 859 * @param height Height to constrain the measurements to. 860 * @return See {@link AuthDialog.LayoutParams} 861 */ 862 @NonNull onMeasureInternal(int width, int height)863 AuthDialog.LayoutParams onMeasureInternal(int width, int height) { 864 int totalHeight = 0; 865 final int numChildren = getChildCount(); 866 for (int i = 0; i < numChildren; i++) { 867 final View child = getChildAt(i); 868 869 if (child.getId() == R.id.space_above_icon 870 || child.getId() == R.id.space_below_icon 871 || child.getId() == R.id.button_bar) { 872 child.measure( 873 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 874 MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, 875 MeasureSpec.EXACTLY)); 876 } else if (child.getId() == R.id.biometric_icon_frame) { 877 final View iconView = findViewById(R.id.biometric_icon); 878 child.measure( 879 MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().width, 880 MeasureSpec.EXACTLY), 881 MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height, 882 MeasureSpec.EXACTLY)); 883 } else if (child.getId() == R.id.biometric_icon) { 884 child.measure( 885 MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 886 MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); 887 } else { 888 child.measure( 889 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 890 MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); 891 } 892 893 if (child.getVisibility() != View.GONE) { 894 totalHeight += child.getMeasuredHeight(); 895 } 896 } 897 898 return new AuthDialog.LayoutParams(width, totalHeight); 899 } 900 901 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)902 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 903 int width = MeasureSpec.getSize(widthMeasureSpec); 904 int height = MeasureSpec.getSize(heightMeasureSpec); 905 906 if (mUseCustomBpSize) { 907 width = mCustomBpWidth; 908 height = mCustomBpHeight; 909 } else { 910 width = Math.min(width, height); 911 } 912 913 mLayoutParams = onMeasureInternal(width, height); 914 915 final Insets navBarInsets = Utils.getNavbarInsets(mContext); 916 final int navBarHeight = navBarInsets.bottom; 917 final int navBarWidth; 918 if (mPanelController.getPosition() == AuthPanelController.POSITION_LEFT) { 919 navBarWidth = navBarInsets.left; 920 } else if (mPanelController.getPosition() == AuthPanelController.POSITION_RIGHT) { 921 navBarWidth = navBarInsets.right; 922 } else { 923 navBarWidth = 0; 924 } 925 926 // The actual auth dialog w/h should include navigation bar size. 927 if (navBarWidth != 0 || navBarHeight != 0) { 928 mLayoutParams = new AuthDialog.LayoutParams( 929 mLayoutParams.mMediumWidth + navBarWidth, 930 mLayoutParams.mMediumHeight + navBarInsets.bottom); 931 } 932 933 setMeasuredDimension(mLayoutParams.mMediumWidth, mLayoutParams.mMediumHeight); 934 } 935 936 @Override onLayout(boolean changed, int left, int top, int right, int bottom)937 public void onLayout(boolean changed, int left, int top, int right, int bottom) { 938 super.onLayout(changed, left, top, right, bottom); 939 940 // Start with initial size only once. Subsequent layout changes don't matter since we 941 // only care about the initial icon position. 942 if (mIconOriginalY == 0) { 943 mIconOriginalY = mIconHolderView.getY(); 944 if (mSavedState == null) { 945 updateSize(!mRequireConfirmation && supportsSmallDialog() ? AuthDialog.SIZE_SMALL 946 : AuthDialog.SIZE_MEDIUM); 947 } else { 948 updateSize(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_DIALOG_SIZE)); 949 950 // Restore indicator text state only after size has been restored 951 final String indicatorText = 952 mSavedState.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING); 953 if (mSavedState.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING)) { 954 onHelp(TYPE_NONE, indicatorText); 955 } else if (mSavedState.getBoolean( 956 AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)) { 957 onAuthenticationFailed(TYPE_NONE, indicatorText); 958 } 959 } 960 } 961 } 962 isDeviceCredentialAllowed()963 private boolean isDeviceCredentialAllowed() { 964 return Utils.isDeviceCredentialAllowed(mPromptInfo); 965 } 966 getIconView()967 public LottieAnimationView getIconView() { 968 return mIconView; 969 } 970 getSize()971 @AuthDialog.DialogSize int getSize() { 972 return mSize; 973 } 974 975 /** If authentication has successfully occurred and the view is done. */ isAuthenticated()976 boolean isAuthenticated() { 977 return mState == STATE_AUTHENTICATED; 978 } 979 980 /** If authentication is currently in progress. */ isAuthenticating()981 boolean isAuthenticating() { 982 return mState == STATE_AUTHENTICATING; 983 } 984 } 985