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

Commit 2b1d3d8f authored by Kevin Chyn's avatar Kevin Chyn
Browse files

3/n: Tapping outside of the dialog should cancel authentication

Authentication should not be canceled in the following states
1) small dialog
2) authenticated (confirmation not required, or confirmation required &&
   confirm button pressed)

Bug: 123378871

Test: manual test of above conditions
Test: atest com.android.systemui.biometrics

Change-Id: I6ed3b0a0ba7cea3e6dc00f550381f51a1694f6a5
parent 504f77d8
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ public class AuthBiometricFaceView extends AuthBiometricView {
        TextView mTextView;
        Handler mHandler;
        boolean mLastPulseLightToDark; // false = dark to light, true = light to dark
        @State int mState;
        @BiometricState int mState;

        IconController(Context context, ImageView iconView, TextView textView) {
            mContext = context;
@@ -170,7 +170,7 @@ public class AuthBiometricFaceView extends AuthBiometricView {
    }

    @Override
    public void updateState(@State int newState) {
    public void updateState(@BiometricState int newState) {
        mIconController.updateState(mState, newState);

        if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
+41 −10
Original line number Diff line number Diff line
@@ -81,7 +81,7 @@ public abstract class AuthBiometricView extends LinearLayout {
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({STATE_IDLE, STATE_AUTHENTICATING_ANIMATING_IN, STATE_AUTHENTICATING, STATE_HELP,
            STATE_ERROR, STATE_PENDING_CONFIRMATION, STATE_AUTHENTICATED})
    @interface State {}
    @interface BiometricState {}

    /**
     * Callback to the parent when a user action has occurred.
@@ -116,9 +116,25 @@ public abstract class AuthBiometricView extends LinearLayout {
            return mBiometricView.findViewById(R.id.button_try_again);
        }

        public TextView getTitleView() {
            return mBiometricView.findViewById(R.id.title);
        }

        public TextView getSubtitleView() {
            return mBiometricView.findViewById(R.id.subtitle);
        }

        public TextView getDescriptionView() {
            return mBiometricView.findViewById(R.id.description);
        }

        public TextView getErrorView() {
            return mBiometricView.findViewById(R.id.error);
        }

        public ImageView getIconView() {
            return mBiometricView.findViewById(R.id.biometric_icon);
        }
    }

    private final Injector mInjector;
@@ -145,7 +161,7 @@ public abstract class AuthBiometricView extends LinearLayout {
    private int mMediumWidth;

    private Callback mCallback;
    protected @State int mState;
    protected @BiometricState int mState;

    private float mIconOriginalY;

@@ -179,6 +195,17 @@ public abstract class AuthBiometricView extends LinearLayout {
        handleResetAfterHelp();
    };

    private final OnClickListener mBackgroundClickListener = (view) -> {
        if (mState == STATE_AUTHENTICATED) {
            Log.w(TAG, "Ignoring background click after authenticated");
            return;
        } else if (mSize == BiometricDialog.SIZE_SMALL) {
            Log.w(TAG, "Ignoring background click during small dialog");
            return;
        }
        mCallback.onAction(Callback.ACTION_USER_CANCELED);
    };

    public AuthBiometricView(Context context) {
        this(context, null);
    }
@@ -212,11 +239,16 @@ public abstract class AuthBiometricView extends LinearLayout {
        mCallback = callback;
    }

    public void setBackgroundView(View backgroundView) {
        backgroundView.setOnClickListener(mBackgroundClickListener);
    }

    public void setRequireConfirmation(boolean requireConfirmation) {
        mRequireConfirmation = requireConfirmation;
    }

    private void updateSize(@BiometricDialog.DialogSize int newSize) {
    @VisibleForTesting
    void updateSize(@BiometricDialog.DialogSize int newSize) {
        Log.v(TAG, "Current: " + mSize + " New: " + newSize);
        if (newSize == BiometricDialog.SIZE_SMALL) {
            mTitleView.setVisibility(View.GONE);
@@ -307,7 +339,7 @@ public abstract class AuthBiometricView extends LinearLayout {
        }
    }

    public void updateState(@State int newState) {
    public void updateState(@BiometricState int newState) {
        Log.v(TAG, "newState: " + newState);
        switch (newState) {
            case STATE_AUTHENTICATING_ANIMATING_IN:
@@ -413,11 +445,10 @@ public abstract class AuthBiometricView extends LinearLayout {

    @VisibleForTesting
    void initializeViews() {
        mTitleView = findViewById(R.id.title);
        mSubtitleView = findViewById(R.id.subtitle);
        mDescriptionView = findViewById(R.id.description);
        mIconView = findViewById(R.id.biometric_icon);

        mTitleView = mInjector.getTitleView();
        mSubtitleView = mInjector.getSubtitleView();
        mDescriptionView = mInjector.getDescriptionView();
        mIconView = mInjector.getIconView();
        mErrorView = mInjector.getErrorView();
        mNegativeButton = mInjector.getNegativeButton();
        mPositiveButton = mInjector.getPositiveButton();
+31 −9
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.biometrics.ui;

import android.annotation.IntDef;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Binder;
@@ -41,6 +42,9 @@ import com.android.systemui.biometrics.BiometricDialog;
import com.android.systemui.biometrics.DialogViewCallback;
import com.android.systemui.keyguard.WakefulnessLifecycle;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Top level container/controller for the BiometricPrompt UI.
 */
@@ -51,8 +55,19 @@ public class AuthContainerView extends LinearLayout
    private static final int ANIMATION_DURATION_SHOW_MS = 250;
    private static final int ANIMATION_DURATION_AWAY_MS = 350; // ms

    private static final int STATE_UNKNOWN = 0;
    private static final int STATE_ANIMATING_IN = 1;
    private static final int STATE_PENDING_DISMISS = 2;
    private static final int STATE_SHOWING = 3;
    private static final int STATE_ANIMATING_OUT = 4;
    private static final int STATE_GONE = 5;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING,
            STATE_ANIMATING_OUT, STATE_GONE})
    @interface ContainerState {}

    final Config mConfig;
    private final Handler mHandler = new Handler();
    private final IBinder mWindowToken = new Binder();
    private final WindowManager mWindowManager;
    private final AuthPanelController mPanelController;
@@ -70,8 +85,7 @@ public class AuthContainerView extends LinearLayout

    @VisibleForTesting final WakefulnessLifecycle mWakefulnessLifecycle;

    private boolean mCompletedAnimatingIn;
    private boolean mPendingDismissDialog;
    private @ContainerState int mContainerState = STATE_UNKNOWN;

    static class Config {
        Context mContext;
@@ -170,6 +184,7 @@ public class AuthContainerView extends LinearLayout
        // TODO: Depends on modality
        mBiometricView = (AuthBiometricFaceView)
                factory.inflate(R.layout.auth_biometric_face_view, null, false);

        mBackgroundView = mContainerView.findViewById(R.id.background);

        mPanelView = mContainerView.findViewById(R.id.panel);
@@ -179,6 +194,7 @@ public class AuthContainerView extends LinearLayout
        mBiometricView.setPanelController(mPanelController);
        mBiometricView.setBiometricPromptBundle(config.mBiometricPromptBundle);
        mBiometricView.setCallback(mBiometricCallback);
        mBiometricView.setBackgroundView(mBackgroundView);

        mScrollView = mContainerView.findViewById(R.id.scrollview);
        mScrollView.addView(mBiometricView);
@@ -210,8 +226,9 @@ public class AuthContainerView extends LinearLayout
        mWakefulnessLifecycle.addObserver(this);

        if (mConfig.mSkipIntro) {
            mCompletedAnimatingIn = true;
            mContainerState = STATE_SHOWING;
        } else {
            mContainerState = STATE_ANIMATING_IN;
            // The background panel and content are different views since we need to be able to
            // animate them separately in other places.
            mPanelView.setY(mTranslationY);
@@ -313,11 +330,17 @@ public class AuthContainerView extends LinearLayout
    }

    private void animateAway(boolean sendReason, @DialogViewCallback.DismissedReason int reason) {
        if (!mCompletedAnimatingIn) {
        if (mContainerState == STATE_ANIMATING_IN) {
            Log.w(TAG, "startDismiss(): waiting for onDialogAnimatedIn");
            mPendingDismissDialog = true;
            mContainerState = STATE_PENDING_DISMISS;
            return;
        }

        if (mContainerState == STATE_ANIMATING_OUT) {
            Log.w(TAG, "Already dismissing, sendReason: " + sendReason + " reason: " + reason);
            return;
        }
        mContainerState = STATE_ANIMATING_OUT;

        final Runnable endActionRunnable = () -> {
            setVisibility(View.INVISIBLE);
@@ -351,13 +374,12 @@ public class AuthContainerView extends LinearLayout
    }

    private void onDialogAnimatedIn() {
        mCompletedAnimatingIn = true;
        if (mPendingDismissDialog) {
        if (mContainerState == STATE_PENDING_DISMISS) {
            Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now");
            animateAway(false /* sendReason */, 0);
            mPendingDismissDialog = false;
            return;
        }
        mContainerState = STATE_SHOWING;
        mBiometricView.onDialogAnimatedIn();
    }

+63 −3
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.systemui.biometrics.ui;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
@@ -30,10 +29,12 @@ import android.testing.TestableLooper.RunWithLooper;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.BiometricDialog;

import org.junit.Before;
import org.junit.Test;
@@ -47,10 +48,16 @@ import org.mockito.MockitoAnnotations;
public class AuthBiometricViewTest extends SysuiTestCase {

    @Mock private AuthBiometricView.Callback mCallback;
    @Mock private AuthPanelController mPanelController;

    @Mock private Button mNegativeButton;
    @Mock private Button mPositiveButton;
    @Mock private Button mTryAgainButton;
    @Mock private TextView mTitleView;
    @Mock private TextView mSubtitleView;
    @Mock private TextView mDescriptionView;
    @Mock private TextView mErrorView;
    @Mock private ImageView mIconView;

    TestableBiometricView mBiometricView;

@@ -154,6 +161,39 @@ public class AuthBiometricViewTest extends SysuiTestCase {
        assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState);
    }

    @Test
    public void testBackgroundClicked_sendsActionUserCanceled() {
        initDialog(mContext, mCallback, new MockInjector());

        View view = new View(mContext);
        mBiometricView.setBackgroundView(view);
        view.performClick();
        verify(mCallback).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
    }

    @Test
    public void testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() {
        initDialog(mContext, mCallback, new MockInjector());

        View view = new View(mContext);
        mBiometricView.setBackgroundView(view);
        mBiometricView.onAuthenticationSucceeded();
        view.performClick();
        verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
    }

    @Test
    public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() {
        initDialog(mContext, mCallback, new MockInjector());
        mBiometricView.setPanelController(mPanelController);
        mBiometricView.updateSize(BiometricDialog.SIZE_SMALL);

        View view = new View(mContext);
        mBiometricView.setBackgroundView(view);
        view.performClick();
        verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
    }

    private void initDialog(Context context, AuthBiometricView.Callback callback,
            MockInjector injector) {
        mBiometricView = new TestableBiometricView(context, null, injector);
@@ -161,7 +201,7 @@ public class AuthBiometricViewTest extends SysuiTestCase {
        mBiometricView.initializeViews();
    }

    class MockInjector extends AuthBiometricView.Injector {
    private class MockInjector extends AuthBiometricView.Injector {
        @Override
        public Button getNegativeButton() {
            return mNegativeButton;
@@ -177,13 +217,33 @@ public class AuthBiometricViewTest extends SysuiTestCase {
            return mTryAgainButton;
        }

        @Override
        public TextView getTitleView() {
            return mTitleView;
        }

        @Override
        public TextView getSubtitleView() {
            return mSubtitleView;
        }

        @Override
        public TextView getDescriptionView() {
            return mDescriptionView;
        }

        @Override
        public TextView getErrorView() {
            return mErrorView;
        }

        @Override
        public ImageView getIconView() {
            return mIconView;
        }
    }

    public class TestableBiometricView extends AuthBiometricView {
    private class TestableBiometricView extends AuthBiometricView {
        TestableBiometricView(Context context, AttributeSet attrs,
                Injector injector) {
            super(context, attrs, injector);