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_FINGERPRINT; 20 import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; 21 22 import android.annotation.IntDef; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.content.Context; 26 import android.graphics.PixelFormat; 27 import android.hardware.biometrics.BiometricAuthenticator.Modality; 28 import android.hardware.biometrics.BiometricConstants; 29 import android.hardware.biometrics.PromptInfo; 30 import android.hardware.face.FaceSensorPropertiesInternal; 31 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 32 import android.os.Binder; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.IBinder; 36 import android.os.Looper; 37 import android.os.UserManager; 38 import android.util.Log; 39 import android.view.Display; 40 import android.view.Gravity; 41 import android.view.KeyEvent; 42 import android.view.LayoutInflater; 43 import android.view.Surface; 44 import android.view.View; 45 import android.view.ViewGroup; 46 import android.view.WindowInsets; 47 import android.view.WindowManager; 48 import android.view.animation.Interpolator; 49 import android.widget.FrameLayout; 50 import android.widget.ImageView; 51 import android.widget.LinearLayout; 52 import android.widget.ScrollView; 53 54 import com.android.internal.annotations.VisibleForTesting; 55 import com.android.systemui.Dependency; 56 import com.android.systemui.R; 57 import com.android.systemui.animation.Interpolators; 58 import com.android.systemui.keyguard.WakefulnessLifecycle; 59 60 import java.lang.annotation.Retention; 61 import java.lang.annotation.RetentionPolicy; 62 import java.util.List; 63 64 /** 65 * Top level container/controller for the BiometricPrompt UI. 66 */ 67 public class AuthContainerView extends LinearLayout 68 implements AuthDialog, WakefulnessLifecycle.Observer { 69 70 private static final String TAG = "BiometricPrompt/AuthContainerView"; 71 private static final int ANIMATION_DURATION_SHOW_MS = 250; 72 private static final int ANIMATION_DURATION_AWAY_MS = 350; // ms 73 74 static final int STATE_UNKNOWN = 0; 75 static final int STATE_ANIMATING_IN = 1; 76 static final int STATE_PENDING_DISMISS = 2; 77 static final int STATE_SHOWING = 3; 78 static final int STATE_ANIMATING_OUT = 4; 79 static final int STATE_GONE = 5; 80 81 @Retention(RetentionPolicy.SOURCE) 82 @IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING, 83 STATE_ANIMATING_OUT, STATE_GONE}) 84 @interface ContainerState {} 85 86 final Config mConfig; 87 final int mEffectiveUserId; 88 @Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps; 89 @Nullable private final List<FaceSensorPropertiesInternal> mFaceProps; 90 private final Handler mHandler; 91 private final Injector mInjector; 92 private final IBinder mWindowToken = new Binder(); 93 private final WindowManager mWindowManager; 94 private final AuthPanelController mPanelController; 95 private final Interpolator mLinearOutSlowIn; 96 @VisibleForTesting final BiometricCallback mBiometricCallback; 97 private final CredentialCallback mCredentialCallback; 98 99 @VisibleForTesting final FrameLayout mFrameLayout; 100 @VisibleForTesting @Nullable AuthBiometricView mBiometricView; 101 @VisibleForTesting @Nullable AuthCredentialView mCredentialView; 102 103 @VisibleForTesting final ImageView mBackgroundView; 104 @VisibleForTesting final ScrollView mBiometricScrollView; 105 private final View mPanelView; 106 107 private final float mTranslationY; 108 109 @VisibleForTesting final WakefulnessLifecycle mWakefulnessLifecycle; 110 111 @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN; 112 113 // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet. 114 @Nullable @AuthDialogCallback.DismissedReason Integer mPendingCallbackReason; 115 // HAT received from LockSettingsService when credential is verified. 116 @Nullable byte[] mCredentialAttestation; 117 118 static class Config { 119 Context mContext; 120 AuthDialogCallback mCallback; 121 PromptInfo mPromptInfo; 122 boolean mRequireConfirmation; 123 int mUserId; 124 String mOpPackageName; 125 int[] mSensorIds; 126 boolean mCredentialAllowed; 127 boolean mSkipIntro; 128 long mOperationId; 129 @BiometricMultiSensorMode int mMultiSensorConfig; 130 } 131 132 public static class Builder { 133 Config mConfig; 134 Builder(Context context)135 public Builder(Context context) { 136 mConfig = new Config(); 137 mConfig.mContext = context; 138 } 139 setCallback(AuthDialogCallback callback)140 public Builder setCallback(AuthDialogCallback callback) { 141 mConfig.mCallback = callback; 142 return this; 143 } 144 setPromptInfo(PromptInfo promptInfo)145 public Builder setPromptInfo(PromptInfo promptInfo) { 146 mConfig.mPromptInfo = promptInfo; 147 return this; 148 } 149 setRequireConfirmation(boolean requireConfirmation)150 public Builder setRequireConfirmation(boolean requireConfirmation) { 151 mConfig.mRequireConfirmation = requireConfirmation; 152 return this; 153 } 154 setUserId(int userId)155 public Builder setUserId(int userId) { 156 mConfig.mUserId = userId; 157 return this; 158 } 159 setOpPackageName(String opPackageName)160 public Builder setOpPackageName(String opPackageName) { 161 mConfig.mOpPackageName = opPackageName; 162 return this; 163 } 164 setSkipIntro(boolean skip)165 public Builder setSkipIntro(boolean skip) { 166 mConfig.mSkipIntro = skip; 167 return this; 168 } 169 setOperationId(long operationId)170 public Builder setOperationId(long operationId) { 171 mConfig.mOperationId = operationId; 172 return this; 173 } 174 175 /** The multi-sensor mode. */ setMultiSensorConfig(@iometricMultiSensorMode int multiSensorConfig)176 public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) { 177 mConfig.mMultiSensorConfig = multiSensorConfig; 178 return this; 179 } 180 build(int[] sensorIds, boolean credentialAllowed, @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps)181 public AuthContainerView build(int[] sensorIds, boolean credentialAllowed, 182 @Nullable List<FingerprintSensorPropertiesInternal> fpProps, 183 @Nullable List<FaceSensorPropertiesInternal> faceProps) { 184 mConfig.mSensorIds = sensorIds; 185 mConfig.mCredentialAllowed = credentialAllowed; 186 return new AuthContainerView(mConfig, new Injector(), fpProps, faceProps); 187 } 188 } 189 190 public static class Injector { getBiometricScrollView(FrameLayout parent)191 ScrollView getBiometricScrollView(FrameLayout parent) { 192 return parent.findViewById(R.id.biometric_scrollview); 193 } 194 inflateContainerView(LayoutInflater factory, ViewGroup root)195 FrameLayout inflateContainerView(LayoutInflater factory, ViewGroup root) { 196 return (FrameLayout) factory.inflate( 197 R.layout.auth_container_view, root, false /* attachToRoot */); 198 } 199 getPanelController(Context context, View panelView)200 AuthPanelController getPanelController(Context context, View panelView) { 201 return new AuthPanelController(context, panelView); 202 } 203 getBackgroundView(FrameLayout parent)204 ImageView getBackgroundView(FrameLayout parent) { 205 return parent.findViewById(R.id.background); 206 } 207 getPanelView(FrameLayout parent)208 View getPanelView(FrameLayout parent) { 209 return parent.findViewById(R.id.panel); 210 } 211 getAnimateCredentialStartDelayMs()212 int getAnimateCredentialStartDelayMs() { 213 return AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS; 214 } 215 getUserManager(Context context)216 UserManager getUserManager(Context context) { 217 return UserManager.get(context); 218 } 219 getCredentialType(Context context, int effectiveUserId)220 int getCredentialType(Context context, int effectiveUserId) { 221 return Utils.getCredentialType(context, effectiveUserId); 222 } 223 } 224 225 @VisibleForTesting 226 final class BiometricCallback implements AuthBiometricView.Callback { 227 @Override onAction(int action)228 public void onAction(int action) { 229 switch (action) { 230 case AuthBiometricView.Callback.ACTION_AUTHENTICATED: 231 animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED); 232 break; 233 case AuthBiometricView.Callback.ACTION_USER_CANCELED: 234 sendEarlyUserCanceled(); 235 animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); 236 break; 237 case AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE: 238 animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE); 239 break; 240 case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN: 241 mConfig.mCallback.onTryAgainPressed(); 242 break; 243 case AuthBiometricView.Callback.ACTION_ERROR: 244 animateAway(AuthDialogCallback.DISMISSED_ERROR); 245 break; 246 case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL: 247 mConfig.mCallback.onDeviceCredentialPressed(); 248 mHandler.postDelayed(() -> { 249 addCredentialView(false /* animatePanel */, true /* animateContents */); 250 }, mInjector.getAnimateCredentialStartDelayMs()); 251 break; 252 case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR: 253 mConfig.mCallback.onStartFingerprintNow(); 254 break; 255 default: 256 Log.e(TAG, "Unhandled action: " + action); 257 } 258 } 259 } 260 261 final class CredentialCallback implements AuthCredentialView.Callback { 262 @Override onCredentialMatched(byte[] attestation)263 public void onCredentialMatched(byte[] attestation) { 264 mCredentialAttestation = attestation; 265 animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); 266 } 267 } 268 269 @VisibleForTesting AuthContainerView(Config config, Injector injector, @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps)270 AuthContainerView(Config config, Injector injector, 271 @Nullable List<FingerprintSensorPropertiesInternal> fpProps, 272 @Nullable List<FaceSensorPropertiesInternal> faceProps) { 273 super(config.mContext); 274 275 mConfig = config; 276 mInjector = injector; 277 mFpProps = fpProps; 278 mFaceProps = faceProps; 279 280 mEffectiveUserId = mInjector.getUserManager(mContext) 281 .getCredentialOwnerProfile(mConfig.mUserId); 282 283 mHandler = new Handler(Looper.getMainLooper()); 284 mWindowManager = mContext.getSystemService(WindowManager.class); 285 mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class); 286 287 mTranslationY = getResources() 288 .getDimension(R.dimen.biometric_dialog_animation_translation_offset); 289 mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; 290 mBiometricCallback = new BiometricCallback(); 291 mCredentialCallback = new CredentialCallback(); 292 293 final LayoutInflater factory = LayoutInflater.from(mContext); 294 mFrameLayout = mInjector.inflateContainerView(factory, this); 295 296 mPanelView = mInjector.getPanelView(mFrameLayout); 297 mPanelController = mInjector.getPanelController(mContext, mPanelView); 298 299 // Inflate biometric view only if necessary. 300 final int sensorCount = config.mSensorIds.length; 301 if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { 302 if (sensorCount == 1) { 303 final int singleSensorAuthId = config.mSensorIds[0]; 304 if (Utils.containsSensorId(mFpProps, singleSensorAuthId)) { 305 FingerprintSensorPropertiesInternal sensorProps = null; 306 for (FingerprintSensorPropertiesInternal prop : mFpProps) { 307 if (prop.sensorId == singleSensorAuthId) { 308 sensorProps = prop; 309 break; 310 } 311 } 312 313 if (sensorProps.isAnyUdfpsType()) { 314 AuthBiometricUdfpsView udfpsView = (AuthBiometricUdfpsView) factory 315 .inflate(R.layout.auth_biometric_udfps_view, null, false); 316 udfpsView.setSensorProps(sensorProps); 317 mBiometricView = udfpsView; 318 } else { 319 mBiometricView = (AuthBiometricFingerprintView) factory 320 .inflate(R.layout.auth_biometric_fingerprint_view, null, false); 321 } 322 } else if (Utils.containsSensorId(mFaceProps, singleSensorAuthId)) { 323 mBiometricView = (AuthBiometricFaceView) 324 factory.inflate(R.layout.auth_biometric_face_view, null, false); 325 } else { 326 // Unknown sensorId 327 Log.e(TAG, "Unknown sensorId: " + singleSensorAuthId); 328 mBiometricView = null; 329 mBackgroundView = null; 330 mBiometricScrollView = null; 331 return; 332 } 333 } else if (sensorCount == 2) { 334 final int[] allSensors = findFaceAndFingerprintSensors(); 335 final int faceSensorId = allSensors[0]; 336 final int fingerprintSensorId = allSensors[1]; 337 338 if (fingerprintSensorId == -1 || faceSensorId == -1) { 339 Log.e(TAG, "Missing fingerprint or face for dual-sensor config"); 340 mBiometricView = null; 341 mBackgroundView = null; 342 mBiometricScrollView = null; 343 return; 344 } 345 346 FingerprintSensorPropertiesInternal fingerprintSensorProps = null; 347 for (FingerprintSensorPropertiesInternal prop : mFpProps) { 348 if (prop.sensorId == fingerprintSensorId) { 349 fingerprintSensorProps = prop; 350 break; 351 } 352 } 353 354 if (fingerprintSensorProps != null) { 355 final AuthBiometricFaceToFingerprintView faceToFingerprintView = 356 (AuthBiometricFaceToFingerprintView) factory.inflate( 357 R.layout.auth_biometric_face_to_fingerprint_view, null, false); 358 faceToFingerprintView.setFingerprintSensorProps(fingerprintSensorProps); 359 faceToFingerprintView.setModalityListener(new ModalityListener() { 360 @Override 361 public void onModalitySwitched(int oldModality, int newModality) { 362 maybeUpdatePositionForUdfps(true /* invalidate */); 363 } 364 }); 365 mBiometricView = faceToFingerprintView; 366 } else { 367 Log.e(TAG, "Fingerprint props not found for sensor ID: " + fingerprintSensorId); 368 mBiometricView = null; 369 mBackgroundView = null; 370 mBiometricScrollView = null; 371 return; 372 } 373 } else { 374 Log.e(TAG, "Unsupported sensor array, length: " + sensorCount); 375 mBiometricView = null; 376 mBackgroundView = null; 377 mBiometricScrollView = null; 378 return; 379 } 380 } 381 382 mBiometricScrollView = mInjector.getBiometricScrollView(mFrameLayout); 383 mBackgroundView = mInjector.getBackgroundView(mFrameLayout); 384 385 addView(mFrameLayout); 386 387 // init view before showing 388 if (mBiometricView != null) { 389 mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation); 390 mBiometricView.setPanelController(mPanelController); 391 mBiometricView.setPromptInfo(mConfig.mPromptInfo); 392 mBiometricView.setCallback(mBiometricCallback); 393 mBiometricView.setBackgroundView(mBackgroundView); 394 mBiometricView.setUserId(mConfig.mUserId); 395 mBiometricView.setEffectiveUserId(mEffectiveUserId); 396 } 397 398 // TODO: De-dupe the logic with AuthCredentialPasswordView 399 setOnKeyListener((v, keyCode, event) -> { 400 if (keyCode != KeyEvent.KEYCODE_BACK) { 401 return false; 402 } 403 if (event.getAction() == KeyEvent.ACTION_UP) { 404 sendEarlyUserCanceled(); 405 animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); 406 } 407 return true; 408 }); 409 410 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 411 setFocusableInTouchMode(true); 412 requestFocus(); 413 } 414 sendEarlyUserCanceled()415 void sendEarlyUserCanceled() { 416 mConfig.mCallback.onSystemEvent( 417 BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL); 418 } 419 420 @Override isAllowDeviceCredentials()421 public boolean isAllowDeviceCredentials() { 422 return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo); 423 } 424 addBiometricView()425 private void addBiometricView() { 426 mBiometricScrollView.addView(mBiometricView); 427 } 428 429 /** 430 * Adds the credential view. When going from biometric to credential view, the biometric 431 * view starts the panel expansion animation. If the credential view is being shown first, 432 * it should own the panel expansion. 433 * @param animatePanel if the credential view needs to own the panel expansion animation 434 */ addCredentialView(boolean animatePanel, boolean animateContents)435 private void addCredentialView(boolean animatePanel, boolean animateContents) { 436 final LayoutInflater factory = LayoutInflater.from(mContext); 437 438 final @Utils.CredentialType int credentialType = mInjector.getCredentialType( 439 mContext, mEffectiveUserId); 440 441 switch (credentialType) { 442 case Utils.CREDENTIAL_PATTERN: 443 mCredentialView = (AuthCredentialView) factory.inflate( 444 R.layout.auth_credential_pattern_view, null, false); 445 break; 446 case Utils.CREDENTIAL_PIN: 447 case Utils.CREDENTIAL_PASSWORD: 448 mCredentialView = (AuthCredentialView) factory.inflate( 449 R.layout.auth_credential_password_view, null, false); 450 break; 451 default: 452 throw new IllegalStateException("Unknown credential type: " + credentialType); 453 } 454 455 // The background is used for detecting taps / cancelling authentication. Since the 456 // credential view is full-screen and should not be canceled from background taps, 457 // disable it. 458 mBackgroundView.setOnClickListener(null); 459 mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 460 461 mCredentialView.setContainerView(this); 462 mCredentialView.setUserId(mConfig.mUserId); 463 mCredentialView.setOperationId(mConfig.mOperationId); 464 mCredentialView.setEffectiveUserId(mEffectiveUserId); 465 mCredentialView.setCredentialType(credentialType); 466 mCredentialView.setCallback(mCredentialCallback); 467 mCredentialView.setPromptInfo(mConfig.mPromptInfo); 468 mCredentialView.setPanelController(mPanelController, animatePanel); 469 mCredentialView.setShouldAnimateContents(animateContents); 470 mFrameLayout.addView(mCredentialView); 471 } 472 473 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)474 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 475 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 476 mPanelController.setContainerDimensions(getMeasuredWidth(), getMeasuredHeight()); 477 } 478 479 @Override onOrientationChanged()480 public void onOrientationChanged() { 481 maybeUpdatePositionForUdfps(true /* invalidate */); 482 } 483 484 @Override onAttachedToWindow()485 public void onAttachedToWindow() { 486 super.onAttachedToWindow(); 487 onAttachedToWindowInternal(); 488 } 489 490 @VisibleForTesting onAttachedToWindowInternal()491 void onAttachedToWindowInternal() { 492 mWakefulnessLifecycle.addObserver(this); 493 494 if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { 495 addBiometricView(); 496 } else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) { 497 addCredentialView(true /* animatePanel */, false /* animateContents */); 498 } else { 499 throw new IllegalStateException("Unknown configuration: " 500 + mConfig.mPromptInfo.getAuthenticators()); 501 } 502 503 maybeUpdatePositionForUdfps(false /* invalidate */); 504 505 if (mConfig.mSkipIntro) { 506 mContainerState = STATE_SHOWING; 507 } else { 508 mContainerState = STATE_ANIMATING_IN; 509 // The background panel and content are different views since we need to be able to 510 // animate them separately in other places. 511 mPanelView.setY(mTranslationY); 512 mBiometricScrollView.setY(mTranslationY); 513 514 setAlpha(0f); 515 postOnAnimation(() -> { 516 mPanelView.animate() 517 .translationY(0) 518 .setDuration(ANIMATION_DURATION_SHOW_MS) 519 .setInterpolator(mLinearOutSlowIn) 520 .withLayer() 521 .withEndAction(this::onDialogAnimatedIn) 522 .start(); 523 mBiometricScrollView.animate() 524 .translationY(0) 525 .setDuration(ANIMATION_DURATION_SHOW_MS) 526 .setInterpolator(mLinearOutSlowIn) 527 .withLayer() 528 .start(); 529 if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { 530 mCredentialView.setY(mTranslationY); 531 mCredentialView.animate() 532 .translationY(0) 533 .setDuration(ANIMATION_DURATION_SHOW_MS) 534 .setInterpolator(mLinearOutSlowIn) 535 .withLayer() 536 .start(); 537 } 538 animate() 539 .alpha(1f) 540 .setDuration(ANIMATION_DURATION_SHOW_MS) 541 .setInterpolator(mLinearOutSlowIn) 542 .withLayer() 543 .start(); 544 }); 545 } 546 } 547 shouldUpdatePositionForUdfps(@onNull View view)548 private static boolean shouldUpdatePositionForUdfps(@NonNull View view) { 549 if (view instanceof AuthBiometricUdfpsView) { 550 return true; 551 } 552 553 if (view instanceof AuthBiometricFaceToFingerprintView) { 554 AuthBiometricFaceToFingerprintView faceToFingerprintView = 555 (AuthBiometricFaceToFingerprintView) view; 556 return faceToFingerprintView.getActiveSensorType() == TYPE_FINGERPRINT 557 && faceToFingerprintView.isFingerprintUdfps(); 558 } 559 560 return false; 561 } 562 maybeUpdatePositionForUdfps(boolean invalidate)563 private boolean maybeUpdatePositionForUdfps(boolean invalidate) { 564 final Display display = getDisplay(); 565 if (display == null) { 566 return false; 567 } 568 if (!shouldUpdatePositionForUdfps(mBiometricView)) { 569 return false; 570 } 571 572 final int displayRotation = display.getRotation(); 573 switch (displayRotation) { 574 case Surface.ROTATION_0: 575 mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); 576 setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 577 break; 578 579 case Surface.ROTATION_90: 580 mPanelController.setPosition(AuthPanelController.POSITION_RIGHT); 581 setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); 582 break; 583 584 case Surface.ROTATION_270: 585 mPanelController.setPosition(AuthPanelController.POSITION_LEFT); 586 setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); 587 break; 588 589 case Surface.ROTATION_180: 590 default: 591 Log.e(TAG, "Unsupported display rotation: " + displayRotation); 592 mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); 593 setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); 594 break; 595 } 596 597 if (invalidate) { 598 mPanelView.invalidateOutline(); 599 mBiometricView.requestLayout(); 600 } 601 602 return true; 603 } 604 setScrollViewGravity(int gravity)605 private void setScrollViewGravity(int gravity) { 606 final FrameLayout.LayoutParams params = 607 (FrameLayout.LayoutParams) mBiometricScrollView.getLayoutParams(); 608 params.gravity = gravity; 609 mBiometricScrollView.setLayoutParams(params); 610 } 611 612 @Override onDetachedFromWindow()613 public void onDetachedFromWindow() { 614 super.onDetachedFromWindow(); 615 mWakefulnessLifecycle.removeObserver(this); 616 } 617 618 @Override onStartedGoingToSleep()619 public void onStartedGoingToSleep() { 620 animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); 621 } 622 623 @Override show(WindowManager wm, @Nullable Bundle savedState)624 public void show(WindowManager wm, @Nullable Bundle savedState) { 625 if (mBiometricView != null) { 626 mBiometricView.restoreState(savedState); 627 } 628 wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle())); 629 } 630 631 @Override dismissWithoutCallback(boolean animate)632 public void dismissWithoutCallback(boolean animate) { 633 if (animate) { 634 animateAway(false /* sendReason */, 0 /* reason */); 635 } else { 636 removeWindowIfAttached(); 637 } 638 } 639 640 @Override dismissFromSystemServer()641 public void dismissFromSystemServer() { 642 animateAway(false /* sendReason */, 0 /* reason */); 643 } 644 645 @Override onAuthenticationSucceeded()646 public void onAuthenticationSucceeded() { 647 mBiometricView.onAuthenticationSucceeded(); 648 } 649 650 @Override onAuthenticationFailed(@odality int modality, String failureReason)651 public void onAuthenticationFailed(@Modality int modality, String failureReason) { 652 mBiometricView.onAuthenticationFailed(modality, failureReason); 653 } 654 655 @Override onHelp(@odality int modality, String help)656 public void onHelp(@Modality int modality, String help) { 657 mBiometricView.onHelp(modality, help); 658 } 659 660 @Override onError(@odality int modality, String error)661 public void onError(@Modality int modality, String error) { 662 mBiometricView.onError(modality, error); 663 } 664 665 @Override onSaveState(@onNull Bundle outState)666 public void onSaveState(@NonNull Bundle outState) { 667 outState.putInt(AuthDialog.KEY_CONTAINER_STATE, mContainerState); 668 // In the case where biometric and credential are both allowed, we can assume that 669 // biometric isn't showing if credential is showing since biometric is shown first. 670 outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING, 671 mBiometricView != null && mCredentialView == null); 672 outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null); 673 674 if (mBiometricView != null) { 675 mBiometricView.onSaveState(outState); 676 } 677 } 678 679 @Override getOpPackageName()680 public String getOpPackageName() { 681 return mConfig.mOpPackageName; 682 } 683 684 @Override animateToCredentialUI()685 public void animateToCredentialUI() { 686 mBiometricView.startTransitionToCredentialUI(); 687 } 688 689 @VisibleForTesting animateAway(int reason)690 void animateAway(int reason) { 691 animateAway(true /* sendReason */, reason); 692 } 693 animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason)694 private void animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason) { 695 if (mContainerState == STATE_ANIMATING_IN) { 696 Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn"); 697 mContainerState = STATE_PENDING_DISMISS; 698 return; 699 } 700 701 if (mContainerState == STATE_ANIMATING_OUT) { 702 Log.w(TAG, "Already dismissing, sendReason: " + sendReason + " reason: " + reason); 703 return; 704 } 705 mContainerState = STATE_ANIMATING_OUT; 706 707 if (sendReason) { 708 mPendingCallbackReason = reason; 709 } else { 710 mPendingCallbackReason = null; 711 } 712 713 final Runnable endActionRunnable = () -> { 714 setVisibility(View.INVISIBLE); 715 removeWindowIfAttached(); 716 }; 717 718 postOnAnimation(() -> { 719 mPanelView.animate() 720 .translationY(mTranslationY) 721 .setDuration(ANIMATION_DURATION_AWAY_MS) 722 .setInterpolator(mLinearOutSlowIn) 723 .withLayer() 724 .withEndAction(endActionRunnable) 725 .start(); 726 mBiometricScrollView.animate() 727 .translationY(mTranslationY) 728 .setDuration(ANIMATION_DURATION_AWAY_MS) 729 .setInterpolator(mLinearOutSlowIn) 730 .withLayer() 731 .start(); 732 if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { 733 mCredentialView.animate() 734 .translationY(mTranslationY) 735 .setDuration(ANIMATION_DURATION_AWAY_MS) 736 .setInterpolator(mLinearOutSlowIn) 737 .withLayer() 738 .start(); 739 } 740 animate() 741 .alpha(0f) 742 .setDuration(ANIMATION_DURATION_AWAY_MS) 743 .setInterpolator(mLinearOutSlowIn) 744 .withLayer() 745 .start(); 746 }); 747 } 748 sendPendingCallbackIfNotNull()749 private void sendPendingCallbackIfNotNull() { 750 Log.d(TAG, "pendingCallback: " + mPendingCallbackReason); 751 if (mPendingCallbackReason != null) { 752 mConfig.mCallback.onDismissed(mPendingCallbackReason, mCredentialAttestation); 753 mPendingCallbackReason = null; 754 } 755 } 756 removeWindowIfAttached()757 private void removeWindowIfAttached() { 758 sendPendingCallbackIfNotNull(); 759 760 if (mContainerState == STATE_GONE) { 761 return; 762 } 763 mContainerState = STATE_GONE; 764 mWindowManager.removeView(this); 765 } 766 767 @VisibleForTesting onDialogAnimatedIn()768 void onDialogAnimatedIn() { 769 if (mContainerState == STATE_PENDING_DISMISS) { 770 Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); 771 animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); 772 return; 773 } 774 mContainerState = STATE_SHOWING; 775 if (mBiometricView != null) { 776 mConfig.mCallback.onDialogAnimatedIn(); 777 mBiometricView.onDialogAnimatedIn(); 778 } 779 } 780 781 @VisibleForTesting getLayoutParams(IBinder windowToken, CharSequence title)782 static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, 783 CharSequence title) { 784 final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED 785 | WindowManager.LayoutParams.FLAG_SECURE; 786 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 787 ViewGroup.LayoutParams.MATCH_PARENT, 788 ViewGroup.LayoutParams.MATCH_PARENT, 789 WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL, 790 windowFlags, 791 PixelFormat.TRANSLUCENT); 792 lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 793 lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~WindowInsets.Type.ime()); 794 lp.setTitle("BiometricPrompt"); 795 lp.accessibilityTitle = title; 796 lp.token = windowToken; 797 return lp; 798 } 799 800 // returns [face, fingerprint] sensor ids (id is -1 if not present) findFaceAndFingerprintSensors()801 private int[] findFaceAndFingerprintSensors() { 802 int faceSensorId = -1; 803 int fingerprintSensorId = -1; 804 805 for (final int sensorId : mConfig.mSensorIds) { 806 if (Utils.containsSensorId(mFpProps, sensorId)) { 807 fingerprintSensorId = sensorId; 808 } else if (Utils.containsSensorId(mFaceProps, sensorId)) { 809 faceSensorId = sensorId; 810 } 811 812 if (fingerprintSensorId != -1 && faceSensorId != -1) { 813 break; 814 } 815 } 816 817 return new int[] {faceSensorId, fingerprintSensorId}; 818 } 819 } 820