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