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