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

Commit 396a8412 authored by Kevin Chyn's avatar Kevin Chyn
Browse files

11/n: Animate panel to full-screen when "Use Password" is pressed

Bug: 140127687
Test: atest com.android.systemui.biometrics
Test: BiometricPromptDemo, enable device credential, press password button

Change-Id: I6a4c6ea7fb4a4f0c55faa049a8e7e71a1c5f19ff
parent 568d3293
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -311,6 +311,13 @@
    <!-- Talkback string when a biometric is authenticated [CHAR LIMIT=NONE] -->
    <string name="biometric_dialog_authenticated">Authenticated</string>

    <!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pin) [CHAR LIMIT=30] -->
    <string name="biometric_dialog_use_pin">Use PIN</string>
    <!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pattern) [CHAR LIMIT=30] -->
    <string name="biometric_dialog_use_pattern">Use pattern</string>
    <!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pass) [CHAR LIMIT=30] -->
    <string name="biometric_dialog_use_password">Use password</string>

    <!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication -->
    <string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string>
    <!-- Content description of the fingerprint icon when the system-provided fingerprint dialog is showing, for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
+85 −8
Original line number Diff line number Diff line
@@ -16,8 +16,6 @@

package com.android.systemui.biometrics;

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

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -25,6 +23,7 @@ import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.hardware.biometrics.BiometricPrompt;
import android.os.Bundle;
@@ -34,7 +33,7 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
import android.widget.Button;
import android.widget.ImageView;
@@ -42,6 +41,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;

import java.lang.annotation.Retention;
@@ -97,6 +97,7 @@ public abstract class AuthBiometricView extends LinearLayout {
        int ACTION_BUTTON_NEGATIVE = 3;
        int ACTION_BUTTON_TRY_AGAIN = 4;
        int ACTION_ERROR = 5;
        int ACTION_USE_DEVICE_CREDENTIAL = 6;

        /**
         * When an action has occurred. The caller will only invoke this when the callback should
@@ -145,6 +146,10 @@ public abstract class AuthBiometricView extends LinearLayout {
        public int getDelayAfterError() {
            return BiometricPrompt.HIDE_DIALOG_DELAY;
        }

        public int getAnimationDuration() {
            return AuthDialog.ANIMATE_DURATION_MS;
        }
    }

    private final Injector mInjector;
@@ -156,6 +161,7 @@ public abstract class AuthBiometricView extends LinearLayout {
    private AuthPanelController mPanelController;
    private Bundle mBundle;
    private boolean mRequireConfirmation;
    private int mUserId;
    @AuthDialog.DialogSize int mSize = AuthDialog.SIZE_UNKNOWN;

    private TextView mTitleView;
@@ -212,6 +218,9 @@ public abstract class AuthBiometricView extends LinearLayout {
        } else if (mSize == AuthDialog.SIZE_SMALL) {
            Log.w(TAG, "Ignoring background click during small dialog");
            return;
        } else if (mSize == AuthDialog.SIZE_LARGE) {
            Log.w(TAG, "Ignoring background click during large dialog");
            return;
        }
        mCallback.onAction(Callback.ACTION_USER_CANCELED);
    };
@@ -267,6 +276,10 @@ public abstract class AuthBiometricView extends LinearLayout {
        backgroundView.setOnClickListener(mBackgroundClickListener);
    }

    public void setUserId(int userId) {
        mUserId = userId;
    }

    public void setRequireConfirmation(boolean requireConfirmation) {
        mRequireConfirmation = requireConfirmation;
    }
@@ -305,10 +318,9 @@ public abstract class AuthBiometricView extends LinearLayout {

            // Animate the text
            final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(0, 1);
            opacityAnimator.setDuration(AuthDialog.ANIMATE_DURATION_MS);
            opacityAnimator.setDuration(mInjector.getAnimationDuration());
            opacityAnimator.addUpdateListener((animation) -> {
                final float opacity = (float) animation.getAnimatedValue();

                mTitleView.setAlpha(opacity);
                mIndicatorView.setAlpha(opacity);
                mNegativeButton.setAlpha(opacity);
@@ -324,7 +336,7 @@ public abstract class AuthBiometricView extends LinearLayout {

            // Choreograph together
            final AnimatorSet as = new AnimatorSet();
            as.setDuration(AuthDialog.ANIMATE_DURATION_MS);
            as.setDuration(mInjector.getAnimationDuration());
            as.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation) {
@@ -360,6 +372,36 @@ public abstract class AuthBiometricView extends LinearLayout {
            mPanelController.updateForContentDimensions(mMediumWidth, mMediumHeight,
                    false /* animate */);
            mSize = newSize;
        } else if (newSize == AuthDialog.SIZE_LARGE) {
            final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0);
            opacityAnimator.setDuration(mInjector.getAnimationDuration());
            opacityAnimator.addUpdateListener((animation) -> {
                final float opacity = (float) animation.getAnimatedValue();
                mTitleView.setAlpha(opacity);
                mSubtitleView.setAlpha(opacity);
                mDescriptionView.setAlpha(opacity);
                mIconView.setAlpha(opacity);
                mIndicatorView.setAlpha(opacity);
                mNegativeButton.setAlpha(opacity);
            });
            opacityAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    AuthBiometricView view = AuthBiometricView.this;
                    if (view.getParent() != null) {
                        ((ViewGroup) view.getParent()).removeView(view);
                    }
                    mSize = newSize;
                }
            });

            mPanelController.setUseFullScreen(true);
            mPanelController.updateForContentDimensions(
                    mPanelController.getContainerWidth(),
                    mPanelController.getContainerHeight(),
                    true /* animate */);
            opacityAnimator.start();
        } else {
            Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize);
        }
