• 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.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