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 
17 package com.android.systemui.bouncer.domain.interactor
18 
19 import android.hardware.biometrics.BiometricSourceType
20 import android.testing.AndroidTestingRunner
21 import android.testing.TestableLooper.RunWithLooper
22 import android.testing.TestableResources
23 import android.view.View
24 import androidx.test.filters.SmallTest
25 import com.android.keyguard.KeyguardSecurityModel
26 import com.android.keyguard.KeyguardUpdateMonitor
27 import com.android.systemui.DejankUtils
28 import com.android.systemui.R
29 import com.android.systemui.SysuiTestCase
30 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
31 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
32 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
33 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
34 import com.android.systemui.bouncer.ui.BouncerView
35 import com.android.systemui.bouncer.ui.BouncerViewDelegate
36 import com.android.systemui.classifier.FalsingCollector
37 import com.android.systemui.flags.FakeFeatureFlags
38 import com.android.systemui.flags.Flags
39 import com.android.systemui.keyguard.DismissCallbackRegistry
40 import com.android.systemui.keyguard.data.repository.FakeTrustRepository
41 import com.android.systemui.plugins.ActivityStarter
42 import com.android.systemui.statusbar.policy.KeyguardStateController
43 import com.android.systemui.util.mockito.any
44 import com.android.systemui.util.mockito.whenever
45 import com.android.systemui.utils.os.FakeHandler
46 import com.google.common.truth.Truth.assertThat
47 import kotlinx.coroutines.test.TestScope
48 import kotlinx.coroutines.test.runCurrent
49 import org.junit.Before
50 import org.junit.Test
51 import org.junit.runner.RunWith
52 import org.mockito.Answers
53 import org.mockito.ArgumentCaptor
54 import org.mockito.Mock
55 import org.mockito.Mockito.anyInt
56 import org.mockito.Mockito.mock
57 import org.mockito.Mockito.never
58 import org.mockito.Mockito.verify
59 import org.mockito.MockitoAnnotations
60 
61 @SmallTest
62 @RunWithLooper(setAsMainLooper = true)
63 @RunWith(AndroidTestingRunner::class)
64 class PrimaryBouncerInteractorTest : SysuiTestCase() {
65     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
66     private lateinit var repository: KeyguardBouncerRepository
67     @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var bouncerView: BouncerView
68     @Mock private lateinit var bouncerViewDelegate: BouncerViewDelegate
69     @Mock private lateinit var keyguardStateController: KeyguardStateController
70     @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
71     @Mock private lateinit var mPrimaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor
72     @Mock private lateinit var falsingCollector: FalsingCollector
73     @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
74     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
75     private lateinit var mainHandler: FakeHandler
76     private lateinit var underTest: PrimaryBouncerInteractor
77     private lateinit var resources: TestableResources
78     private lateinit var trustRepository: FakeTrustRepository
79     private lateinit var featureFlags: FakeFeatureFlags
80     private lateinit var testScope: TestScope
81 
82     @Before
83     fun setUp() {
84         MockitoAnnotations.initMocks(this)
85         whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
86             .thenReturn(KeyguardSecurityModel.SecurityMode.PIN)
87 
88         DejankUtils.setImmediate(true)
89         testScope = TestScope()
90         mainHandler = FakeHandler(android.os.Looper.getMainLooper())
91         trustRepository = FakeTrustRepository()
92         featureFlags = FakeFeatureFlags().apply { set(Flags.DELAY_BOUNCER, true) }
93         underTest =
94             PrimaryBouncerInteractor(
95                 repository,
96                 bouncerView,
97                 mainHandler,
98                 keyguardStateController,
99                 keyguardSecurityModel,
100                 mPrimaryBouncerCallbackInteractor,
101                 falsingCollector,
102                 dismissCallbackRegistry,
103                 context,
104                 keyguardUpdateMonitor,
105                 trustRepository,
106                 featureFlags,
107                 testScope.backgroundScope,
108             )
109         whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
110         whenever(repository.primaryBouncerShow.value).thenReturn(false)
111         whenever(bouncerView.delegate).thenReturn(bouncerViewDelegate)
112         resources = context.orCreateTestableResources
113     }
114 
115     @Test
116     fun testShow_isScrimmed() {
117         underTest.show(true)
118         verify(repository).setKeyguardAuthenticated(null)
119         verify(repository).setPrimaryStartingToHide(false)
120         verify(repository).setPrimaryScrimmed(true)
121         verify(repository).setPanelExpansion(EXPANSION_VISIBLE)
122         verify(repository).setPrimaryShowingSoon(true)
123         verify(keyguardStateController).notifyPrimaryBouncerShowing(true)
124         verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToShow()
125         verify(repository).setPrimaryShow(true)
126         verify(repository).setPrimaryShowingSoon(false)
127         verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.VISIBLE)
128     }
129 
130     @Test
131     fun testShow_isNotScrimmed() {
132         verify(repository, never()).setPanelExpansion(EXPANSION_VISIBLE)
133     }
134 
135     @Test
136     fun testShow_keyguardIsDone() {
137         whenever(bouncerView.delegate?.showNextSecurityScreenOrFinish()).thenReturn(true)
138         verify(keyguardStateController, never()).notifyPrimaryBouncerShowing(true)
139         verify(mPrimaryBouncerCallbackInteractor, never()).dispatchStartingToShow()
140     }
141 
142     @Test
143     fun testShow_isResumed() {
144         whenever(repository.primaryBouncerShow.value).thenReturn(true)
145         whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
146             .thenReturn(KeyguardSecurityModel.SecurityMode.SimPuk)
147 
148         underTest.show(true)
149         verify(repository).setPrimaryShow(false)
150         verify(repository).setPrimaryShow(true)
151     }
152 
153     @Test
154     fun testHide() {
155         underTest.hide()
156         verify(falsingCollector).onBouncerHidden()
157         verify(keyguardStateController).notifyPrimaryBouncerShowing(false)
158         verify(repository).setPrimaryShowingSoon(false)
159         verify(repository).setPrimaryShow(false)
160         verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.INVISIBLE)
161         verify(repository).setPrimaryStartDisappearAnimation(null)
162         verify(repository).setPanelExpansion(EXPANSION_HIDDEN)
163     }
164 
165     @Test
166     fun testExpansion() {
167         whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
168         underTest.setPanelExpansion(0.6f)
169         verify(repository).setPanelExpansion(0.6f)
170         verify(mPrimaryBouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
171     }
172 
173     @Test
174     fun testExpansion_fullyShown() {
175         whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
176         whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
177         underTest.setPanelExpansion(EXPANSION_VISIBLE)
178         verify(falsingCollector).onBouncerShown()
179         verify(mPrimaryBouncerCallbackInteractor).dispatchFullyShown()
180     }
181 
182     @Test
183     fun testExpansion_fullyHidden() {
184         whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
185         whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
186         underTest.setPanelExpansion(EXPANSION_HIDDEN)
187         verify(repository).setPrimaryShow(false)
188         verify(falsingCollector).onBouncerHidden()
189         verify(mPrimaryBouncerCallbackInteractor).dispatchReset()
190         verify(mPrimaryBouncerCallbackInteractor).dispatchFullyHidden()
191     }
192 
193     @Test
194     fun testExpansion_startingToHide() {
195         whenever(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
196         underTest.setPanelExpansion(0.1f)
197         verify(repository).setPrimaryStartingToHide(true)
198         verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToHide()
199     }
200 
201     @Test
202     fun testShowMessage() {
203         val argCaptor = ArgumentCaptor.forClass(BouncerShowMessageModel::class.java)
204         underTest.showMessage("abc", null)
205         verify(repository).setShowMessage(argCaptor.capture())
206         assertThat(argCaptor.value.message).isEqualTo("abc")
207     }
208 
209     @Test
210     fun testDismissAction() {
211         val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
212         val cancelAction = mock(Runnable::class.java)
213         underTest.setDismissAction(onDismissAction, cancelAction)
214         verify(bouncerViewDelegate).setDismissAction(onDismissAction, cancelAction)
215     }
216 
217     @Test
218     fun testUpdateResources() {
219         underTest.updateResources()
220         verify(repository).setResourceUpdateRequests(true)
221     }
222 
223     @Test
224     fun testNotifyKeyguardAuthenticated() {
225         underTest.notifyKeyguardAuthenticated(true)
226         verify(repository).setKeyguardAuthenticated(true)
227     }
228 
229     @Test
230     fun testNotifyShowedMessage() {
231         underTest.onMessageShown()
232         verify(repository).setShowMessage(null)
233     }
234 
235     @Test
236     fun testSetKeyguardPosition() {
237         underTest.setKeyguardPosition(0f)
238         verify(repository).setKeyguardPosition(0f)
239     }
240 
241     @Test
242     fun testNotifyKeyguardAuthenticatedHandled() {
243         underTest.notifyKeyguardAuthenticatedHandled()
244         verify(repository).setKeyguardAuthenticated(null)
245     }
246 
247     @Test
248     fun testNotifyUpdatedResources() {
249         underTest.notifyUpdatedResources()
250         verify(repository).setResourceUpdateRequests(false)
251     }
252 
253     @Test
254     fun testSetBackButtonEnabled() {
255         underTest.setBackButtonEnabled(true)
256         verify(repository).setIsBackButtonEnabled(true)
257     }
258 
259     @Test
260     fun testStartDisappearAnimation_willRunDismissFromKeyguard() {
261         whenever(bouncerViewDelegate.willRunDismissFromKeyguard()).thenReturn(true)
262 
263         val runnable = mock(Runnable::class.java)
264         underTest.startDisappearAnimation(runnable)
265         // End runnable should run immediately
266         verify(runnable).run()
267         // ... while the disappear animation should never be run
268         verify(repository, never()).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
269     }
270 
271     @Test
272     fun testStartDisappearAnimation_willNotRunDismissFromKeyguard_() {
273         whenever(bouncerViewDelegate.willRunDismissFromKeyguard()).thenReturn(false)
274 
275         val runnable = mock(Runnable::class.java)
276         underTest.startDisappearAnimation(runnable)
277         verify(repository).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
278     }
279 
280     @Test
281     fun testIsFullShowing() {
282         whenever(repository.primaryBouncerShow.value).thenReturn(true)
283         whenever(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
284         whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
285         assertThat(underTest.isFullyShowing()).isTrue()
286         whenever(repository.primaryBouncerShow.value).thenReturn(false)
287         assertThat(underTest.isFullyShowing()).isFalse()
288     }
289 
290     @Test
291     fun testIsScrimmed() {
292         whenever(repository.primaryBouncerScrimmed.value).thenReturn(true)
293         assertThat(underTest.isScrimmed()).isTrue()
294         whenever(repository.primaryBouncerScrimmed.value).thenReturn(false)
295         assertThat(underTest.isScrimmed()).isFalse()
296     }
297 
298     @Test
299     fun testIsInTransit() {
300         whenever(repository.primaryBouncerShowingSoon.value).thenReturn(true)
301         assertThat(underTest.isInTransit()).isTrue()
302         whenever(repository.primaryBouncerShowingSoon.value).thenReturn(false)
303         assertThat(underTest.isInTransit()).isFalse()
304         whenever(repository.panelExpansionAmount.value).thenReturn(0.5f)
305         assertThat(underTest.isInTransit()).isTrue()
306     }
307 
308     @Test
309     fun testIsAnimatingAway() {
310         whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(Runnable {})
311         assertThat(underTest.isAnimatingAway()).isTrue()
312         whenever(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
313         assertThat(underTest.isAnimatingAway()).isFalse()
314     }
315 
316     @Test
317     fun testWillDismissWithAction() {
318         whenever(bouncerViewDelegate.willDismissWithActions()).thenReturn(true)
319         assertThat(underTest.willDismissWithAction()).isTrue()
320         whenever(bouncerViewDelegate.willDismissWithActions()).thenReturn(false)
321         assertThat(underTest.willDismissWithAction()).isFalse()
322     }
323 
324     @Test
325     fun testSideFpsVisibility() {
326         updateSideFpsVisibilityParameters(
327             isVisible = true,
328             sfpsEnabled = true,
329             fpsDetectionRunning = true,
330             isUnlockingWithFpAllowed = true,
331             isAnimatingAway = false
332         )
333         underTest.updateSideFpsVisibility()
334         verify(repository).setSideFpsShowing(true)
335     }
336 
337     @Test
338     fun testSideFpsVisibility_notVisible() {
339         updateSideFpsVisibilityParameters(
340             isVisible = false,
341             sfpsEnabled = true,
342             fpsDetectionRunning = true,
343             isUnlockingWithFpAllowed = true,
344             isAnimatingAway = false
345         )
346         underTest.updateSideFpsVisibility()
347         verify(repository).setSideFpsShowing(false)
348     }
349 
350     @Test
351     fun testSideFpsVisibility_sfpsNotEnabled() {
352         updateSideFpsVisibilityParameters(
353             isVisible = true,
354             sfpsEnabled = false,
355             fpsDetectionRunning = true,
356             isUnlockingWithFpAllowed = true,
357             isAnimatingAway = false
358         )
359         underTest.updateSideFpsVisibility()
360         verify(repository).setSideFpsShowing(false)
361     }
362 
363     @Test
364     fun testSideFpsVisibility_fpsDetectionNotRunning() {
365         updateSideFpsVisibilityParameters(
366             isVisible = true,
367             sfpsEnabled = true,
368             fpsDetectionRunning = false,
369             isUnlockingWithFpAllowed = true,
370             isAnimatingAway = false
371         )
372         underTest.updateSideFpsVisibility()
373         verify(repository).setSideFpsShowing(false)
374     }
375 
376     @Test
377     fun testSideFpsVisibility_UnlockingWithFpNotAllowed() {
378         updateSideFpsVisibilityParameters(
379             isVisible = true,
380             sfpsEnabled = true,
381             fpsDetectionRunning = true,
382             isUnlockingWithFpAllowed = false,
383             isAnimatingAway = false
384         )
385         underTest.updateSideFpsVisibility()
386         verify(repository).setSideFpsShowing(false)
387     }
388 
389     @Test
390     fun testSideFpsVisibility_AnimatingAway() {
391         updateSideFpsVisibilityParameters(
392             isVisible = true,
393             sfpsEnabled = true,
394             fpsDetectionRunning = true,
395             isUnlockingWithFpAllowed = true,
396             isAnimatingAway = true
397         )
398         underTest.updateSideFpsVisibility()
399         verify(repository).setSideFpsShowing(false)
400     }
401 
402     @Test
403     fun delayBouncerWhenFaceAuthPossible() {
404         mainHandler.setMode(FakeHandler.Mode.QUEUEING)
405 
406         // GIVEN bouncer should be delayed due to face auth
407         whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true)
408         whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
409             .thenReturn(true)
410         whenever(keyguardUpdateMonitor.doesCurrentPostureAllowFaceAuth()).thenReturn(true)
411 
412         // WHEN bouncer show is requested
413         underTest.show(true)
414 
415         // THEN primary show & primary showing soon aren't updated immediately
416         verify(repository, never()).setPrimaryShow(true)
417         verify(repository, never()).setPrimaryShowingSoon(false)
418 
419         // WHEN all queued messages are dispatched
420         mainHandler.dispatchQueuedMessages()
421 
422         // THEN primary show & primary showing soon are updated
423         verify(repository).setPrimaryShow(true)
424         verify(repository).setPrimaryShowingSoon(false)
425     }
426 
427     @Test
428     fun noDelayBouncer_biometricsAllowed_postureDoesNotAllowFaceAuth() {
429         mainHandler.setMode(FakeHandler.Mode.QUEUEING)
430 
431         // GIVEN bouncer should not be delayed because device isn't in the right posture for
432         // face auth
433         whenever(keyguardStateController.isFaceAuthEnabled).thenReturn(true)
434         whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(BiometricSourceType.FACE))
435             .thenReturn(true)
436         whenever(keyguardUpdateMonitor.doesCurrentPostureAllowFaceAuth()).thenReturn(false)
437 
438         // WHEN bouncer show is requested
439         underTest.show(true)
440 
441         // THEN primary show & primary showing soon are updated immediately
442         verify(repository).setPrimaryShow(true)
443         verify(repository).setPrimaryShowingSoon(false)
444     }
445 
446     @Test
447     fun delayBouncerWhenActiveUnlockPossible() {
448         testScope.run {
449             mainHandler.setMode(FakeHandler.Mode.QUEUEING)
450 
451             // GIVEN bouncer should be delayed due to active unlock
452             trustRepository.setCurrentUserActiveUnlockAvailable(true)
453             whenever(keyguardUpdateMonitor.canTriggerActiveUnlockBasedOnDeviceState())
454                 .thenReturn(true)
455             runCurrent()
456 
457             // WHEN bouncer show is requested
458             underTest.show(true)
459 
460             // THEN primary show & primary showing soon were scheduled to update
461             verify(repository, never()).setPrimaryShow(true)
462             verify(repository, never()).setPrimaryShowingSoon(false)
463 
464             // WHEN all queued messages are dispatched
465             mainHandler.dispatchQueuedMessages()
466 
467             // THEN primary show & primary showing soon are updated
468             verify(repository).setPrimaryShow(true)
469             verify(repository).setPrimaryShowingSoon(false)
470         }
471     }
472 
473     private fun updateSideFpsVisibilityParameters(
474         isVisible: Boolean,
475         sfpsEnabled: Boolean,
476         fpsDetectionRunning: Boolean,
477         isUnlockingWithFpAllowed: Boolean,
478         isAnimatingAway: Boolean
479     ) {
480         whenever(repository.primaryBouncerShow.value).thenReturn(isVisible)
481         resources.addOverride(R.bool.config_show_sidefps_hint_on_bouncer, sfpsEnabled)
482         whenever(keyguardUpdateMonitor.isFingerprintDetectionRunning)
483             .thenReturn(fpsDetectionRunning)
484         whenever(keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
485             .thenReturn(isUnlockingWithFpAllowed)
486         whenever(repository.primaryBouncerStartingDisappearAnimation.value)
487             .thenReturn(if (isAnimatingAway) Runnable {} else null)
488     }
489 }
490