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.app.admin.DevicePolicyManager
19 import android.hardware.biometrics.BiometricAuthenticator
20 import android.hardware.biometrics.BiometricConstants
21 import android.hardware.biometrics.BiometricManager
22 import android.hardware.biometrics.PromptInfo
23 import android.hardware.face.FaceSensorPropertiesInternal
24 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
25 import android.os.Handler
26 import android.os.IBinder
27 import android.os.UserManager
28 import android.testing.TestableLooper
29 import android.testing.TestableLooper.RunWithLooper
30 import android.testing.ViewUtils
31 import android.view.KeyEvent
32 import android.view.View
33 import android.view.WindowInsets
34 import android.view.WindowManager
35 import android.widget.ScrollView
36 import androidx.test.ext.junit.runners.AndroidJUnit4
37 import androidx.test.filters.SmallTest
38 import com.android.internal.jank.InteractionJankMonitor
39 import com.android.internal.widget.LockPatternUtils
40 import com.android.systemui.R
41 import com.android.systemui.SysuiTestCase
42 import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
43 import com.android.systemui.biometrics.data.repository.FakePromptRepository
44 import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
45 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
46 import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
47 import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
48 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
49 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
50 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
51 import com.android.systemui.flags.FakeFeatureFlags
52 import com.android.systemui.flags.Flags
53 import com.android.systemui.keyguard.WakefulnessLifecycle
54 import com.android.systemui.statusbar.VibratorHelper
55 import com.android.systemui.util.concurrency.FakeExecutor
56 import com.android.systemui.util.time.FakeSystemClock
57 import com.google.common.truth.Truth.assertThat
58 import kotlinx.coroutines.Dispatchers
59 import kotlinx.coroutines.test.StandardTestDispatcher
60 import kotlinx.coroutines.test.TestScope
61 import kotlinx.coroutines.test.runCurrent
62 import org.junit.After
63 import org.junit.Before
64 import org.junit.Ignore
65 import org.junit.Rule
66 import org.junit.Test
67 import org.junit.runner.RunWith
68 import org.mockito.Mock
69 import org.mockito.Mockito.anyBoolean
70 import org.mockito.Mockito.anyInt
71 import org.mockito.Mockito.anyLong
72 import org.mockito.Mockito.eq
73 import org.mockito.Mockito.never
74 import org.mockito.Mockito.times
75 import org.mockito.Mockito.verify
76 import org.mockito.junit.MockitoJUnit
77 import org.mockito.Mockito.`when` as whenever
78 
79 @RunWith(AndroidJUnit4::class)
80 @RunWithLooper(setAsMainLooper = true)
81 @SmallTest
82 open class AuthContainerViewTest : SysuiTestCase() {
83 
84     @JvmField @Rule
85     var mockitoRule = MockitoJUnit.rule()
86 
87     private val featureFlags = FakeFeatureFlags()
88 
89     @Mock
90     lateinit var callback: AuthDialogCallback
91     @Mock
92     lateinit var userManager: UserManager
93     @Mock
94     lateinit var lockPatternUtils: LockPatternUtils
95     @Mock
96     lateinit var wakefulnessLifecycle: WakefulnessLifecycle
97     @Mock
98     lateinit var panelInteractionDetector: AuthDialogPanelInteractionDetector
99     @Mock
100     lateinit var windowToken: IBinder
101     @Mock
102     lateinit var interactionJankMonitor: InteractionJankMonitor
103     @Mock
104     lateinit var vibrator: VibratorHelper
105 
106     // TODO(b/278622168): remove with flag
107     open val useNewBiometricPrompt = false
108 
109     private val testScope = TestScope(StandardTestDispatcher())
110     private val fakeExecutor = FakeExecutor(FakeSystemClock())
111     private val biometricPromptRepository = FakePromptRepository()
112     private val fingerprintRepository = FakeFingerprintPropertyRepository()
113     private val displayStateRepository = FakeDisplayStateRepository()
114     private val credentialInteractor = FakeCredentialInteractor()
115     private val bpCredentialInteractor = PromptCredentialInteractor(
116         Dispatchers.Main.immediate,
117         biometricPromptRepository,
118         credentialInteractor,
119     )
120     private val promptSelectorInteractor by lazy {
121         PromptSelectorInteractorImpl(
122             fingerprintRepository,
123             biometricPromptRepository,
124             lockPatternUtils,
125         )
126     }
127 
128     private val displayStateInteractor = DisplayStateInteractorImpl(
129         testScope.backgroundScope,
130         mContext,
131         fakeExecutor,
132         displayStateRepository
133     )
134 
135 
136     private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor)
137 
138     private var authContainer: TestAuthContainerView? = null
139 
140     @Before
141     fun setup() {
142         featureFlags.set(Flags.BIOMETRIC_BP_STRONG, useNewBiometricPrompt)
143         featureFlags.set(Flags.ONE_WAY_HAPTICS_API_MIGRATION, false)
144     }
145 
146     @After
147     fun tearDown() {
148         if (authContainer?.isAttachedToWindow == true) {
149             ViewUtils.detachView(authContainer)
150         }
151     }
152 
153     @Test
154     fun testNotifiesAnimatedIn() {
155         initializeFingerprintContainer()
156         verify(callback).onDialogAnimatedIn(
157             authContainer?.requestId ?: 0L,
158             true /* startFingerprintNow */
159         )
160     }
161 
162     @Test
163     fun testDismissesOnBack() {
164         val container = initializeFingerprintContainer(addToView = true)
165         assertThat(container.parent).isNotNull()
166         val root = container.rootView
167 
168         // Simulate back invocation
169         container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK))
170         container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK))
171         waitForIdleSync()
172 
173         assertThat(container.parent).isNull()
174         assertThat(root.isAttachedToWindow).isFalse()
175     }
176 
177     @Test
178     fun testCredentialPasswordDismissesOnBack() {
179         val container = initializeCredentialPasswordContainer(addToView = true)
180         assertThat(container.parent).isNotNull()
181         val root = container.rootView
182 
183         // Simulate back invocation
184         container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK))
185         container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK))
186         waitForIdleSync()
187 
188         assertThat(container.parent).isNull()
189         assertThat(root.isAttachedToWindow).isFalse()
190     }
191 
192     @Test
193     fun testIgnoresAnimatedInWhenDismissed() {
194         val container = initializeFingerprintContainer(addToView = false)
195         container.dismissFromSystemServer()
196         waitForIdleSync()
197 
198         verify(callback, never()).onDialogAnimatedIn(anyLong(), anyBoolean())
199 
200         container.addToView()
201         waitForIdleSync()
202 
203         // attaching the view resets the state and allows this to happen again
204         verify(callback).onDialogAnimatedIn(
205             authContainer?.requestId ?: 0L,
206             true /* startFingerprintNow */
207         )
208     }
209 
210     @Test
211     fun testDismissBeforeIntroEnd() {
212         val container = initializeFingerprintContainer()
213         waitForIdleSync()
214 
215         // STATE_ANIMATING_IN = 1
216         container?.mContainerState = 1
217 
218         container.dismissWithoutCallback(false)
219 
220         // the first time is triggered by initializeFingerprintContainer()
221         // the second time was triggered by dismissWithoutCallback()
222         verify(callback, times(2)).onDialogAnimatedIn(
223             authContainer?.requestId ?: 0L,
224             true /* startFingerprintNow */
225         )
226     }
227 
228     @Test
229     fun testActionCancel_panelInteractionDetectorDisable() {
230         val container = initializeFingerprintContainer()
231         container.mBiometricCallback.onAction(
232                 AuthBiometricView.Callback.ACTION_USER_CANCELED
233         )
234         waitForIdleSync()
235         verify(panelInteractionDetector).disable()
236     }
237 
238 
239     @Test
240     fun testActionAuthenticated_sendsDismissedAuthenticated() {
241         val container = initializeFingerprintContainer()
242         container.mBiometricCallback.onAction(
243             AuthBiometricView.Callback.ACTION_AUTHENTICATED
244         )
245         waitForIdleSync()
246 
247         verify(callback).onDismissed(
248                 eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED),
249                 eq<ByteArray?>(null), /* credentialAttestation */
250                 eq(authContainer?.requestId ?: 0L)
251         )
252         assertThat(container.parent).isNull()
253     }
254 
255     @Test
256     fun testActionUserCanceled_sendsDismissedUserCanceled() {
257         val container = initializeFingerprintContainer()
258         container.mBiometricCallback.onAction(
259             AuthBiometricView.Callback.ACTION_USER_CANCELED
260         )
261         waitForIdleSync()
262 
263         verify(callback).onSystemEvent(
264                 eq(BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL),
265                 eq(authContainer?.requestId ?: 0L)
266         )
267         verify(callback).onDismissed(
268                 eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
269                 eq<ByteArray?>(null), /* credentialAttestation */
270                 eq(authContainer?.requestId ?: 0L)
271         )
272         assertThat(container.parent).isNull()
273     }
274 
275     @Test
276     fun testActionButtonNegative_sendsDismissedButtonNegative() {
277         val container = initializeFingerprintContainer()
278         container.mBiometricCallback.onAction(
279             AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE
280         )
281         waitForIdleSync()
282 
283         verify(callback).onDismissed(
284                 eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE),
285                 eq<ByteArray?>(null), /* credentialAttestation */
286                 eq(authContainer?.requestId ?: 0L)
287         )
288         assertThat(container.parent).isNull()
289     }
290 
291     @Test
292     fun testActionTryAgain_sendsTryAgain() {
293         val container = initializeFingerprintContainer(
294             authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
295         )
296         container.mBiometricCallback.onAction(
297             AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN
298         )
299         waitForIdleSync()
300 
301         verify(callback).onTryAgainPressed(authContainer?.requestId ?: 0L)
302     }
303 
304     @Test
305     fun testActionError_sendsDismissedError() {
306         val container = initializeFingerprintContainer()
307         container.mBiometricCallback.onAction(
308             AuthBiometricView.Callback.ACTION_ERROR
309         )
310         waitForIdleSync()
311 
312         verify(callback).onDismissed(
313                 eq(AuthDialogCallback.DISMISSED_ERROR),
314                 eq<ByteArray?>(null), /* credentialAttestation */
315                 eq(authContainer?.requestId ?: 0L)
316         )
317         assertThat(authContainer!!.parent).isNull()
318     }
319 
320     @Ignore("b/279650412")
321     @Test
322     fun testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() {
323         val container = initializeFingerprintContainer(
324             authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or
325                     BiometricManager.Authenticators.DEVICE_CREDENTIAL
326         )
327         container.mBiometricCallback.onAction(
328             AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL
329         )
330         waitForIdleSync()
331 
332         verify(callback).onDeviceCredentialPressed(authContainer?.requestId ?: 0L)
333         assertThat(container.hasCredentialView()).isTrue()
334     }
335 
336     @Test
337     fun testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() {
338         val container = initializeFingerprintContainer(
339             authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or
340                     BiometricManager.Authenticators.DEVICE_CREDENTIAL
341         )
342         container.animateToCredentialUI(false)
343         waitForIdleSync()
344 
345         assertThat(container.hasCredentialView()).isTrue()
346     }
347 
348     @Test
349     fun testShowBiometricUI() {
350         val container = initializeFingerprintContainer()
351 
352         waitForIdleSync()
353 
354         assertThat(container.hasCredentialView()).isFalse()
355         assertThat(container.hasBiometricPrompt()).isTrue()
356     }
357 
358     @Test
359     fun testShowCredentialUI() {
360         val container = initializeFingerprintContainer(
361             authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
362         )
363         waitForIdleSync()
364 
365         assertThat(container.hasCredentialView()).isTrue()
366         assertThat(container.hasBiometricPrompt()).isFalse()
367     }
368 
369     @Test
370     fun testCredentialViewUsesEffectiveUserId() {
371         whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(200)
372         whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(200))).thenReturn(
373             DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
374         )
375 
376         val container = initializeFingerprintContainer(
377             authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
378         )
379         waitForIdleSync()
380 
381         assertThat(container.hasCredentialPatternView()).isTrue()
382         assertThat(container.hasBiometricPrompt()).isFalse()
383     }
384 
385     @Test
386     fun testCredentialUI_disablesClickingOnBackground() {
387         val container = initializeCredentialPasswordContainer()
388         assertThat(container.hasBiometricPrompt()).isFalse()
389         assertThat(
390             container.findViewById<View>(R.id.background)?.isImportantForAccessibility
391         ).isFalse()
392 
393         container.findViewById<View>(R.id.background)?.performClick()
394         waitForIdleSync()
395 
396         assertThat(container.hasCredentialPasswordView()).isTrue()
397         assertThat(container.hasBiometricPrompt()).isFalse()
398     }
399 
400     @Test
401     fun testLayoutParams_hasSecureWindowFlag() {
402         val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
403         assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0).isTrue()
404     }
405 
406     @Test
407     fun testLayoutParams_hasShowWhenLockedFlag() {
408         val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
409         assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) != 0)
410                 .isTrue()
411     }
412 
413     @Test
414     fun testLayoutParams_hasDimbehindWindowFlag() {
415         val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
416         val lpFlags = layoutParams.flags
417         val lpDimAmount = layoutParams.dimAmount
418 
419         assertThat((lpFlags and WindowManager.LayoutParams.FLAG_DIM_BEHIND) != 0).isTrue()
420         assertThat(lpDimAmount).isGreaterThan(0f)
421     }
422 
423     @Test
424     fun testLayoutParams_excludesImeInsets() {
425         val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
426         assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.ime()) == 0).isTrue()
427     }
428 
429     @Test
430     fun coexFaceRestartsOnTouch() {
431         val container = initializeCoexContainer()
432 
433         container.onPointerDown()
434         waitForIdleSync()
435 
436         container.onAuthenticationFailed(BiometricAuthenticator.TYPE_FACE, "failed")
437         waitForIdleSync()
438 
439         verify(callback, never()).onTryAgainPressed(anyLong())
440 
441         container.onPointerDown()
442         waitForIdleSync()
443 
444         verify(callback).onTryAgainPressed(authContainer?.requestId ?: 0L)
445     }
446 
447     private fun initializeCredentialPasswordContainer(
448             addToView: Boolean = true,
449     ): TestAuthContainerView {
450         whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20)
451         whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn(
452             DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
453         )
454 
455         // In the credential view, clicking on the background (to cancel authentication) is not
456         // valid. Thus, the listener should be null, and it should not be in the accessibility
457         // hierarchy.
458         val container = initializeFingerprintContainer(
459                 authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL,
460                 addToView = addToView,
461         )
462         waitForIdleSync()
463 
464         assertThat(container.hasCredentialPasswordView()).isTrue()
465         return container
466     }
467 
468     private fun initializeFingerprintContainer(
469         authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK,
470         addToView: Boolean = true
471     ) = initializeContainer(
472         TestAuthContainerView(
473             authenticators = authenticators,
474             fingerprintProps = fingerprintSensorPropertiesInternal()
475         ),
476         addToView
477     )
478 
479     private fun initializeCoexContainer(
480         authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK,
481         addToView: Boolean = true
482     ) = initializeContainer(
483         TestAuthContainerView(
484             authenticators = authenticators,
485             fingerprintProps = fingerprintSensorPropertiesInternal(),
486             faceProps = faceSensorPropertiesInternal()
487         ),
488         addToView
489     )
490 
491     private fun initializeContainer(
492         view: TestAuthContainerView,
493         addToView: Boolean
494     ): TestAuthContainerView {
495         authContainer = view
496 
497         if (addToView) {
498             authContainer!!.addToView()
499         }
500 
501         return authContainer!!
502     }
503 
504     private inner class TestAuthContainerView(
505         authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK,
506         fingerprintProps: List<FingerprintSensorPropertiesInternal> = listOf(),
507         faceProps: List<FaceSensorPropertiesInternal> = listOf()
508     ) : AuthContainerView(
509         Config().apply {
510             mContext = this@AuthContainerViewTest.context
511             mCallback = callback
512             mSensorIds = (fingerprintProps.map { it.sensorId } +
513                 faceProps.map { it.sensorId }).toIntArray()
514             mSkipAnimation = true
515             mPromptInfo = PromptInfo().apply {
516                 this.authenticators = authenticators
517             }
518         },
519         featureFlags,
520         testScope.backgroundScope,
521         fingerprintProps,
522         faceProps,
523         wakefulnessLifecycle,
524         panelInteractionDetector,
525         userManager,
526         lockPatternUtils,
527         interactionJankMonitor,
528         { promptSelectorInteractor },
529         { bpCredentialInteractor },
530         PromptViewModel(
531             displayStateInteractor,
532             promptSelectorInteractor,
533             vibrator,
534             context,
535             featureFlags
536         ),
537         { credentialViewModel },
538         Handler(TestableLooper.get(this).looper),
539         fakeExecutor,
540         vibrator
541     ) {
542         override fun postOnAnimation(runnable: Runnable) {
543             runnable.run()
544         }
545     }
546 
547     override fun waitForIdleSync() {
548         testScope.runCurrent()
549         TestableLooper.get(this).processAllMessages()
550     }
551 
552     private fun AuthContainerView.addToView() {
553         ViewUtils.attachView(this)
554         waitForIdleSync()
555         assertThat(isAttachedToWindow()).isTrue()
556     }
557 
558     @Test
559     fun testLayoutParams_hasCutoutModeAlwaysFlag() {
560         val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
561         val lpFlags = layoutParams.flags
562 
563         assertThat((lpFlags and WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS)
564                 != 0).isTrue()
565     }
566 
567     @Test
568     fun testLayoutParams_excludesSystemBarInsets() {
569         val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
570         assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.systemBars()) == 0).isTrue()
571     }
572 }
573 
574 private fun AuthContainerView.hasBiometricPrompt() =
575     (findViewById<ScrollView>(R.id.biometric_scrollview)?.childCount ?: 0) > 0
576 
577 private fun AuthContainerView.hasCredentialView() =
578     hasCredentialPatternView() || hasCredentialPasswordView()
579 
580 private fun AuthContainerView.hasCredentialPatternView() =
581     findViewById<View>(R.id.lockPattern) != null
582 
583 private fun AuthContainerView.hasCredentialPasswordView() =
584     findViewById<View>(R.id.lockPassword) != null
585