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