• 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.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
20 import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
21 
22 import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
23 
24 import android.animation.Animator;
25 import android.annotation.DurationMillisLong;
26 import android.annotation.IntDef;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.content.Context;
30 import android.graphics.PixelFormat;
31 import android.hardware.biometrics.BiometricAuthenticator.Modality;
32 import android.hardware.biometrics.BiometricConstants;
33 import android.hardware.biometrics.PromptInfo;
34 import android.hardware.face.FaceSensorPropertiesInternal;
35 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
36 import android.os.Binder;
37 import android.os.Bundle;
38 import android.os.Handler;
39 import android.os.IBinder;
40 import android.os.Looper;
41 import android.os.UserManager;
42 import android.util.Log;
43 import android.view.Display;
44 import android.view.Gravity;
45 import android.view.KeyEvent;
46 import android.view.LayoutInflater;
47 import android.view.Surface;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.view.WindowInsets;
51 import android.view.WindowManager;
52 import android.view.animation.Interpolator;
53 import android.widget.FrameLayout;
54 import android.widget.ImageView;
55 import android.widget.LinearLayout;
56 import android.widget.ScrollView;
57 import android.window.OnBackInvokedCallback;
58 import android.window.OnBackInvokedDispatcher;
59 
60 import com.android.internal.annotations.VisibleForTesting;
61 import com.android.internal.jank.InteractionJankMonitor;
62 import com.android.internal.widget.LockPatternUtils;
63 import com.android.systemui.R;
64 import com.android.systemui.animation.Interpolators;
65 import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
66 import com.android.systemui.dagger.qualifiers.Background;
67 import com.android.systemui.keyguard.WakefulnessLifecycle;
68 import com.android.systemui.util.concurrency.DelayableExecutor;
69 
70 import java.io.PrintWriter;
71 import java.lang.annotation.Retention;
72 import java.lang.annotation.RetentionPolicy;
73 import java.util.HashSet;
74 import java.util.List;
75 import java.util.Set;
76 
77 /**
78  * Top level container/controller for the BiometricPrompt UI.
79  */
80 public class AuthContainerView extends LinearLayout
81         implements AuthDialog, WakefulnessLifecycle.Observer {
82 
83     private static final String TAG = "AuthContainerView";
84 
85     private static final int ANIMATION_DURATION_SHOW_MS = 250;
86     private static final int ANIMATION_DURATION_AWAY_MS = 350;
87 
88     private static final int STATE_UNKNOWN = 0;
89     private static final int STATE_ANIMATING_IN = 1;
90     private static final int STATE_PENDING_DISMISS = 2;
91     private static final int STATE_SHOWING = 3;
92     private static final int STATE_ANIMATING_OUT = 4;
93     private static final int STATE_GONE = 5;
94 
95     private static final float BACKGROUND_DIM_AMOUNT = 0.5f;
96 
97     /** Shows biometric prompt dialog animation. */
98     private static final String SHOW = "show";
99     /** Dismiss biometric prompt dialog animation.  */
100     private static final String DISMISS = "dismiss";
101     /** Transit biometric prompt dialog to pin, password, pattern credential panel. */
102     private static final String TRANSIT = "transit";
103 
104     @Retention(RetentionPolicy.SOURCE)
105     @IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
106             STATE_ANIMATING_OUT, STATE_GONE})
107     private @interface ContainerState {}
108 
109     private final Config mConfig;
110     private final int mEffectiveUserId;
111     private final Handler mHandler;
112     private final IBinder mWindowToken = new Binder();
113     private final WindowManager mWindowManager;
114     private final Interpolator mLinearOutSlowIn;
115     private final CredentialCallback mCredentialCallback;
116     private final LockPatternUtils mLockPatternUtils;
117     private final WakefulnessLifecycle mWakefulnessLifecycle;
118     private final InteractionJankMonitor mInteractionJankMonitor;
119 
120     @VisibleForTesting final BiometricCallback mBiometricCallback;
121 
122     @Nullable private AuthBiometricView mBiometricView;
123     @VisibleForTesting @Nullable AuthCredentialView mCredentialView;
124     private final AuthPanelController mPanelController;
125     private final FrameLayout mFrameLayout;
126     private final ImageView mBackgroundView;
127     private final ScrollView mBiometricScrollView;
128     private final View mPanelView;
129     private final float mTranslationY;
130     @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
131     private final Set<Integer> mFailedModalities = new HashSet<Integer>();
132     private OnBackInvokedDispatcher mOnBackInvokedDispatcher;
133     private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
134 
135     private final @Background DelayableExecutor mBackgroundExecutor;
136     private boolean mIsOrientationChanged = false;
137 
138     // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
139     @Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
140     // HAT received from LockSettingsService when credential is verified.
141     @Nullable private byte[] mCredentialAttestation;
142 
143     @VisibleForTesting
144     static class Config {
145         Context mContext;
146         AuthDialogCallback mCallback;
147         PromptInfo mPromptInfo;
148         boolean mRequireConfirmation;
149         int mUserId;
150         String mOpPackageName;
151         int[] mSensorIds;
152         boolean mSkipIntro;
153         long mOperationId;
154         long mRequestId = -1;
155         boolean mSkipAnimation = false;
156         @BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT;
157         ScaleFactorProvider mScaleProvider;
158     }
159 
160     public static class Builder {
161         Config mConfig;
162 
Builder(Context context)163         public Builder(Context context) {
164             mConfig = new Config();
165             mConfig.mContext = context;
166         }
167 
setCallback(AuthDialogCallback callback)168         public Builder setCallback(AuthDialogCallback callback) {
169             mConfig.mCallback = callback;
170             return this;
171         }
172 
setPromptInfo(PromptInfo promptInfo)173         public Builder setPromptInfo(PromptInfo promptInfo) {
174             mConfig.mPromptInfo = promptInfo;
175             return this;
176         }
177 
setRequireConfirmation(boolean requireConfirmation)178         public Builder setRequireConfirmation(boolean requireConfirmation) {
179             mConfig.mRequireConfirmation = requireConfirmation;
180             return this;
181         }
182 
setUserId(int userId)183         public Builder setUserId(int userId) {
184             mConfig.mUserId = userId;
185             return this;
186         }
187 
setOpPackageName(String opPackageName)188         public Builder setOpPackageName(String opPackageName) {
189             mConfig.mOpPackageName = opPackageName;
190             return this;
191         }
192 
setSkipIntro(boolean skip)193         public Builder setSkipIntro(boolean skip) {
194             mConfig.mSkipIntro = skip;
195             return this;
196         }
197 
setOperationId(@urationMillisLong long operationId)198         public Builder setOperationId(@DurationMillisLong long operationId) {
199             mConfig.mOperationId = operationId;
200             return this;
201         }
202 
203         /** Unique id for this request. */
setRequestId(long requestId)204         public Builder setRequestId(long requestId) {
205             mConfig.mRequestId = requestId;
206             return this;
207         }
208 
209         @VisibleForTesting
setSkipAnimationDuration(boolean skip)210         public Builder setSkipAnimationDuration(boolean skip) {
211             mConfig.mSkipAnimation = skip;
212             return this;
213         }
214 
215         /** The multi-sensor mode. */
setMultiSensorConfig(@iometricMultiSensorMode int multiSensorConfig)216         public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) {
217             mConfig.mMultiSensorConfig = multiSensorConfig;
218             return this;
219         }
220 
setScaleFactorProvider(ScaleFactorProvider scaleProvider)221         public Builder setScaleFactorProvider(ScaleFactorProvider scaleProvider) {
222             mConfig.mScaleProvider = scaleProvider;
223             return this;
224         }
225 
build(@ackground DelayableExecutor bgExecutor, int[] sensorIds, @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps, @NonNull WakefulnessLifecycle wakefulnessLifecycle, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor)226         public AuthContainerView build(@Background DelayableExecutor bgExecutor, int[] sensorIds,
227                 @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
228                 @Nullable List<FaceSensorPropertiesInternal> faceProps,
229                 @NonNull WakefulnessLifecycle wakefulnessLifecycle,
230                 @NonNull UserManager userManager,
231                 @NonNull LockPatternUtils lockPatternUtils,
232                 @NonNull InteractionJankMonitor jankMonitor) {
233             mConfig.mSensorIds = sensorIds;
234             return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
235                     userManager, lockPatternUtils, jankMonitor, new Handler(Looper.getMainLooper()),
236                     bgExecutor);
237         }
238     }
239 
240     @VisibleForTesting
241     final class BiometricCallback implements AuthBiometricView.Callback {
242         @Override
onAction(int action)243         public void onAction(int action) {
244             switch (action) {
245                 case AuthBiometricView.Callback.ACTION_AUTHENTICATED:
246                     animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
247                     break;
248                 case AuthBiometricView.Callback.ACTION_USER_CANCELED:
249                     sendEarlyUserCanceled();
250                     animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
251                     break;
252                 case AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE:
253                     animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
254                     break;
255                 case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN:
256                     mFailedModalities.clear();
257                     mConfig.mCallback.onTryAgainPressed(getRequestId());
258                     break;
259                 case AuthBiometricView.Callback.ACTION_ERROR:
260                     animateAway(AuthDialogCallback.DISMISSED_ERROR);
261                     break;
262                 case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL:
263                     mConfig.mCallback.onDeviceCredentialPressed(getRequestId());
264                     mHandler.postDelayed(() -> {
265                         addCredentialView(false /* animatePanel */, true /* animateContents */);
266                     }, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS);
267                     break;
268                 default:
269                     Log.e(TAG, "Unhandled action: " + action);
270             }
271         }
272     }
273 
274     final class CredentialCallback implements AuthCredentialView.Callback {
275         @Override
onCredentialMatched(byte[] attestation)276         public void onCredentialMatched(byte[] attestation) {
277             mCredentialAttestation = attestation;
278             animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
279         }
280     }
281 
282     @VisibleForTesting
AuthContainerView(Config config, @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps, @NonNull WakefulnessLifecycle wakefulnessLifecycle, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, @NonNull Handler mainHandler, @NonNull @Background DelayableExecutor bgExecutor)283     AuthContainerView(Config config,
284             @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
285             @Nullable List<FaceSensorPropertiesInternal> faceProps,
286             @NonNull WakefulnessLifecycle wakefulnessLifecycle,
287             @NonNull UserManager userManager,
288             @NonNull LockPatternUtils lockPatternUtils,
289             @NonNull InteractionJankMonitor jankMonitor,
290             @NonNull Handler mainHandler,
291             @NonNull @Background DelayableExecutor bgExecutor) {
292         super(config.mContext);
293 
294         mConfig = config;
295         mLockPatternUtils = lockPatternUtils;
296         mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
297         mHandler = mainHandler;
298         mWindowManager = mContext.getSystemService(WindowManager.class);
299         mWakefulnessLifecycle = wakefulnessLifecycle;
300 
301         mTranslationY = getResources()
302                 .getDimension(R.dimen.biometric_dialog_animation_translation_offset);
303         mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
304         mBiometricCallback = new BiometricCallback();
305         mCredentialCallback = new CredentialCallback();
306 
307         final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
308         mFrameLayout = (FrameLayout) layoutInflater.inflate(
309                 R.layout.auth_container_view, this, false /* attachToRoot */);
310         addView(mFrameLayout);
311         mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
312         mBackgroundView = mFrameLayout.findViewById(R.id.background);
313         mPanelView = mFrameLayout.findViewById(R.id.panel);
314         mPanelController = new AuthPanelController(mContext, mPanelView);
315         mBackgroundExecutor = bgExecutor;
316         mInteractionJankMonitor = jankMonitor;
317 
318         // Inflate biometric view only if necessary.
319         if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
320             final FingerprintSensorPropertiesInternal fpProperties =
321                     Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds);
322             final FaceSensorPropertiesInternal faceProperties =
323                     Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds);
324 
325             if (fpProperties != null && faceProperties != null) {
326                 final AuthBiometricFingerprintAndFaceView fingerprintAndFaceView =
327                         (AuthBiometricFingerprintAndFaceView) layoutInflater.inflate(
328                                 R.layout.auth_biometric_fingerprint_and_face_view, null, false);
329                 fingerprintAndFaceView.setSensorProperties(fpProperties);
330                 fingerprintAndFaceView.setScaleFactorProvider(config.mScaleProvider);
331                 fingerprintAndFaceView.updateOverrideIconLayoutParamsSize();
332                 mBiometricView = fingerprintAndFaceView;
333             } else if (fpProperties != null) {
334                 final AuthBiometricFingerprintView fpView =
335                         (AuthBiometricFingerprintView) layoutInflater.inflate(
336                                 R.layout.auth_biometric_fingerprint_view, null, false);
337                 fpView.setSensorProperties(fpProperties);
338                 fpView.setScaleFactorProvider(config.mScaleProvider);
339                 fpView.updateOverrideIconLayoutParamsSize();
340                 mBiometricView = fpView;
341             } else if (faceProperties != null) {
342                 mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate(
343                         R.layout.auth_biometric_face_view, null, false);
344             } else {
345                 Log.e(TAG, "No sensors found!");
346             }
347         }
348 
349         // init view before showing
350         if (mBiometricView != null) {
351             mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
352             mBiometricView.setPanelController(mPanelController);
353             mBiometricView.setPromptInfo(mConfig.mPromptInfo);
354             mBiometricView.setCallback(mBiometricCallback);
355             mBiometricView.setBackgroundView(mBackgroundView);
356             mBiometricView.setUserId(mConfig.mUserId);
357             mBiometricView.setEffectiveUserId(mEffectiveUserId);
358             mBiometricView.setJankListener(getJankListener(mBiometricView, TRANSIT,
359                     AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS));
360         }
361 
362         // TODO: De-dupe the logic with AuthCredentialPasswordView
363         setOnKeyListener((v, keyCode, event) -> {
364             if (keyCode != KeyEvent.KEYCODE_BACK) {
365                 return false;
366             }
367             if (event.getAction() == KeyEvent.ACTION_UP) {
368                 onBackInvoked();
369             }
370             return true;
371         });
372 
373         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
374         setFocusableInTouchMode(true);
375         requestFocus();
376     }
377 
onBackInvoked()378     private void onBackInvoked() {
379         sendEarlyUserCanceled();
380         animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
381     }
382 
sendEarlyUserCanceled()383     void sendEarlyUserCanceled() {
384         mConfig.mCallback.onSystemEvent(
385                 BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL, getRequestId());
386     }
387 
388     @Override
isAllowDeviceCredentials()389     public boolean isAllowDeviceCredentials() {
390         return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo);
391     }
392 
393     /**
394      * Adds the credential view. When going from biometric to credential view, the biometric
395      * view starts the panel expansion animation. If the credential view is being shown first,
396      * it should own the panel expansion.
397      * @param animatePanel if the credential view needs to own the panel expansion animation
398      */
addCredentialView(boolean animatePanel, boolean animateContents)399     private void addCredentialView(boolean animatePanel, boolean animateContents) {
400         final LayoutInflater factory = LayoutInflater.from(mContext);
401 
402         @Utils.CredentialType final int credentialType = Utils.getCredentialType(
403                 mLockPatternUtils, mEffectiveUserId);
404 
405         switch (credentialType) {
406             case Utils.CREDENTIAL_PATTERN:
407                 mCredentialView = (AuthCredentialView) factory.inflate(
408                         R.layout.auth_credential_pattern_view, null, false);
409                 break;
410             case Utils.CREDENTIAL_PIN:
411             case Utils.CREDENTIAL_PASSWORD:
412                 mCredentialView = (AuthCredentialView) factory.inflate(
413                         R.layout.auth_credential_password_view, null, false);
414                 break;
415             default:
416                 throw new IllegalStateException("Unknown credential type: " + credentialType);
417         }
418 
419         // The background is used for detecting taps / cancelling authentication. Since the
420         // credential view is full-screen and should not be canceled from background taps,
421         // disable it.
422         mBackgroundView.setOnClickListener(null);
423         mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
424 
425         mCredentialView.setContainerView(this);
426         mCredentialView.setUserId(mConfig.mUserId);
427         mCredentialView.setOperationId(mConfig.mOperationId);
428         mCredentialView.setEffectiveUserId(mEffectiveUserId);
429         mCredentialView.setCredentialType(credentialType);
430         mCredentialView.setCallback(mCredentialCallback);
431         mCredentialView.setPromptInfo(mConfig.mPromptInfo);
432         mCredentialView.setPanelController(mPanelController, animatePanel);
433         mCredentialView.setShouldAnimateContents(animateContents);
434         mCredentialView.setBackgroundExecutor(mBackgroundExecutor);
435         mFrameLayout.addView(mCredentialView);
436     }
437 
438     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)439     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
440         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
441         mPanelController.setContainerDimensions(getMeasuredWidth(), getMeasuredHeight());
442     }
443 
444     @Override
onOrientationChanged()445     public void onOrientationChanged() {
446         maybeUpdatePositionForUdfps(true /* invalidate */);
447         mIsOrientationChanged = true;
448     }
449 
450     @Override
onWindowFocusChanged(boolean hasWindowFocus)451     public void onWindowFocusChanged(boolean hasWindowFocus) {
452         super.onWindowFocusChanged(hasWindowFocus);
453         if (!hasWindowFocus) {
454             //it's a workaround to avoid closing BP incorrectly
455             //BP gets a onWindowFocusChanged(false) and then gets a onWindowFocusChanged(true)
456             if (mIsOrientationChanged) {
457                 mIsOrientationChanged = false;
458                 return;
459             }
460             Log.v(TAG, "Lost window focus, dismissing the dialog");
461             animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
462         }
463     }
464 
465     @Override
onAttachedToWindow()466     public void onAttachedToWindow() {
467         super.onAttachedToWindow();
468 
469         mWakefulnessLifecycle.addObserver(this);
470 
471         if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
472             mBiometricScrollView.addView(mBiometricView);
473         } else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
474             addCredentialView(true /* animatePanel */, false /* animateContents */);
475         } else {
476             throw new IllegalStateException("Unknown configuration: "
477                     + mConfig.mPromptInfo.getAuthenticators());
478         }
479 
480         maybeUpdatePositionForUdfps(false /* invalidate */);
481 
482         if (mConfig.mSkipIntro) {
483             mContainerState = STATE_SHOWING;
484         } else {
485             mContainerState = STATE_ANIMATING_IN;
486             setY(mTranslationY);
487             setAlpha(0f);
488             final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
489             postOnAnimation(() -> {
490                 animate()
491                         .alpha(1f)
492                         .translationY(0)
493                         .setDuration(animateDuration)
494                         .setInterpolator(mLinearOutSlowIn)
495                         .withLayer()
496                         .setListener(getJankListener(this, SHOW, animateDuration))
497                         .withEndAction(this::onDialogAnimatedIn)
498                         .start();
499             });
500         }
501         mOnBackInvokedDispatcher = findOnBackInvokedDispatcher();
502         if (mOnBackInvokedDispatcher != null) {
503             mOnBackInvokedDispatcher.registerOnBackInvokedCallback(
504                     OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackCallback);
505         }
506     }
507 
getJankListener(View v, String type, long timeout)508     private Animator.AnimatorListener getJankListener(View v, String type, long timeout) {
509         return new Animator.AnimatorListener() {
510             @Override
511             public void onAnimationStart(@androidx.annotation.NonNull Animator animation) {
512                 if (!v.isAttachedToWindow()) {
513                     Log.w(TAG, "Un-attached view should not begin Jank trace.");
514                     return;
515                 }
516                 mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder.withView(
517                         CUJ_BIOMETRIC_PROMPT_TRANSITION, v).setTag(type).setTimeout(timeout));
518             }
519 
520             @Override
521             public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) {
522                 if (!v.isAttachedToWindow()) {
523                     Log.w(TAG, "Un-attached view should not end Jank trace.");
524                     return;
525                 }
526                 mInteractionJankMonitor.end(CUJ_BIOMETRIC_PROMPT_TRANSITION);
527             }
528 
529             @Override
530             public void onAnimationCancel(@androidx.annotation.NonNull Animator animation) {
531                 if (!v.isAttachedToWindow()) {
532                     Log.w(TAG, "Un-attached view should not cancel Jank trace.");
533                     return;
534                 }
535                 mInteractionJankMonitor.cancel(CUJ_BIOMETRIC_PROMPT_TRANSITION);
536             }
537 
538             @Override
539             public void onAnimationRepeat(@androidx.annotation.NonNull Animator animation) {
540                 // no-op
541             }
542         };
543     }
544 
545     private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
546         if (view instanceof AuthBiometricFingerprintView) {
547             return ((AuthBiometricFingerprintView) view).isUdfps();
548         }
549 
550         return false;
551     }
552 
553     private boolean maybeUpdatePositionForUdfps(boolean invalidate) {
554         final Display display = getDisplay();
555         if (display == null) {
556             return false;
557         }
558         if (!shouldUpdatePositionForUdfps(mBiometricView)) {
559             return false;
560         }
561 
562         final int displayRotation = display.getRotation();
563         switch (displayRotation) {
564             case Surface.ROTATION_0:
565                 mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
566                 setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
567                 break;
568 
569             case Surface.ROTATION_90:
570                 mPanelController.setPosition(AuthPanelController.POSITION_RIGHT);
571                 setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
572                 break;
573 
574             case Surface.ROTATION_270:
575                 mPanelController.setPosition(AuthPanelController.POSITION_LEFT);
576                 setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
577                 break;
578 
579             case Surface.ROTATION_180:
580             default:
581                 Log.e(TAG, "Unsupported display rotation: " + displayRotation);
582                 mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
583                 setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
584                 break;
585         }
586 
587         if (invalidate) {
588             mPanelView.invalidateOutline();
589             mBiometricView.requestLayout();
590         }
591 
592         return true;
593     }
594 
595     private void setScrollViewGravity(int gravity) {
596         final FrameLayout.LayoutParams params =
597                 (FrameLayout.LayoutParams) mBiometricScrollView.getLayoutParams();
598         params.gravity = gravity;
599         mBiometricScrollView.setLayoutParams(params);
600     }
601 
602     @Override
603     public void onDetachedFromWindow() {
604         super.onDetachedFromWindow();
605         if (mOnBackInvokedDispatcher != null) {
606             mOnBackInvokedDispatcher.unregisterOnBackInvokedCallback(mBackCallback);
607             mOnBackInvokedDispatcher = null;
608         }
609         mWakefulnessLifecycle.removeObserver(this);
610     }
611 
612     @Override
613     public void onStartedGoingToSleep() {
614         animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
615     }
616 
617     @Override
618     public void show(WindowManager wm, @Nullable Bundle savedState) {
619         if (mBiometricView != null) {
620             mBiometricView.restoreState(savedState);
621         }
622 
623         if (savedState != null) {
624             mIsOrientationChanged = savedState.getBoolean(
625                     AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED);
626         }
627 
628         wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle()));
629     }
630 
631     private void forceExecuteAnimatedIn() {
632         if (mContainerState == STATE_ANIMATING_IN) {
633             //clear all animators
634             if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
635                 mCredentialView.animate().cancel();
636             }
637             mPanelView.animate().cancel();
638             mBiometricView.animate().cancel();
639             animate().cancel();
640             onDialogAnimatedIn();
641         }
642     }
643 
644     @Override
645     public void dismissWithoutCallback(boolean animate) {
646         if (animate) {
647             animateAway(false /* sendReason */, 0 /* reason */);
648         } else {
649             forceExecuteAnimatedIn();
650             removeWindowIfAttached();
651         }
652     }
653 
654     @Override
655     public void dismissFromSystemServer() {
656         animateAway(false /* sendReason */, 0 /* reason */);
657     }
658 
659     @Override
660     public void onAuthenticationSucceeded(@Modality int modality) {
661         if (mBiometricView != null) {
662             mBiometricView.onAuthenticationSucceeded(modality);
663         } else {
664             Log.e(TAG, "onAuthenticationSucceeded(): mBiometricView is null");
665         }
666     }
667 
668     @Override
669     public void onAuthenticationFailed(@Modality int modality, String failureReason) {
670         if (mBiometricView != null) {
671             mFailedModalities.add(modality);
672             mBiometricView.onAuthenticationFailed(modality, failureReason);
673         } else {
674             Log.e(TAG, "onAuthenticationFailed(): mBiometricView is null");
675         }
676     }
677 
678     @Override
679     public void onHelp(@Modality int modality, String help) {
680         if (mBiometricView != null) {
681             mBiometricView.onHelp(modality, help);
682         } else {
683             Log.e(TAG, "onHelp(): mBiometricView is null");
684         }
685     }
686 
687     @Override
688     public void onError(@Modality int modality, String error) {
689         if (mBiometricView != null) {
690             mBiometricView.onError(modality, error);
691         } else {
692             Log.e(TAG, "onError(): mBiometricView is null");
693         }
694     }
695 
696     @Override
697     public void onPointerDown() {
698         if (mBiometricView != null) {
699             if (mBiometricView.onPointerDown(mFailedModalities)) {
700                 Log.d(TAG, "retrying failed modalities (pointer down)");
701                 mBiometricCallback.onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
702             }
703         } else {
704             Log.e(TAG, "onPointerDown(): mBiometricView is null");
705         }
706     }
707 
708     @Override
709     public void onSaveState(@NonNull Bundle outState) {
710         outState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY,
711                 mContainerState == STATE_ANIMATING_OUT);
712         // In the case where biometric and credential are both allowed, we can assume that
713         // biometric isn't showing if credential is showing since biometric is shown first.
714         outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING,
715                 mBiometricView != null && mCredentialView == null);
716         outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null);
717 
718         outState.putBoolean(AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED, mIsOrientationChanged);
719 
720         if (mBiometricView != null) {
721             mBiometricView.onSaveState(outState);
722         }
723     }
724 
725     @Override
726     public String getOpPackageName() {
727         return mConfig.mOpPackageName;
728     }
729 
730     @Override
731     public long getRequestId() {
732         return mConfig.mRequestId;
733     }
734 
735     @Override
736     public void animateToCredentialUI() {
737         if (mBiometricView != null) {
738             mBiometricView.startTransitionToCredentialUI();
739         } else {
740             Log.e(TAG, "animateToCredentialUI(): mBiometricView is null");
741         }
742     }
743 
744     void animateAway(@AuthDialogCallback.DismissedReason int reason) {
745         animateAway(true /* sendReason */, reason);
746     }
747 
748     private void animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason) {
749         if (mContainerState == STATE_ANIMATING_IN) {
750             Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
751             mContainerState = STATE_PENDING_DISMISS;
752             return;
753         }
754 
755         if (mContainerState == STATE_ANIMATING_OUT) {
756             Log.w(TAG, "Already dismissing, sendReason: " + sendReason + " reason: " + reason);
757             return;
758         }
759         mContainerState = STATE_ANIMATING_OUT;
760 
761         // Request hiding soft-keyboard before animating away credential UI, in case IME insets
762         // animation get delayed by dismissing animation.
763         if (isAttachedToWindow() && getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
764             getWindowInsetsController().hide(WindowInsets.Type.ime());
765         }
766 
767         if (sendReason) {
768             mPendingCallbackReason = reason;
769         } else {
770             mPendingCallbackReason = null;
771         }
772 
773         final Runnable endActionRunnable = () -> {
774             setVisibility(View.INVISIBLE);
775             removeWindowIfAttached();
776         };
777 
778         final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
779         postOnAnimation(() -> {
780             animate()
781                     .alpha(0f)
782                     .translationY(mTranslationY)
783                     .setDuration(animateDuration)
784                     .setInterpolator(mLinearOutSlowIn)
785                     .setListener(getJankListener(this, DISMISS, animateDuration))
786                     .setUpdateListener(animation -> {
787                         if (mWindowManager == null || getViewRootImpl() == null) {
788                             Log.w(TAG, "skip updateViewLayout() for dim animation.");
789                             return;
790                         }
791                         final WindowManager.LayoutParams lp = getViewRootImpl().mWindowAttributes;
792                         lp.dimAmount = (1.0f - (Float) animation.getAnimatedValue())
793                                 * BACKGROUND_DIM_AMOUNT;
794                         mWindowManager.updateViewLayout(this, lp);
795                     })
796                     .withLayer()
797                     .withEndAction(endActionRunnable)
798                     .start();
799         });
800     }
801 
802     private void sendPendingCallbackIfNotNull() {
803         Log.d(TAG, "pendingCallback: " + mPendingCallbackReason);
804         if (mPendingCallbackReason != null) {
805             mConfig.mCallback.onDismissed(mPendingCallbackReason,
806                     mCredentialAttestation, getRequestId());
807             mPendingCallbackReason = null;
808         }
809     }
810 
811     private void removeWindowIfAttached() {
812         sendPendingCallbackIfNotNull();
813 
814         if (mContainerState == STATE_GONE) {
815             return;
816         }
817         mContainerState = STATE_GONE;
818         if (isAttachedToWindow()) {
819             mWindowManager.removeViewImmediate(this);
820         }
821     }
822 
823     private void onDialogAnimatedIn() {
824         if (mContainerState == STATE_PENDING_DISMISS) {
825             Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
826             animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
827             return;
828         }
829         if (mContainerState == STATE_ANIMATING_OUT || mContainerState == STATE_GONE) {
830             Log.d(TAG, "onDialogAnimatedIn(): ignore, already animating out or gone - state: "
831                     + mContainerState);
832             return;
833         }
834         mContainerState = STATE_SHOWING;
835         if (mBiometricView != null) {
836             mConfig.mCallback.onDialogAnimatedIn(getRequestId());
837             mBiometricView.onDialogAnimatedIn();
838         }
839     }
840 
841     @VisibleForTesting
842     static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
843         final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
844                 | WindowManager.LayoutParams.FLAG_SECURE
845                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
846                 | WindowManager.LayoutParams.FLAG_DIM_BEHIND;
847         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
848                 ViewGroup.LayoutParams.MATCH_PARENT,
849                 ViewGroup.LayoutParams.MATCH_PARENT,
850                 WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
851                 windowFlags,
852                 PixelFormat.TRANSLUCENT);
853         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
854         lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~WindowInsets.Type.ime());
855         lp.setTitle("BiometricPrompt");
856         lp.accessibilityTitle = title;
857         lp.dimAmount = BACKGROUND_DIM_AMOUNT;
858         lp.token = windowToken;
859         return lp;
860     }
861 
862     @Override
863     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
864         pw.println("    isAttachedToWindow=" + isAttachedToWindow());
865         pw.println("    containerState=" + mContainerState);
866         pw.println("    pendingCallbackReason=" + mPendingCallbackReason);
867         pw.println("    config exist=" + (mConfig != null));
868         if (mConfig != null) {
869             pw.println("    config.sensorIds exist=" + (mConfig.mSensorIds != null));
870         }
871         final AuthBiometricView biometricView = mBiometricView;
872         pw.println("    scrollView=" + findViewById(R.id.biometric_scrollview));
873         pw.println("      biometricView=" + biometricView);
874         if (biometricView != null) {
875             int[] ids = {
876                     R.id.title,
877                     R.id.subtitle,
878                     R.id.description,
879                     R.id.biometric_icon_frame,
880                     R.id.biometric_icon,
881                     R.id.indicator,
882                     R.id.button_bar,
883                     R.id.button_negative,
884                     R.id.button_use_credential,
885                     R.id.button_confirm,
886                     R.id.button_try_again
887             };
888             for (final int id: ids) {
889                 pw.println("        " + biometricView.findViewById(id));
890             }
891         }
892     }
893 }
894