blob: c9cc3e29f68e224fa4cb8d721e91aec0ecce54e3 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
import android.animation.Animator;
import android.annotation.DurationMillisLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.PixelFormat;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.PromptInfo;
import android.hardware.face.FaceSensorPropertiesInternal;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.UserManager;
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.util.concurrency.DelayableExecutor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Top level container/controller for the BiometricPrompt UI.
*/
public class AuthContainerView extends LinearLayout
implements AuthDialog, WakefulnessLifecycle.Observer {
private static final String TAG = "AuthContainerView";
private static final int ANIMATION_DURATION_SHOW_MS = 250;
private static final int ANIMATION_DURATION_AWAY_MS = 350;
private static final int STATE_UNKNOWN = 0;
private static final int STATE_ANIMATING_IN = 1;
private static final int STATE_PENDING_DISMISS = 2;
private static final int STATE_SHOWING = 3;
private static final int STATE_ANIMATING_OUT = 4;
private static final int STATE_GONE = 5;
private static final float BACKGROUND_DIM_AMOUNT = 0.5f;
/** Shows biometric prompt dialog animation. */
private static final String SHOW = "show";
/** Dismiss biometric prompt dialog animation. */
private static final String DISMISS = "dismiss";
/** Transit biometric prompt dialog to pin, password, pattern credential panel. */
private static final String TRANSIT = "transit";
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
STATE_ANIMATING_OUT, STATE_GONE})
private @interface ContainerState {}
private final Config mConfig;
private final int mEffectiveUserId;
private final Handler mHandler;
private final IBinder mWindowToken = new Binder();
private final WindowManager mWindowManager;
private final Interpolator mLinearOutSlowIn;
private final CredentialCallback mCredentialCallback;
private final LockPatternUtils mLockPatternUtils;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private final InteractionJankMonitor mInteractionJankMonitor;
@VisibleForTesting final BiometricCallback mBiometricCallback;
@Nullable private AuthBiometricView mBiometricView;
@Nullable private AuthCredentialView mCredentialView;
private final AuthPanelController mPanelController;
private final FrameLayout mFrameLayout;
private final ImageView mBackgroundView;
private final ScrollView mBiometricScrollView;
private final View mPanelView;
private final float mTranslationY;
@ContainerState private int mContainerState = STATE_UNKNOWN;
private final Set<Integer> mFailedModalities = new HashSet<Integer>();
private final @Background DelayableExecutor mBackgroundExecutor;
// Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
@Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
// HAT received from LockSettingsService when credential is verified.
@Nullable private byte[] mCredentialAttestation;
@VisibleForTesting
static class Config {
Context mContext;
AuthDialogCallback mCallback;
PromptInfo mPromptInfo;
boolean mRequireConfirmation;
int mUserId;
String mOpPackageName;
int[] mSensorIds;
boolean mSkipIntro;
long mOperationId;
long mRequestId = -1;
boolean mSkipAnimation = false;
@BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT;
ScaleFactorProvider mScaleProvider;
}
public static class Builder {
Config mConfig;
public Builder(Context context) {
mConfig = new Config();
mConfig.mContext = context;
}
public Builder setCallback(AuthDialogCallback callback) {
mConfig.mCallback = callback;
return this;
}
public Builder setPromptInfo(PromptInfo promptInfo) {
mConfig.mPromptInfo = promptInfo;
return this;
}
public Builder setRequireConfirmation(boolean requireConfirmation) {
mConfig.mRequireConfirmation = requireConfirmation;
return this;
}
public Builder setUserId(int userId) {
mConfig.mUserId = userId;
return this;
}
public Builder setOpPackageName(String opPackageName) {
mConfig.mOpPackageName = opPackageName;
return this;
}
public Builder setSkipIntro(boolean skip) {
mConfig.mSkipIntro = skip;
return this;
}
public Builder setOperationId(@DurationMillisLong long operationId) {
mConfig.mOperationId = operationId;
return this;
}
/** Unique id for this request. */
public Builder setRequestId(long requestId) {
mConfig.mRequestId = requestId;
return this;
}
@VisibleForTesting
public Builder setSkipAnimationDuration(boolean skip) {
mConfig.mSkipAnimation = skip;
return this;
}
/** The multi-sensor mode. */
public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) {
mConfig.mMultiSensorConfig = multiSensorConfig;
return this;
}
public Builder setScaleFactorProvider(ScaleFactorProvider scaleProvider) {
mConfig.mScaleProvider = scaleProvider;
return this;
}
public AuthContainerView build(@Background DelayableExecutor bgExecutor, int[] sensorIds,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull InteractionJankMonitor jankMonitor) {
mConfig.mSensorIds = sensorIds;
return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
userManager, lockPatternUtils, jankMonitor, new Handler(Looper.getMainLooper()),
bgExecutor);
}
}
@VisibleForTesting
final class BiometricCallback implements AuthBiometricView.Callback {
@Override
public void onAction(int action) {
switch (action) {
case AuthBiometricView.Callback.ACTION_AUTHENTICATED:
animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED);
break;
case AuthBiometricView.Callback.ACTION_USER_CANCELED:
sendEarlyUserCanceled();
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
break;
case AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE:
animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE);
break;
case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN:
mFailedModalities.clear();
mConfig.mCallback.onTryAgainPressed();
break;
case AuthBiometricView.Callback.ACTION_ERROR:
animateAway(AuthDialogCallback.DISMISSED_ERROR);
break;
case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL:
mConfig.mCallback.onDeviceCredentialPressed();
mHandler.postDelayed(() -> {
addCredentialView(false /* animatePanel */, true /* animateContents */);
}, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS);
break;
default:
Log.e(TAG, "Unhandled action: " + action);
}
}
}
final class CredentialCallback implements AuthCredentialView.Callback {
@Override
public void onCredentialMatched(byte[] attestation) {
mCredentialAttestation = attestation;
animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
}
}
@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) {
super(config.mContext);
mConfig = config;
mLockPatternUtils = lockPatternUtils;
mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
mHandler = mainHandler;
mWindowManager = mContext.getSystemService(WindowManager.class);
mWakefulnessLifecycle = wakefulnessLifecycle;
mTranslationY = getResources()
.getDimension(R.dimen.biometric_dialog_animation_translation_offset);
mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
mBiometricCallback = new BiometricCallback();
mCredentialCallback = new CredentialCallback();
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
mFrameLayout = (FrameLayout) layoutInflater.inflate(
R.layout.auth_container_view, this, false /* attachToRoot */);
addView(mFrameLayout);
mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
mBackgroundView = mFrameLayout.findViewById(R.id.background);
mPanelView = mFrameLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
mInteractionJankMonitor = jankMonitor;
// Inflate biometric view only if necessary.
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
final FingerprintSensorPropertiesInternal fpProperties =
Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds);
final FaceSensorPropertiesInternal faceProperties =
Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds);
if (fpProperties != null && faceProperties != null) {
final AuthBiometricFingerprintAndFaceView fingerprintAndFaceView =
(AuthBiometricFingerprintAndFaceView) layoutInflater.inflate(
R.layout.auth_biometric_fingerprint_and_face_view, null, false);
fingerprintAndFaceView.setSensorProperties(fpProperties);
fingerprintAndFaceView.setScaleFactorProvider(config.mScaleProvider);
fingerprintAndFaceView.updateOverrideIconLayoutParamsSize();
mBiometricView = fingerprintAndFaceView;
} else if (fpProperties != null) {
final AuthBiometricFingerprintView fpView =
(AuthBiometricFingerprintView) layoutInflater.inflate(
R.layout.auth_biometric_fingerprint_view, null, false);
fpView.setSensorProperties(fpProperties);
fpView.setScaleFactorProvider(config.mScaleProvider);
fpView.updateOverrideIconLayoutParamsSize();
mBiometricView = fpView;
} else if (faceProperties != null) {
mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate(
R.layout.auth_biometric_face_view, null, false);
} else {
Log.e(TAG, "No sensors found!");
}
}
// init view before showing
if (mBiometricView != null) {
mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
mBiometricView.setPanelController(mPanelController);
mBiometricView.setPromptInfo(mConfig.mPromptInfo);
mBiometricView.setCallback(mBiometricCallback);
mBiometricView.setBackgroundView(mBackgroundView);
mBiometricView.setUserId(mConfig.mUserId);
mBiometricView.setEffectiveUserId(mEffectiveUserId);
mBiometricView.setJankListener(getJankListener(mBiometricView, TRANSIT,
AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS));
}
// TODO: De-dupe the logic with AuthCredentialPasswordView
setOnKeyListener((v, keyCode, event) -> {
if (keyCode != KeyEvent.KEYCODE_BACK) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_UP) {
sendEarlyUserCanceled();
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
}
return true;
});
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
setFocusableInTouchMode(true);
requestFocus();
}
void sendEarlyUserCanceled() {
mConfig.mCallback.onSystemEvent(
BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL);
}
@Override
public boolean isAllowDeviceCredentials() {
return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo);
}
/**
* Adds the credential view. When going from biometric to credential view, the biometric
* view starts the panel expansion animation. If the credential view is being shown first,
* it should own the panel expansion.
* @param animatePanel if the credential view needs to own the panel expansion animation
*/
private void addCredentialView(boolean animatePanel, boolean animateContents) {
final LayoutInflater factory = LayoutInflater.from(mContext);
@Utils.CredentialType final int credentialType = Utils.getCredentialType(
mLockPatternUtils, mEffectiveUserId);
switch (credentialType) {
case Utils.CREDENTIAL_PATTERN:
mCredentialView = (AuthCredentialView) factory.inflate(
R.layout.auth_credential_pattern_view, null, false);
break;
case Utils.CREDENTIAL_PIN:
case Utils.CREDENTIAL_PASSWORD:
mCredentialView = (AuthCredentialView) factory.inflate(
R.layout.auth_credential_password_view, null, false);
break;
default:
throw new IllegalStateException("Unknown credential type: " + credentialType);
}
// The background is used for detecting taps / cancelling authentication. Since the
// credential view is full-screen and should not be canceled from background taps,
// disable it.
mBackgroundView.setOnClickListener(null);
mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
mCredentialView.setContainerView(this);
mCredentialView.setUserId(mConfig.mUserId);
mCredentialView.setOperationId(mConfig.mOperationId);
mCredentialView.setEffectiveUserId(mEffectiveUserId);
mCredentialView.setCredentialType(credentialType);
mCredentialView.setCallback(mCredentialCallback);
mCredentialView.setPromptInfo(mConfig.mPromptInfo);
mCredentialView.setPanelController(mPanelController, animatePanel);
mCredentialView.setShouldAnimateContents(animateContents);
mCredentialView.setBackgroundExecutor(mBackgroundExecutor);
mFrameLayout.addView(mCredentialView);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mPanelController.setContainerDimensions(getMeasuredWidth(), getMeasuredHeight());
}
@Override
public void onOrientationChanged() {
maybeUpdatePositionForUdfps(true /* invalidate */);
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus) {
Log.v(TAG, "Lost window focus, dismissing the dialog");
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
}
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mWakefulnessLifecycle.addObserver(this);
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
mBiometricScrollView.addView(mBiometricView);
} else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
addCredentialView(true /* animatePanel */, false /* animateContents */);
} else {
throw new IllegalStateException("Unknown configuration: "
+ mConfig.mPromptInfo.getAuthenticators());
}
maybeUpdatePositionForUdfps(false /* invalidate */);
if (mConfig.mSkipIntro) {
mContainerState = STATE_SHOWING;
} else {
mContainerState = STATE_ANIMATING_IN;
// The background panel and content are different views since we need to be able to
// animate them separately in other places.
mPanelView.setY(mTranslationY);
mBiometricScrollView.setY(mTranslationY);
setAlpha(0f);
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
postOnAnimation(() -> {
mPanelView.animate()
.translationY(0)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.setListener(getJankListener(mPanelView, SHOW, animateDuration))
.withLayer()
.withEndAction(this::onDialogAnimatedIn)
.start();
mBiometricScrollView.animate()
.translationY(0)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.setListener(getJankListener(mBiometricScrollView, SHOW, animateDuration))
.withLayer()
.start();
if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
mCredentialView.setY(mTranslationY);
mCredentialView.animate()
.translationY(0)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.setListener(getJankListener(mCredentialView, SHOW, animateDuration))
.withLayer()
.start();
}
animate()
.alpha(1f)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.withLayer()
.setListener(getJankListener(this, SHOW, animateDuration))
.start();
});
}
}
private Animator.AnimatorListener getJankListener(View v, String type, long timeout) {
return new Animator.AnimatorListener() {
@Override
public void onAnimationStart(@androidx.annotation.NonNull Animator animation) {
if (!v.isAttachedToWindow()) {
Log.w(TAG, "Un-attached view should not begin Jank trace.");
return;
}
mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder.withView(
CUJ_BIOMETRIC_PROMPT_TRANSITION, v).setTag(type).setTimeout(timeout));
}
@Override
public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) {
if (!v.isAttachedToWindow()) {
Log.w(TAG, "Un-attached view should not end Jank trace.");
return;
}
mInteractionJankMonitor.end(CUJ_BIOMETRIC_PROMPT_TRANSITION);
}
@Override
public void onAnimationCancel(@androidx.annotation.NonNull Animator animation) {
if (!v.isAttachedToWindow()) {
Log.w(TAG, "Un-attached view should not cancel Jank trace.");
return;
}
mInteractionJankMonitor.cancel(CUJ_BIOMETRIC_PROMPT_TRANSITION);
}
@Override
public void onAnimationRepeat(@androidx.annotation.NonNull Animator animation) {
// no-op
}
};
}
private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
if (view instanceof AuthBiometricFingerprintView) {
return ((AuthBiometricFingerprintView) view).isUdfps();
}
return false;
}
private boolean maybeUpdatePositionForUdfps(boolean invalidate) {
final Display display = getDisplay();
if (display == null) {
return false;
}
if (!shouldUpdatePositionForUdfps(mBiometricView)) {
return false;
}
final int displayRotation = display.getRotation();
switch (displayRotation) {
case Surface.ROTATION_0:
mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
break;
case Surface.ROTATION_90:
mPanelController.setPosition(AuthPanelController.POSITION_RIGHT);
setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
break;
case Surface.ROTATION_270:
mPanelController.setPosition(AuthPanelController.POSITION_LEFT);
setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
break;
case Surface.ROTATION_180:
default:
Log.e(TAG, "Unsupported display rotation: " + displayRotation);
mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
break;
}
if (invalidate) {
mPanelView.invalidateOutline();
mBiometricView.requestLayout();
}
return true;
}
private void setScrollViewGravity(int gravity) {
final FrameLayout.LayoutParams params =
(FrameLayout.LayoutParams) mBiometricScrollView.getLayoutParams();
params.gravity = gravity;
mBiometricScrollView.setLayoutParams(params);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mWakefulnessLifecycle.removeObserver(this);
}
@Override
public void onStartedGoingToSleep() {
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
}
@Override
public void show(WindowManager wm, @Nullable Bundle savedState) {
if (mBiometricView != null) {
mBiometricView.restoreState(savedState);
}
wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle()));
}
@Override
public void dismissWithoutCallback(boolean animate) {
if (animate) {
animateAway(false /* sendReason */, 0 /* reason */);
} else {
removeWindowIfAttached();
}
}
@Override
public void dismissFromSystemServer() {
animateAway(false /* sendReason */, 0 /* reason */);
}
@Override
public void onAuthenticationSucceeded(@Modality int modality) {
mBiometricView.onAuthenticationSucceeded(modality);
}
@Override
public void onAuthenticationFailed(@Modality int modality, String failureReason) {
mFailedModalities.add(modality);
mBiometricView.onAuthenticationFailed(modality, failureReason);
}
@Override
public void onHelp(@Modality int modality, String help) {
mBiometricView.onHelp(modality, help);
}
@Override
public void onError(@Modality int modality, String error) {
mBiometricView.onError(modality, error);
}
@Override
public void onPointerDown() {
if (mBiometricView.onPointerDown(mFailedModalities)) {
Log.d(TAG, "retrying failed modalities (pointer down)");
mBiometricCallback.onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
}
}
@Override
public void onSaveState(@NonNull Bundle outState) {
outState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY,
mContainerState == STATE_ANIMATING_OUT);
// In the case where biometric and credential are both allowed, we can assume that
// biometric isn't showing if credential is showing since biometric is shown first.
outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING,
mBiometricView != null && mCredentialView == null);
outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null);
if (mBiometricView != null) {
mBiometricView.onSaveState(outState);
}
}
@Override
public String getOpPackageName() {
return mConfig.mOpPackageName;
}
@Override
public long getRequestId() {
return mConfig.mRequestId;
}
@Override
public void animateToCredentialUI() {
mBiometricView.startTransitionToCredentialUI();
}
void animateAway(@AuthDialogCallback.DismissedReason int reason) {
animateAway(true /* sendReason */, reason);
}
private void animateAway(boolean sendReason, @AuthDialogCallback.DismissedReason int reason) {
if (mContainerState == STATE_ANIMATING_IN) {
Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
mContainerState = STATE_PENDING_DISMISS;
return;
}
if (mContainerState == STATE_ANIMATING_OUT) {
Log.w(TAG, "Already dismissing, sendReason: " + sendReason + " reason: " + reason);
return;
}
mContainerState = STATE_ANIMATING_OUT;
if (sendReason) {
mPendingCallbackReason = reason;
} else {
mPendingCallbackReason = null;
}
final Runnable endActionRunnable = () -> {
setVisibility(View.INVISIBLE);
removeWindowIfAttached();
};
final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
postOnAnimation(() -> {
mPanelView.animate()
.translationY(mTranslationY)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.setListener(getJankListener(mPanelView, DISMISS, animateDuration))
.withLayer()
.withEndAction(endActionRunnable)
.start();
mBiometricScrollView.animate()
.translationY(mTranslationY)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.setListener(getJankListener(mBiometricScrollView, DISMISS, animateDuration))
.withLayer()
.start();
if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
mCredentialView.animate()
.translationY(mTranslationY)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.setListener(getJankListener(mCredentialView, DISMISS, animateDuration))
.withLayer()
.start();
}
animate()
.alpha(0f)
.setDuration(animateDuration)
.setInterpolator(mLinearOutSlowIn)
.setListener(getJankListener(this, DISMISS, animateDuration))
.setUpdateListener(animation -> {
if (mWindowManager == null || getViewRootImpl() == null) {
Log.w(TAG, "skip updateViewLayout() for dim animation.");
return;
}
final WindowManager.LayoutParams lp = getViewRootImpl().mWindowAttributes;
lp.dimAmount = (1.0f - (Float) animation.getAnimatedValue())
* BACKGROUND_DIM_AMOUNT;
mWindowManager.updateViewLayout(this, lp);
})
.withLayer()
.start();
});
}
private void sendPendingCallbackIfNotNull() {
Log.d(TAG, "pendingCallback: " + mPendingCallbackReason);
if (mPendingCallbackReason != null) {
mConfig.mCallback.onDismissed(mPendingCallbackReason, mCredentialAttestation);
mPendingCallbackReason = null;
}
}
private void removeWindowIfAttached() {
sendPendingCallbackIfNotNull();
if (mContainerState == STATE_GONE) {
return;
}
mContainerState = STATE_GONE;
if (isAttachedToWindow()) {
mWindowManager.removeView(this);
}
}
private void onDialogAnimatedIn() {
if (mContainerState == STATE_PENDING_DISMISS) {
Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
return;
}
if (mContainerState == STATE_ANIMATING_OUT || mContainerState == STATE_GONE) {
Log.d(TAG, "onDialogAnimatedIn(): ignore, already animating out or gone - state: "
+ mContainerState);
return;
}
mContainerState = STATE_SHOWING;
if (mBiometricView != null) {
mConfig.mCallback.onDialogAnimatedIn();
mBiometricView.onDialogAnimatedIn();
}
}
@VisibleForTesting
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
| WindowManager.LayoutParams.FLAG_SECURE
| WindowManager.LayoutParams.FLAG_DIM_BEHIND;
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
windowFlags,
PixelFormat.TRANSLUCENT);
lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~WindowInsets.Type.ime());
lp.setTitle("BiometricPrompt");
lp.accessibilityTitle = title;
lp.dimAmount = BACKGROUND_DIM_AMOUNT;
lp.token = windowToken;
return lp;
}
@Override
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println(" isAttachedToWindow=" + isAttachedToWindow());
pw.println(" containerState=" + mContainerState);
pw.println(" pendingCallbackReason=" + mPendingCallbackReason);
pw.println(" config exist=" + (mConfig != null));
if (mConfig != null) {
pw.println(" config.sensorIds exist=" + (mConfig.mSensorIds != null));
}
final AuthBiometricView biometricView = mBiometricView;
pw.println(" scrollView=" + findViewById(R.id.biometric_scrollview));
pw.println(" biometricView=" + biometricView);
if (biometricView != null) {
int[] ids = {
R.id.title,
R.id.subtitle,
R.id.description,
R.id.biometric_icon_frame,
R.id.biometric_icon,
R.id.indicator,
R.id.button_bar,
R.id.button_negative,
R.id.button_use_credential,
R.id.button_confirm,
R.id.button_try_again
};
for (final int id: ids) {
pw.println(" " + biometricView.findViewById(id));
}
}
}
}