Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4077d36b authored by Kevin Chyn's avatar Kevin Chyn Committed by Android (Google) Code Review
Browse files

Merge "Move BiometricDialog animations to base class"

parents c3a5d10e e5e8d05e
Loading
Loading
Loading
Loading
+251 −19
Original line number Diff line number Diff line
@@ -18,8 +18,14 @@ package com.android.systemui.biometrics.ui;

import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.graphics.Outline;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
@@ -38,6 +44,7 @@ import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
@@ -71,6 +78,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
    public static final String KEY_ERROR_TEXT_STRING = "key_error_text_string";
    public static final String KEY_ERROR_TEXT_IS_TEMPORARY = "key_error_text_is_temporary";
    public static final String KEY_ERROR_TEXT_COLOR = "key_error_text_color";
    public static final String KEY_DIALOG_SIZE = "key_dialog_size";

    private static final int ANIMATION_DURATION_SHOW = 250; // ms
    private static final int ANIMATION_DURATION_AWAY = 350; // ms
@@ -83,8 +91,18 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
    protected static final int STATE_PENDING_CONFIRMATION = 3;
    protected static final int STATE_AUTHENTICATED = 4;

    @VisibleForTesting
    final WakefulnessLifecycle mWakefulnessLifecycle;
    // Dialog layout/animation
    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
    @VisibleForTesting static final int SIZE_UNKNOWN = 0;
    @VisibleForTesting static final int SIZE_SMALL = 1;
    @VisibleForTesting static final int SIZE_GROWING = 2;
    @VisibleForTesting static final int SIZE_BIG = 3;
    @IntDef({SIZE_UNKNOWN, SIZE_SMALL, SIZE_GROWING, SIZE_BIG})
    @interface DialogSize {}

    @VisibleForTesting final WakefulnessLifecycle mWakefulnessLifecycle;
    private final AccessibilityManager mAccessibilityManager;
    private final IBinder mWindowToken = new Binder();
    private final Interpolator mLinearOutSlowIn;
@@ -95,25 +113,18 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
    private final int mErrorColor;
    private final float mDialogWidth;
    protected final DialogViewCallback mCallback;
    private final DialogOutlineProvider mOutlineProvider = new DialogOutlineProvider();

    protected final ViewGroup mLayout;
    protected final LinearLayout mDialog;
    @VisibleForTesting
    final TextView mTitleText;
    @VisibleForTesting
    final TextView mSubtitleText;
    @VisibleForTesting
    final TextView mDescriptionText;
    @VisibleForTesting
    final ImageView mBiometricIcon;
    @VisibleForTesting
    final TextView mErrorText;
    @VisibleForTesting
    final Button mPositiveButton;
    @VisibleForTesting
    final Button mNegativeButton;
    @VisibleForTesting
    final Button mTryAgainButton;
    @VisibleForTesting final TextView mTitleText;
    @VisibleForTesting final TextView mSubtitleText;
    @VisibleForTesting final TextView mDescriptionText;
    @VisibleForTesting final ImageView mBiometricIcon;
    @VisibleForTesting final TextView mErrorText;
    @VisibleForTesting final Button mPositiveButton;
    @VisibleForTesting final Button mNegativeButton;
    @VisibleForTesting final Button mTryAgainButton;

    protected final int mTextColor;

@@ -126,6 +137,8 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
    private boolean mSkipIntro;
    protected boolean mRequireConfirmation;
    private int mUserId; // used to determine if we should show work background
    private @DialogSize int mSize;
    private float mIconOriginalY;

    private boolean mCompletedAnimatingIn;
    private boolean mPendingDismissDialog;
@@ -137,6 +150,7 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
    protected abstract boolean shouldGrayAreaDismissDialog();
    protected abstract void handleResetMessage();
    protected abstract void updateIcon(int oldState, int newState);
    protected abstract boolean supportsSmallDialog();

    private final Runnable mShowAnimationRunnable = new Runnable() {
        @Override
@@ -166,6 +180,30 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
                }
            };

    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 = Utils.dpToPixels(mContext, IMPLICIT_Y_PADDING);
            return mDialog.getHeight() - mBiometricIcon.getHeight() - 2 * (int) padding;
        }

        void setOutlineY(float y) {
            mY = y;
        }
    }

    protected Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
