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

Commit e5e8d05e authored by Kevin Chyn's avatar Kevin Chyn
Browse files

Move BiometricDialog animations to base class

Bug: 123378871
Test: atest BiometricDialogViewTest
Test: manual test on devices
Change-Id: Id2e9ecaa3dc41c8da5179fa5c3764e53fcb78dc7
parent 9a28809f
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);
    }
}