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