| /* |
| * 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 android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.graphics.PixelFormat; |
| import android.hardware.biometrics.BiometricAuthenticator; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowManager; |
| import android.view.animation.Interpolator; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.ScrollView; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.systemui.Dependency; |
| import com.android.systemui.Interpolators; |
| import com.android.systemui.R; |
| import com.android.systemui.keyguard.WakefulnessLifecycle; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| |
| /** |
| * Top level container/controller for the BiometricPrompt UI. |
| */ |
| public class AuthContainerView extends LinearLayout |
| implements AuthDialog, WakefulnessLifecycle.Observer { |
| |
| private static final String TAG = "BiometricPrompt/AuthContainerView"; |
| private static final int ANIMATION_DURATION_SHOW_MS = 250; |
| private static final int ANIMATION_DURATION_AWAY_MS = 350; // ms |
| |
| 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; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING, |
| STATE_ANIMATING_OUT, STATE_GONE}) |
| @interface ContainerState {} |
| |
| final Config mConfig; |
| private final IBinder mWindowToken = new Binder(); |
| private final WindowManager mWindowManager; |
| private final AuthPanelController mPanelController; |
| private final Interpolator mLinearOutSlowIn; |
| @VisibleForTesting final BiometricCallback mBiometricCallback; |
| |
| private final ViewGroup mContainerView; |
| private final AuthBiometricView mBiometricView; |
| |
| private final ImageView mBackgroundView; |
| private final ScrollView mScrollView; |
| private final View mPanelView; |
| |
| private final float mTranslationY; |
| |
| @VisibleForTesting final WakefulnessLifecycle mWakefulnessLifecycle; |
| |
| private @ContainerState int mContainerState = STATE_UNKNOWN; |
| |
| static class Config { |
| Context mContext; |
| AuthDialogCallback mCallback; |
| Bundle mBiometricPromptBundle; |
| boolean mRequireConfirmation; |
| int mUserId; |
| String mOpPackageName; |
| int mModalityMask; |
| boolean mSkipIntro; |
| } |
| |
| 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 setBiometricPromptBundle(Bundle bundle) { |
| mConfig.mBiometricPromptBundle = bundle; |
| 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 AuthContainerView build(int modalityMask) { |
| mConfig.mModalityMask = modalityMask; |
| return new AuthContainerView(mConfig); |
| } |
| } |
| |
| @VisibleForTesting |
| final class BiometricCallback implements AuthBiometricView.Callback { |
| @Override |
| public void onAction(int action) { |
| switch (action) { |
| case AuthBiometricView.Callback.ACTION_AUTHENTICATED: |
| animateAway(AuthDialogCallback.DISMISSED_AUTHENTICATED); |
| break; |
| case AuthBiometricView.Callback.ACTION_USER_CANCELED: |
| 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: |
| mConfig.mCallback.onTryAgainPressed(); |
| break; |
| case AuthBiometricView.Callback.ACTION_ERROR: |
| animateAway(AuthDialogCallback.DISMISSED_ERROR); |
| break; |
| default: |
| Log.e(TAG, "Unhandled action: " + action); |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| AuthContainerView(Config config) { |
| super(config.mContext); |
| |
| mConfig = config; |
| mWindowManager = mContext.getSystemService(WindowManager.class); |
| mWakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class); |
| |
| mTranslationY = getResources() |
| .getDimension(R.dimen.biometric_dialog_animation_translation_offset); |
| mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; |
| mBiometricCallback = new BiometricCallback(); |
| |
| final LayoutInflater factory = LayoutInflater.from(mContext); |
| mContainerView = (ViewGroup) factory.inflate( |
| R.layout.auth_container_view, this, false /* attachToRoot */); |
| |
| mPanelView = mContainerView.findViewById(R.id.panel); |
| mPanelController = new AuthPanelController(mContext, mPanelView); |
| |
| // TODO: Update with new controllers if multi-modal authentication can occur simultaneously |
| if (config.mModalityMask == BiometricAuthenticator.TYPE_FINGERPRINT) { |
| mBiometricView = (AuthBiometricFingerprintView) |
| factory.inflate(R.layout.auth_biometric_fingerprint_view, null, false); |
| } else if (config.mModalityMask == BiometricAuthenticator.TYPE_FACE) { |
| mBiometricView = (AuthBiometricFaceView) |
| factory.inflate(R.layout.auth_biometric_face_view, null, false); |
| } else { |
| Log.e(TAG, "Unsupported modality mask: " + config.mModalityMask); |
| mBiometricView = null; |
| mBackgroundView = null; |
| mScrollView = null; |
| return; |
| } |
| |
| mBackgroundView = mContainerView.findViewById(R.id.background); |
| |
| mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation); |
| mBiometricView.setPanelController(mPanelController); |
| mBiometricView.setBiometricPromptBundle(config.mBiometricPromptBundle); |
| mBiometricView.setCallback(mBiometricCallback); |
| mBiometricView.setBackgroundView(mBackgroundView); |
| |
| mScrollView = mContainerView.findViewById(R.id.scrollview); |
| mScrollView.addView(mBiometricView); |
| addView(mContainerView); |
| |
| setOnKeyListener((v, keyCode, event) -> { |
| if (keyCode != KeyEvent.KEYCODE_BACK) { |
| return false; |
| } |
| if (event.getAction() == KeyEvent.ACTION_UP) { |
| animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); |
| } |
| return true; |
| }); |
| |
| setFocusableInTouchMode(true); |
| requestFocus(); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| mPanelController.setContainerDimensions(getMeasuredWidth(), getMeasuredHeight()); |
| } |
| |
| @Override |
| public void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| mWakefulnessLifecycle.addObserver(this); |
| |
| 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); |
| mScrollView.setY(mTranslationY); |
| |
| setAlpha(0f); |
| postOnAnimation(() -> { |
| mPanelView.animate() |
| .translationY(0) |
| .setDuration(ANIMATION_DURATION_SHOW_MS) |
| .setInterpolator(mLinearOutSlowIn) |
| .withLayer() |
| .withEndAction(this::onDialogAnimatedIn) |
| .start(); |
| mScrollView.animate() |
| .translationY(0) |
| .setDuration(ANIMATION_DURATION_SHOW_MS) |
| .setInterpolator(mLinearOutSlowIn) |
| .withLayer() |
| .start(); |
| animate() |
| .alpha(1f) |
| .setDuration(ANIMATION_DURATION_SHOW_MS) |
| .setInterpolator(mLinearOutSlowIn) |
| .withLayer() |
| .start(); |
| }); |
| } |
| } |
| |
| @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) { |
| mBiometricView.restoreState(savedState); |
| wm.addView(this, getLayoutParams(mWindowToken)); |
| } |
| |
| @Override |
| public void dismissWithoutCallback(boolean animate) { |
| if (animate) { |
| animateAway(false /* sendReason */, 0 /* reason */); |
| } else { |
| removeWindowIfAttached(); |
| } |
| } |
| |
| @Override |
| public void dismissFromSystemServer() { |
| removeWindowIfAttached(); |
| } |
| |
| @Override |
| public void onAuthenticationSucceeded() { |
| mBiometricView.onAuthenticationSucceeded(); |
| } |
| |
| @Override |
| public void onAuthenticationFailed(String failureReason) { |
| mBiometricView.onAuthenticationFailed(failureReason); |
| } |
| |
| @Override |
| public void onHelp(String help) { |
| mBiometricView.onHelp(help); |
| } |
| |
| @Override |
| public void onError(String error) { |
| mBiometricView.onError(error); |
| } |
| |
| @Override |
| public void onSaveState(@NonNull Bundle outState) { |
| mBiometricView.onSaveState(outState); |
| } |
| |
| @Override |
| public String getOpPackageName() { |
| return mConfig.mOpPackageName; |
| } |
| |
| @VisibleForTesting |
| void animateAway(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; |
| |
| final Runnable endActionRunnable = () -> { |
| setVisibility(View.INVISIBLE); |
| removeWindowIfAttached(); |
| if (sendReason) { |
| mConfig.mCallback.onDismissed(reason); |
| } |
| }; |
| |
| postOnAnimation(() -> { |
| mPanelView.animate() |
| .translationY(mTranslationY) |
| .setDuration(ANIMATION_DURATION_AWAY_MS) |
| .setInterpolator(mLinearOutSlowIn) |
| .withLayer() |
| .withEndAction(endActionRunnable) |
| .start(); |
| mScrollView.animate() |
| .translationY(mTranslationY) |
| .setDuration(ANIMATION_DURATION_AWAY_MS) |
| .setInterpolator(mLinearOutSlowIn) |
| .withLayer() |
| .start(); |
| animate() |
| .alpha(0f) |
| .setDuration(ANIMATION_DURATION_AWAY_MS) |
| .setInterpolator(mLinearOutSlowIn) |
| .withLayer() |
| .start(); |
| }); |
| } |
| |
| private void removeWindowIfAttached() { |
| if (mContainerState == STATE_GONE) { |
| return; |
| } |
| mContainerState = STATE_GONE; |
| mWindowManager.removeView(this); |
| } |
| |
| private void onDialogAnimatedIn() { |
| if (mContainerState == STATE_PENDING_DISMISS) { |
| Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); |
| animateAway(false /* sendReason */, 0); |
| return; |
| } |
| mContainerState = STATE_SHOWING; |
| mBiometricView.onDialogAnimatedIn(); |
| } |
| |
| /** |
| * @param windowToken token for the window |
| * @return |
| */ |
| public static WindowManager.LayoutParams getLayoutParams(IBinder windowToken) { |
| final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.MATCH_PARENT, |
| WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL, |
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, |
| PixelFormat.TRANSLUCENT); |
| lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; |
| lp.setTitle("BiometricPrompt"); |
| lp.token = windowToken; |
| return lp; |
| } |
| } |