@@ -527,9 +569,14 @@ public abstract class AuthBiometricView extends LinearLayout {
        mNegativeButton.setOnClickListener((view) -> {
            if (mState == STATE_PENDING_CONFIRMATION) {
                mCallback.onAction(Callback.ACTION_USER_CANCELED);
            } else {
                if (isDeviceCredentialAllowed()) {
                    updateSize(AuthDialog.SIZE_LARGE);
                    mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
                } else {
                    mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE);
                }
            }
        });

        mPositiveButton.setOnClickListener((view) -> {
@@ -557,7 +604,33 @@ public abstract class AuthBiometricView extends LinearLayout {
    @VisibleForTesting
    void onAttachedToWindowInternal() {
        setText(mTitleView, mBundle.getString(BiometricPrompt.KEY_TITLE));
        setText(mNegativeButton, mBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT));

        final String negativeText;
        if (isDeviceCredentialAllowed()) {
            final LockPatternUtils lpu = new LockPatternUtils(mContext);
            switch (lpu.getKeyguardStoredPasswordQuality(mUserId)) {
                case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING:
                    negativeText = getResources().getString(R.string.biometric_dialog_use_pattern);
                    break;
                case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
                case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
                    negativeText = getResources().getString(R.string.biometric_dialog_use_pin);
                    break;
                case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC:
                case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC:
                case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX:
                case DevicePolicyManager.PASSWORD_QUALITY_MANAGED:
                    negativeText = getResources().getString(R.string.biometric_dialog_use_password);
                    break;
                default:
                    negativeText = getResources().getString(R.string.biometric_dialog_use_password);
                    break;
            }

        } else {
            negativeText = mBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT);
        }
        setText(mNegativeButton, negativeText);

        setTextOrHide(mSubtitleView, mBundle.getString(BiometricPrompt.KEY_SUBTITLE));
        setTextOrHide(mDescriptionView, mBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
@@ -655,4 +728,8 @@ public abstract class AuthBiometricView extends LinearLayout {
            }
        }
    }

    private boolean isDeviceCredentialAllowed() {
        return mBundle.getBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL);
    }
}
+10 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricPrompt;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
@@ -169,6 +170,9 @@ public class AuthContainerView extends LinearLayout
                case AuthBiometricView.Callback.ACTION_ERROR:
                    animateAway(AuthDialogCallback.DISMISSED_ERROR);
                    break;
                case AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL:
                    Log.v(TAG, "ACTION_USE_DEVICE_CREDENTIAL");
                    break;
                default:
                    Log.e(TAG, "Unhandled action: " + action);
            }
@@ -228,6 +232,7 @@ public class AuthContainerView extends LinearLayout
        mBiometricView.setBiometricPromptBundle(config.mBiometricPromptBundle);
        mBiometricView.setCallback(mBiometricCallback);
        mBiometricView.setBackgroundView(mBackgroundView);
        mBiometricView.setUserId(mConfig.mUserId);

        mScrollView = mContainerView.findViewById(R.id.scrollview);
        mScrollView.addView(mBiometricView);
