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