1 /*
2  * Copyright (C) 2023 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.content.pm.UserInfo
20 import android.testing.TestableLooper
21 import androidx.test.ext.junit.runners.AndroidJUnit4
22 import androidx.test.filters.SmallTest
23 import com.android.keyguard.KeyguardSecurityModel
24 import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
25 import com.android.systemui.R.string.kg_too_many_failed_attempts_countdown
26 import com.android.systemui.R.string.kg_unlock_with_pin_or_fp
27 import com.android.systemui.SysuiTestCase
28 import com.android.systemui.bouncer.data.factory.BouncerMessageFactory
29 import com.android.systemui.bouncer.data.repository.FakeBouncerMessageRepository
30 import com.android.systemui.bouncer.shared.model.BouncerMessageModel
31 import com.android.systemui.bouncer.shared.model.Message
32 import com.android.systemui.coroutines.FlowValue
33 import com.android.systemui.coroutines.collectLastValue
34 import com.android.systemui.flags.FakeFeatureFlags
35 import com.android.systemui.flags.Flags
36 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
37 import com.android.systemui.user.data.repository.FakeUserRepository
38 import com.android.systemui.util.mockito.KotlinArgumentCaptor
39 import com.android.systemui.util.mockito.whenever
40 import com.google.common.truth.Truth.assertThat
41 import kotlinx.coroutines.test.TestScope
42 import kotlinx.coroutines.test.runTest
43 import org.junit.Before
44 import org.junit.Test
45 import org.junit.runner.RunWith
46 import org.mockito.ArgumentMatchers.eq
47 import org.mockito.Mock
48 import org.mockito.Mockito.verify
49 import org.mockito.MockitoAnnotations
50 
51 @SmallTest
52 @TestableLooper.RunWithLooper(setAsMainLooper = true)
53 @RunWith(AndroidJUnit4::class)
54 class BouncerMessageInteractorTest : SysuiTestCase() {
55 
56     @Mock private lateinit var securityModel: KeyguardSecurityModel
57     @Mock private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
58     @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
59     private lateinit var countDownTimerCallback: KotlinArgumentCaptor<CountDownTimerCallback>
60     private lateinit var underTest: BouncerMessageInteractor
61     private lateinit var repository: FakeBouncerMessageRepository
62     private lateinit var userRepository: FakeUserRepository
63     private lateinit var testScope: TestScope
64     private lateinit var bouncerMessage: FlowValue<BouncerMessageModel?>
65 
66     @Before
67     fun setUp() {
68         MockitoAnnotations.initMocks(this)
69         repository = FakeBouncerMessageRepository()
70         userRepository = FakeUserRepository()
71         userRepository.setUserInfos(listOf(PRIMARY_USER))
72         testScope = TestScope()
73         countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java)
74         biometricSettingsRepository = FakeBiometricSettingsRepository()
75 
76         allowTestableLooperAsMainThread()
77         whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
78         biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
79     }
80 
81     suspend fun TestScope.init() {
82         userRepository.setSelectedUserInfo(PRIMARY_USER)
83         val featureFlags = FakeFeatureFlags()
84         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
85         underTest =
86             BouncerMessageInteractor(
87                 repository = repository,
88                 factory = BouncerMessageFactory(biometricSettingsRepository, securityModel),
89                 userRepository = userRepository,
90                 countDownTimerUtil = countDownTimerUtil,
91                 featureFlags = featureFlags
92             )
93         bouncerMessage = collectLastValue(underTest.bouncerMessage)
94     }
95 
96     @Test
97     fun onIncorrectSecurityInput_setsTheBouncerModelInTheRepository() =
98         testScope.runTest {
99             init()
100             underTest.onPrimaryAuthIncorrectAttempt()
101 
102             assertThat(repository.primaryAuthMessage).isNotNull()
103             assertThat(
104                     context.resources.getString(
105                         repository.primaryAuthMessage.value!!.message!!.messageResId!!
106                     )
107                 )
108                 .isEqualTo("Wrong PIN. Try again.")
109         }
110 
111     @Test
112     fun onUserStartsPrimaryAuthInput_clearsAllSetBouncerMessages() =
113         testScope.runTest {
114             init()
115             repository.setCustomMessage(message("not empty"))
116             repository.setFaceAcquisitionMessage(message("not empty"))
117             repository.setFingerprintAcquisitionMessage(message("not empty"))
118             repository.setPrimaryAuthMessage(message("not empty"))
119 
120             underTest.onPrimaryBouncerUserInput()
121 
122             assertThat(repository.customMessage.value).isNull()
123             assertThat(repository.faceAcquisitionMessage.value).isNull()
124             assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
125             assertThat(repository.primaryAuthMessage.value).isNull()
126         }
127 
128     @Test
129     fun onBouncerBeingHidden_clearsAllSetBouncerMessages() =
130         testScope.runTest {
131             init()
132             repository.setCustomMessage(message("not empty"))
133             repository.setFaceAcquisitionMessage(message("not empty"))
134             repository.setFingerprintAcquisitionMessage(message("not empty"))
135             repository.setPrimaryAuthMessage(message("not empty"))
136 
137             underTest.onBouncerBeingHidden()
138 
139             assertThat(repository.customMessage.value).isNull()
140             assertThat(repository.faceAcquisitionMessage.value).isNull()
141             assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
142             assertThat(repository.primaryAuthMessage.value).isNull()
143         }
144 
145     @Test
146     fun setCustomMessage_setsRepositoryValue() =
147         testScope.runTest {
148             init()
149 
150             underTest.setCustomMessage("not empty")
151 
152             val customMessage = repository.customMessage
153             assertThat(customMessage.value!!.message!!.messageResId)
154                 .isEqualTo(kg_unlock_with_pin_or_fp)
155             assertThat(customMessage.value!!.secondaryMessage!!.message).isEqualTo("not empty")
156 
157             underTest.setCustomMessage(null)
158             assertThat(customMessage.value).isNull()
159         }
160 
161     @Test
162     fun setFaceMessage_setsRepositoryValue() =
163         testScope.runTest {
164             init()
165 
166             underTest.setFaceAcquisitionMessage("not empty")
167 
168             val faceAcquisitionMessage = repository.faceAcquisitionMessage
169 
170             assertThat(faceAcquisitionMessage.value!!.message!!.messageResId)
171                 .isEqualTo(kg_unlock_with_pin_or_fp)
172             assertThat(faceAcquisitionMessage.value!!.secondaryMessage!!.message)
173                 .isEqualTo("not empty")
174 
175             underTest.setFaceAcquisitionMessage(null)
176             assertThat(faceAcquisitionMessage.value).isNull()
177         }
178 
179     @Test
180     fun setFingerprintMessage_setsRepositoryValue() =
181         testScope.runTest {
182             init()
183 
184             underTest.setFingerprintAcquisitionMessage("not empty")
185 
186             val fingerprintAcquisitionMessage = repository.fingerprintAcquisitionMessage
187 
188             assertThat(fingerprintAcquisitionMessage.value!!.message!!.messageResId)
189                 .isEqualTo(kg_unlock_with_pin_or_fp)
190             assertThat(fingerprintAcquisitionMessage.value!!.secondaryMessage!!.message)
191                 .isEqualTo("not empty")
192 
193             underTest.setFingerprintAcquisitionMessage(null)
194             assertThat(fingerprintAcquisitionMessage.value).isNull()
195         }
196 
197     @Test
198     fun onPrimaryAuthLockout_startsTimerForSpecifiedNumberOfSeconds() =
199         testScope.runTest {
200             init()
201 
202             underTest.onPrimaryAuthLockedOut(3)
203 
204             verify(countDownTimerUtil)
205                 .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
206 
207             countDownTimerCallback.value.onTick(2000L)
208 
209             val primaryMessage = repository.primaryAuthMessage.value!!.message!!
210             assertThat(primaryMessage.messageResId!!)
211                 .isEqualTo(kg_too_many_failed_attempts_countdown)
212             assertThat(primaryMessage.formatterArgs).isEqualTo(mapOf(Pair("count", 2)))
213         }
214 
215     @Test
216     fun onPrimaryAuthLockout_timerComplete_resetsRepositoryMessages() =
217         testScope.runTest {
218             init()
219             repository.setCustomMessage(message("not empty"))
220             repository.setFaceAcquisitionMessage(message("not empty"))
221             repository.setFingerprintAcquisitionMessage(message("not empty"))
222             repository.setPrimaryAuthMessage(message("not empty"))
223 
224             underTest.onPrimaryAuthLockedOut(3)
225 
226             verify(countDownTimerUtil)
227                 .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
228 
229             countDownTimerCallback.value.onFinish()
230 
231             assertThat(repository.customMessage.value).isNull()
232             assertThat(repository.faceAcquisitionMessage.value).isNull()
233             assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
234             assertThat(repository.primaryAuthMessage.value).isNull()
235         }
236 
237     @Test
238     fun bouncerMessage_hasPriorityOrderOfMessages() =
239         testScope.runTest {
240             init()
241             repository.setBiometricAuthMessage(message("biometric message"))
242             repository.setFaceAcquisitionMessage(message("face acquisition message"))
243             repository.setFingerprintAcquisitionMessage(message("fingerprint acquisition message"))
244             repository.setPrimaryAuthMessage(message("primary auth message"))
245             repository.setAuthFlagsMessage(message("auth flags message"))
246             repository.setBiometricLockedOutMessage(message("biometrics locked out"))
247             repository.setCustomMessage(message("custom message"))
248 
249             assertThat(bouncerMessage()).isEqualTo(message("primary auth message"))
250 
251             repository.setPrimaryAuthMessage(null)
252 
253             assertThat(bouncerMessage()).isEqualTo(message("biometric message"))
254 
255             repository.setBiometricAuthMessage(null)
256 
257             assertThat(bouncerMessage()).isEqualTo(message("fingerprint acquisition message"))
258 
259             repository.setFingerprintAcquisitionMessage(null)
260 
261             assertThat(bouncerMessage()).isEqualTo(message("face acquisition message"))
262 
263             repository.setFaceAcquisitionMessage(null)
264 
265             assertThat(bouncerMessage()).isEqualTo(message("custom message"))
266 
267             repository.setCustomMessage(null)
268 
269             assertThat(bouncerMessage()).isEqualTo(message("auth flags message"))
270 
271             repository.setAuthFlagsMessage(null)
272 
273             assertThat(bouncerMessage()).isEqualTo(message("biometrics locked out"))
274 
275             repository.setBiometricLockedOutMessage(null)
276 
277             // sets the default message if everything else is null
278             assertThat(bouncerMessage()!!.message!!.messageResId)
279                 .isEqualTo(kg_unlock_with_pin_or_fp)
280         }
281 
282     private fun message(value: String): BouncerMessageModel {
283         return BouncerMessageModel(message = Message(message = value))
284     }
285 
286     companion object {
287         private const val PRIMARY_USER_ID = 0
288         private val PRIMARY_USER =
289             UserInfo(
290                 /* id= */ PRIMARY_USER_ID,
291                 /* name= */ "primary user",
292                 /* flags= */ UserInfo.FLAG_PRIMARY
293             )
294     }
295 }
296