@@ -346,7 +384,6 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
        mWakefulnessLifecycle.addObserver(mWakefulnessObserver);

        final ImageView backgroundView = mLayout.findViewById(R.id.background);

        if (mUserManager.isManagedProfile(mUserId)) {
            final Drawable image = getResources().getDrawable(R.drawable.work_challenge_background,
                    mContext.getTheme());
@@ -428,6 +465,42 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
        mSkipIntro = false;
    }

    /**
     * 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 (getSize() != 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 (getSize() == SIZE_SMALL) {
                updateSize(SIZE_SMALL);
            }
            return;
        }

        // If we don't require confirmation, show the small dialog first (until errors occur).
        if (!requiresConfirmation() && supportsSmallDialog()) {
            updateSize(SIZE_SMALL);
        } else {
            updateSize(SIZE_BIG);
        }
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
@@ -435,6 +508,133 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
        mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
    }

    @VisibleForTesting
    void updateSize(@DialogSize int newSize) {
        final float padding = Utils.dpToPixels(mContext, IMPLICIT_Y_PADDING);
        final float iconSmallPositionY = mDialog.getHeight() - mBiometricIcon.getHeight() - padding;

        if (newSize == SIZE_SMALL) {
            if (!supportsSmallDialog()) {
                Log.e(TAG, "Small dialog unsupported");
                return;
            }

            // 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;
            announceAccessibilityEvent();
        } 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(Utils.dpToPixels(mContext, 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;
        }
    }

    private void setDismissesDialog(View v) {
        v.setClickable(true);
        v.setOnClickListener(v1 -> {
@@ -599,6 +799,12 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
     */
    @Override
    public void onError(String 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);
        }

        updateState(STATE_ERROR);
        showTemporaryMessage(error);
        showTryAgainButton(false /* show */);
@@ -619,11 +825,16 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
        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());
        bundle.putInt(KEY_DIALOG_SIZE, mSize);
    }

    @Override
    public void restoreState(Bundle bundle) {
        mRestoredState = bundle;

        // Keep in mind that this happens before onAttachedToWindow()
        mSize = bundle.getInt(KEY_DIALOG_SIZE);

        final int tryAgainVisibility = bundle.getInt(KEY_TRY_AGAIN_VISIBILITY);
        mTryAgainButton.setVisibility(tryAgainVisibility);
        final int confirmVisibility = bundle.getInt(KEY_CONFIRM_VISIBILITY);
@@ -678,7 +889,28 @@ public abstract class BiometricDialogView extends LinearLayout implements Biomet
        mState = newState;
    }

    protected @DialogSize int getSize() {
        return mSize;
    }

    protected void showTryAgainButton(boolean show) {
        if (show && getSize() == 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);
                announceAccessibilityEvent();
            }
        }

        if (show) {
            mPositiveButton.setVisibility(View.GONE);
            announceAccessibilityEvent();
        }
    }

    protected void onDialogAnimatedIn() {
+8 −248

File changed.

Preview size limit exceeded, changes collapsed.

+5 −0
Original line number Diff line number Diff line
@@ -80,6 +80,11 @@ public class FingerprintDialogView extends BiometricDialogView {
        }
    }

    @Override
    protected boolean supportsSmallDialog() {
        return false;
    }

    protected boolean shouldAnimateForTransition(int oldState, int newState) {
        if (newState == STATE_ERROR) {
            return true;
+32 −0
Original line number Diff line number Diff line
/*
 * 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.ui;

import android.content.Context;
import android.util.DisplayMetrics;

public class Utils {
    static float dpToPixels(Context context, float dp) {
        return dp * ((float) context.getResources().getDisplayMetrics().densityDpi
                / DisplayMetrics.DENSITY_DEFAULT);
    }

    static float pixelsToDp(Context context, float pixels) {
        return pixels / ((float) context.getResources().getDisplayMetrics().densityDpi
                / DisplayMetrics.DENSITY_DEFAULT);
    }
}