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