@@ -406,6 +411,11 @@ public class AuthContainerView extends LinearLayout
        });
    }

    private boolean isDeviceCredentialAllowed() {
        return mConfig.mBiometricPromptBundle.getBoolean(
                BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, false);
    }

    private void sendPendingCallbackIfNotNull() {
        Log.d(TAG, "pendingCallback: " + mPendingCallbackReason);
        if (mPendingCallbackReason != null) {
+34 −7
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.biometrics;

import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Outline;
@@ -24,7 +25,6 @@ import android.view.View;
import android.view.ViewOutlineProvider;

import com.android.systemui.R;
import com.android.systemui.biometrics.AuthDialog;

/**
 * Controls the back panel and its animations for the BiometricPrompt UI.
@@ -32,13 +32,15 @@ import com.android.systemui.biometrics.AuthDialog;
public class AuthPanelController extends ViewOutlineProvider {

    private static final String TAG = "BiometricPrompt/AuthPanelController";
    private static final boolean DEBUG = false;
    private static final boolean DEBUG = true;

    private final Context mContext;
    private final View mPanelView;
    private final float mCornerRadius;
    private final int mBiometricMargin;

    private boolean mUseFullScreen;

    private int mContainerWidth;
    private int mContainerHeight;

@@ -49,11 +51,15 @@ public class AuthPanelController extends ViewOutlineProvider {
    public void getOutline(View view, Outline outline) {
        final int left = (mContainerWidth - mContentWidth) / 2;
        final int right = mContainerWidth - left;

        final int margin = mUseFullScreen ? 0 : mBiometricMargin;
        final float cornerRadius = mUseFullScreen ? 0 : mCornerRadius;

        final int top = mContentHeight < mContainerHeight
                ? mContainerHeight - mContentHeight - mBiometricMargin
                : mBiometricMargin;
        final int bottom = mContainerHeight - mBiometricMargin;
        outline.setRoundRect(left, top, right, bottom, mCornerRadius);
                ? mContainerHeight - mContentHeight - margin
                : margin;
        final int bottom = mContainerHeight - margin;
        outline.setRoundRect(left, top, right, bottom, cornerRadius);
    }

    public void setContainerDimensions(int containerWidth, int containerHeight) {
@@ -64,6 +70,10 @@ public class AuthPanelController extends ViewOutlineProvider {
        mContainerHeight = containerHeight;
    }

    public void setUseFullScreen(boolean fullScreen) {
        mUseFullScreen = fullScreen;
    }

    public void updateForContentDimensions(int contentWidth, int contentHeight, boolean animate) {
        if (DEBUG) {
            Log.v(TAG, "Content Width: " + contentWidth
@@ -78,12 +88,21 @@ public class AuthPanelController extends ViewOutlineProvider {

        if (animate) {
            ValueAnimator heightAnimator = ValueAnimator.ofInt(mContentHeight, contentHeight);
            heightAnimator.setDuration(AuthDialog.ANIMATE_DURATION_MS);
            heightAnimator.addUpdateListener((animation) -> {
                mContentHeight = (int) animation.getAnimatedValue();
                mPanelView.invalidateOutline();
            });
            heightAnimator.start();

            ValueAnimator widthAnimator = ValueAnimator.ofInt(mContentWidth, contentWidth);
            widthAnimator.addUpdateListener((animation) -> {
                mContentWidth = (int) animation.getAnimatedValue();
            });

            AnimatorSet as = new AnimatorSet();
            as.setDuration(AuthDialog.ANIMATE_DURATION_MS);
            as.play(heightAnimator).with(widthAnimator);
            as.start();
        } else {
            mContentWidth = contentWidth;
            mContentHeight = contentHeight;
@@ -91,6 +110,14 @@ public class AuthPanelController extends ViewOutlineProvider {
        }
    }

    int getContainerWidth() {
        return mContainerWidth;
    }

    int getContainerHeight() {
        return mContainerHeight;
    }

    AuthPanelController(Context context, View panelView) {
        mContext = context;
        mPanelView = panelView;
+58 −29
Original line number Diff line number Diff line
@@ -70,7 +70,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {

    @Test
    public void testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() {
        initDialog(mContext, mCallback, new MockInjector());
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());

        // The onAuthenticated runnable is posted when authentication succeeds.
        mBiometricView.onAuthenticationSucceeded();
@@ -81,7 +81,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {

    @Test
    public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() {
        initDialog(mContext, mCallback, new MockInjector());
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());

        mBiometricView.setRequireConfirmation(true);
        mBiometricView.onAuthenticationSucceeded();
@@ -97,7 +97,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
    @Test
    public void testPositiveButton_sendsActionAuthenticated() {
        Button button = new Button(mContext);
        initDialog(mContext, mCallback, new MockInjector() {
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
           @Override
            public Button getPositiveButton() {
               return button;
@@ -114,7 +114,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
    @Test
    public void testNegativeButton_beforeAuthentication_sendsActionButtonNegative() {
        Button button = new Button(mContext);
        initDialog(mContext, mCallback, new MockInjector() {
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
            @Override
            public Button getNegativeButton() {
                return button;
@@ -131,7 +131,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
    @Test
    public void testNegativeButton_whenPendingConfirmation_sendsActionUserCanceled() {
        Button button = new Button(mContext);
        initDialog(mContext, mCallback, new MockInjector() {
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
            @Override
            public Button getNegativeButton() {
                return button;
@@ -149,7 +149,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
    @Test
    public void testTryAgainButton_sendsActionTryAgain() {
        Button button = new Button(mContext);
        initDialog(mContext, mCallback, new MockInjector() {
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
            @Override
            public Button getTryAgainButton() {
                return button;
@@ -165,7 +165,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {

    @Test
    public void testError_sendsActionError() {
        initDialog(mContext, mCallback, new MockInjector());
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
        final String testError = "testError";
        mBiometricView.onError(testError);
        waitForIdleSync();
@@ -176,7 +176,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {

    @Test
    public void testBackgroundClicked_sendsActionUserCanceled() {
        initDialog(mContext, mCallback, new MockInjector());
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());

        View view = new View(mContext);
        mBiometricView.setBackgroundView(view);
@@ -186,7 +186,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {

    @Test
    public void testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() {
        initDialog(mContext, mCallback, new MockInjector());
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());

        View view = new View(mContext);
        mBiometricView.setBackgroundView(view);
@@ -197,8 +197,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {

    @Test
    public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() {
        initDialog(mContext, mCallback, new MockInjector());
        mBiometricView.setPanelController(mPanelController);
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector());
        mBiometricView.updateSize(AuthDialog.SIZE_SMALL);

        View view = new View(mContext);
@@ -213,7 +212,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {

        Button tryAgainButton = new Button(mContext);
        TextView indicatorView = new TextView(mContext);
        initDialog(mContext, mCallback, new MockInjector() {
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() {
            @Override
            public Button getTryAgainButton() {
                return tryAgainButton;
@@ -249,11 +248,13 @@ public class AuthBiometricViewTest extends SysuiTestCase {
        // Create new dialog and restore the previous state into it
        Button tryAgainButton2 = new Button(mContext);
        TextView indicatorView2 = new TextView(mContext);
        initDialog(mContext, mCallback, state, new MockInjector() {
        initDialog(mContext, false /* allowDeviceCredential */, mCallback, state,
                new MockInjector() {
                    @Override
                    public Button getTryAgainButton() {
                        return tryAgainButton2;
                    }

                    @Override
                    public TextView getIndicatorView() {
                        return indicatorView2;
@@ -271,26 +272,49 @@ public class AuthBiometricViewTest extends SysuiTestCase {
        // dialog size is known.
    }

    private Bundle buildBiometricPromptBundle() {
    @Test
    public void testNegativeButton_whenDeviceCredentialAllowed() throws InterruptedException {
        Button negativeButton = new Button(mContext);
        initDialog(mContext, true /* allowDeviceCredential */, mCallback, new MockInjector() {
            @Override
            public Button getNegativeButton() {
                return negativeButton;
            }
        });

        negativeButton.performClick();
        waitForIdleSync();

        verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL);
    }

    private Bundle buildBiometricPromptBundle(boolean allowDeviceCredential) {
        Bundle bundle = new Bundle();
        bundle.putCharSequence(BiometricPrompt.KEY_TITLE, "Title");
        if (allowDeviceCredential) {
            bundle.putBoolean(BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL, true);
        } else {
            bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, "Negative");
        }
        return bundle;
    }

    private void initDialog(Context context, AuthBiometricView.Callback callback,
    private void initDialog(Context context, boolean allowDeviceCredential,
            AuthBiometricView.Callback callback,
            Bundle savedState, MockInjector injector) {
        mBiometricView = new TestableBiometricView(context, null, injector);
        mBiometricView.setBiometricPromptBundle(buildBiometricPromptBundle());
        mBiometricView.setBiometricPromptBundle(buildBiometricPromptBundle(allowDeviceCredential));
        mBiometricView.setCallback(callback);
        mBiometricView.restoreState(savedState);
        mBiometricView.onFinishInflateInternal();
        mBiometricView.onAttachedToWindowInternal();

        mBiometricView.setPanelController(mPanelController);
    }

    private void initDialog(Context context, AuthBiometricView.Callback callback,
            MockInjector injector) {
        initDialog(context, callback, null /* savedState */, injector);
    private void initDialog(Context context, boolean allowDeviceCredential,
            AuthBiometricView.Callback callback, MockInjector injector) {
        initDialog(context, allowDeviceCredential, callback, null /* savedState */, injector);
    }

    private class MockInjector extends AuthBiometricView.Injector {
@@ -338,6 +362,11 @@ public class AuthBiometricViewTest extends SysuiTestCase {
        public int getDelayAfterError() {
            return 0; // Keep this at 0 for tests to invoke callback immediately.
        }

        @Override
        public int getAnimationDuration() {
            return 0;
        }
    }

    private class TestableBiometricView extends AuthBiometricView {