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.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
21 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
22 
23 import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
24 import static com.android.systemui.Flags.constraintBp;
25 
26 import android.animation.Animator;
27 import android.annotation.IntDef;
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.app.AlertDialog;
31 import android.content.Context;
32 import android.content.res.Configuration;
33 import android.content.res.TypedArray;
34 import android.graphics.Color;
35 import android.graphics.PixelFormat;
36 import android.hardware.biometrics.BiometricAuthenticator.Modality;
37 import android.hardware.biometrics.BiometricConstants;
38 import android.hardware.biometrics.BiometricManager.Authenticators;
39 import android.hardware.biometrics.Flags;
40 import android.hardware.biometrics.PromptInfo;
41 import android.hardware.face.FaceSensorPropertiesInternal;
42 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
43 import android.os.Binder;
44 import android.os.Handler;
45 import android.os.IBinder;
46 import android.os.Looper;
47 import android.os.UserManager;
48 import android.util.Log;
49 import android.view.Display;
50 import android.view.DisplayInfo;
51 import android.view.Gravity;
52 import android.view.KeyEvent;
53 import android.view.LayoutInflater;
54 import android.view.Surface;
55 import android.view.View;
56 import android.view.ViewGroup;
57 import android.view.WindowInsets;
58 import android.view.WindowManager;
59 import android.view.animation.Interpolator;
60 import android.widget.FrameLayout;
61 import android.widget.ImageView;
62 import android.widget.LinearLayout;
63 import android.widget.ScrollView;
64 import android.window.OnBackInvokedCallback;
65 import android.window.OnBackInvokedDispatcher;
66 
67 import androidx.constraintlayout.widget.ConstraintLayout;
68 import androidx.core.view.AccessibilityDelegateCompat;
69 import androidx.core.view.ViewCompat;
70 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
71 
72 import com.android.app.animation.Interpolators;
73 import com.android.internal.annotations.VisibleForTesting;
74 import com.android.internal.jank.InteractionJankMonitor;
75 import com.android.internal.widget.LockPatternUtils;
76 import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
77 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
78 import com.android.systemui.biometrics.shared.model.BiometricModalities;
79 import com.android.systemui.biometrics.shared.model.PromptKind;
80 import com.android.systemui.biometrics.ui.BiometricPromptLayout;
81 import com.android.systemui.biometrics.ui.CredentialView;
82 import com.android.systemui.biometrics.ui.binder.BiometricViewBinder;
83 import com.android.systemui.biometrics.ui.binder.BiometricViewSizeBinder;
84 import com.android.systemui.biometrics.ui.binder.Spaghetti;
85 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
86 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
87 import com.android.systemui.dagger.qualifiers.Background;
88 import com.android.systemui.keyguard.WakefulnessLifecycle;
89 import com.android.systemui.res.R;
90 import com.android.systemui.statusbar.VibratorHelper;
91 import com.android.systemui.util.concurrency.DelayableExecutor;
92 
93 import kotlinx.coroutines.CoroutineScope;
94 
95 import java.io.PrintWriter;
96 import java.lang.annotation.Retention;
97 import java.lang.annotation.RetentionPolicy;
98 import java.util.HashSet;
99 import java.util.List;
100 import java.util.Set;
101 
102 import javax.inject.Provider;
103 
104 /**
105  * Top level container/controller for the BiometricPrompt UI.
106  *
107  * @deprecated TODO(b/287311775): remove and merge view/layouts into new prompt.
108  */
109 @Deprecated
110 public class AuthContainerView extends LinearLayout
111         implements AuthDialog, WakefulnessLifecycle.Observer, CredentialView.Host {
112 
113     private static final String TAG = "AuthContainerView";
114 
115     private static final int ANIMATION_DURATION_SHOW_MS = 250;
116     private static final int ANIMATION_DURATION_AWAY_MS = 350;
117     private static final int ANIMATE_CREDENTIAL_START_DELAY_MS = 300;
118 
119     private static final int STATE_UNKNOWN = 0;
120     private static final int STATE_ANIMATING_IN = 1;
121     private static final int STATE_PENDING_DISMISS = 2;
122     private static final int STATE_SHOWING = 3;
123     private static final int STATE_ANIMATING_OUT = 4;
124     private static final int STATE_GONE = 5;
125 
126     private static final float BACKGROUND_DIM_AMOUNT = 0.5f;
127 
128     /** Shows biometric prompt dialog animation. */
129     private static final String SHOW = "show";
130     /** Dismiss biometric prompt dialog animation.  */
131     private static final String DISMISS = "dismiss";
132     /** Transit biometric prompt dialog to pin, password, pattern credential panel. */
133     private static final String TRANSIT = "transit";
134 
135     @Retention(RetentionPolicy.SOURCE)
136     @IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
137             STATE_ANIMATING_OUT, STATE_GONE})
138     private @interface ContainerState {}
139 
140     private final Config mConfig;
141     private final int mEffectiveUserId;
142     private final Handler mHandler;
143     private final IBinder mWindowToken = new Binder();
144     private final WindowManager mWindowManager;
145     private final Interpolator mLinearOutSlowIn;
146     private final LockPatternUtils mLockPatternUtils;
147     private final WakefulnessLifecycle mWakefulnessLifecycle;
148     private final AuthDialogPanelInteractionDetector mPanelInteractionDetector;
149     private final InteractionJankMonitor mInteractionJankMonitor;
150     private final CoroutineScope mApplicationCoroutineScope;
151 
152     // TODO(b/287311775): these should be migrated out once ready
153     private final @NonNull Provider<PromptSelectorInteractor> mPromptSelectorInteractorProvider;
154     // TODO(b/287311775): these should be migrated out of the view
155     private final Provider<CredentialViewModel> mCredentialViewModelProvider;
156     private final PromptViewModel mPromptViewModel;
157 
158     @VisibleForTesting final BiometricCallback mBiometricCallback;
159 
160     @Nullable private Spaghetti mBiometricView;
161     @Nullable private View mCredentialView;
162     private final AuthPanelController mPanelController;
163     private final ViewGroup mLayout;
164     private final ImageView mBackgroundView;
165     private final ScrollView mBiometricScrollView;
166     private final View mPanelView;
167     private final List<FingerprintSensorPropertiesInternal> mFpProps;
168     private final List<FaceSensorPropertiesInternal> mFaceProps;
169     private final float mTranslationY;
170     @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
171     private final Set<Integer> mFailedModalities = new HashSet<Integer>();
172     private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
173 
174     private final @Background DelayableExecutor mBackgroundExecutor;
175 
176     // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
177     @Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
178     // HAT received from LockSettingsService when credential is verified.
179     @Nullable private byte[] mCredentialAttestation;
180 
181     // TODO(b/313469218): remove when legacy prompt is replaced
182     @Deprecated
183     static class Config {
184         Context mContext;
185         AuthDialogCallback mCallback;
186         PromptInfo mPromptInfo;
187         boolean mRequireConfirmation;
188         int mUserId;
189         String mOpPackageName;
190         int[] mSensorIds;
191         boolean mSkipIntro;
192         long mOperationId;
193         long mRequestId = -1;
194         boolean mSkipAnimation = false;
195         ScaleFactorProvider mScaleProvider;
196     }
197 
198     @VisibleForTesting
199     final class BiometricCallback implements Spaghetti.Callback {
200         @Override
onAuthenticated()201         public void onAuthenticated() {
202             animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
203         }
204 
205         @Override
onUserCanceled()206         public void onUserCanceled() {
207             sendEarlyUserCanceled();
208             animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
209         }
210 
211         @Override
onButtonNegative()212         public void onButtonNegative() {
213             animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
214         }
215 
216         @Override
onButtonTryAgain()217         public void onButtonTryAgain() {
218             mFailedModalities.clear();
219             mConfig.mCallback.onTryAgainPressed(getRequestId());
220         }
221 
222         @Override
onContentViewMoreOptionsButtonPressed()223         public void onContentViewMoreOptionsButtonPressed() {
224             animateAway(AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS);
225         }
226 
227         @Override
onError()228         public void onError() {
229             animateAway(AuthDialogCallback.DISMISSED_ERROR);
230         }
231 
232         @Override
onUseDeviceCredential()233         public void onUseDeviceCredential() {
234             mConfig.mCallback.onDeviceCredentialPressed(getRequestId());
235             if (constraintBp()) {
236                 addCredentialView(false /* animatePanel */, true /* animateContents */);
237             } else {
238                 mHandler.postDelayed(() -> {
239                     addCredentialView(false /* animatePanel */, true /* animateContents */);
240                 }, mConfig.mSkipAnimation ? 0 : ANIMATE_CREDENTIAL_START_DELAY_MS);
241             }
242 
243             // TODO(b/313469218): Remove Config
244             mConfig.mPromptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL);
245         }
246 
247         @Override
onStartDelayedFingerprintSensor()248         public void onStartDelayedFingerprintSensor() {
249             mConfig.mCallback.onStartFingerprintNow(getRequestId());
250         }
251 
252         @Override
onAuthenticatedAndConfirmed()253         public void onAuthenticatedAndConfirmed() {
254             animateAway(AuthDialogCallback.DISMISSED_BUTTON_POSITIVE);
255         }
256     }
257 
258     @Override
onCredentialMatched(@onNull byte[] attestation)259     public void onCredentialMatched(@NonNull byte[] attestation) {
260         mCredentialAttestation = attestation;
261         animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
262     }
263 
264     @Override
onCredentialAborted()265     public void onCredentialAborted() {
266         sendEarlyUserCanceled();
267         animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
268     }
269 
270     @Override
onCredentialAttemptsRemaining(int remaining, @NonNull String messageBody)271     public void onCredentialAttemptsRemaining(int remaining, @NonNull String messageBody) {
272         // Only show dialog if <=1 attempts are left before wiping.
273         if (remaining == 1) {
274             showLastAttemptBeforeWipeDialog(messageBody);
275         } else if (remaining <= 0) {
276             showNowWipingDialog(messageBody);
277         }
278     }
279 
showLastAttemptBeforeWipeDialog(@onNull String messageBody)280     private void showLastAttemptBeforeWipeDialog(@NonNull String messageBody) {
281         final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
282                 .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title)
283                 .setMessage(messageBody)
284                 .setPositiveButton(android.R.string.ok, null)
285                 .create();
286         alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
287         alertDialog.show();
288     }
289 
showNowWipingDialog(@onNull String messageBody)290     private void showNowWipingDialog(@NonNull String messageBody) {
291         final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
292                 .setMessage(messageBody)
293                 .setPositiveButton(
294                         com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss,
295                         null /* OnClickListener */)
296                 .setOnDismissListener(
297                         dialog -> animateAway(AuthDialogCallback.DISMISSED_ERROR))
298                 .create();
299         alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
300         alertDialog.show();
301     }
302 
303     // 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 AuthDialogPanelInteractionDetector panelInteractionDetector, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor, @NonNull PromptViewModel promptViewModel, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull @Background DelayableExecutor bgExecutor, @NonNull VibratorHelper vibratorHelper)304     AuthContainerView(@NonNull Config config,
305             @NonNull CoroutineScope applicationCoroutineScope,
306             @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
307             @Nullable List<FaceSensorPropertiesInternal> faceProps,
308             @NonNull WakefulnessLifecycle wakefulnessLifecycle,
309             @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
310             @NonNull UserManager userManager,
311             @NonNull LockPatternUtils lockPatternUtils,
312             @NonNull InteractionJankMonitor jankMonitor,
313             @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor,
314             @NonNull PromptViewModel promptViewModel,
315             @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
316             @NonNull @Background DelayableExecutor bgExecutor,
317             @NonNull VibratorHelper vibratorHelper) {
318         this(config, applicationCoroutineScope, fpProps, faceProps,
319                 wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
320                 jankMonitor, promptSelectorInteractor, promptViewModel,
321                 credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor,
322                 vibratorHelper);
323     }
324 
325     @VisibleForTesting
AuthContainerView(@onNull Config config, @NonNull CoroutineScope applicationCoroutineScope, @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps, @NonNull WakefulnessLifecycle wakefulnessLifecycle, @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider, @NonNull PromptViewModel promptViewModel, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull Handler mainHandler, @NonNull @Background DelayableExecutor bgExecutor, @NonNull VibratorHelper vibratorHelper)326     AuthContainerView(@NonNull Config config,
327             @NonNull CoroutineScope applicationCoroutineScope,
328             @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
329             @Nullable List<FaceSensorPropertiesInternal> faceProps,
330             @NonNull WakefulnessLifecycle wakefulnessLifecycle,
331             @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
332             @NonNull UserManager userManager,
333             @NonNull LockPatternUtils lockPatternUtils,
334             @NonNull InteractionJankMonitor jankMonitor,
335             @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
336             @NonNull PromptViewModel promptViewModel,
337             @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
338             @NonNull Handler mainHandler,
339             @NonNull @Background DelayableExecutor bgExecutor,
340             @NonNull VibratorHelper vibratorHelper) {
341         super(config.mContext);
342 
343         mConfig = config;
344         mLockPatternUtils = lockPatternUtils;
345         mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
346         mHandler = mainHandler;
347         mWindowManager = mContext.getSystemService(WindowManager.class);
348         mWakefulnessLifecycle = wakefulnessLifecycle;
349         mPanelInteractionDetector = panelInteractionDetector;
350         mApplicationCoroutineScope = applicationCoroutineScope;
351 
352         mPromptViewModel = promptViewModel;
353         mTranslationY = getResources()
354                 .getDimension(R.dimen.biometric_dialog_animation_translation_offset);
355         mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
356         mBiometricCallback = new BiometricCallback();
357 
358         mFpProps = fpProps;
359         mFaceProps = faceProps;
360         final BiometricModalities biometricModalities = new BiometricModalities(
361                 Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
362                 Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds));
363 
364         final boolean isLandscape = mContext.getResources().getConfiguration().orientation
365                 == Configuration.ORIENTATION_LANDSCAPE;
366         mPromptSelectorInteractorProvider = promptSelectorInteractorProvider;
367         mPromptSelectorInteractorProvider.get().setPrompt(mConfig.mPromptInfo, mEffectiveUserId,
368                 getRequestId(), biometricModalities, mConfig.mOperationId, mConfig.mOpPackageName,
369                 false /*onSwitchToCredential*/, isLandscape);
370 
371         final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
372         final PromptKind kind = mPromptViewModel.getPromptKind().getValue();
373         if (constraintBp() && kind.isBiometric()) {
374             if (kind.isTwoPaneLandscapeBiometric()) {
375                 mLayout = (ConstraintLayout) layoutInflater.inflate(
376                         R.layout.biometric_prompt_two_pane_layout, this, false /* attachToRoot */);
377             } else {
378                 mLayout = (ConstraintLayout) layoutInflater.inflate(
379                         R.layout.biometric_prompt_one_pane_layout, this, false /* attachToRoot */);
380             }
381         } else {
382             mLayout = (FrameLayout) layoutInflater.inflate(
383                     R.layout.auth_container_view, this, false /* attachToRoot */);
384         }
385         mBiometricScrollView = mLayout.findViewById(R.id.biometric_scrollview);
386         addView(mLayout);
387         mBackgroundView = mLayout.findViewById(R.id.background);
388         ViewCompat.setAccessibilityDelegate(mBackgroundView, new AccessibilityDelegateCompat() {
389             @Override
390             public void onInitializeAccessibilityNodeInfo(View host,
391                     AccessibilityNodeInfoCompat info) {
392                 super.onInitializeAccessibilityNodeInfo(host, info);
393                 info.addAction(
394                         new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
395                                 AccessibilityNodeInfoCompat.ACTION_CLICK,
396                                 mContext.getString(R.string.biometric_dialog_cancel_authentication)
397                         )
398                 );
399             }
400         });
401 
402         mPanelView = mLayout.findViewById(R.id.panel);
403         if (!constraintBp()) {
404             final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
405                     android.R.attr.colorBackgroundFloating});
406             mPanelView.setBackgroundColor(ta.getColor(0, Color.WHITE));
407             ta.recycle();
408         }
409         mPanelController = new AuthPanelController(mContext, mPanelView);
410         mBackgroundExecutor = bgExecutor;
411         mInteractionJankMonitor = jankMonitor;
412         mCredentialViewModelProvider = credentialViewModelProvider;
413 
414         showPrompt(config, layoutInflater, promptViewModel,
415                 Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
416                 Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds),
417                 vibratorHelper);
418 
419         // TODO: De-dupe the logic with AuthCredentialPasswordView
420         setOnKeyListener((v, keyCode, event) -> {
421             if (keyCode != KeyEvent.KEYCODE_BACK) {
422                 return false;
423             }
424             if (event.getAction() == KeyEvent.ACTION_UP) {
425                 onBackInvoked();
426             }
427             return true;
428         });
429 
430         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
431         setFocusableInTouchMode(true);
432         requestFocus();
433     }
434 
showPrompt(@onNull Config config, @NonNull LayoutInflater layoutInflater, @NonNull PromptViewModel viewModel, @Nullable FingerprintSensorPropertiesInternal fpProps, @Nullable FaceSensorPropertiesInternal faceProps, @NonNull VibratorHelper vibratorHelper )435     private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
436             @NonNull PromptViewModel viewModel,
437             @Nullable FingerprintSensorPropertiesInternal fpProps,
438             @Nullable FaceSensorPropertiesInternal faceProps,
439             @NonNull VibratorHelper vibratorHelper
440     ) {
441         if (mPromptViewModel.getPromptKind().getValue().isBiometric()) {
442             addBiometricView(config, layoutInflater, viewModel, fpProps, faceProps, vibratorHelper);
443         } else if (mPromptViewModel.getPromptKind().getValue().isCredential()) {
444             if (constraintBp()) {
445                 addCredentialView(true, false);
446             }
447         } else {
448             mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId());
449         }
450     }
451 
addBiometricView(@onNull Config config, @NonNull LayoutInflater layoutInflater, @NonNull PromptViewModel viewModel, @Nullable FingerprintSensorPropertiesInternal fpProps, @Nullable FaceSensorPropertiesInternal faceProps, @NonNull VibratorHelper vibratorHelper)452     private void addBiometricView(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
453             @NonNull PromptViewModel viewModel,
454             @Nullable FingerprintSensorPropertiesInternal fpProps,
455             @Nullable FaceSensorPropertiesInternal faceProps,
456             @NonNull VibratorHelper vibratorHelper) {
457 
458         if (constraintBp()) {
459             mBiometricView = BiometricViewBinder.bind(mLayout, viewModel, null,
460                     // TODO(b/201510778): This uses the wrong timeout in some cases
461                     getJankListener(mLayout, TRANSIT,
462                             BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
463                     mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
464                     vibratorHelper);
465         } else {
466             final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
467                     R.layout.biometric_prompt_layout, null, false);
468             mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
469                     // TODO(b/201510778): This uses the wrong timeout in some cases
470                     getJankListener(view, TRANSIT,
471                             BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
472                     mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
473                     vibratorHelper);
474 
475             // TODO(b/251476085): migrate these dependencies
476             if (fpProps != null && fpProps.isAnyUdfpsType()) {
477                 view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps),
478                         config.mScaleProvider);
479             }
480         }
481     }
482 
onBackInvoked()483     private void onBackInvoked() {
484         sendEarlyUserCanceled();
485         animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
486     }
487 
sendEarlyUserCanceled()488     void sendEarlyUserCanceled() {
489         mConfig.mCallback.onSystemEvent(
490                 BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL, getRequestId());
491     }
492 
493     @Override
isAllowDeviceCredentials()494     public boolean isAllowDeviceCredentials() {
495         return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo);
496     }
497 
498     /**
499      * Adds the credential view. When going from biometric to credential view, the biometric
500      * view starts the panel expansion animation. If the credential view is being shown first,
501      * it should own the panel expansion.
502      * @param animatePanel if the credential view needs to own the panel expansion animation
503      */
addCredentialView(boolean animatePanel, boolean animateContents)504     private void addCredentialView(boolean animatePanel, boolean animateContents) {
505         final LayoutInflater factory = LayoutInflater.from(mContext);
506 
507         PromptKind credentialType = Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId);
508         final int layoutResourceId;
509         if (credentialType instanceof PromptKind.Pattern) {
510             layoutResourceId = R.layout.auth_credential_pattern_view;
511         } else if (credentialType instanceof PromptKind.Pin) {
512             layoutResourceId = R.layout.auth_credential_pin_view;
513         } else if (credentialType instanceof PromptKind.Password) {
514             layoutResourceId = R.layout.auth_credential_password_view;
515         } else {
516             throw new IllegalStateException("Unknown credential type: " + credentialType);
517         }
518         // TODO(b/288175645): Once AuthContainerView is removed, set 0dp in credential view xml
519         //  files with the corresponding left/right or top/bottom constraints being set to "parent".
520         mCredentialView = factory.inflate(layoutResourceId, mLayout, false);
521 
522         // The background is used for detecting taps / cancelling authentication. Since the
523         // credential view is full-screen and should not be canceled from background taps,
524         // disable it.
525         mBackgroundView.setOnClickListener(null);
526         mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
527         final CredentialViewModel vm = mCredentialViewModelProvider.get();
528         vm.setAnimateContents(animateContents);
529         ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel,
530                 mBiometricCallback);
531 
532         mLayout.addView(mCredentialView);
533     }
534 
535     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)536     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
537         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
538         mPanelController.setContainerDimensions(getMeasuredWidth(), getMeasuredHeight());
539     }
540 
541     @Override
onOrientationChanged()542     public void onOrientationChanged() {
543         if (!constraintBp()) {
544             updatePositionByCapability(true /* invalidate */);
545         }
546     }
547 
548     @Override
onAttachedToWindow()549     public void onAttachedToWindow() {
550         super.onAttachedToWindow();
551 
552         if (mContainerState == STATE_ANIMATING_OUT) {
553             return;
554         }
555 
556         mWakefulnessLifecycle.addObserver(this);
557         if (constraintBp()) {
558             // Do nothing on attachment with constraintLayout
559         } else if (mPromptViewModel.getPromptKind().getValue().isBiometric()) {
560             mBiometricScrollView.addView(mBiometricView.asView());
561         } else if (mPromptViewModel.getPromptKind().getValue().isCredential()) {
562             addCredentialView(true /* animatePanel */, false /* animateContents */);
563         } else {
564             throw new IllegalStateException("Unknown configuration: "
565                     + mConfig.mPromptInfo.getAuthenticators());
566         }
567 
568         if (!constraintBp()) {
569             mPanelInteractionDetector.enable(
570                     () -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED));
571             updatePositionByCapability(false /* invalidate */);
572         }
573 
574         if (mConfig.mSkipIntro) {
575             mContainerState = STATE_SHOWING;
576         } else {
577             mContainerState = STATE_ANIMATING_IN;
578             setY(mTranslationY);
579             setAlpha(0f);
580             final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
581             postOnAnimation(() -> {
582                 animate()
583                         .alpha(1f)
584                         .translationY(0)
585                         .setDuration(animateDuration)
586                         .setInterpolator(mLinearOutSlowIn)
587                         .withLayer()
588                         .setListener(getJankListener(this, SHOW, animateDuration))
589                         .withEndAction(this::onDialogAnimatedIn)
590                         .start();
591             });
592         }
593         OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
594         if (dispatcher != null) {
595             dispatcher.registerOnBackInvokedCallback(
596                     OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackCallback);
597         }
598     }
599 
getJankListener(View v, String type, long timeout)600     private Animator.AnimatorListener getJankListener(View v, String type, long timeout) {
601         return new Animator.AnimatorListener() {
602             @Override
603             public void onAnimationStart(@androidx.annotation.NonNull Animator animation) {
604                 if (!v.isAttachedToWindow()) {
605                     Log.w(TAG, "Un-attached view should not begin Jank trace.");
606                     return;
607                 }
608                 mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder.withView(
609                         CUJ_BIOMETRIC_PROMPT_TRANSITION, v).setTag(type).setTimeout(timeout));
610             }
611 
612             @Override
613             public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) {
614                 if (!v.isAttachedToWindow()) {
615                     Log.w(TAG, "Un-attached view should not end Jank trace.");
616                     return;
617                 }
618                 mInteractionJankMonitor.end(CUJ_BIOMETRIC_PROMPT_TRANSITION);
619             }
620 
621             @Override
622             public void onAnimationCancel(@androidx.annotation.NonNull Animator animation) {
623                 if (!v.isAttachedToWindow()) {
624                     Log.w(TAG, "Un-attached view should not cancel Jank trace.");
625                     return;
626                 }
627                 mInteractionJankMonitor.cancel(CUJ_BIOMETRIC_PROMPT_TRANSITION);
628             }
629 
630             @Override
631             public void onAnimationRepeat(@androidx.annotation.NonNull Animator animation) {
632                 // no-op
633             }
634         };
635     }
636 
637     private void updatePositionByCapability(boolean forceInvalidate) {
638         final FingerprintSensorPropertiesInternal fpProp = Utils.findFirstSensorProperties(
639                 mFpProps, mConfig.mSensorIds);
640         final FaceSensorPropertiesInternal faceProp = Utils.findFirstSensorProperties(
641                 mFaceProps, mConfig.mSensorIds);
642         if (fpProp != null && fpProp.isAnyUdfpsType()) {
643             maybeUpdatePositionForUdfps(forceInvalidate /* invalidate */);
644         }
645         if (faceProp != null && mBiometricView != null && mBiometricView.isFaceOnly()) {
646             alwaysUpdatePositionAtScreenBottom(forceInvalidate /* invalidate */);
647         }
648         if (fpProp != null && fpProp.sensorType == TYPE_POWER_BUTTON) {
649             alwaysUpdatePositionAtScreenBottom(forceInvalidate /* invalidate */);
650         }
651     }
652 
653     private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
654         if (view instanceof BiometricPromptLayout) {
655             // this will force the prompt to align itself on the edge of the screen
656             // instead of centering (temporary workaround to prevent small implicit view
657             // from breaking due to the way gravity / margins are set in the legacy
658             // AuthPanelController
659             return true;
660         }
661 
662         return false;
663     }
664 
665     private boolean maybeUpdatePositionForUdfps(boolean invalidate) {
666         final Display display = getDisplay();
667         if (display == null) {
668             return false;
669         }
670 
671         final DisplayInfo cachedDisplayInfo = new DisplayInfo();
672         display.getDisplayInfo(cachedDisplayInfo);
673         if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) {
674             return false;
675         }
676 
677         final int displayRotation = cachedDisplayInfo.rotation;
678         switch (displayRotation) {
679             case Surface.ROTATION_0:
680                 mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
681                 setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
682                 break;
683 
684             case Surface.ROTATION_90:
685                 mPanelController.setPosition(AuthPanelController.POSITION_RIGHT);
686                 setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
687                 break;
688 
689             case Surface.ROTATION_270:
690                 mPanelController.setPosition(AuthPanelController.POSITION_LEFT);
691                 setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
692                 break;
693 
694             case Surface.ROTATION_180:
695             default:
696                 Log.e(TAG, "Unsupported display rotation: " + displayRotation);
697                 mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
698                 setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
699                 break;
700         }
701 
702         if (invalidate) {
703             mPanelView.invalidateOutline();
704         }
705 
706         return true;
707     }
708 
709     private boolean alwaysUpdatePositionAtScreenBottom(boolean invalidate) {
710         final Display display = getDisplay();
711         if (display == null) {
712             return false;
713         }
714         if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) {
715             return false;
716         }
717 
718         final int displayRotation = display.getRotation();
719         switch (displayRotation) {
720             case Surface.ROTATION_0:
721             case Surface.ROTATION_90:
722             case Surface.ROTATION_270:
723             case Surface.ROTATION_180:
724                 mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
725                 setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
726                 break;
727             default:
728                 Log.e(TAG, "Unsupported display rotation: " + displayRotation);
729                 mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
730                 setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
731                 break;
732         }
733 
734         if (invalidate) {
735             mPanelView.invalidateOutline();
736         }
737 
738         return true;
739     }
740 
741     private void setScrollViewGravity(int gravity) {
742         final FrameLayout.LayoutParams params =
743                 (FrameLayout.LayoutParams) mBiometricScrollView.getLayoutParams();
744         params.gravity = gravity;
745         mBiometricScrollView.setLayoutParams(params);
746     }
747 
748     @Override
749     public void onDetachedFromWindow() {
750         mPanelInteractionDetector.disable();
751         OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
752         if (dispatcher != null) {
753             findOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mBackCallback);
754         }
755         super.onDetachedFromWindow();
756         mWakefulnessLifecycle.removeObserver(this);
757     }
758 
759     @Override
760     public void onStartedGoingToSleep() {
761         animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
762     }
763 
764     @Override
765     public void show(WindowManager wm) {
766         wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle()));
767     }
768 
769     private void forceExecuteAnimatedIn() {
770         if (mContainerState == STATE_ANIMATING_IN) {
771             //clear all animators
772             if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
773                 mCredentialView.animate().cancel();
774             }
775             mPanelView.animate().cancel();
776             mBiometricView.cancelAnimation();
777             animate().cancel();
778             onDialogAnimatedIn();
779         }
780     }
781 
782     @Override
783     public void dismissWithoutCallback(boolean animate) {
784         if (animate) {
785             animateAway(false /* sendReason */, 0 /* reason */);
786         } else {
787             forceExecuteAnimatedIn();
788             removeWindowIfAttached();
789         }
790     }
791 
792     @Override
793     public void dismissFromSystemServer() {
794         animateAway(false /* sendReason */, 0 /* reason */);
795     }
796 
797     @Override
798     public void onAuthenticationSucceeded(@Modality int modality) {
799         if (mBiometricView != null) {
800             mBiometricView.onAuthenticationSucceeded(modality);
801         } else {
802             Log.e(TAG, "onAuthenticationSucceeded(): mBiometricView is null");
803         }
804     }
805 
806     @Override
807     public void onAuthenticationFailed(@Modality int modality, String failureReason) {
808         if (mBiometricView != null) {
809             mFailedModalities.add(modality);
810             mBiometricView.onAuthenticationFailed(modality, failureReason);
811         } else {
812             Log.e(TAG, "onAuthenticationFailed(): mBiometricView is null");
813         }
814     }
815 
816     @Override
817     public void onHelp(@Modality int modality, String help) {
818         if (mBiometricView != null) {
819             mBiometricView.onHelp(modality, help);
820         } else {
821             Log.e(TAG, "onHelp(): mBiometricView is null");
822         }
823     }
824 
825     @Override
826     public void onError(@Modality int modality, String error) {
827         if (mBiometricView != null) {
828             mBiometricView.onError(modality, error);
829         } else {
830             Log.e(TAG, "onError(): mBiometricView is null");
831         }
832     }
833 
834     @Override
835     public void onPointerDown() {
836         if (mBiometricView != null) {
837             if (mFailedModalities.contains(TYPE_FACE)) {
838                 Log.d(TAG, "retrying failed modalities (pointer down)");
839                 mFailedModalities.remove(TYPE_FACE);
840                 mBiometricCallback.onButtonTryAgain();
841             }
842         } else {
843             Log.e(TAG, "onPointerDown(): mBiometricView is null");
844         }
845     }
846 
847     @Override
848     public String getOpPackageName() {
849         return mConfig.mOpPackageName;
850     }
851 
852     @Override
853     public long getRequestId() {
854         return mConfig.mRequestId;
855     }
856 
857     @Override
858     public void animateToCredentialUI(boolean isError) {
859         if (mBiometricView != null) {
860             mBiometricView.startTransitionToCredentialUI(isError);
861         } else {
862             Log.e(TAG, "animateToCredentialUI(): mBiometricView is null");
863         }
864     }
865 
866     void animateAway(@AuthDialogCallback.DismissedReason int reason) {
867         animateAway(true /* sendReason */, reason);
868     }
869 
870     private void animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason) {
871         if (mContainerState == STATE_ANIMATING_IN) {
872             Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
873             mContainerState = STATE_PENDING_DISMISS;
874             return;
875         }
876 
877         if (mContainerState == STATE_ANIMATING_OUT) {
878             Log.w(TAG, "Already dismissing, sendReason: " + sendReason + " reason: " + reason);
879             return;
880         }
881         mContainerState = STATE_ANIMATING_OUT;
882 
883         // Request hiding soft-keyboard before animating away credential UI, in case IME insets
884         // animation get delayed by dismissing animation.
885         if (isAttachedToWindow() && getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
886             getWindowInsetsController().hide(WindowInsets.Type.ime());
887         }
888 
889         if (sendReason) {
890             mPendingCallbackReason = reason;
891         } else {
892             mPendingCallbackReason = null;
893         }
894 
895         final Runnable endActionRunnable = () -> {
896             setVisibility(View.INVISIBLE);
897             if (Flags.customBiometricPrompt() && constraintBp()) {
898                 // TODO(b/288175645): resetPrompt calls should be lifecycle aware
899                 mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId());
900             }
901             removeWindowIfAttached();
902         };
903 
904         final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
905         postOnAnimation(() -> {
906             animate()
907                     .alpha(0f)
908                     .translationY(mTranslationY)
909                     .setDuration(animateDuration)
910                     .setInterpolator(mLinearOutSlowIn)
911                     .setListener(getJankListener(this, DISMISS, animateDuration))
912                     .setUpdateListener(animation -> {
913                         if (mWindowManager == null || getViewRootImpl() == null) {
914                             Log.w(TAG, "skip updateViewLayout() for dim animation.");
915                             return;
916                         }
917                         final WindowManager.LayoutParams lp = getViewRootImpl().mWindowAttributes;
918                         lp.dimAmount = (1.0f - (Float) animation.getAnimatedValue())
919                                 * BACKGROUND_DIM_AMOUNT;
920                         mWindowManager.updateViewLayout(this, lp);
921                     })
922                     .withLayer()
923                     .withEndAction(endActionRunnable)
924                     .start();
925         });
926     }
927 
928     private void sendPendingCallbackIfNotNull() {
929         Log.d(TAG, "pendingCallback: " + mPendingCallbackReason);
930         if (mPendingCallbackReason != null) {
931             mConfig.mCallback.onDismissed(mPendingCallbackReason,
932                     mCredentialAttestation, getRequestId());
933             mPendingCallbackReason = null;
934         }
935     }
936 
937     private void removeWindowIfAttached() {
938         sendPendingCallbackIfNotNull();
939 
940         if (mContainerState == STATE_GONE) {
941             return;
942         }
943         mContainerState = STATE_GONE;
944         if (isAttachedToWindow()) {
945             mWindowManager.removeViewImmediate(this);
946         }
947     }
948 
949     private void onDialogAnimatedIn() {
950         if (mContainerState == STATE_PENDING_DISMISS) {
951             Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
952             animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
953             return;
954         }
955         if (mContainerState == STATE_ANIMATING_OUT || mContainerState == STATE_GONE) {
956             Log.d(TAG, "onDialogAnimatedIn(): ignore, already animating out or gone - state: "
957                     + mContainerState);
958             return;
959         }
960         mContainerState = STATE_SHOWING;
961         if (mBiometricView != null) {
962             final boolean delayFingerprint = mBiometricView.isCoex() && !mConfig.mRequireConfirmation;
963             mConfig.mCallback.onDialogAnimatedIn(getRequestId(), !delayFingerprint);
964             mBiometricView.onDialogAnimatedIn(!delayFingerprint);
965         }
966     }
967 
968     @Override
969     public PromptViewModel getViewModel() {
970         return mPromptViewModel;
971     }
972 
973     @VisibleForTesting
974     static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
975         final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
976                 | WindowManager.LayoutParams.FLAG_SECURE
977                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
978                 | WindowManager.LayoutParams.FLAG_DIM_BEHIND;
979         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
980                 ViewGroup.LayoutParams.MATCH_PARENT,
981                 ViewGroup.LayoutParams.MATCH_PARENT,
982                 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
983                 windowFlags,
984                 PixelFormat.TRANSLUCENT);
985         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
986         lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~WindowInsets.Type.ime()
987                 & ~WindowInsets.Type.systemBars());
988         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
989         lp.setTitle("BiometricPrompt");
990         lp.accessibilityTitle = title;
991         lp.dimAmount = BACKGROUND_DIM_AMOUNT;
992         lp.token = windowToken;
993         return lp;
994     }
995 
996     @Override
997     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
998         pw.println("    isAttachedToWindow=" + isAttachedToWindow());
999         pw.println("    containerState=" + mContainerState);
1000         pw.println("    pendingCallbackReason=" + mPendingCallbackReason);
1001         pw.println("    config exist=" + (mConfig != null));
1002         if (mConfig != null) {
1003             pw.println("    config.sensorIds exist=" + (mConfig.mSensorIds != null));
1004         }
1005     }
1006 }
1007