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

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

10/n: Remove old biometric UI

Test: atest com.android.systemui.biometrics
Bug: 123378871
Change-Id: Ic000148f4e4540d143581ca6cc296dba34a3db08
parent ddd0433c
Loading
Loading
Loading
Loading
+8 −23
Original line number Diff line number Diff line
@@ -29,7 +29,6 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
import android.view.WindowManager;

@@ -46,8 +45,6 @@ import java.util.List;
 */
public class AuthController extends SystemUI implements CommandQueue.Callbacks,
        AuthDialogCallback {
    private static final String DISABLE_NEW_DIALOG =
            "com.android.systemui.biometrics.AuthController.DISABLE_NEW_DIALOG";

    private static final String TAG = "BiometricPrompt/AuthController";
    private static final boolean DEBUG = true;
@@ -316,8 +313,6 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,

    protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation,
            int userId, int type, String opPackageName, boolean skipIntro) {
        if (Settings.Secure.getIntForUser(
                mContext.getContentResolver(), DISABLE_NEW_DIALOG, userId, 0) == 0) {
        return new AuthContainerView.Builder(mContext)
                .setCallback(this)
                .setBiometricPromptBundle(biometricPromptBundle)
@@ -326,15 +321,5 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
                .setOpPackageName(opPackageName)
                .setSkipIntro(skipIntro)
                .build(type);
        } else {
            return new BiometricDialogView.Builder(mContext)
                    .setCallback(this)
                    .setBiometricPromptBundle(biometricPromptBundle)
                    .setRequireConfirmation(requireConfirmation)
                    .setUserId(userId)
                    .setOpPackageName(opPackageName)
                    .setSkipIntro(skipIntro)
                    .build(type);
        }
    }
}
+0 −963

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −252
Original line number Diff line number Diff line
/*
 * 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.content.Context;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.hardware.biometrics.BiometricPrompt;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

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 = "BiometricPrompt/FaceDialogView";

    private static final String KEY_DIALOG_ANIMATED_IN = "key_dialog_animated_in";

    private static final int HIDE_DIALOG_DELAY = 500; // ms

    private IconController mIconController;
    private boolean mDialogAnimatedIn;

    /**
     * Class that handles the biometric icon animations.
     */
    private final class IconController extends Animatable2.AnimationCallback {

        private boolean mLastPulseDirection; // false = dark to light, true = light to dark

        int mState;

        IconController() {
            mState = STATE_IDLE;
        }

        public void animateOnce(int iconRes) {
            animateIcon(iconRes, false);
        }

        public void showStatic(int iconRes) {
            mBiometricIcon.setImageDrawable(mContext.getDrawable(iconRes));
        }

        public void startPulsing() {
            mLastPulseDirection = false;
            animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true);
        }

        public void showIcon(int iconRes) {
            final Drawable drawable = mContext.getDrawable(iconRes);
            mBiometricIcon.setImageDrawable(drawable);
        }

        private void animateIcon(int iconRes, boolean repeat) {
            final AnimatedVectorDrawable icon =
                    (AnimatedVectorDrawable) mContext.getDrawable(iconRes);
            mBiometricIcon.setImageDrawable(icon);
            icon.forceAnimationOnUI();
            if (repeat) {
                icon.registerAnimationCallback(this);
            }
            icon.start();
        }

        private void pulseInNextDirection() {
            int iconRes = mLastPulseDirection ? R.drawable.face_dialog_pulse_dark_to_light
                    : R.drawable.face_dialog_pulse_light_to_dark;
            animateIcon(iconRes, true /* repeat */);
            mLastPulseDirection = !mLastPulseDirection;
        }

        @Override
        public void onAnimationEnd(Drawable drawable) {
            super.onAnimationEnd(drawable);

            if (mState == STATE_AUTHENTICATING) {
                // Still authenticating, pulse the icon
                pulseInNextDirection();
            }
        }
    }

    private final Runnable mErrorToIdleAnimationRunnable = () -> {
        updateState(STATE_IDLE);
        mErrorText.setVisibility(View.INVISIBLE);
        announceAccessibilityEvent();
    };

    protected FaceDialogView(Context context, AuthDialogCallback callback, Injector injector) {
        super(context, callback, injector);
        mIconController = new IconController();
    }

    @Override
    public void onSaveState(Bundle bundle) {
        super.onSaveState(bundle);
        bundle.putBoolean(KEY_DIALOG_ANIMATED_IN, mDialogAnimatedIn);
    }


    @Override
    protected void handleResetMessage() {
        mErrorText.setTextColor(mTextColor);
        mErrorText.setVisibility(View.INVISIBLE);
        announceAccessibilityEvent();
    }

    @Override
    public void restoreState(Bundle bundle) {
        super.restoreState(bundle);
        mDialogAnimatedIn = bundle.getBoolean(KEY_DIALOG_ANIMATED_IN);
    }

    @Override
    public void onAuthenticationFailed(String message) {
        super.onAuthenticationFailed(message);
        showTryAgainButton(true);
    }

    @Override
    protected int getHintStringResourceId() {
        return 0;
    }

    @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 void updateIcon(int oldState, int newState) {
        mIconController.mState = newState;

        if (newState == STATE_AUTHENTICATING) {
            mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
            if (mDialogAnimatedIn) {
                mIconController.startPulsing();
            } else {
                mIconController.showIcon(R.drawable.face_dialog_pulse_dark_to_light);
            }
            mBiometricIcon.setContentDescription(mContext.getString(
                    R.string.biometric_dialog_face_icon_description_authenticating));
        } else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) {
            mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark);
            mBiometricIcon.setContentDescription(mContext.getString(
                    R.string.biometric_dialog_face_icon_description_confirmed));
        } else if (oldState == STATE_ERROR && newState == STATE_IDLE) {
            mIconController.animateOnce(R.drawable.face_dialog_error_to_idle);
            mBiometricIcon.setContentDescription(mContext.getString(
                    R.string.biometric_dialog_face_icon_description_idle));
        } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATED) {
            mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
            mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark);
            mBiometricIcon.setContentDescription(mContext.getString(
                    R.string.biometric_dialog_face_icon_description_authenticated));
        } else if (newState == STATE_ERROR) {
            // It's easier to only check newState and gate showing the animation on the
            // mErrorToIdleAnimationRunnable as a proxy, than add a ton of extra state. For example,
            // we may go from error -> error due to configuration change which is valid and we
            // should show the animation, or we can go from error -> error by receiving repeated
            // acquire messages in which case we do not want to repeatedly start the animation.
            if (!mHandler.hasCallbacks(mErrorToIdleAnimationRunnable)) {
                mIconController.animateOnce(R.drawable.face_dialog_dark_to_error);
                mHandler.postDelayed(mErrorToIdleAnimationRunnable,
                        BiometricPrompt.HIDE_DIALOG_DELAY);
            }
        } else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
            mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark);
            mBiometricIcon.setContentDescription(mContext.getString(
                    R.string.biometric_dialog_face_icon_description_authenticated));
        } else if (newState == STATE_PENDING_CONFIRMATION) {
            mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
            mIconController.animateOnce(R.drawable.face_dialog_wink_from_dark);
            mBiometricIcon.setContentDescription(mContext.getString(
                    R.string.biometric_dialog_face_icon_description_authenticated));
        } else if (newState == STATE_IDLE) {
            mIconController.showStatic(R.drawable.face_dialog_idle_static);
            mBiometricIcon.setContentDescription(mContext.getString(
                    R.string.biometric_dialog_face_icon_description_idle));
        } else {
            Log.w(TAG, "Unknown animation from " + oldState + " -> " + newState);
        }

        // Note that this must be after the newState == STATE_ERROR check above since this affects
        // the logic.
        if (oldState == STATE_ERROR && newState == STATE_ERROR) {
            // Keep the error icon and text around for a while longer if we keep receiving
            // STATE_ERROR
            mHandler.removeCallbacks(mErrorToIdleAnimationRunnable);
            mHandler.postDelayed(mErrorToIdleAnimationRunnable, BiometricPrompt.HIDE_DIALOG_DELAY);
        }
    }

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

    @Override
    public void onDialogAnimatedIn() {
        super.onDialogAnimatedIn();
        mDialogAnimatedIn = true;
        mIconController.startPulsing();
    }

    @Override
    protected int getDelayAfterAuthenticatedDurationMs() {
        return HIDE_DIALOG_DELAY;
    }

    @Override
    protected boolean shouldGrayAreaDismissDialog() {
        if (getSize() == SIZE_SMALL) {
            return false;
        }
        return true;
    }


}
+0 −134
Original line number Diff line number Diff line
/*
 * 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.content.Context;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;

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 FingerprintDialogView extends BiometricDialogView {

    private static final String TAG = "BiometricPrompt/FingerprintDialogView";

    protected FingerprintDialogView(Context context, AuthDialogCallback callback,
            Injector injector) {
        super(context, callback, injector);
    }

    @Override
    protected void handleResetMessage() {
        updateState(STATE_AUTHENTICATING);
        mErrorText.setText(getHintStringResourceId());
        mErrorText.setTextColor(mTextColor);
    }

    @Override
    protected int getHintStringResourceId() {
        return R.string.fingerprint_dialog_touch_sensor;
    }

    @Override
    protected int getAuthenticatedAccessibilityResourceId() {
        return com.android.internal.R.string.fingerprint_authenticated;
    }

    @Override
    protected int getIconDescriptionResourceId() {
        return R.string.accessibility_fingerprint_dialog_fingerprint_icon;
    }

    @Override
    protected void updateIcon(int lastState, int newState) {
        final Drawable icon = getAnimationForTransition(lastState, newState);
        if (icon == null) {
            Log.e(TAG, "Animation not found, " + lastState + " -> " + newState);
            return;
        }

        final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
                ? (AnimatedVectorDrawable) icon
                : null;

        mBiometricIcon.setImageDrawable(icon);

        if (animation != null && shouldAnimateForTransition(lastState, newState)) {
            animation.forceAnimationOnUI();
            animation.start();
        }
    }

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

    protected boolean shouldAnimateForTransition(int oldState, int newState) {
        if (newState == STATE_ERROR) {
            return true;
        } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATING) {
            return true;
        } else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
            // TODO(b/77328470): add animation when fingerprint is authenticated
            return false;
        } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATED) {
            // TODO(b/77328470): add animation when fingerprint is authenticated
            return false;
        } else if (newState == STATE_AUTHENTICATING) {
            return false;
        }
        return false;
    }

    @Override
    protected int getDelayAfterAuthenticatedDurationMs() {
        return 0;
    }

    @Override
    protected boolean shouldGrayAreaDismissDialog() {
        // Fingerprint dialog always dismisses when region outside the dialog is tapped
        return true;
    }

    protected Drawable getAnimationForTransition(int oldState, int newState) {
        int iconRes;
        if (newState == STATE_ERROR) {
            iconRes = R.drawable.fingerprint_dialog_fp_to_error;
        } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATING) {
            iconRes = R.drawable.fingerprint_dialog_error_to_fp;
        } else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) {
            // TODO(b/77328470): add animation when fingerprint is authenticated
            iconRes = R.drawable.fingerprint_dialog_fp_to_error;
        } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATED) {
            // TODO(b/77328470): add animation when fingerprint is authenticated
            iconRes = R.drawable.fingerprint_dialog_fp_to_error;
        } else if (newState == STATE_AUTHENTICATING) {
            iconRes = R.drawable.fingerprint_dialog_fp_to_error;
        } else {
            return null;
        }
        return mContext.getDrawable(iconRes);
    }
}
+0 −201
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;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotSame;
import static junit.framework.Assert.assertTrue;

import static org.mockito.Mockito.spy;

import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.hardware.biometrics.BiometricPrompt;
import android.os.Bundle;
import android.os.UserManager;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper.RunWithLooper;
import android.view.View;

import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.WakefulnessLifecycle;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
public class BiometricDialogViewTest extends SysuiTestCase {

    FaceDialogView mFaceDialogView;

    private static final String TITLE = "Title";
    private static final String SUBTITLE = "Subtitle";
    private static final String DESCRIPTION = "Description";
    private static final String NEGATIVE_BUTTON = "Negative Button";

    private static final String TEST_HELP = "Help";

    TestableContext mTestableContext;
    @Mock
    private AuthDialogCallback mCallback;
    @Mock
    private UserManager mUserManager;
    @Mock
    private DevicePolicyManager mDpm;

    private static class Injector extends BiometricDialogView.Injector {
        @Override
        public WakefulnessLifecycle getWakefulnessLifecycle() {
            final WakefulnessLifecycle lifecycle = new WakefulnessLifecycle();
            lifecycle.dispatchFinishedWakingUp();
            return lifecycle;
        }
    }

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mTestableContext = spy(mContext);
        mTestableContext.addMockSystemService(UserManager.class, mUserManager);
        mTestableContext.addMockSystemService(DevicePolicyManager.class, mDpm);
    }

    @Test
    public void testContentStates_confirmationRequired_authenticated() {
        mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
                true /* requireConfirmation */);
        mFaceDialogView.onAttachedToWindow();

        // When starting authentication
        assertEquals(View.VISIBLE, mFaceDialogView.mTitleText.getVisibility());
        assertEquals(View.VISIBLE, mFaceDialogView.mSubtitleText.getVisibility());
        assertEquals(View.VISIBLE, mFaceDialogView.mDescriptionText.getVisibility());
        assertEquals(View.INVISIBLE, mFaceDialogView.mErrorText.getVisibility());
        assertEquals(View.VISIBLE, mFaceDialogView.mPositiveButton.getVisibility());
        assertEquals(View.VISIBLE, mFaceDialogView.mNegativeButton.getVisibility());
        assertEquals(View.GONE, mFaceDialogView.mTryAgainButton.getVisibility());

        // Contents are as expected
        assertTrue(TITLE.contentEquals(mFaceDialogView.mTitleText.getText()));
        assertTrue(SUBTITLE.contentEquals(mFaceDialogView.mSubtitleText.getText()));
        assertTrue(DESCRIPTION.contentEquals(mFaceDialogView.mDescriptionText.getText()));
        assertTrue(mFaceDialogView.mPositiveButton.getText().toString()
                .contentEquals(mContext.getString(R.string.biometric_dialog_confirm)));
        assertTrue(NEGATIVE_BUTTON.contentEquals(mFaceDialogView.mNegativeButton.getText()));
        assertTrue(mFaceDialogView.mTryAgainButton.getText().toString()
                .contentEquals(mContext.getString(R.string.biometric_dialog_try_again)));

        // When help message is received
        mFaceDialogView.onHelp(TEST_HELP);
        assertEquals(mFaceDialogView.mErrorText.getVisibility(), View.VISIBLE);
        assertTrue(TEST_HELP.contentEquals(mFaceDialogView.mErrorText.getText()));

        // When authenticated, confirm button comes out
        mFaceDialogView.onAuthenticationSucceeded();
        assertEquals(View.VISIBLE, mFaceDialogView.mPositiveButton.getVisibility());
        assertEquals(true, mFaceDialogView.mPositiveButton.isEnabled());
    }

    @Test
    public void testContentStates_confirmationNotRequired_authenticated() {
        mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
                false /* requireConfirmation */);
        mFaceDialogView.onAttachedToWindow();
        mFaceDialogView.updateSize(FaceDialogView.SIZE_SMALL);

        assertEquals(View.INVISIBLE, mFaceDialogView.mTitleText.getVisibility());
        assertNotSame(View.VISIBLE, mFaceDialogView.mSubtitleText.getVisibility());
        assertNotSame(View.VISIBLE, mFaceDialogView.mDescriptionText.getVisibility());
        assertEquals(View.INVISIBLE, mFaceDialogView.mErrorText.getVisibility());
        assertEquals(View.GONE, mFaceDialogView.mPositiveButton.getVisibility());
        assertEquals(View.GONE, mFaceDialogView.mTryAgainButton.getVisibility());
        assertEquals(View.GONE, mFaceDialogView.mTryAgainButton.getVisibility());
    }

    @Test
    public void testContentStates_confirmationNotRequired_help() {
        mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
                false /* requireConfirmation */);
        mFaceDialogView.onAttachedToWindow();

        mFaceDialogView.onHelp(TEST_HELP);
        assertEquals(mFaceDialogView.mErrorText.getVisibility(), View.VISIBLE);
        assertTrue(TEST_HELP.contentEquals(mFaceDialogView.mErrorText.getText()));
    }

    @Test
    public void testBack_sendsUserCanceled() {
        // TODO: Need robolectric framework to wait for handler to complete
    }

    @Test
    public void testScreenOff_sendsUserCanceled() {
        // TODO: Need robolectric framework to wait for handler to complete
    }

    @Test
    public void testRestoreState_contentStatesCorrect() {
        mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
                false /* requireConfirmation */);
        mFaceDialogView.onAttachedToWindow();
        mFaceDialogView.onAuthenticationFailed(TEST_HELP);

        final Bundle bundle = new Bundle();
        mFaceDialogView.onSaveState(bundle);

        mFaceDialogView = buildFaceDialogView(mTestableContext, mCallback,
                false /* requireConfirmation */);
        mFaceDialogView.restoreState(bundle);
        mFaceDialogView.onAttachedToWindow();

        assertEquals(View.VISIBLE, mFaceDialogView.mTryAgainButton.getVisibility());
    }

    private FaceDialogView buildFaceDialogView(Context context, AuthDialogCallback callback,
            boolean requireConfirmation) {
        return (FaceDialogView) new BiometricDialogView.Builder(context)
                .setCallback(callback)
                .setBiometricPromptBundle(createTestDialogBundle())
                .setRequireConfirmation(requireConfirmation)
                .setUserId(0)
                .setOpPackageName("test_package")
                .build(BiometricDialogView.Builder.TYPE_FACE, new Injector());
    }

    private Bundle createTestDialogBundle() {
        Bundle bundle = new Bundle();

        bundle.putCharSequence(BiometricPrompt.KEY_TITLE, TITLE);
        bundle.putCharSequence(BiometricPrompt.KEY_SUBTITLE, SUBTITLE);
        bundle.putCharSequence(BiometricPrompt.KEY_DESCRIPTION, DESCRIPTION);
        bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, NEGATIVE_BUTTON);

        // RequireConfirmation is a hint to BiometricService. This can be forced to be required
        // by user settings, and should be tested in BiometricService.
        bundle.putBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, true);

        return bundle;
    }
}