blob: 9fba44b76863056c32745f2231fb00dbebd0f5ef [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.biometrics;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Outline;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewOutlineProvider;
import com.android.systemui.R;
/**
* This class loads the view for the system-provided dialog. The view consists of:
* Application Icon, Title, Subtitle, Description, Biometric Icon, Error/Help message area,
* and positive/negative buttons.
*/
public class FaceDialogView extends BiometricDialogView {
private static final String TAG = "FaceDialogView";
private static final String KEY_DIALOG_SIZE = "key_dialog_size";
private static final int HIDE_DIALOG_DELAY = 500; // ms
private static final int IMPLICIT_Y_PADDING = 16; // dp
private static final int GROW_DURATION = 150; // ms
private static final int TEXT_ANIMATE_DISTANCE = 32; // dp
private static final int SIZE_UNKNOWN = 0;
private static final int SIZE_SMALL = 1;
private static final int SIZE_GROWING = 2;
private static final int SIZE_BIG = 3;
private int mSize;
private float mIconOriginalY;
private DialogOutlineProvider mOutlineProvider = new DialogOutlineProvider();
private final class DialogOutlineProvider extends ViewOutlineProvider {
float mY;
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(
0 /* left */,
(int) mY, /* top */
mDialog.getWidth() /* right */,
mDialog.getBottom(), /* bottom */
getResources().getDimension(R.dimen.biometric_dialog_corner_size));
}
int calculateSmall() {
final float padding = dpToPixels(IMPLICIT_Y_PADDING);
return mDialog.getHeight() - mBiometricIcon.getHeight() - 2 * (int) padding;
}
void setOutlineY(float y) {
mY = y;
}
}
public FaceDialogView(Context context,
DialogViewCallback callback) {
super(context, callback);
}
private void updateSize(int newSize) {
final float padding = dpToPixels(IMPLICIT_Y_PADDING);
final float iconSmallPositionY = mDialog.getHeight() - mBiometricIcon.getHeight() - padding;
if (newSize == SIZE_SMALL) {
// These fields are required and/or always hold a spot on the UI, so should be set to
// INVISIBLE so they keep their position
mTitleText.setVisibility(View.INVISIBLE);
mErrorText.setVisibility(View.INVISIBLE);
mNegativeButton.setVisibility(View.INVISIBLE);
// These fields are optional, so set them to gone or invisible depending on their
// usage. If they're empty, they're already set to GONE in BiometricDialogView.
if (!TextUtils.isEmpty(mSubtitleText.getText())) {
mSubtitleText.setVisibility(View.INVISIBLE);
}
if (!TextUtils.isEmpty(mDescriptionText.getText())) {
mDescriptionText.setVisibility(View.INVISIBLE);
}
// Move the biometric icon to the small spot
mBiometricIcon.setY(iconSmallPositionY);
// Clip the dialog to the small size
mDialog.setOutlineProvider(mOutlineProvider);
mOutlineProvider.setOutlineY(mOutlineProvider.calculateSmall());
mDialog.setClipToOutline(true);
mDialog.invalidateOutline();
mSize = newSize;
} else if (mSize == SIZE_SMALL && newSize == SIZE_BIG) {
mSize = SIZE_GROWING;
// Animate the outline
final ValueAnimator outlineAnimator =
ValueAnimator.ofFloat(mOutlineProvider.calculateSmall(), 0);
outlineAnimator.addUpdateListener((animation) -> {
final float y = (float) animation.getAnimatedValue();
mOutlineProvider.setOutlineY(y);
mDialog.invalidateOutline();
});
// Animate the icon back to original big position
final ValueAnimator iconAnimator =
ValueAnimator.ofFloat(iconSmallPositionY, mIconOriginalY);
iconAnimator.addUpdateListener((animation) -> {
final float y = (float) animation.getAnimatedValue();
mBiometricIcon.setY(y);
});
// Animate the error text so it slides up with the icon
final ValueAnimator textSlideAnimator =
ValueAnimator.ofFloat(dpToPixels(TEXT_ANIMATE_DISTANCE), 0);
textSlideAnimator.addUpdateListener((animation) -> {
final float y = (float) animation.getAnimatedValue();
mErrorText.setTranslationY(y);
});
// Opacity animator for things that should fade in (title, subtitle, details, negative
// button)
final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1);
opacityAnimator.addUpdateListener((animation) -> {
final float opacity = (float) animation.getAnimatedValue();
// These fields are required and/or always hold a spot on the UI
mTitleText.setAlpha(opacity);
mErrorText.setAlpha(opacity);
mNegativeButton.setAlpha(opacity);
mTryAgainButton.setAlpha(opacity);
// These fields are optional, so only animate them if they're supposed to be showing
if (!TextUtils.isEmpty(mSubtitleText.getText())) {
mSubtitleText.setAlpha(opacity);
}
if (!TextUtils.isEmpty(mDescriptionText.getText())) {
mDescriptionText.setAlpha(opacity);
}
});
// Choreograph together
final AnimatorSet as = new AnimatorSet();
as.setDuration(GROW_DURATION);
as.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
// Set the visibility of opacity-animating views back to VISIBLE
mTitleText.setVisibility(View.VISIBLE);
mErrorText.setVisibility(View.VISIBLE);
mNegativeButton.setVisibility(View.VISIBLE);
mTryAgainButton.setVisibility(View.VISIBLE);
if (!TextUtils.isEmpty(mSubtitleText.getText())) {
mSubtitleText.setVisibility(View.VISIBLE);
}
if (!TextUtils.isEmpty(mDescriptionText.getText())) {
mDescriptionText.setVisibility(View.VISIBLE);
}
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mSize = SIZE_BIG;
}
});
as.play(outlineAnimator).with(iconAnimator).with(opacityAnimator)
.with(textSlideAnimator);
as.start();
} else if (mSize == SIZE_BIG) {
mDialog.setClipToOutline(false);
mDialog.invalidateOutline();
mBiometricIcon.setY(mIconOriginalY);
mSize = newSize;
}
}
@Override
public void onSaveState(Bundle bundle) {
super.onSaveState(bundle);
bundle.putInt(KEY_DIALOG_SIZE, mSize);
}
@Override
protected void handleClearMessage(boolean requireTryAgain) {
// Clears the temporary message and shows the help message. If requireTryAgain is true,
// we will start the authenticating state again.
if (!requireTryAgain) {
updateState(STATE_AUTHENTICATING);
mErrorText.setText(getHintStringResourceId());
mErrorText.setTextColor(mTextColor);
mErrorText.setVisibility(View.VISIBLE);
} else {
updateState(STATE_IDLE);
mErrorText.setVisibility(View.INVISIBLE);
}
}
@Override
public void restoreState(Bundle bundle) {
super.restoreState(bundle);
// Keep in mind that this happens before onAttachedToWindow()
mSize = bundle.getInt(KEY_DIALOG_SIZE);
}
/**
* Do small/big layout here instead of onAttachedToWindow, since:
* 1) We need the big layout to be measured, etc for small -> big animation
* 2) We need the dialog measurements to know where to move the biometric icon to
*
* BiometricDialogView already sets the views to their default big state, so here we only
* need to hide the ones that are unnecessary.
*/
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mIconOriginalY == 0) {
mIconOriginalY = mBiometricIcon.getY();
}
// UNKNOWN means size hasn't been set yet. First time we create the dialog.
// onLayout can happen when visibility of views change (during animation, etc).
if (mSize != SIZE_UNKNOWN) {
// Probably not the cleanest way to do this, but since dialog is big by default,
// and small dialogs can persist across orientation changes, we need to set it to
// small size here again.
if (mSize == SIZE_SMALL) {
updateSize(SIZE_SMALL);
}
return;
}
// If we don't require confirmation, show the small dialog first (until errors occur).
if (!requiresConfirmation()) {
updateSize(SIZE_SMALL);
} else {
updateSize(SIZE_BIG);
}
}
@Override
public void showErrorMessage(String error) {
super.showErrorMessage(error);
// All error messages will cause the dialog to go from small -> big. Error messages
// are messages such as lockout, auth failed, etc.
if (mSize == SIZE_SMALL) {
updateSize(SIZE_BIG);
}
}
@Override
public void showTryAgainButton(boolean show) {
if (show && mSize == SIZE_SMALL) {
// Do not call super, we will nicely animate the alpha together with the rest
// of the elements in here.
updateSize(SIZE_BIG);
} else {
if (show) {
mTryAgainButton.setVisibility(View.VISIBLE);
} else {
mTryAgainButton.setVisibility(View.GONE);
}
}
}
@Override
protected int getHintStringResourceId() {
return R.string.face_dialog_looking_for_face;
}
@Override
protected int getAuthenticatedAccessibilityResourceId() {
if (mRequireConfirmation) {
return com.android.internal.R.string.face_authenticated_confirmation_required;
} else {
return com.android.internal.R.string.face_authenticated_no_confirmation_required;
}
}
@Override
protected int getIconDescriptionResourceId() {
return R.string.accessibility_face_dialog_face_icon;
}
@Override
protected boolean shouldAnimateForTransition(int oldState, int newState) {
if (oldState == STATE_ERROR && newState == STATE_IDLE) {
return true;
} else if (oldState == STATE_IDLE && newState == STATE_AUTHENTICATING) {
return false;
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_ERROR) {
return true;
} else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATING) {
return true;
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_PENDING_CONFIRMATION) {
return true;
} else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
return true;
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
return true;
}
return false;
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return HIDE_DIALOG_DELAY;
}
@Override
protected boolean shouldGrayAreaDismissDialog() {
if (mSize == SIZE_SMALL) {
return false;
}
return true;
}
@Override
protected Drawable getAnimationForTransition(int oldState, int newState) {
int iconRes;
if (oldState == STATE_ERROR && newState == STATE_IDLE) {
iconRes = R.drawable.face_dialog_error_to_face;
} else if (oldState == STATE_IDLE && newState == STATE_AUTHENTICATING) {
iconRes = R.drawable.face_dialog_face_to_error;
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_ERROR) {
iconRes = R.drawable.face_dialog_face_to_error;
} else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATING) {
iconRes = R.drawable.face_dialog_error_to_face;
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_PENDING_CONFIRMATION) {
iconRes = R.drawable.face_dialog_face_gray_to_face_blue;
} else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
iconRes = R.drawable.face_dialog_face_blue_to_checkmark;
} else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
iconRes = R.drawable.face_dialog_face_gray_to_checkmark;
} else {
return null;
}
return mContext.getDrawable(iconRes);
}
private float dpToPixels(float dp) {
return dp * ((float) mContext.getResources().getDisplayMetrics().densityDpi
/ DisplayMetrics.DENSITY_DEFAULT);
}
private float pixelsToDp(float pixels) {
return pixels / ((float) mContext.getResources().getDisplayMetrics().densityDpi
/ DisplayMetrics.DENSITY_DEFAULT);
}
}