• 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_FACE;
20 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
21 
22 import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
23 
24 import android.animation.Animator;
25 import android.annotation.IntDef;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.app.AlertDialog;
29 import android.content.Context;
30 import android.content.res.Configuration;
31 import android.graphics.PixelFormat;
32 import android.hardware.biometrics.BiometricAuthenticator.Modality;
33 import android.hardware.biometrics.BiometricConstants;
34 import android.hardware.biometrics.BiometricManager.Authenticators;
35 import android.hardware.biometrics.BiometricPrompt;
36 import android.hardware.biometrics.PromptInfo;
37 import android.hardware.face.FaceSensorPropertiesInternal;
38 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
39 import android.os.Binder;
40 import android.os.IBinder;
41 import android.os.UserManager;
42 import android.util.Log;
43 import android.view.KeyEvent;
44 import android.view.LayoutInflater;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.view.WindowInsets;
48 import android.view.WindowManager;
49 import android.view.animation.Interpolator;
50 import android.widget.FrameLayout;
51 import android.widget.ImageView;
52 import android.widget.LinearLayout;
53 import android.window.OnBackInvokedCallback;
54 import android.window.OnBackInvokedDispatcher;
55 
56 import androidx.constraintlayout.widget.ConstraintLayout;
57 
58 import com.android.app.animation.Interpolators;
59 import com.android.internal.annotations.VisibleForTesting;
60 import com.android.internal.jank.InteractionJankMonitor;
61 import com.android.internal.widget.LockPatternUtils;
62 import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
63 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
64 import com.android.systemui.biometrics.plugins.AuthContextPlugins;
65 import com.android.systemui.biometrics.shared.model.BiometricModalities;
66 import com.android.systemui.biometrics.shared.model.PromptKind;
67 import com.android.systemui.biometrics.ui.CredentialView;
68 import com.android.systemui.biometrics.ui.binder.BiometricViewBinder;
69 import com.android.systemui.biometrics.ui.binder.BiometricViewSizeBinder;
70 import com.android.systemui.biometrics.ui.binder.Spaghetti;
71 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
72 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
73 import com.android.systemui.dagger.qualifiers.Background;
74 import com.android.systemui.keyguard.WakefulnessLifecycle;
75 import com.android.systemui.res.R;
76 import com.android.systemui.statusbar.VibratorHelper;
77 import com.android.systemui.util.concurrency.DelayableExecutor;
78 import com.android.systemui.utils.windowmanager.WindowManagerUtils;
79 
80 import com.google.android.msdl.domain.MSDLPlayer;
81 
82 import kotlinx.coroutines.CoroutineScope;
83 
84 import java.io.PrintWriter;
85 import java.lang.annotation.Retention;
86 import java.lang.annotation.RetentionPolicy;
87 import java.util.HashSet;
88 import java.util.List;
89 import java.util.Set;
90 
91 import javax.inject.Provider;
92 
93 /**
94  * Top level container/controller for the BiometricPrompt UI.
95  *
96  * @deprecated TODO(b/287311775): remove and merge view/layouts into new prompt.
97  */
98 @Deprecated
99 public class AuthContainerView extends LinearLayout
100         implements WakefulnessLifecycle.Observer, CredentialView.Host {
101 
102     private static final String TAG = "AuthContainerView";
103 
104     private static final int ANIMATION_DURATION_SHOW_MS = 250;
105     private static final int ANIMATION_DURATION_AWAY_MS = 350;
106 
107     private static final int STATE_UNKNOWN = 0;
108     private static final int STATE_ANIMATING_IN = 1;
109     private static final int STATE_PENDING_DISMISS = 2;
110     private static final int STATE_SHOWING = 3;
111     private static final int STATE_ANIMATING_OUT = 4;
112     private static final int STATE_GONE = 5;
113 
114     private static final float BACKGROUND_DIM_AMOUNT = 0.5f;
115 
116     /** Shows biometric prompt dialog animation. */
117     private static final String SHOW = "show";
118     /** Dismiss biometric prompt dialog animation.  */
119     private static final String DISMISS = "dismiss";
120     /** Transit biometric prompt dialog to pin, password, pattern credential panel. */
121     private static final String TRANSIT = "transit";
122 
123     @Retention(RetentionPolicy.SOURCE)
124     @IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
125             STATE_ANIMATING_OUT, STATE_GONE})
126     private @interface ContainerState {}
127 
128     private final Config mConfig;
129     private final int mEffectiveUserId;
130     private final IBinder mWindowToken = new Binder();
131     private final WindowManager mWindowManager;
132     @Nullable private final AuthContextPlugins mAuthContextPlugins;
133     private final Interpolator mLinearOutSlowIn;
134     private final LockPatternUtils mLockPatternUtils;
135     private final WakefulnessLifecycle mWakefulnessLifecycle;
136     private final InteractionJankMonitor mInteractionJankMonitor;
137     private final CoroutineScope mApplicationCoroutineScope;
138 
139     // TODO(b/287311775): these should be migrated out once ready
140     private final @NonNull Provider<PromptSelectorInteractor> mPromptSelectorInteractorProvider;
141     // TODO(b/287311775): these should be migrated out of the view
142     private final Provider<CredentialViewModel> mCredentialViewModelProvider;
143     private final PromptViewModel mPromptViewModel;
144 
145     @VisibleForTesting final BiometricCallback mBiometricCallback;
146 
147     @Nullable private Spaghetti mBiometricView;
148     @Nullable private View mCredentialView;
149     private final AuthPanelController mPanelController;
150     private final ViewGroup mLayout;
151     private final ImageView mBackgroundView;
152     private final View mPanelView;
153     private final float mTranslationY;
154     @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
155     private final Set<Integer> mFailedModalities = new HashSet<Integer>();
156     private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
157 
158 
159     private final MSDLPlayer mMSDLPlayer;
160 
161     // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
162     @Nullable @BiometricPrompt.DismissedReason private Integer mPendingCallbackReason;
163     // HAT received from LockSettingsService when credential is verified.
164     @Nullable private byte[] mCredentialAttestation;
165 
166     // TODO(b/313469218): remove when legacy prompt is replaced
167     @Deprecated
168     static class Config {
169         Context mContext;
170         AuthDialogCallback mCallback;
171         PromptInfo mPromptInfo;
172         boolean mRequireConfirmation;
173         int mUserId;
174         String mOpPackageName;
175         int[] mSensorIds;
176         boolean mSkipIntro;
177         long mOperationId;
178         long mRequestId = -1;
179         boolean mSkipAnimation = false;
180         ScaleFactorProvider mScaleProvider;
181     }
182 
183     @VisibleForTesting
184     final class BiometricCallback implements Spaghetti.Callback {
185         @Override
onAuthenticated()186         public void onAuthenticated() {
187             animateAway(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED);
188         }
189 
190         @Override
onUserCanceled()191         public void onUserCanceled() {
192             sendEarlyUserCanceled();
193             animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
194         }
195 
196         @Override
onButtonNegative()197         public void onButtonNegative() {
198             animateAway(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
199         }
200 
201         @Override
onButtonTryAgain()202         public void onButtonTryAgain() {
203             mFailedModalities.clear();
204             mConfig.mCallback.onTryAgainPressed(getRequestId());
205         }
206 
207         @Override
onContentViewMoreOptionsButtonPressed()208         public void onContentViewMoreOptionsButtonPressed() {
209             animateAway(BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS);
210         }
211 
212         @Override
onError()213         public void onError() {
214             animateAway(BiometricPrompt.DISMISSED_REASON_ERROR);
215         }
216 
217         @Override
onUseDeviceCredential()218         public void onUseDeviceCredential() {
219             mConfig.mCallback.onDeviceCredentialPressed(getRequestId());
220             addCredentialView(false /* animatePanel */, true /* animateContents */);
221 
222             // TODO(b/313469218): Remove Config
223             mConfig.mPromptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL);
224         }
225 
226         @Override
onStartDelayedFingerprintSensor()227         public void onStartDelayedFingerprintSensor() {
228             mConfig.mCallback.onStartFingerprintNow(getRequestId());
229         }
230 
231         @Override
onAuthenticatedAndConfirmed()232         public void onAuthenticatedAndConfirmed() {
233             animateAway(BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED);
234         }
235     }
236 
237     @Override
onCredentialMatched(@onNull byte[] attestation)238     public void onCredentialMatched(@NonNull byte[] attestation) {
239         mCredentialAttestation = attestation;
240         animateAway(BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED);
241     }
242 
243     @Override
onCredentialAborted()244     public void onCredentialAborted() {
245         sendEarlyUserCanceled();
246         animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
247     }
248 
249     @Override
onCredentialAttemptsRemaining(int remaining, @NonNull String messageBody)250     public void onCredentialAttemptsRemaining(int remaining, @NonNull String messageBody) {
251         // Only show dialog if <=1 attempts are left before wiping.
252         if (remaining == 1) {
253             showLastAttemptBeforeWipeDialog(messageBody);
254         } else if (remaining <= 0) {
255             showNowWipingDialog(messageBody);
256         }
257     }
258 
showLastAttemptBeforeWipeDialog(@onNull String messageBody)259     private void showLastAttemptBeforeWipeDialog(@NonNull String messageBody) {
260         final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
261                 .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title)
262                 .setMessage(messageBody)
263                 .setPositiveButton(android.R.string.ok, null)
264                 .create();
265         alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
266         alertDialog.show();
267     }
268 
showNowWipingDialog(@onNull String messageBody)269     private void showNowWipingDialog(@NonNull String messageBody) {
270         final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
271                 .setMessage(messageBody)
272                 .setPositiveButton(
273                         com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss,
274                         null /* OnClickListener */)
275                 .setOnDismissListener(
276                         dialog -> animateAway(BiometricPrompt.DISMISSED_REASON_ERROR))
277                 .create();
278         alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
279         alertDialog.show();
280     }
281 
282     // TODO(b/251476085): remove Config and further decompose these properties out of view classes
AuthContainerView(@onNull Config config, @NonNull CoroutineScope applicationCoroutineScope, @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps, @NonNull WakefulnessLifecycle wakefulnessLifecycle, @NonNull UserManager userManager, @Nullable AuthContextPlugins authContextPlugins, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider, @NonNull PromptViewModel promptViewModel, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull @Background DelayableExecutor bgExecutor, @NonNull VibratorHelper vibratorHelper, @NonNull MSDLPlayer msdlPlayer)283     AuthContainerView(@NonNull Config config,
284             @NonNull CoroutineScope applicationCoroutineScope,
285             @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
286             @Nullable List<FaceSensorPropertiesInternal> faceProps,
287             @NonNull WakefulnessLifecycle wakefulnessLifecycle,
288             @NonNull UserManager userManager,
289             @Nullable AuthContextPlugins authContextPlugins,
290             @NonNull LockPatternUtils lockPatternUtils,
291             @NonNull InteractionJankMonitor jankMonitor,
292             @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
293             @NonNull PromptViewModel promptViewModel,
294             @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
295             @NonNull @Background DelayableExecutor bgExecutor,
296             @NonNull VibratorHelper vibratorHelper,
297             @NonNull MSDLPlayer msdlPlayer) {
298         super(config.mContext);
299 
300         mConfig = config;
301         mLockPatternUtils = lockPatternUtils;
302         mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
303         mWindowManager = WindowManagerUtils.getWindowManager(getContext());
304         mAuthContextPlugins = authContextPlugins;
305         mWakefulnessLifecycle = wakefulnessLifecycle;
306         mApplicationCoroutineScope = applicationCoroutineScope;
307 
308         mPromptViewModel = promptViewModel;
309         mTranslationY = getResources()
310                 .getDimension(R.dimen.biometric_dialog_animation_translation_offset);
311         mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
312         mBiometricCallback = new BiometricCallback();
313         mMSDLPlayer = msdlPlayer;
314 
315         final BiometricModalities biometricModalities = new BiometricModalities(
316                 Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
317                 Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds));
318 
319         final boolean isLandscape = mContext.getResources().getConfiguration().orientation
320                 == Configuration.ORIENTATION_LANDSCAPE;
321         mPromptSelectorInteractorProvider = promptSelectorInteractorProvider;
322         mPromptSelectorInteractorProvider.get().setPrompt(mConfig.mPromptInfo, mConfig.mUserId,
323                 getRequestId(), biometricModalities, mConfig.mOperationId, mConfig.mOpPackageName,
324                 false /*onSwitchToCredential*/, isLandscape);
325 
326         final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
327         final PromptKind kind = mPromptViewModel.getPromptKind().getValue();
328         if (kind.isBiometric()) {
329             if (kind.isTwoPaneLandscapeBiometric()) {
330                 mLayout = (ConstraintLayout) layoutInflater.inflate(
331                         R.layout.biometric_prompt_two_pane_layout, this, false /* attachToRoot */);
332             } else {
333                 mLayout = (ConstraintLayout) layoutInflater.inflate(
334                         R.layout.biometric_prompt_one_pane_layout, this, false /* attachToRoot */);
335             }
336         } else {
337             mLayout = (FrameLayout) layoutInflater.inflate(
338                     R.layout.auth_container_view, this, false /* attachToRoot */);
339         }
340         addView(mLayout);
341         mBackgroundView = mLayout.findViewById(R.id.background);
342 
343         mPanelView = mLayout.findViewById(R.id.panel);
344         mPanelController = new AuthPanelController(mContext, mPanelView);
345         mInteractionJankMonitor = jankMonitor;
346         mCredentialViewModelProvider = credentialViewModelProvider;
347 
348         showPrompt(promptViewModel, vibratorHelper);
349 
350         // TODO: De-dupe the logic with AuthCredentialPasswordView
351         setOnKeyListener((v, keyCode, event) -> {
352             if (keyCode != KeyEvent.KEYCODE_BACK) {
353                 return false;
354             }
355             if (event.getAction() == KeyEvent.ACTION_UP) {
356                 onBackInvoked();
357             }
358             return true;
359         });
360 
361         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
362         requestFocus();
363     }
364 
showPrompt(@onNull PromptViewModel viewModel, @NonNull VibratorHelper vibratorHelper)365     private void showPrompt(@NonNull PromptViewModel viewModel,
366             @NonNull VibratorHelper vibratorHelper) {
367         if (mPromptViewModel.getPromptKind().getValue().isBiometric()) {
368             addBiometricView(viewModel, vibratorHelper);
369         } else if (mPromptViewModel.getPromptKind().getValue().isCredential()) {
370             addCredentialView(true, false);
371         } else {
372             mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId());
373         }
374     }
375 
addBiometricView(@onNull PromptViewModel viewModel, @NonNull VibratorHelper vibratorHelper)376     private void addBiometricView(@NonNull PromptViewModel viewModel,
377             @NonNull VibratorHelper vibratorHelper) {
378         mBiometricView = BiometricViewBinder.bind(mLayout, viewModel,
379                 // TODO(b/201510778): This uses the wrong timeout in some cases
380                 getJankListener(mLayout, TRANSIT,
381                         BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
382                 mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
383                 vibratorHelper, mMSDLPlayer);
384     }
385 
386     @VisibleForTesting
onBackInvoked()387     public void onBackInvoked() {
388         sendEarlyUserCanceled();
389         animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
390     }
391 
sendEarlyUserCanceled()392     void sendEarlyUserCanceled() {
393         mConfig.mCallback.onSystemEvent(
394                 BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL, getRequestId());
395     }
396 
isAllowDeviceCredentials()397     public boolean isAllowDeviceCredentials() {
398         return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo);
399     }
400 
401     /**
402      * Adds the credential view. When going from biometric to credential view, the biometric
403      * view starts the panel expansion animation. If the credential view is being shown first,
404      * it should own the panel expansion.
405      * @param animatePanel if the credential view needs to own the panel expansion animation
406      */
addCredentialView(boolean animatePanel, boolean animateContents)407     private void addCredentialView(boolean animatePanel, boolean animateContents) {
408         final LayoutInflater factory = LayoutInflater.from(mContext);
409 
410         PromptKind credentialType = Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId);
411         final int layoutResourceId;
412         if (credentialType instanceof PromptKind.Pattern) {
413             layoutResourceId = R.layout.auth_credential_pattern_view;
414         } else if (credentialType instanceof PromptKind.Pin) {
415             layoutResourceId = R.layout.auth_credential_pin_view;
416         } else if (credentialType instanceof PromptKind.Password) {
417             layoutResourceId = R.layout.auth_credential_password_view;
418         } else {
419             throw new IllegalStateException("Unknown credential type: " + credentialType);
420         }
421         // TODO(b/288175645): Once AuthContainerView is removed, set 0dp in credential view xml
422         //  files with the corresponding left/right or top/bottom constraints being set to "parent".
423         mCredentialView = factory.inflate(layoutResourceId, mLayout, false);
424 
425         // The background is used for detecting taps / cancelling authentication. Since the
426         // credential view is full-screen and should not be canceled from background taps,
427         // disable it.
428         mBackgroundView.setOnClickListener(null);
429         mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
430         final CredentialViewModel vm = mCredentialViewModelProvider.get();
431         vm.setAnimateContents(animateContents);
432         ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel,
433                 mBiometricCallback, mAuthContextPlugins);
434 
435         mLayout.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 
onOrientationChanged()444     public void onOrientationChanged() {
445     }
446 
447     @Override
onAttachedToWindow()448     public void onAttachedToWindow() {
449         super.onAttachedToWindow();
450 
451         if (mContainerState == STATE_ANIMATING_OUT) {
452             return;
453         }
454 
455         mWakefulnessLifecycle.addObserver(this);
456         if (mConfig.mSkipIntro) {
457             mContainerState = STATE_SHOWING;
458         } else {
459             mContainerState = STATE_ANIMATING_IN;
460             setY(mTranslationY);
461             setAlpha(0f);
462             final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
463             postOnAnimation(() -> {
464                 animate()
465                         .alpha(1f)
466                         .translationY(0)
467                         .setDuration(animateDuration)
468                         .setInterpolator(mLinearOutSlowIn)
469                         .withLayer()
470                         .setListener(getJankListener(this, SHOW, animateDuration))
471                         .withEndAction(this::onDialogAnimatedIn)
472                         .start();
473             });
474         }
475         OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
476         if (dispatcher != null) {
477             dispatcher.registerOnBackInvokedCallback(
478                     OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackCallback);
479         }
480     }
481 
getJankListener(View v, String type, long timeout)482     private Animator.AnimatorListener getJankListener(View v, String type, long timeout) {
483         return new Animator.AnimatorListener() {
484             @Override
485             public void onAnimationStart(@androidx.annotation.NonNull Animator animation) {
486                 if (!v.isAttachedToWindow()) {
487                     Log.w(TAG, "Un-attached view should not begin Jank trace.");
488                     return;
489                 }
490                 mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder.withView(
491                         CUJ_BIOMETRIC_PROMPT_TRANSITION, v).setTag(type).setTimeout(timeout));
492             }
493 
494             @Override
495             public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) {
496                 if (!v.isAttachedToWindow()) {
497                     Log.w(TAG, "Un-attached view should not end Jank trace.");
498                     return;
499                 }
500                 mInteractionJankMonitor.end(CUJ_BIOMETRIC_PROMPT_TRANSITION);
501             }
502 
503             @Override
504             public void onAnimationCancel(@androidx.annotation.NonNull Animator animation) {
505                 if (!v.isAttachedToWindow()) {
506                     Log.w(TAG, "Un-attached view should not cancel Jank trace.");
507                     return;
508                 }
509                 mInteractionJankMonitor.cancel(CUJ_BIOMETRIC_PROMPT_TRANSITION);
510             }
511 
512             @Override
513             public void onAnimationRepeat(@androidx.annotation.NonNull Animator animation) {
514                 // no-op
515             }
516         };
517     }
518 
519     @Override
520     public void onDetachedFromWindow() {
521         OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
522         if (dispatcher != null) {
523             findOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mBackCallback);
524         }
525         super.onDetachedFromWindow();
526         mWakefulnessLifecycle.removeObserver(this);
527     }
528 
529     @Override
530     public void onStartedGoingToSleep() {
531         animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
532     }
533 
534     public void show(WindowManager wm) {
535         wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle(),
536                 mPromptViewModel.getPromptKind().getValue().isCredential()));
537     }
538 
539     private void forceExecuteAnimatedIn() {
540         if (mContainerState == STATE_ANIMATING_IN) {
541             //clear all animators
542             if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
543                 mCredentialView.animate().cancel();
544             }
545             mPanelView.animate().cancel();
546             mBiometricView.cancelAnimation();
547             animate().cancel();
548             onDialogAnimatedIn();
549         }
550     }
551 
552     public void dismissWithoutCallback(boolean animate) {
553         if (animate) {
554             animateAway(false /* sendReason */, 0 /* reason */);
555         } else {
556             forceExecuteAnimatedIn();
557             removeWindowIfAttached();
558         }
559     }
560 
561     public void dismissFromSystemServer() {
562         animateAway(false /* sendReason */, 0 /* reason */);
563     }
564 
565     public void onAuthenticationSucceeded(@Modality int modality) {
566         if (mBiometricView != null) {
567             mBiometricView.onAuthenticationSucceeded(modality);
568         } else {
569             Log.e(TAG, "onAuthenticationSucceeded(): mBiometricView is null");
570         }
571     }
572 
573     public void onAuthenticationFailed(@Modality int modality, String failureReason) {
574         if (mBiometricView != null) {
575             mFailedModalities.add(modality);
576             mBiometricView.onAuthenticationFailed(modality, failureReason);
577         } else {
578             Log.e(TAG, "onAuthenticationFailed(): mBiometricView is null");
579         }
580     }
581 
582     public void onHelp(@Modality int modality, String help) {
583         if (mBiometricView != null) {
584             mBiometricView.onHelp(modality, help);
585         } else {
586             Log.e(TAG, "onHelp(): mBiometricView is null");
587         }
588     }
589 
590     public void onError(@Modality int modality, String error) {
591         if (mBiometricView != null) {
592             mBiometricView.onError(modality, error);
593         } else {
594             Log.e(TAG, "onError(): mBiometricView is null");
595         }
596     }
597 
598     public void onPointerDown() {
599         if (mBiometricView != null) {
600             if (mFailedModalities.contains(TYPE_FACE)) {
601                 Log.d(TAG, "retrying failed modalities (pointer down)");
602                 mFailedModalities.remove(TYPE_FACE);
603                 mBiometricCallback.onButtonTryAgain();
604             }
605         } else {
606             Log.e(TAG, "onPointerDown(): mBiometricView is null");
607         }
608     }
609 
610     public String getOpPackageName() {
611         return mConfig.mOpPackageName;
612     }
613 
614     public String getClassNameIfItIsConfirmDeviceCredentialActivity() {
615         return  mConfig.mPromptInfo.getClassNameIfItIsConfirmDeviceCredentialActivity();
616     }
617 
618     public long getRequestId() {
619         return mConfig.mRequestId;
620     }
621 
622     public void animateToCredentialUI(boolean isError) {
623         if (mBiometricView != null) {
624             mBiometricView.startTransitionToCredentialUI(isError);
625         } else {
626             Log.e(TAG, "animateToCredentialUI(): mBiometricView is null");
627         }
628     }
629 
630     void animateAway(@BiometricPrompt.DismissedReason int reason) {
631         animateAway(true /* sendReason */, reason);
632     }
633 
634     private void animateAway(boolean sendReason, @BiometricPrompt.DismissedReason int reason) {
635         if (mContainerState == STATE_ANIMATING_IN) {
636             Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
637             mContainerState = STATE_PENDING_DISMISS;
638             return;
639         }
640 
641         if (mContainerState == STATE_ANIMATING_OUT) {
642             Log.w(TAG, "Already dismissing, sendReason: " + sendReason + " reason: " + reason);
643             return;
644         }
645         mContainerState = STATE_ANIMATING_OUT;
646 
647         // Request hiding soft-keyboard before animating away credential UI, in case IME insets
648         // animation get delayed by dismissing animation.
649         if (isAttachedToWindow() && getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
650             getWindowInsetsController().hide(WindowInsets.Type.ime());
651         }
652 
653         if (sendReason) {
654             mPendingCallbackReason = reason;
655         } else {
656             mPendingCallbackReason = null;
657         }
658 
659         final Runnable endActionRunnable = () -> {
660             setVisibility(View.INVISIBLE);
661                 // TODO(b/288175645): resetPrompt calls should be lifecycle aware
662                 mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId());
663             removeWindowIfAttached();
664         };
665 
666         final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
667         postOnAnimation(() -> {
668             animate()
669                     .alpha(0f)
670                     .translationY(mTranslationY)
671                     .setDuration(animateDuration)
672                     .setInterpolator(mLinearOutSlowIn)
673                     .setListener(getJankListener(this, DISMISS, animateDuration))
674                     .setUpdateListener(animation -> {
675                         if (mWindowManager == null || getViewRootImpl() == null) {
676                             Log.w(TAG, "skip updateViewLayout() for dim animation.");
677                             return;
678                         }
679                         final WindowManager.LayoutParams lp = getViewRootImpl().mWindowAttributes;
680                         lp.dimAmount = (1.0f - (Float) animation.getAnimatedValue())
681                                 * BACKGROUND_DIM_AMOUNT;
682                         mWindowManager.updateViewLayout(this, lp);
683                     })
684                     .withLayer()
685                     .withEndAction(endActionRunnable)
686                     .start();
687         });
688     }
689 
690     private void sendPendingCallbackIfNotNull() {
691         Log.d(TAG, "pendingCallback: " + mPendingCallbackReason);
692         if (mPendingCallbackReason != null) {
693             mConfig.mCallback.onDismissed(mPendingCallbackReason,
694                     mCredentialAttestation, getRequestId());
695             mPendingCallbackReason = null;
696         }
697     }
698 
699     private void removeWindowIfAttached() {
700         sendPendingCallbackIfNotNull();
701 
702         if (mContainerState == STATE_GONE) {
703             return;
704         }
705         mContainerState = STATE_GONE;
706         if (isAttachedToWindow()) {
707             mWindowManager.removeViewImmediate(this);
708         }
709     }
710 
711     private void onDialogAnimatedIn() {
712         if (mContainerState == STATE_PENDING_DISMISS) {
713             Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
714             animateAway(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
715             return;
716         }
717         if (mContainerState == STATE_ANIMATING_OUT || mContainerState == STATE_GONE) {
718             Log.d(TAG, "onDialogAnimatedIn(): ignore, already animating out or gone - state: "
719                     + mContainerState);
720             return;
721         }
722         mContainerState = STATE_SHOWING;
723         if (mBiometricView != null) {
724             final boolean delayFingerprint = mBiometricView.isCoex() && !mConfig.mRequireConfirmation;
725             mConfig.mCallback.onDialogAnimatedIn(getRequestId(), !delayFingerprint);
726             mBiometricView.onDialogAnimatedIn(!delayFingerprint);
727         }
728     }
729 
730     public PromptViewModel getViewModel() {
731         return mPromptViewModel;
732     }
733 
734     @VisibleForTesting
735     static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title,
736             boolean isCredentialView) {
737         final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
738                 | WindowManager.LayoutParams.FLAG_SECURE
739                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
740                 | WindowManager.LayoutParams.FLAG_DIM_BEHIND;
741         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
742                 ViewGroup.LayoutParams.MATCH_PARENT,
743                 ViewGroup.LayoutParams.MATCH_PARENT,
744                 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
745                 windowFlags,
746                 PixelFormat.TRANSLUCENT);
747         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
748         lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~WindowInsets.Type.ime()
749                 & ~WindowInsets.Type.systemBars());
750         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
751         lp.setTitle("BiometricPrompt");
752         lp.accessibilityTitle = isCredentialView ? " " : title;
753         lp.dimAmount = BACKGROUND_DIM_AMOUNT;
754         lp.token = windowToken;
755         return lp;
756     }
757 
758     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
759         pw.println("    isAttachedToWindow=" + isAttachedToWindow());
760         pw.println("    containerState=" + mContainerState);
761         pw.println("    pendingCallbackReason=" + mPendingCallbackReason);
762         pw.println("    config exist=" + (mConfig != null));
763         if (mConfig != null) {
764             pw.println("    config.sensorIds exist=" + (mConfig.mSensorIds != null));
765         }
766     }
767 }
768