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.keyguard.data.repository
18 
19 import android.graphics.Point
20 import android.testing.TestableLooper
21 import androidx.test.ext.junit.runners.AndroidJUnit4
22 import androidx.test.filters.SmallTest
23 import com.android.systemui.RoboPilotTest
24 import com.android.systemui.SysuiTestCase
25 import com.android.systemui.animation.AnimatorTestRule
26 import com.android.systemui.coroutines.collectLastValue
27 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
28 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
29 import com.android.systemui.keyguard.shared.model.WakeSleepReason
30 import com.android.systemui.keyguard.shared.model.WakefulnessModel
31 import com.android.systemui.keyguard.shared.model.WakefulnessState
32 import com.android.systemui.statusbar.CircleReveal
33 import com.android.systemui.statusbar.LightRevealEffect
34 import junit.framework.Assert.assertEquals
35 import junit.framework.Assert.assertFalse
36 import kotlinx.coroutines.ExperimentalCoroutinesApi
37 import kotlinx.coroutines.launch
38 import kotlinx.coroutines.test.UnconfinedTestDispatcher
39 import kotlinx.coroutines.test.runCurrent
40 import kotlinx.coroutines.test.runTest
41 import org.junit.Before
42 import org.junit.Rule
43 import org.junit.Test
44 import org.junit.runner.RunWith
45 import org.mockito.MockitoAnnotations
46 
47 @SmallTest
48 @RoboPilotTest
49 @OptIn(ExperimentalCoroutinesApi::class)
50 @RunWith(AndroidJUnit4::class)
51 class LightRevealScrimRepositoryTest : SysuiTestCase() {
52     private lateinit var fakeKeyguardRepository: FakeKeyguardRepository
53     private lateinit var underTest: LightRevealScrimRepositoryImpl
54 
55     @get:Rule val animatorTestRule = AnimatorTestRule()
56 
57     @Before
58     fun setUp() {
59         MockitoAnnotations.initMocks(this)
60         fakeKeyguardRepository = FakeKeyguardRepository()
61         underTest = LightRevealScrimRepositoryImpl(fakeKeyguardRepository, context)
62     }
63 
64     @Test
65     fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() = runTest {
66         val values = mutableListOf<LightRevealEffect>()
67         val job = launch { underTest.revealEffect.collect { values.add(it) } }
68 
69         fakeKeyguardRepository.setWakefulnessModel(
70             WakefulnessModel(
71                 WakefulnessState.STARTING_TO_WAKE,
72                 WakeSleepReason.OTHER,
73                 WakeSleepReason.OTHER
74             )
75         )
76         // We should initially emit the default reveal effect.
77         runCurrent()
78         values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT })
79 
80         // The source and sensor locations are still null, so we should still be using the
81         // default reveal despite a biometric unlock.
82         fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
83 
84         runCurrent()
85         values.assertEffectsMatchPredicates(
86             { it == DEFAULT_REVEAL_EFFECT },
87         )
88 
89         // We got a source but still have no sensor locations, so should be sticking with
90         // the default effect.
91         fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
92 
93         runCurrent()
94         values.assertEffectsMatchPredicates(
95             { it == DEFAULT_REVEAL_EFFECT },
96         )
97 
98         // We got a location for the face sensor, but we unlocked with fingerprint.
99         val faceLocation = Point(250, 0)
100         fakeKeyguardRepository.setFaceSensorLocation(faceLocation)
101 
102         runCurrent()
103         values.assertEffectsMatchPredicates(
104             { it == DEFAULT_REVEAL_EFFECT },
105         )
106 
107         // Now we have fingerprint sensor locations, and wake and unlock via fingerprint.
108         val fingerprintLocation = Point(500, 500)
109         fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation)
110         fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
111         fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
112 
113         // We should now have switched to the circle reveal, at the fingerprint location.
114         runCurrent()
115         values.assertEffectsMatchPredicates(
116             { it == DEFAULT_REVEAL_EFFECT },
117             {
118                 it is CircleReveal &&
119                     it.centerX == fingerprintLocation.x &&
120                     it.centerY == fingerprintLocation.y
121             },
122         )
123 
124         // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals.
125         val valuesPrevSize = values.size
126         fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
127         fakeKeyguardRepository.setBiometricUnlockState(
128             BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
129         )
130         assertEquals(valuesPrevSize, values.size)
131 
132         // Non-biometric unlock, we should return to the default reveal.
133         fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
134 
135         runCurrent()
136         values.assertEffectsMatchPredicates(
137             { it == DEFAULT_REVEAL_EFFECT },
138             {
139                 it is CircleReveal &&
140                     it.centerX == fingerprintLocation.x &&
141                     it.centerY == fingerprintLocation.y
142             },
143             { it == DEFAULT_REVEAL_EFFECT },
144         )
145 
146         // We already have a face location, so switching to face source should update the
147         // CircleReveal.
148         fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR)
149         runCurrent()
150         fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
151         runCurrent()
152 
153         values.assertEffectsMatchPredicates(
154             { it == DEFAULT_REVEAL_EFFECT },
155             {
156                 it is CircleReveal &&
157                     it.centerX == fingerprintLocation.x &&
158                     it.centerY == fingerprintLocation.y
159             },
160             { it == DEFAULT_REVEAL_EFFECT },
161             { it is CircleReveal && it.centerX == faceLocation.x && it.centerY == faceLocation.y },
162         )
163 
164         job.cancel()
165     }
166 
167     @Test
168     @TestableLooper.RunWithLooper(setAsMainLooper = true)
169     fun revealAmount_emitsTo1AfterAnimationStarted() =
170         runTest(UnconfinedTestDispatcher()) {
171             val value by collectLastValue(underTest.revealAmount)
172             underTest.startRevealAmountAnimator(true)
173             assertEquals(0.0f, value)
174             animatorTestRule.advanceTimeBy(500L)
175             assertEquals(1.0f, value)
176         }
177     @Test
178     @TestableLooper.RunWithLooper(setAsMainLooper = true)
179     fun revealAmount_emitsTo0AfterAnimationStartedReversed() =
180         runTest(UnconfinedTestDispatcher()) {
181             val value by collectLastValue(underTest.revealAmount)
182             underTest.startRevealAmountAnimator(false)
183             assertEquals(1.0f, value)
184             animatorTestRule.advanceTimeBy(500L)
185             assertEquals(0.0f, value)
186         }
187 
188     /**
189      * Asserts that the list of LightRevealEffects satisfies the list of predicates, in order, with
190      * no leftover elements.
191      */
192     private fun List<LightRevealEffect>.assertEffectsMatchPredicates(
193         vararg predicates: (LightRevealEffect) -> Boolean
194     ) {
195         println(this)
196         assertEquals(predicates.size, this.size)
197 
198         assertFalse(
199             zip(predicates) { effect, predicate -> predicate(effect) }.any { matched -> !matched }
200         )
201     }
202 }
203