/* * Copyright (C) 2018 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.app.admin.DevicePolicyManager; import android.content.Context; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.hardware.biometrics.BiometricPrompt; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.UserManager; import android.text.TextUtils; import android.util.DisplayMetrics; 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.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.util.leak.RotationUtils; /** * Abstract base class. Shows a dialog for BiometricPrompt. */ public abstract class BiometricDialogView extends LinearLayout { private static final String TAG = "BiometricDialogView"; private static final String KEY_TRY_AGAIN_VISIBILITY = "key_try_again_visibility"; private static final String KEY_CONFIRM_VISIBILITY = "key_confirm_visibility"; private static final String KEY_STATE = "key_state"; private static final String KEY_ERROR_TEXT_VISIBILITY = "key_error_text_visibility"; private static final String KEY_ERROR_TEXT_STRING = "key_error_text_string"; private static final String KEY_ERROR_TEXT_IS_TEMPORARY = "key_error_text_is_temporary"; private static final String KEY_ERROR_TEXT_COLOR = "key_error_text_color"; private static final int ANIMATION_DURATION_SHOW = 250; // ms private static final int ANIMATION_DURATION_AWAY = 350; // ms protected static final int MSG_RESET_MESSAGE = 1; protected static final int STATE_IDLE = 0; protected static final int STATE_AUTHENTICATING = 1; protected static final int STATE_ERROR = 2; protected static final int STATE_PENDING_CONFIRMATION = 3; protected static final int STATE_AUTHENTICATED = 4; private final IBinder mWindowToken = new Binder(); private final Interpolator mLinearOutSlowIn; private final WindowManager mWindowManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; private final float mAnimationTranslationOffset; private final int mErrorColor; private final float mDialogWidth; protected final DialogViewCallback mCallback; protected final ViewGroup mLayout; protected final LinearLayout mDialog; protected final TextView mTitleText; protected final TextView mSubtitleText; protected final TextView mDescriptionText; protected final ImageView mBiometricIcon; protected final TextView mErrorText; protected final Button mPositiveButton; protected final Button mNegativeButton; protected final Button mTryAgainButton; protected final int mTextColor; private Bundle mBundle; private Bundle mRestoredState; private int mState = STATE_IDLE; private boolean mAnimatingAway; private boolean mWasForceRemoved; private boolean mSkipIntro; protected boolean mRequireConfirmation; private int mUserId; // used to determine if we should show work background protected abstract int getHintStringResourceId(); protected abstract int getAuthenticatedAccessibilityResourceId(); protected abstract int getIconDescriptionResourceId(); protected abstract int getDelayAfterAuthenticatedDurationMs(); protected abstract boolean shouldGrayAreaDismissDialog(); protected abstract void handleResetMessage(); protected abstract void updateIcon(int oldState, int newState); private final Runnable mShowAnimationRunnable = new Runnable() { @Override public void run() { mLayout.animate() .alpha(1f) .setDuration(ANIMATION_DURATION_SHOW) .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); mDialog.animate() .translationY(0) .setDuration(ANIMATION_DURATION_SHOW) .setInterpolator(mLinearOutSlowIn) .withLayer() .withEndAction(() -> onDialogAnimatedIn()) .start(); } }; protected Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch(msg.what) { case MSG_RESET_MESSAGE: handleResetMessage(); break; default: Log.e(TAG, "Unhandled message: " + msg.what); break; } } }; public BiometricDialogView(Context context, DialogViewCallback callback) { super(context); mCallback = callback; mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; mWindowManager = mContext.getSystemService(WindowManager.class); mUserManager = mContext.getSystemService(UserManager.class); mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); mAnimationTranslationOffset = getResources() .getDimension(R.dimen.biometric_dialog_animation_translation_offset); mErrorColor = getResources().getColor(R.color.biometric_dialog_error); mTextColor = getResources().getColor(R.color.biometric_dialog_gray); DisplayMetrics metrics = new DisplayMetrics(); mWindowManager.getDefaultDisplay().getMetrics(metrics); mDialogWidth = Math.min(metrics.widthPixels, metrics.heightPixels); // Create the dialog LayoutInflater factory = LayoutInflater.from(getContext()); mLayout = (ViewGroup) factory.inflate(R.layout.biometric_dialog, this, false); addView(mLayout); mLayout.setOnKeyListener(new View.OnKeyListener() { boolean downPressed = false; @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (keyCode != KeyEvent.KEYCODE_BACK) { return false; } if (event.getAction() == KeyEvent.ACTION_DOWN && downPressed == false) { downPressed = true; } else if (event.getAction() == KeyEvent.ACTION_DOWN) { downPressed = false; } else if (event.getAction() == KeyEvent.ACTION_UP && downPressed == true) { downPressed = false; mCallback.onUserCanceled(); } return true; } }); final View space = mLayout.findViewById(R.id.space); final View leftSpace = mLayout.findViewById(R.id.left_space); final View rightSpace = mLayout.findViewById(R.id.right_space); mDialog = mLayout.findViewById(R.id.dialog); mTitleText = mLayout.findViewById(R.id.title); mSubtitleText = mLayout.findViewById(R.id.subtitle); mDescriptionText = mLayout.findViewById(R.id.description); mBiometricIcon = mLayout.findViewById(R.id.biometric_icon); mErrorText = mLayout.findViewById(R.id.error); mNegativeButton = mLayout.findViewById(R.id.button2); mPositiveButton = mLayout.findViewById(R.id.button1); mTryAgainButton = mLayout.findViewById(R.id.button_try_again); mBiometricIcon.setContentDescription( getResources().getString(getIconDescriptionResourceId())); setDismissesDialog(space); setDismissesDialog(leftSpace); setDismissesDialog(rightSpace); mNegativeButton.setOnClickListener((View v) -> { if (mState == STATE_PENDING_CONFIRMATION || mState == STATE_AUTHENTICATED) { mCallback.onUserCanceled(); } else { mCallback.onNegativePressed(); } }); mPositiveButton.setOnClickListener((View v) -> { updateState(STATE_AUTHENTICATED); mHandler.postDelayed(() -> { mCallback.onPositivePressed(); }, getDelayAfterAuthenticatedDurationMs()); }); mTryAgainButton.setOnClickListener((View v) -> { handleResetMessage(); updateState(STATE_AUTHENTICATING); showTryAgainButton(false /* show */); mCallback.onTryAgainPressed(); }); // Must set these in order for the back button events to be received. mLayout.setFocusableInTouchMode(true); mLayout.requestFocus(); } public void onSaveState(Bundle bundle) { bundle.putInt(KEY_TRY_AGAIN_VISIBILITY, mTryAgainButton.getVisibility()); bundle.putInt(KEY_CONFIRM_VISIBILITY, mPositiveButton.getVisibility()); bundle.putInt(KEY_STATE, mState); bundle.putInt(KEY_ERROR_TEXT_VISIBILITY, mErrorText.getVisibility()); bundle.putCharSequence(KEY_ERROR_TEXT_STRING, mErrorText.getText()); bundle.putBoolean(KEY_ERROR_TEXT_IS_TEMPORARY, mHandler.hasMessages(MSG_RESET_MESSAGE)); bundle.putInt(KEY_ERROR_TEXT_COLOR, mErrorText.getCurrentTextColor()); } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); final ImageView backgroundView = mLayout.findViewById(R.id.background); if (mUserManager.isManagedProfile(mUserId)) { final Drawable image = getResources().getDrawable(R.drawable.work_challenge_background, mContext.getTheme()); image.setColorFilter(mDevicePolicyManager.getOrganizationColorForUser(mUserId), PorterDuff.Mode.DARKEN); backgroundView.setImageDrawable(image); } else { backgroundView.setImageDrawable(null); backgroundView.setBackgroundColor(R.color.biometric_dialog_dim_color); } mNegativeButton.setVisibility(View.VISIBLE); if (RotationUtils.getRotation(mContext) != RotationUtils.ROTATION_NONE) { mDialog.getLayoutParams().width = (int) mDialogWidth; } if (mRestoredState == null) { updateState(STATE_AUTHENTICATING); mErrorText.setText(getHintStringResourceId()); mErrorText.setContentDescription(mContext.getString(getHintStringResourceId())); mErrorText.setVisibility(View.VISIBLE); } else { updateState(mState); } CharSequence titleText = mBundle.getCharSequence(BiometricPrompt.KEY_TITLE); mTitleText.setVisibility(View.VISIBLE); mTitleText.setText(titleText); final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE); if (TextUtils.isEmpty(subtitleText)) { mSubtitleText.setVisibility(View.GONE); } else { mSubtitleText.setVisibility(View.VISIBLE); mSubtitleText.setText(subtitleText); } final CharSequence descriptionText = mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION); if (TextUtils.isEmpty(descriptionText)) { mDescriptionText.setVisibility(View.GONE); } else { mDescriptionText.setVisibility(View.VISIBLE); mDescriptionText.setText(descriptionText); } mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT)); if (requiresConfirmation() && mRestoredState == null) { mPositiveButton.setVisibility(View.VISIBLE); mPositiveButton.setEnabled(false); } if (mWasForceRemoved || mSkipIntro) { // Show the dialog immediately mLayout.animate().cancel(); mDialog.animate().cancel(); mDialog.setAlpha(1.0f); mDialog.setTranslationY(0); mLayout.setAlpha(1.0f); } else { // Dim the background and slide the dialog up mDialog.setTranslationY(mAnimationTranslationOffset); mLayout.setAlpha(0f); postOnAnimation(mShowAnimationRunnable); } mWasForceRemoved = false; mSkipIntro = false; } private void setDismissesDialog(View v) { v.setClickable(true); v.setOnClickListener(v1 -> { if (mState != STATE_AUTHENTICATED && shouldGrayAreaDismissDialog()) { mCallback.onUserCanceled(); } }); } public void startDismiss() { mAnimatingAway = true; // This is where final cleanup should occur. final Runnable endActionRunnable = new Runnable() { @Override public void run() { mWindowManager.removeView(BiometricDialogView.this); mAnimatingAway = false; // Set the icons / text back to normal state handleResetMessage(); showTryAgainButton(false /* show */); updateState(STATE_IDLE); } }; postOnAnimation(new Runnable() { @Override public void run() { mLayout.animate() .alpha(0f) .setDuration(ANIMATION_DURATION_AWAY) .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); mDialog.animate() .translationY(mAnimationTranslationOffset) .setDuration(ANIMATION_DURATION_AWAY) .setInterpolator(mLinearOutSlowIn) .withLayer() .withEndAction(endActionRunnable) .start(); } }); } /** * Force remove the window, cancelling any animation that's happening. This should only be * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method * will cause the dialog to show without an animation the next time it's attached. */ public void forceRemove() { mLayout.animate().cancel(); mDialog.animate().cancel(); mWindowManager.removeView(BiometricDialogView.this); mAnimatingAway = false; mWasForceRemoved = true; } /** * Skip the intro animation */ public void setSkipIntro(boolean skip) { mSkipIntro = skip; } public boolean isAnimatingAway() { return mAnimatingAway; } public void setBundle(Bundle bundle) { mBundle = bundle; } public void setRequireConfirmation(boolean requireConfirmation) { mRequireConfirmation = requireConfirmation; } public boolean requiresConfirmation() { return mRequireConfirmation; } public void setUserId(int userId) { mUserId = userId; } public ViewGroup getLayout() { return mLayout; } // Shows an error/help message protected void showTemporaryMessage(String message) { mHandler.removeMessages(MSG_RESET_MESSAGE); mErrorText.setText(message); mErrorText.setTextColor(mErrorColor); mErrorText.setContentDescription(message); mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE), BiometricPrompt.HIDE_DIALOG_DELAY); } /** * Transient help message (acquire) is received, dialog stays showing. Sensor stays in * "authenticating" state. * @param message */ public void onHelpReceived(String message) { updateState(STATE_ERROR); showTemporaryMessage(message); } public void onAuthenticationFailed(String message) { updateState(STATE_ERROR); showTemporaryMessage(message); } /** * Hard error is received, dialog will be dismissed soon. * @param error */ public void onErrorReceived(String error) { updateState(STATE_ERROR); showTemporaryMessage(error); showTryAgainButton(false /* show */); mCallback.onErrorShown(); // TODO: Split between fp and face } public void updateState(int newState) { if (newState == STATE_PENDING_CONFIRMATION) { mHandler.removeMessages(MSG_RESET_MESSAGE); mErrorText.setVisibility(View.INVISIBLE); mPositiveButton.setVisibility(View.VISIBLE); mPositiveButton.setEnabled(true); } else if (newState == STATE_AUTHENTICATED) { mPositiveButton.setVisibility(View.GONE); mNegativeButton.setVisibility(View.GONE); mErrorText.setVisibility(View.INVISIBLE); } if (newState == STATE_PENDING_CONFIRMATION || newState == STATE_AUTHENTICATED) { mNegativeButton.setText(R.string.cancel); } updateIcon(mState, newState); mState = newState; } public void showTryAgainButton(boolean show) { } public void onDialogAnimatedIn() { } public void restoreState(Bundle bundle) { mRestoredState = bundle; mTryAgainButton.setVisibility(bundle.getInt(KEY_TRY_AGAIN_VISIBILITY)); mPositiveButton.setVisibility(bundle.getInt(KEY_CONFIRM_VISIBILITY)); mState = bundle.getInt(KEY_STATE); mErrorText.setText(bundle.getCharSequence(KEY_ERROR_TEXT_STRING)); mErrorText.setContentDescription(bundle.getCharSequence(KEY_ERROR_TEXT_STRING)); mErrorText.setVisibility(bundle.getInt(KEY_ERROR_TEXT_VISIBILITY)); mErrorText.setTextColor(bundle.getInt(KEY_ERROR_TEXT_COLOR)); if (bundle.getBoolean(KEY_ERROR_TEXT_IS_TEMPORARY)) { mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RESET_MESSAGE), BiometricPrompt.HIDE_DIALOG_DELAY); } } protected int getState() { return mState; } public WindowManager.LayoutParams getLayoutParams() { 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("BiometricDialogView"); lp.token = mWindowToken; return lp; } }