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