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