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