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