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