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