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