blob: 8013a9e29dfd2af56083d9369d2cc4d807175e33 [file] [log] [blame]
/*
* 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.fingerprint;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.drawable.AnimatedVectorDrawable;
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.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
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;
/**
* This class loads the view for the system-provided dialog. The view consists of:
* Application Icon, Title, Subtitle, Description, Fingerprint Icon, Error/Help message area,
* and positive/negative buttons.
*/
public class FingerprintDialogView extends LinearLayout {
private static final String TAG = "FingerprintDialogView";
private static final int ANIMATION_DURATION_SHOW = 250; // ms
private static final int ANIMATION_DURATION_AWAY = 350; // ms
private static final int STATE_NONE = 0;
private static final int STATE_FINGERPRINT = 1;
private static final int STATE_FINGERPRINT_ERROR = 2;
private static final int STATE_FINGERPRINT_AUTHENTICATED = 3;
private final IBinder mWindowToken = new Binder();
private final Interpolator mLinearOutSlowIn;
private final WindowManager mWindowManager;
private final float mAnimationTranslationOffset;
private final int mErrorColor;
private final int mTextColor;
private final int mFingerprintColor;
private ViewGroup mLayout;
private final TextView mErrorText;
private Handler mHandler;
private Bundle mBundle;
private final LinearLayout mDialog;
private int mLastState;
private boolean mAnimatingAway;
private boolean mWasForceRemoved;
private final float mDisplayWidth;
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()
.start();
}
};
public FingerprintDialogView(Context context, Handler handler) {
super(context);
mHandler = handler;
mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mAnimationTranslationOffset = getResources()
.getDimension(R.dimen.fingerprint_dialog_animation_translation_offset);
mErrorColor = Color.parseColor(
getResources().getString(R.color.fingerprint_dialog_error_color));
mTextColor = Color.parseColor(
getResources().getString(R.color.fingerprint_dialog_text_light_color));
mFingerprintColor = Color.parseColor(
getResources().getString(R.color.fingerprint_dialog_fingerprint_color));
DisplayMetrics metrics = new DisplayMetrics();
mWindowManager.getDefaultDisplay().getMetrics(metrics);
mDisplayWidth = metrics.widthPixels;
// Create the dialog
LayoutInflater factory = LayoutInflater.from(getContext());
mLayout = (ViewGroup) factory.inflate(R.layout.fingerprint_dialog, this, false);
addView(mLayout);
mDialog = mLayout.findViewById(R.id.dialog);
mErrorText = mLayout.findViewById(R.id.error);
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;
mHandler.obtainMessage(FingerprintDialogImpl.MSG_USER_CANCELED).sendToTarget();
}
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);
final Button negative = mLayout.findViewById(R.id.button2);
final Button positive = mLayout.findViewById(R.id.button1);
setDismissesDialog(space);
setDismissesDialog(leftSpace);
setDismissesDialog(rightSpace);
negative.setOnClickListener((View v) -> {
mHandler.obtainMessage(FingerprintDialogImpl.MSG_BUTTON_NEGATIVE).sendToTarget();
});
positive.setOnClickListener((View v) -> {
mHandler.obtainMessage(FingerprintDialogImpl.MSG_BUTTON_POSITIVE).sendToTarget();
});
mLayout.setFocusableInTouchMode(true);
mLayout.requestFocus();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
final TextView title = mLayout.findViewById(R.id.title);
final TextView subtitle = mLayout.findViewById(R.id.subtitle);
final TextView description = mLayout.findViewById(R.id.description);
final Button negative = mLayout.findViewById(R.id.button2);
final Button positive = mLayout.findViewById(R.id.button1);
mDialog.getLayoutParams().width = (int) mDisplayWidth;
mLastState = STATE_NONE;
updateFingerprintIcon(STATE_FINGERPRINT);
title.setText(mBundle.getCharSequence(BiometricPrompt.KEY_TITLE));
title.setSelected(true);
final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
if (TextUtils.isEmpty(subtitleText)) {
subtitle.setVisibility(View.GONE);
} else {
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(subtitleText);
}
final CharSequence descriptionText = mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
if (TextUtils.isEmpty(descriptionText)) {
description.setVisibility(View.GONE);
} else {
description.setVisibility(View.VISIBLE);
description.setText(descriptionText);
}
negative.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));
final CharSequence positiveText =
mBundle.getCharSequence(BiometricPrompt.KEY_POSITIVE_TEXT);
positive.setText(positiveText); // needs to be set for marquee to work
if (positiveText != null) {
positive.setVisibility(View.VISIBLE);
} else {
positive.setVisibility(View.GONE);
}
if (!mWasForceRemoved) {
// Dim the background and slide the dialog up
mDialog.setTranslationY(mAnimationTranslationOffset);
mLayout.setAlpha(0f);
postOnAnimation(mShowAnimationRunnable);
} else {
// Show the dialog immediately
mLayout.animate().cancel();
mDialog.animate().cancel();
mDialog.setAlpha(1.0f);
mDialog.setTranslationY(0);
mLayout.setAlpha(1.0f);
}
mWasForceRemoved = false;
}
private void setDismissesDialog(View v) {
v.setClickable(true);
v.setOnTouchListener((View view, MotionEvent event) -> {
mHandler.obtainMessage(FingerprintDialogImpl.MSG_HIDE_DIALOG, true /* userCanceled */)
.sendToTarget();
return true;
});
}
public void startDismiss() {
mAnimatingAway = true;
final Runnable endActionRunnable = new Runnable() {
@Override
public void run() {
mWindowManager.removeView(FingerprintDialogView.this);
mAnimatingAway = false;
}
};
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(FingerprintDialogView.this);
mAnimatingAway = false;
mWasForceRemoved = true;
}
public boolean isAnimatingAway() {
return mAnimatingAway;
}
public void setBundle(Bundle bundle) {
mBundle = bundle;
}
// Clears the temporary message and shows the help message.
protected void resetMessage() {
updateFingerprintIcon(STATE_FINGERPRINT);
mErrorText.setText(R.string.fingerprint_dialog_touch_sensor);
mErrorText.setTextColor(mTextColor);
}
// Shows an error/help message
private void showTemporaryMessage(String message) {
mHandler.removeMessages(FingerprintDialogImpl.MSG_CLEAR_MESSAGE);
updateFingerprintIcon(STATE_FINGERPRINT_ERROR);
mErrorText.setText(message);
mErrorText.setTextColor(mErrorColor);
mErrorText.setContentDescription(message);
mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_CLEAR_MESSAGE),
BiometricPrompt.HIDE_DIALOG_DELAY);
}
public void showHelpMessage(String message) {
showTemporaryMessage(message);
}
public void showErrorMessage(String error) {
showTemporaryMessage(error);
mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_HIDE_DIALOG,
false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY);
}
private void updateFingerprintIcon(int newState) {
Drawable icon = getAnimationForTransition(mLastState, newState);
if (icon == null) {
Log.e(TAG, "Animation not found");
return;
}
final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
? (AnimatedVectorDrawable) icon
: null;
final ImageView fingerprint_icon = mLayout.findViewById(R.id.fingerprint_icon);
fingerprint_icon.setImageDrawable(icon);
if (animation != null && shouldAnimateForTransition(mLastState, newState)) {
animation.forceAnimationOnUI();
animation.start();
}
mLastState = newState;
}
private boolean shouldAnimateForTransition(int oldState, int newState) {
if (oldState == STATE_NONE && newState == STATE_FINGERPRINT) {
return false;
} else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
return true;
} else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
return true;
} else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_AUTHENTICATED) {
// TODO(b/77328470): add animation when fingerprint is authenticated
return false;
}
return false;
}
private Drawable getAnimationForTransition(int oldState, int newState) {
int iconRes;
if (oldState == STATE_NONE && newState == STATE_FINGERPRINT) {
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
} else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
iconRes = R.drawable.fingerprint_dialog_fp_to_error;
} else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
iconRes = R.drawable.fingerprint_dialog_error_to_fp;
} else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_AUTHENTICATED) {
// TODO(b/77328470): add animation when fingerprint is authenticated
iconRes = R.drawable.fingerprint_dialog_error_to_fp;
}
else {
return null;
}
return mContext.getDrawable(iconRes);
}
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("FingerprintDialogView");
lp.token = mWindowToken;
return lp;
}
}