1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.systemui.biometrics 17 18 import android.hardware.biometrics.BiometricAuthenticator 19 import android.os.Bundle 20 import androidx.test.ext.junit.runners.AndroidJUnit4 21 import android.testing.TestableLooper 22 import android.testing.TestableLooper.RunWithLooper 23 import android.view.View 24 import androidx.test.filters.SmallTest 25 import com.android.systemui.R 26 import com.android.systemui.RoboPilotTest 27 import com.android.systemui.SysuiTestCase 28 import com.google.common.truth.Truth.assertThat 29 import org.junit.After 30 import org.junit.Before 31 import org.junit.Rule 32 import org.junit.Test 33 import org.junit.runner.RunWith 34 import org.mockito.ArgumentMatchers 35 import org.mockito.ArgumentMatchers.eq 36 import org.mockito.Mock 37 import org.mockito.Mockito.never 38 import org.mockito.Mockito.verify 39 import org.mockito.junit.MockitoJUnit 40 41 @RunWith(AndroidJUnit4::class) 42 @RunWithLooper(setAsMainLooper = true) 43 @SmallTest 44 @RoboPilotTest 45 class AuthBiometricFingerprintViewTest : SysuiTestCase() { 46 47 @JvmField 48 @Rule 49 val mockitoRule = MockitoJUnit.rule() 50 51 @Mock 52 private lateinit var callback: AuthBiometricView.Callback 53 54 @Mock 55 private lateinit var panelController: AuthPanelController 56 57 private lateinit var biometricView: AuthBiometricView 58 59 private fun createView(allowDeviceCredential: Boolean = false): AuthBiometricFingerprintView { 60 val view: AuthBiometricFingerprintView = 61 R.layout.auth_biometric_fingerprint_view.asTestAuthBiometricView( 62 mContext, callback, panelController, allowDeviceCredential = allowDeviceCredential 63 ) 64 waitForIdleSync() 65 return view 66 } 67 68 @Before 69 fun setup() { 70 biometricView = createView() 71 } 72 73 @After 74 fun tearDown() { 75 biometricView.destroyDialog() 76 } 77 78 @Test 79 fun testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() { 80 biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) 81 TestableLooper.get(this).moveTimeForward(1000) 82 waitForIdleSync() 83 84 assertThat(biometricView.isAuthenticated).isTrue() 85 verify(callback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED) 86 } 87 88 @Test 89 fun testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() { 90 biometricView.setRequireConfirmation(true) 91 biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) 92 TestableLooper.get(this).moveTimeForward(1000) 93 waitForIdleSync() 94 95 // TODO: this should be tested in the subclasses 96 if (biometricView.supportsRequireConfirmation()) { 97 verify(callback, never()).onAction(ArgumentMatchers.anyInt()) 98 assertThat(biometricView.mNegativeButton.visibility).isEqualTo(View.GONE) 99 assertThat(biometricView.mCancelButton.visibility).isEqualTo(View.VISIBLE) 100 assertThat(biometricView.mCancelButton.isEnabled).isTrue() 101 assertThat(biometricView.mConfirmButton.isEnabled).isTrue() 102 assertThat(biometricView.mIndicatorView.text) 103 .isEqualTo(mContext.getText(R.string.biometric_dialog_tap_confirm)) 104 assertThat(biometricView.mIndicatorView.visibility).isEqualTo(View.VISIBLE) 105 } else { 106 assertThat(biometricView.isAuthenticated).isTrue() 107 verify(callback).onAction(eq(AuthBiometricView.Callback.ACTION_AUTHENTICATED)) 108 } 109 } 110 111 @Test 112 fun testPositiveButton_sendsActionAuthenticated() { 113 biometricView.mConfirmButton.performClick() 114 TestableLooper.get(this).moveTimeForward(1000) 115 waitForIdleSync() 116 117 verify(callback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED) 118 assertThat(biometricView.isAuthenticated).isTrue() 119 } 120 121 @Test 122 fun testNegativeButton_beforeAuthentication_sendsActionButtonNegative() { 123 biometricView.onDialogAnimatedIn(fingerprintWasStarted = true) 124 biometricView.mNegativeButton.performClick() 125 TestableLooper.get(this).moveTimeForward(1000) 126 waitForIdleSync() 127 128 verify(callback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE) 129 } 130 131 @Test 132 fun testCancelButton_whenPendingConfirmation_sendsActionUserCanceled() { 133 biometricView.setRequireConfirmation(true) 134 biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) 135 136 assertThat(biometricView.mNegativeButton.visibility).isEqualTo(View.GONE) 137 biometricView.mCancelButton.performClick() 138 TestableLooper.get(this).moveTimeForward(1000) 139 waitForIdleSync() 140 141 verify(callback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED) 142 } 143 144 @Test 145 fun testTryAgainButton_sendsActionTryAgain() { 146 biometricView.mTryAgainButton.performClick() 147 TestableLooper.get(this).moveTimeForward(1000) 148 waitForIdleSync() 149 150 verify(callback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN) 151 assertThat(biometricView.mTryAgainButton.visibility).isEqualTo(View.GONE) 152 assertThat(biometricView.isAuthenticating).isTrue() 153 } 154 155 @Test 156 fun testOnErrorSendsActionError() { 157 biometricView.onError(BiometricAuthenticator.TYPE_FACE, "testError") 158 TestableLooper.get(this).moveTimeForward(1000) 159 waitForIdleSync() 160 161 verify(callback).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR)) 162 } 163 164 @Test 165 fun testOnErrorShowsMessage() { 166 // prevent error state from instantly returning to authenticating in the test 167 biometricView.mAnimationDurationHideDialog = 10_000 168 169 val message = "another error" 170 biometricView.onError(BiometricAuthenticator.TYPE_FACE, message) 171 TestableLooper.get(this).moveTimeForward(1000) 172 waitForIdleSync() 173 174 assertThat(biometricView.isAuthenticating).isFalse() 175 assertThat(biometricView.isAuthenticated).isFalse() 176 assertThat(biometricView.mIndicatorView.visibility).isEqualTo(View.VISIBLE) 177 assertThat(biometricView.mIndicatorView.text).isEqualTo(message) 178 } 179 180 @Test 181 fun testBackgroundClicked_sendsActionUserCanceled() { 182 val view = View(mContext) 183 biometricView.setBackgroundView(view) 184 view.performClick() 185 186 verify(callback).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)) 187 } 188 189 @Test 190 fun testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() { 191 val view = View(mContext) 192 biometricView.setBackgroundView(view) 193 biometricView.onAuthenticationSucceeded(BiometricAuthenticator.TYPE_FINGERPRINT) 194 waitForIdleSync() 195 view.performClick() 196 197 verify(callback, never()) 198 .onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)) 199 } 200 201 @Test 202 fun testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() { 203 biometricView.mLayoutParams = AuthDialog.LayoutParams(0, 0) 204 biometricView.updateSize(AuthDialog.SIZE_SMALL) 205 val view = View(mContext) 206 biometricView.setBackgroundView(view) 207 view.performClick() 208 209 verify(callback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)) 210 } 211 212 @Test 213 fun testIgnoresUselessHelp() { 214 biometricView.mAnimationDurationHideDialog = 10_000 215 biometricView.onDialogAnimatedIn(fingerprintWasStarted = true) 216 waitForIdleSync() 217 218 assertThat(biometricView.isAuthenticating).isTrue() 219 220 val helpText = biometricView.mIndicatorView.text 221 biometricView.onHelp(BiometricAuthenticator.TYPE_FINGERPRINT, "") 222 waitForIdleSync() 223 224 // text should not change 225 assertThat(biometricView.mIndicatorView.text).isEqualTo(helpText) 226 verify(callback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR)) 227 } 228 229 @Test 230 fun testRestoresState() { 231 val requireConfirmation = true 232 biometricView.mAnimationDurationHideDialog = 10_000 233 val failureMessage = "testFailureMessage" 234 biometricView.setRequireConfirmation(requireConfirmation) 235 biometricView.onAuthenticationFailed(BiometricAuthenticator.TYPE_FACE, failureMessage) 236 waitForIdleSync() 237 238 val state = Bundle() 239 biometricView.onSaveState(state) 240 assertThat(biometricView.mTryAgainButton.visibility).isEqualTo(View.GONE) 241 assertThat(state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)) 242 .isEqualTo(View.GONE) 243 assertThat(state.getInt(AuthDialog.KEY_BIOMETRIC_STATE)) 244 .isEqualTo(AuthBiometricView.STATE_ERROR) 245 assertThat(biometricView.mIndicatorView.visibility).isEqualTo(View.VISIBLE) 246 assertThat(state.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)).isTrue() 247 assertThat(biometricView.mIndicatorView.text).isEqualTo(failureMessage) 248 assertThat(state.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING)) 249 .isEqualTo(failureMessage) 250 251 // TODO: Test dialog size. Should move requireConfirmation to buildBiometricPromptBundle 252 253 // Create new dialog and restore the previous state into it 254 biometricView.destroyDialog() 255 biometricView = createView() 256 biometricView.restoreState(state) 257 biometricView.mAnimationDurationHideDialog = 10_000 258 biometricView.setRequireConfirmation(requireConfirmation) 259 waitForIdleSync() 260 261 assertThat(biometricView.mTryAgainButton.visibility).isEqualTo(View.GONE) 262 assertThat(biometricView.mIndicatorView.visibility).isEqualTo(View.VISIBLE) 263 264 // TODO: Test restored text. Currently cannot test this, since it gets restored only after 265 // dialog size is known. 266 } 267 268 @Test 269 fun testCredentialButton_whenDeviceCredentialAllowed() { 270 biometricView.destroyDialog() 271 biometricView = createView(allowDeviceCredential = true) 272 273 assertThat(biometricView.mUseCredentialButton.visibility).isEqualTo(View.VISIBLE) 274 assertThat(biometricView.mNegativeButton.visibility).isEqualTo(View.GONE) 275 276 biometricView.mUseCredentialButton.performClick() 277 waitForIdleSync() 278 279 verify(callback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL) 280 } 281 282 override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages() 283 } 284