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