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