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 // select to enable marquee unless a screen reader is enabled 640 mIndicatorView.setSelected(!mAccessibilityManager.isEnabled() 641 || !mAccessibilityManager.isTouchExplorationEnabled()); 642 mHandler.postDelayed(resetMessageRunnable, mInjector.getDelayAfterError()); 643 644 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 645 } 646 647 @Override onFinishInflate()648 protected void onFinishInflate() { 649 super.onFinishInflate(); 650 onFinishInflateInternal(); 651 } 652 653 /** 654 * After inflation, but before things like restoreState, onAttachedToWindow, etc. 655 */ 656 @VisibleForTesting onFinishInflateInternal()657 void onFinishInflateInternal() { 658 mTitleView = mInjector.getTitleView(); 659 mSubtitleView = mInjector.getSubtitleView(); 660 mDescriptionView = mInjector.getDescriptionView(); 661 mIconView = mInjector.getIconView(); 662 mIconHolderView = mInjector.getIconHolderView(); 663 mIndicatorView = mInjector.getIndicatorView(); 664 665 // Negative-side (left) buttons 666 mNegativeButton = mInjector.getNegativeButton(); 667 mCancelButton = mInjector.getCancelButton(); 668 mUseCredentialButton = mInjector.getUseCredentialButton(); 669 670 // Positive-side (right) buttons 671 mConfirmButton = mInjector.getConfirmButton(); 672 mTryAgainButton = mInjector.getTryAgainButton(); 673 674 mNegativeButton.setOnClickListener((view) -> { 675 mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE); 676 }); 677 678 mCancelButton.setOnClickListener((view) -> { 679 mCallback.onAction(Callback.ACTION_USER_CANCELED); 680 }); 681 682 mUseCredentialButton.setOnClickListener((view) -> { 683 startTransitionToCredentialUI(); 684 }); 685 686 mConfirmButton.setOnClickListener((view) -> { 687 updateState(STATE_AUTHENTICATED); 688 }); 689 690 mTryAgainButton.setOnClickListener((view) -> { 691 updateState(STATE_AUTHENTICATING); 692 mCallback.onAction(Callback.ACTION_BUTTON_TRY_AGAIN); 693 mTryAgainButton.setVisibility(View.GONE); 694 Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); 695 }); 696 } 697 698 /** 699 * Kicks off the animation process and invokes the callback. 700 */ startTransitionToCredentialUI()701 void startTransitionToCredentialUI() { 702 updateSize(AuthDialog.SIZE_LARGE); 703 mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL); 704 } 705 706 @Override onAttachedToWindow()707 protected void onAttachedToWindow() { 708 super.onAttachedToWindow(); 709 onAttachedToWindowInternal(); 710 } 711 712 /** 713 * Contains all the testable logic that should be invoked when {@link #onAttachedToWindow()} is 714 * invoked. 715 */ 716 @VisibleForTesting onAttachedToWindowInternal()717 void onAttachedToWindowInternal() { 718 mTitleView.setText(mPromptInfo.getTitle()); 719 720 if (isDeviceCredentialAllowed()) { 721 final CharSequence credentialButtonText; 722 final @Utils.CredentialType int credentialType = 723 Utils.getCredentialType(mContext, mEffectiveUserId); 724 switch (credentialType) { 725 case Utils.CREDENTIAL_PIN: 726 credentialButtonText = 727 getResources().getString(R.string.biometric_dialog_use_pin); 728 break; 729 case Utils.CREDENTIAL_PATTERN: 730 credentialButtonText = 731 getResources().getString(R.string.biometric_dialog_use_pattern); 732 break; 733 case Utils.CREDENTIAL_PASSWORD: 734 credentialButtonText = 735 getResources().getString(R.string.biometric_dialog_use_password); 736 break; 737 default: 738 credentialButtonText = 739 getResources().getString(R.string.biometric_dialog_use_password); 740 break; 741 } 742 743 mNegativeButton.setVisibility(View.GONE); 744 745 mUseCredentialButton.setText(credentialButtonText); 746 mUseCredentialButton.setVisibility(View.VISIBLE); 747 } else { 748 mNegativeButton.setText(mPromptInfo.getNegativeButtonText()); 749 } 750 751 setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle()); 752 753 setTextOrHide(mDescriptionView, mPromptInfo.getDescription()); 754 755 if (mSavedState == null) { 756 updateState(STATE_AUTHENTICATING_ANIMATING_IN); 757 } else { 758 // Restore as much state as possible first 759 updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); 760 761 // Restore positive button(s) state 762 mConfirmButton.setVisibility( 763 mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_CONFIRM_VISIBILITY)); 764 if (mConfirmButton.getVisibility() == View.GONE) { 765 setRequireConfirmation(false); 766 } 767 mTryAgainButton.setVisibility( 768 mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)); 769 770 } 771 } 772 773 @Override onDetachedFromWindow()774 protected void onDetachedFromWindow() { 775 super.onDetachedFromWindow(); 776 777 // Empty the handler, otherwise things like ACTION_AUTHENTICATED may be duplicated once 778 // the new dialog is restored. 779 mHandler.removeCallbacksAndMessages(null /* all */); 780 } 781 782 /** 783 * Contains all of the testable logic that should be invoked when {@link #onMeasure(int, int)} 784 * is invoked. In addition, this allows subclasses to implement custom measuring logic while 785 * allowing the base class to have common code to apply the custom measurements. 786 * 787 * @param width Width to constrain the measurements to. 788 * @param height Height to constrain the measurements to. 789 * @return See {@link AuthDialog.LayoutParams} 790 */ 791 @NonNull onMeasureInternal(int width, int height)792 AuthDialog.LayoutParams onMeasureInternal(int width, int height) { 793 int totalHeight = 0; 794 final int numChildren = getChildCount(); 795 for (int i = 0; i < numChildren; i++) { 796 final View child = getChildAt(i); 797 798 if (child.getId() == R.id.space_above_icon 799 || child.getId() == R.id.space_below_icon 800 || child.getId() == R.id.button_bar) { 801 child.measure( 802 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 803 MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, 804 MeasureSpec.EXACTLY)); 805 } else if (child.getId() == R.id.biometric_icon_frame) { 806 final View iconView = findViewById(R.id.biometric_icon); 807 child.measure( 808 MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().width, 809 MeasureSpec.EXACTLY), 810 MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height, 811 MeasureSpec.EXACTLY)); 812 } else if (child.getId() == R.id.biometric_icon) { 813 child.measure( 814 MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 815 MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); 816 } else { 817 child.measure( 818 MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 819 MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); 820 } 821 822 if (child.getVisibility() != View.GONE) { 823 totalHeight += child.getMeasuredHeight(); 824 } 825 } 826 827 return new AuthDialog.LayoutParams(width, totalHeight); 828 } 829 isLargeDisplay()830 private boolean isLargeDisplay() { 831 return com.android.systemui.util.Utils.shouldUseSplitNotificationShade(getResources()); 832 } 833 834 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)835 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 836 final int width = MeasureSpec.getSize(widthMeasureSpec); 837 final int height = MeasureSpec.getSize(heightMeasureSpec); 838 839 final boolean isLargeDisplay = isLargeDisplay(); 840 841 final int newWidth; 842 if (isLargeDisplay) { 843 // TODO(b/201811580): Unless we can come up with a one-size-fits-all equation, we may 844 // want to consider moving this to an overlay. 845 newWidth = 2 * Math.min(width, height) / 3; 846 } else { 847 newWidth = Math.min(width, height); 848 } 849 850 // Use "newWidth" instead, so the landscape dialog width is the same as the portrait 851 // width. 852 mLayoutParams = onMeasureInternal(newWidth, height); 853 setMeasuredDimension(mLayoutParams.mMediumWidth, mLayoutParams.mMediumHeight); 854 } 855 856 @Override onLayout(boolean changed, int left, int top, int right, int bottom)857 public void onLayout(boolean changed, int left, int top, int right, int bottom) { 858 super.onLayout(changed, left, top, right, bottom); 859 onLayoutInternal(); 860 } 861 862 /** 863 * Contains all the testable logic that should be invoked when 864 * {@link #onLayout(boolean, int, int, int, int)}, is invoked. 865 */ 866 @VisibleForTesting onLayoutInternal()867 void onLayoutInternal() { 868 // Start with initial size only once. Subsequent layout changes don't matter since we 869 // only care about the initial icon position. 870 if (mIconOriginalY == 0) { 871 mIconOriginalY = mIconHolderView.getY(); 872 if (mSavedState == null) { 873 updateSize(!mRequireConfirmation && supportsSmallDialog() ? AuthDialog.SIZE_SMALL 874 : AuthDialog.SIZE_MEDIUM); 875 } else { 876 updateSize(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_DIALOG_SIZE)); 877 878 // Restore indicator text state only after size has been restored 879 final String indicatorText = 880 mSavedState.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING); 881 if (mSavedState.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING)) { 882 onHelp(TYPE_NONE, indicatorText); 883 } else if (mSavedState.getBoolean( 884 AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)) { 885 onAuthenticationFailed(TYPE_NONE, indicatorText); 886 } 887 } 888 } 889 } 890 isDeviceCredentialAllowed()891 private boolean isDeviceCredentialAllowed() { 892 return Utils.isDeviceCredentialAllowed(mPromptInfo); 893 } 894 getSize()895 @AuthDialog.DialogSize int getSize() { 896 return mSize; 897 } 898 } 899