diff --git a/packages/SystemUI/res/layout/auth_biometric_view.xml b/packages/SystemUI/res/layout/auth_biometric_view.xml deleted file mode 100644 index ee4da25f22842..0000000000000 --- a/packages/SystemUI/res/layout/auth_biometric_view.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index 1413f4a815743..74d7d5fed2d8d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -59,7 +59,7 @@ import java.util.Set; /** * Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers. */ -public class AuthBiometricView extends LinearLayout { +public abstract class AuthBiometricView extends LinearLayout { private static final String TAG = "AuthBiometricView"; @@ -116,7 +116,7 @@ public class AuthBiometricView extends LinearLayout { void onAction(int action); } - protected final Handler mHandler; + private final Handler mHandler; private final AccessibilityManager mAccessibilityManager; private final LockPatternUtils mLockPatternUtils; protected final int mTextColorError; @@ -155,8 +155,8 @@ public class AuthBiometricView extends LinearLayout { // Measurements when biometric view is showing text, buttons, etc. @Nullable @VisibleForTesting AuthDialog.LayoutParams mLayoutParams; - protected Callback mCallback; - protected @BiometricState int mState; + private Callback mCallback; + @BiometricState private int mState; private float mIconOriginalY; @@ -250,18 +250,9 @@ public class AuthBiometricView extends LinearLayout { return false; } - /** - * Create the controller for managing the icons transitions during the prompt. - * - * Subclass should override. - */ + /** Create the controller for managing the icons transitions during the prompt.*/ @NonNull - protected AuthIconController createIconController() { - return new AuthIconController(mContext, mIconView) { - @Override - public void updateIcon(int lastState, int newState) {} - }; - } + protected abstract AuthIconController createIconController(); void setPanelController(AuthPanelController panelController) { mPanelController = panelController; @@ -881,4 +872,14 @@ public class AuthBiometricView extends LinearLayout { @AuthDialog.DialogSize int getSize() { return mSize; } + + /** If authentication has successfully occurred and the view is done. */ + boolean isAuthenticated() { + return mState == STATE_AUTHENTICATED; + } + + /** If authentication is currently in progress. */ + boolean isAuthenticating() { + return mState == STATE_AUTHENTICATING; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt new file mode 100644 index 0000000000000..328ad39cddd56 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022 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.hardware.biometrics.BiometricAuthenticator.TYPE_FACE +import android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +@SmallTest +class AuthBiometricFingerprintAndFaceViewTest : SysuiTestCase() { + + @JvmField @Rule + var mockitoRule = MockitoJUnit.rule() + + @Mock + private lateinit var callback: AuthBiometricView.Callback + @Mock + private lateinit var panelController: AuthPanelController + + private lateinit var biometricView: AuthBiometricFingerprintAndFaceView + + @Before + fun setup() { + biometricView = R.layout.auth_biometric_fingerprint_and_face_view + .asTestAuthBiometricView(mContext, callback, panelController) + waitForIdleSync() + } + + @After + fun tearDown() { + biometricView.destroyDialog() + } + + @Test + fun fingerprintSuccessDoesNotRequireExplicitConfirmation() { + biometricView.onDialogAnimatedIn() + biometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT) + waitForIdleSync() + + assertThat(biometricView.isAuthenticated).isTrue() + verify(callback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED) + } + + @Test + fun faceSuccessRequiresExplicitConfirmation() { + biometricView.onDialogAnimatedIn() + biometricView.onAuthenticationSucceeded(TYPE_FACE) + waitForIdleSync() + + assertThat(biometricView.isAuthenticated).isFalse() + assertThat(biometricView.isAuthenticating).isFalse() + assertThat(biometricView.mConfirmButton.visibility).isEqualTo(View.GONE) + verify(callback, never()).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED) + + // icon acts as confirm button + biometricView.mIconView.performClick() + waitForIdleSync() + + assertThat(biometricView.isAuthenticated).isTrue() + verify(callback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED) + } + + @Test + fun ignoresFaceErrors() { + biometricView.onDialogAnimatedIn() + biometricView.onError(TYPE_FACE, "not a face") + waitForIdleSync() + + assertThat(biometricView.isAuthenticating).isTrue() + verify(callback, never()).onAction(AuthBiometricView.Callback.ACTION_ERROR) + + biometricView.onError(TYPE_FINGERPRINT, "that's a nope") + waitForIdleSync() + + verify(callback).onAction(AuthBiometricView.Callback.ACTION_ERROR) + } + + override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages() +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt new file mode 100644 index 0000000000000..687cb517b2f49 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2022 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.hardware.biometrics.BiometricAuthenticator +import android.os.Bundle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +@SmallTest +class AuthBiometricFingerprintViewTest : SysuiTestCase() { + + @JvmField @Rule + val mockitoRule = MockitoJUnit.rule() + + @Mock + private lateinit var callback: AuthBiometricView.Callback + @Mock + private lateinit var panelController: AuthPanelController + + private lateinit var biometricView: AuthBiometricView + + private fun createView(allowDeviceCredential: Boolean = false): AuthBiometricFingerprintView { + val view = R.layout.auth_biometric_fingerprint_view.asTestAuthBiometricView( + mContext, callback, panelController, allowDeviceCredential = allowDeviceCredential + ) as AuthBiometricFingerprintView + waitForIdleSync() + return view + } + + @Before + fun setup() { + biometricView = createView() + } + + @After + fun tearDown() { + biometricView.destroyDialog() + } + + @Test + fun testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() { + biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) + waitForIdleSync() + + assertThat(biometricView.isAuthenticated).isTrue() + verify(callback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED) + } + + @Test + fun testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() { + biometricView.setRequireConfirmation(true) + biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) + waitForIdleSync() + + // TODO: this should be tested in the subclasses + if (biometricView.supportsRequireConfirmation()) { + verify(callback, never()).onAction(ArgumentMatchers.anyInt()) + assertThat(biometricView.mNegativeButton.visibility).isEqualTo(View.GONE) + assertThat(biometricView.mCancelButton.visibility).isEqualTo(View.VISIBLE) + assertThat(biometricView.mCancelButton.isEnabled).isTrue() + assertThat(biometricView.mConfirmButton.isEnabled).isTrue() + assertThat(biometricView.mIndicatorView.text) + .isEqualTo(mContext.getText(R.string.biometric_dialog_tap_confirm)) + assertThat(biometricView.mIndicatorView.visibility).isEqualTo(View.VISIBLE) + } else { + assertThat(biometricView.isAuthenticated).isTrue() + verify(callback).onAction(eq(AuthBiometricView.Callback.ACTION_AUTHENTICATED)) + } + } + + @Test + fun testPositiveButton_sendsActionAuthenticated() { + biometricView.mConfirmButton.performClick() + waitForIdleSync() + + verify(callback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED) + assertThat(biometricView.isAuthenticated).isTrue() + } + + @Test + fun testNegativeButton_beforeAuthentication_sendsActionButtonNegative() { + biometricView.onDialogAnimatedIn() + biometricView.mNegativeButton.performClick() + waitForIdleSync() + + verify(callback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE) + } + + @Test + fun testCancelButton_whenPendingConfirmation_sendsActionUserCanceled() { + biometricView.setRequireConfirmation(true) + biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) + + assertThat(biometricView.mNegativeButton.visibility).isEqualTo(View.GONE) + biometricView.mCancelButton.performClick() + waitForIdleSync() + + verify(callback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED) + } + + @Test + fun testTryAgainButton_sendsActionTryAgain() { + biometricView.mTryAgainButton.performClick() + waitForIdleSync() + + verify(callback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN) + assertThat(biometricView.mTryAgainButton.visibility).isEqualTo(View.GONE) + assertThat(biometricView.isAuthenticating).isTrue() + } + + @Test + fun testOnErrorSendsActionError() { + biometricView.onError(BiometricAuthenticator.TYPE_FACE, "testError") + waitForIdleSync() + + verify(callback).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR)) + } + + @Test + fun testOnErrorShowsMessage() { + // prevent error state from instantly returning to authenticating in the test + biometricView.mAnimationDurationHideDialog = 10_000 + + val message = "another error" + biometricView.onError(BiometricAuthenticator.TYPE_FACE, message) + waitForIdleSync() + + assertThat(biometricView.isAuthenticating).isFalse() + assertThat(biometricView.isAuthenticated).isFalse() + assertThat(biometricView.mIndicatorView.visibility).isEqualTo(View.VISIBLE) + assertThat(biometricView.mIndicatorView.text).isEqualTo(message) + } + + @Test + fun testBackgroundClicked_sendsActionUserCanceled() { + val view = View(mContext) + biometricView.setBackgroundView(view) + view.performClick() + + verify(callback).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)) + } + + @Test + fun testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() { + val view = View(mContext) + biometricView.setBackgroundView(view) + biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) + view.performClick() + + verify(callback, never()) + .onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)) + } + + @Test + fun testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() { + biometricView.mLayoutParams = AuthDialog.LayoutParams(0, 0) + biometricView.updateSize(AuthDialog.SIZE_SMALL) + val view = View(mContext) + biometricView.setBackgroundView(view) + view.performClick() + + verify(callback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)) + } + + @Test + fun testIgnoresUselessHelp() { + biometricView.mAnimationDurationHideDialog = 10_000 + biometricView.onDialogAnimatedIn() + waitForIdleSync() + + assertThat(biometricView.isAuthenticating).isTrue() + + val helpText = biometricView.mIndicatorView.text + biometricView.onHelp(BiometricAuthenticator.TYPE_FINGERPRINT, "") + waitForIdleSync() + + // text should not change + assertThat(biometricView.mIndicatorView.text).isEqualTo(helpText) + verify(callback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR)) + } + + @Test + fun testRestoresState() { + val requireConfirmation = true + biometricView.mAnimationDurationHideDialog = 10_000 + val failureMessage = "testFailureMessage" + biometricView.setRequireConfirmation(requireConfirmation) + biometricView.onAuthenticationFailed(BiometricAuthenticator.TYPE_FACE, failureMessage) + waitForIdleSync() + + val state = Bundle() + biometricView.onSaveState(state) + assertThat(biometricView.mTryAgainButton.visibility).isEqualTo(View.GONE) + assertThat(state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)) + .isEqualTo(View.GONE) + assertThat(state.getInt(AuthDialog.KEY_BIOMETRIC_STATE)) + .isEqualTo(AuthBiometricView.STATE_ERROR) + assertThat(biometricView.mIndicatorView.visibility).isEqualTo(View.VISIBLE) + assertThat(state.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)).isTrue() + assertThat(biometricView.mIndicatorView.text).isEqualTo(failureMessage) + assertThat(state.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING)) + .isEqualTo(failureMessage) + + // TODO: Test dialog size. Should move requireConfirmation to buildBiometricPromptBundle + + // Create new dialog and restore the previous state into it + biometricView.destroyDialog() + biometricView = createView() + biometricView.restoreState(state) + biometricView.mAnimationDurationHideDialog = 10_000 + biometricView.setRequireConfirmation(requireConfirmation) + waitForIdleSync() + + assertThat(biometricView.mTryAgainButton.visibility).isEqualTo(View.GONE) + assertThat(biometricView.mIndicatorView.visibility).isEqualTo(View.VISIBLE) + + // TODO: Test restored text. Currently cannot test this, since it gets restored only after + // dialog size is known. + } + + @Test + fun testCredentialButton_whenDeviceCredentialAllowed() { + biometricView.destroyDialog() + biometricView = createView(allowDeviceCredential = true) + + assertThat(biometricView.mUseCredentialButton.visibility).isEqualTo(View.VISIBLE) + assertThat(biometricView.mNegativeButton.visibility).isEqualTo(View.GONE) + + biometricView.mUseCredentialButton.performClick() + waitForIdleSync() + + verify(callback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL) + } + + override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages() +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java deleted file mode 100644 index f99b20d47ab8e..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * 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 android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; -import static android.hardware.biometrics.BiometricManager.Authenticators; - -import static com.android.systemui.biometrics.AuthBiometricView.Callback.ACTION_AUTHENTICATED; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.hardware.biometrics.PromptInfo; -import android.os.Bundle; -import android.test.suitebuilder.annotation.SmallTest; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.testing.TestableLooper.RunWithLooper; -import android.testing.ViewUtils; -import android.view.LayoutInflater; -import android.view.View; - -import com.android.systemui.R; -import com.android.systemui.SysuiTestCase; - -import org.junit.After; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -@RunWith(AndroidTestingRunner.class) -@RunWithLooper -@SmallTest -public class AuthBiometricViewTest extends SysuiTestCase { - - @Rule - public final MockitoRule mMockitoRule = MockitoJUnit.rule(); - - @Mock - private AuthBiometricView.Callback mCallback; - @Mock - private AuthPanelController mPanelController; - - private AuthBiometricView mBiometricView; - - @After - public void tearDown() { - destroyDialog(); - } - - @Test - public void testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() { - initDialog(false /* allowDeviceCredential */, mCallback); - - // The onAuthenticated runnable is posted when authentication succeeds. - mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT); - waitForIdleSync(); - assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState); - verify(mCallback).onAction(ACTION_AUTHENTICATED); - } - - @Test - public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() { - initDialog(false /* allowDeviceCredential */, mCallback); - - mBiometricView.setRequireConfirmation(true); - mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT); - waitForIdleSync(); - - // TODO: this should be tested in the subclasses - if (mBiometricView.supportsRequireConfirmation()) { - assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState); - - verify(mCallback, never()).onAction(anyInt()); - - assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility()); - assertEquals(View.VISIBLE, mBiometricView.mCancelButton.getVisibility()); - assertTrue(mBiometricView.mCancelButton.isEnabled()); - - assertTrue(mBiometricView.mConfirmButton.isEnabled()); - assertEquals(mContext.getText(R.string.biometric_dialog_tap_confirm), - mBiometricView.mIndicatorView.getText()); - assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility()); - } else { - assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState); - verify(mCallback).onAction(eq(ACTION_AUTHENTICATED)); - } - - } - - @Test - public void testPositiveButton_sendsActionAuthenticated() { - initDialog(false /* allowDeviceCredential */, mCallback); - - mBiometricView.mConfirmButton.performClick(); - waitForIdleSync(); - - verify(mCallback).onAction(ACTION_AUTHENTICATED); - assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState); - } - - @Test - public void testNegativeButton_beforeAuthentication_sendsActionButtonNegative() { - initDialog(false /* allowDeviceCredential */, mCallback); - - mBiometricView.onDialogAnimatedIn(); - mBiometricView.mNegativeButton.performClick(); - waitForIdleSync(); - - verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE); - } - - @Test - public void testCancelButton_whenPendingConfirmation_sendsActionUserCanceled() { - initDialog(false /* allowDeviceCredential */, mCallback); - - mBiometricView.setRequireConfirmation(true); - mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT); - - assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility()); - - mBiometricView.mCancelButton.performClick(); - waitForIdleSync(); - - verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED); - } - - @Test - public void testTryAgainButton_sendsActionTryAgain() { - initDialog(false /* allowDeviceCredential */, mCallback); - - mBiometricView.mTryAgainButton.performClick(); - waitForIdleSync(); - - verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN); - assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState); - } - - @Test - @Ignore("flaky, b/189031816") - public void testError_sendsActionError() { - initDialog(false /* allowDeviceCredential */, mCallback); - final String testError = "testError"; - mBiometricView.onError(TYPE_FACE, testError); - waitForIdleSync(); - - verify(mCallback).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR)); - assertEquals(AuthBiometricView.STATE_IDLE, mBiometricView.mState); - } - - @Test - public void testBackgroundClicked_sendsActionUserCanceled() { - initDialog(false /* allowDeviceCredential */, mCallback); - - 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(false /* allowDeviceCredential */, mCallback); - - View view = new View(mContext); - mBiometricView.setBackgroundView(view); - mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT); - view.performClick(); - verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)); - } - - @Test - public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() { - initDialog(false /* allowDeviceCredential */, mCallback); - mBiometricView.mLayoutParams = new AuthDialog.LayoutParams(0, 0); - mBiometricView.updateSize(AuthDialog.SIZE_SMALL); - - View view = new View(mContext); - mBiometricView.setBackgroundView(view); - view.performClick(); - verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)); - } - - @Test - public void testIgnoresUselessHelp() { - initDialog(false /* allowDeviceCredential */, mCallback); - - mBiometricView.onDialogAnimatedIn(); - waitForIdleSync(); - - assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState); - - mBiometricView.onHelp(TYPE_FINGERPRINT, ""); - waitForIdleSync(); - - assertEquals("", mBiometricView.mIndicatorView.getText()); - verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR)); - assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState); - } - - @Test - public void testRestoresState() { - final boolean requireConfirmation = true; - - initDialog(false /* allowDeviceCredential */, mCallback, null, 10000); - - final String failureMessage = "testFailureMessage"; - mBiometricView.setRequireConfirmation(requireConfirmation); - mBiometricView.onAuthenticationFailed(TYPE_FACE, failureMessage); - waitForIdleSync(); - - Bundle state = new Bundle(); - mBiometricView.onSaveState(state); - - assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility()); - assertEquals(View.GONE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)); - - assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState); - assertEquals(AuthBiometricView.STATE_ERROR, state.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); - - assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility()); - assertTrue(state.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)); - - assertEquals(failureMessage, mBiometricView.mIndicatorView.getText()); - assertEquals(failureMessage, state.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING)); - - // TODO: Test dialog size. Should move requireConfirmation to buildBiometricPromptBundle - - // Create new dialog and restore the previous state into it - destroyDialog(); - initDialog(false /* allowDeviceCredential */, mCallback, state, 10000); - mBiometricView.mAnimationDurationHideDialog = 10000; - mBiometricView.setRequireConfirmation(requireConfirmation); - waitForIdleSync(); - - assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility()); - assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState); - assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility()); - - // TODO: Test restored text. Currently cannot test this, since it gets restored only after - // dialog size is known. - } - - @Test - public void testCredentialButton_whenDeviceCredentialAllowed() throws InterruptedException { - initDialog(true /* allowDeviceCredential */, mCallback); - - assertEquals(View.VISIBLE, mBiometricView.mUseCredentialButton.getVisibility()); - assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility()); - mBiometricView.mUseCredentialButton.performClick(); - waitForIdleSync(); - - verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL); - } - - private PromptInfo buildPromptInfo(boolean allowDeviceCredential) { - PromptInfo promptInfo = new PromptInfo(); - promptInfo.setTitle("Title"); - int authenticators = Authenticators.BIOMETRIC_WEAK; - if (allowDeviceCredential) { - authenticators |= Authenticators.DEVICE_CREDENTIAL; - } else { - promptInfo.setNegativeButtonText("Negative"); - } - promptInfo.setAuthenticators(authenticators); - return promptInfo; - } - - private void initDialog(boolean allowDeviceCredential, AuthBiometricView.Callback callback) { - initDialog(allowDeviceCredential, callback, - null /* savedState */, 0 /* hideDelay */); - } - - private void initDialog(boolean allowDeviceCredential, - AuthBiometricView.Callback callback, Bundle savedState, int hideDelay) { - final LayoutInflater inflater = LayoutInflater.from(mContext); - mBiometricView = (AuthBiometricView) inflater.inflate( - R.layout.auth_biometric_view, null, false); - mBiometricView.mAnimationDurationLong = 0; - mBiometricView.mAnimationDurationShort = 0; - mBiometricView.mAnimationDurationHideDialog = hideDelay; - mBiometricView.setPromptInfo(buildPromptInfo(allowDeviceCredential)); - mBiometricView.setCallback(callback); - mBiometricView.restoreState(savedState); - ViewUtils.attachView(mBiometricView); - mBiometricView.setPanelController(mPanelController); - waitForIdleSync(); - } - - private void destroyDialog() { - if (mBiometricView != null && mBiometricView.isAttachedToWindow()) { - ViewUtils.detachView(mBiometricView); - } - } - - @Override - protected void waitForIdleSync() { - TestableLooper.get(this).processAllMessages(); - super.waitForIdleSync(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 2341928b2565d..bbc3968cbd242 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -16,13 +16,11 @@ package com.android.systemui.biometrics import android.app.admin.DevicePolicyManager +import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager -import android.hardware.biometrics.ComponentInfoInternal import android.hardware.biometrics.PromptInfo -import android.hardware.biometrics.SensorProperties import android.hardware.face.FaceSensorPropertiesInternal -import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.os.Handler import android.os.IBinder @@ -40,7 +38,6 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -57,12 +54,12 @@ import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever @RunWith(AndroidTestingRunner::class) -@RunWithLooper +@RunWithLooper(setAsMainLooper = true) @SmallTest class AuthContainerViewTest : SysuiTestCase() { @JvmField @Rule - var rule = MockitoJUnit.rule() + var mockitoRule = MockitoJUnit.rule() @Mock lateinit var callback: AuthDialogCallback @@ -86,13 +83,13 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testNotifiesAnimatedIn() { - initializeContainer() + initializeFingerprintContainer() verify(callback).onDialogAnimatedIn() } @Test fun testIgnoresAnimatedInWhenDismissed() { - val container = initializeContainer(addToView = false) + val container = initializeFingerprintContainer(addToView = false) container.dismissFromSystemServer() waitForIdleSync() @@ -107,7 +104,7 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testActionAuthenticated_sendsDismissedAuthenticated() { - val container = initializeContainer() + val container = initializeFingerprintContainer() container.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_AUTHENTICATED ) @@ -122,7 +119,7 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testActionUserCanceled_sendsDismissedUserCanceled() { - val container = initializeContainer() + val container = initializeFingerprintContainer() container.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_USER_CANCELED ) @@ -140,7 +137,7 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testActionButtonNegative_sendsDismissedButtonNegative() { - val container = initializeContainer() + val container = initializeFingerprintContainer() container.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE ) @@ -155,7 +152,9 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testActionTryAgain_sendsTryAgain() { - val container = initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK) + val container = initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK + ) container.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN ) @@ -166,8 +165,8 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testActionError_sendsDismissedError() { - val container = initializeContainer() - authContainer!!.mBiometricCallback.onAction( + val container = initializeFingerprintContainer() + container.mBiometricCallback.onAction( AuthBiometricView.Callback.ACTION_ERROR ) waitForIdleSync() @@ -181,8 +180,8 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() { - val container = initializeContainer( - BiometricManager.Authenticators.BIOMETRIC_WEAK or + val container = initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.DEVICE_CREDENTIAL ) container.mBiometricCallback.onAction( @@ -196,8 +195,8 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() { - val container = initializeContainer( - BiometricManager.Authenticators.BIOMETRIC_WEAK or + val container = initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.DEVICE_CREDENTIAL ) container.animateToCredentialUI() @@ -208,7 +207,7 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testShowBiometricUI() { - val container = initializeContainer() + val container = initializeFingerprintContainer() waitForIdleSync() @@ -218,7 +217,9 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testShowCredentialUI() { - val container = initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + val container = initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) waitForIdleSync() assertThat(container.hasCredentialView()).isTrue() @@ -232,7 +233,9 @@ class AuthContainerViewTest : SysuiTestCase() { DevicePolicyManager.PASSWORD_QUALITY_SOMETHING ) - val container = initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + val container = initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) waitForIdleSync() assertThat(container.hasCredentialPatternView()).isTrue() @@ -249,7 +252,9 @@ class AuthContainerViewTest : SysuiTestCase() { // In the credential view, clicking on the background (to cancel authentication) is not // valid. Thus, the listener should be null, and it should not be in the accessibility // hierarchy. - val container = initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + val container = initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) waitForIdleSync() assertThat(container.hasCredentialPasswordView()).isTrue() @@ -277,49 +282,52 @@ class AuthContainerViewTest : SysuiTestCase() { assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.ime()) == 0).isTrue() } - private fun initializeContainer( + @Test + fun coexFaceRestartsOnTouch() { + val container = initializeCoexContainer() + + container.onPointerDown() + waitForIdleSync() + + container.onAuthenticationFailed(BiometricAuthenticator.TYPE_FACE, "failed") + waitForIdleSync() + + verify(callback, never()).onTryAgainPressed() + + container.onPointerDown() + waitForIdleSync() + + verify(callback).onTryAgainPressed() + } + + private fun initializeFingerprintContainer( authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, addToView: Boolean = true + ) = initializeContainer( + TestAuthContainerView( + authenticators = authenticators, + fingerprintProps = fingerprintSensorPropertiesInternal() + ), + addToView + ) + + private fun initializeCoexContainer( + authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, + addToView: Boolean = true + ) = initializeContainer( + TestAuthContainerView( + authenticators = authenticators, + fingerprintProps = fingerprintSensorPropertiesInternal(), + faceProps = faceSensorPropertiesInternal() + ), + addToView + ) + + private fun initializeContainer( + view: TestAuthContainerView, + addToView: Boolean ): TestAuthContainerView { - val config = AuthContainerView.Config() - config.mContext = mContext - config.mCallback = callback - config.mSensorIds = intArrayOf(0) - config.mSkipAnimation = true - config.mPromptInfo = PromptInfo() - config.mPromptInfo.authenticators = authenticators - val componentInfo = listOf( - ComponentInfoInternal( - "faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */ - ), - ComponentInfoInternal( - "matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */ - ) - ) - val fpProps = listOf( - FingerprintSensorPropertiesInternal( - 0, - SensorProperties.STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_REAR, - false /* resetLockoutRequiresHardwareAuthToken */ - ) - ) - authContainer = TestAuthContainerView( - config, - fpProps, - listOf(), - wakefulnessLifecycle, - userManager, - lockPatternUtils, - Handler(TestableLooper.get(this).looper), - FakeExecutor(FakeSystemClock()) - ) + authContainer = view if (addToView) { authContainer!!.addToView() @@ -329,27 +337,34 @@ class AuthContainerViewTest : SysuiTestCase() { } private inner class TestAuthContainerView( - config: Config, - fpProps: List, - faceProps: List, - wakefulnessLifecycle: WakefulnessLifecycle, - userManager: UserManager, - lockPatternUtils: LockPatternUtils, - mainHandler: Handler, - bgExecutor: DelayableExecutor + authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, + fingerprintProps: List = listOf(), + faceProps: List = listOf() ) : AuthContainerView( - config, fpProps, faceProps, - wakefulnessLifecycle, userManager, lockPatternUtils, mainHandler, bgExecutor + Config().apply { + mContext = this@AuthContainerViewTest.context + mCallback = callback + mSensorIds = (fingerprintProps.map { it.sensorId } + + faceProps.map { it.sensorId }).toIntArray() + mSkipAnimation = true + mPromptInfo = PromptInfo().apply { + this.authenticators = authenticators + } + }, + fingerprintProps, + faceProps, + wakefulnessLifecycle, + userManager, + lockPatternUtils, + Handler(TestableLooper.get(this).looper), + FakeExecutor(FakeSystemClock()) ) { override fun postOnAnimation(runnable: Runnable) { runnable.run() } } - override fun waitForIdleSync() { - TestableLooper.get(this).processAllMessages() - super.waitForIdleSync() - } + override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages() private fun AuthContainerView.addToView() { ViewUtils.attachView(this) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt index 57f36172c998f..8820c164cba4b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -16,27 +16,123 @@ package com.android.systemui.biometrics +import android.annotation.IdRes +import android.content.Context +import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.ComponentInfoInternal -import android.hardware.biometrics.SensorLocationInternal +import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.SensorProperties +import android.hardware.face.FaceSensorPropertiesInternal +import android.hardware.face.FaceSensorProperties import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import android.os.Bundle -/** Creates properties from the sensor location with test values. */ -fun SensorLocationInternal.asFingerprintSensorProperties( - sensorId: Int = 22, - @SensorProperties.Strength sensorStrength: Int = SensorProperties.STRENGTH_WEAK, - @FingerprintSensorProperties.SensorType sensorType: Int = - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - maxEnrollmentsPerUser: Int = 1, - info: List = listOf(ComponentInfoInternal("a", "b", "c", "d", "e")), - resetLockoutRequiresHardwareAuthToken: Boolean = false -) = FingerprintSensorPropertiesInternal( - sensorId, - sensorStrength, - maxEnrollmentsPerUser, - info, - sensorType, - resetLockoutRequiresHardwareAuthToken, - listOf(this) -) +import android.testing.ViewUtils +import android.view.LayoutInflater + +/** + * Inflate the given BiometricPrompt layout and initialize it with test parameters. + * + * This attaches the view so be sure to call [destroyDialog] at the end of the test. + */ +@IdRes +internal fun Int.asTestAuthBiometricView( + context: Context, + callback: AuthBiometricView.Callback, + panelController: AuthPanelController, + allowDeviceCredential: Boolean = false, + savedState: Bundle? = null, + hideDelay: Int = 0 +): T { + val view = LayoutInflater.from(context).inflate(this, null, false) as T + view.mAnimationDurationLong = 0 + view.mAnimationDurationShort = 0 + view.mAnimationDurationHideDialog = hideDelay + view.setPromptInfo(buildPromptInfo(allowDeviceCredential)) + view.setCallback(callback) + view.restoreState(savedState) + view.setPanelController(panelController) + + ViewUtils.attachView(view) + + return view +} + +private fun buildPromptInfo(allowDeviceCredential: Boolean): PromptInfo { + val promptInfo = PromptInfo() + promptInfo.title = "Title" + var authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK + if (allowDeviceCredential) { + authenticators = authenticators or BiometricManager.Authenticators.DEVICE_CREDENTIAL + } else { + promptInfo.negativeButtonText = "Negative" + } + promptInfo.authenticators = authenticators + return promptInfo +} + +/** Detach the view, if needed. */ +internal fun AuthBiometricView?.destroyDialog() { + if (this != null && isAttachedToWindow) { + ViewUtils.detachView(this) + } +} + +/** Create [FingerprintSensorPropertiesInternal] for a test. */ +internal fun fingerprintSensorPropertiesInternal( + ids: List = listOf(0) +): List { + val componentInfo = listOf( + ComponentInfoInternal( + "fingerprintSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */ + ), + ComponentInfoInternal( + "matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */ + ) + ) + return ids.map { id -> + FingerprintSensorPropertiesInternal( + id, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + componentInfo, + FingerprintSensorProperties.TYPE_REAR, + false /* resetLockoutRequiresHardwareAuthToken */ + ) + } +} + +/** Create [FaceSensorPropertiesInternal] for a test. */ +internal fun faceSensorPropertiesInternal( + ids: List = listOf(1) +): List { + val componentInfo = listOf( + ComponentInfoInternal( + "faceSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */ + ), + ComponentInfoInternal( + "matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */ + ) + ) + return ids.map { id -> + FaceSensorPropertiesInternal( + id, + SensorProperties.STRENGTH_STRONG, + 2 /* maxEnrollmentsPerUser */, + componentInfo, + FaceSensorProperties.TYPE_RGB, + true /* supportsFaceDetection */, + true /* supportsSelfIllumination */, + false /* resetLockoutRequiresHardwareAuthToken */ + ) + } +}