summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt94
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt204
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt291
6 files changed, 666 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt
new file mode 100644
index 000000000000..7c394a62a5f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication
+
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryModule
+import dagger.Module
+
+@Module(
+ includes =
+ [
+ AuthenticationRepositoryModule::class,
+ ],
+)
+object AuthenticationModule
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
new file mode 100644
index 000000000000..cd195f6f9f97
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication.data.repository
+
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Defines interface for classes that can access authentication-related application state. */
+interface AuthenticationRepository {
+
+ /**
+ * Whether the device is unlocked.
+ *
+ * A device that is not yet unlocked requires unlocking by completing an authentication
+ * challenge according to the current authentication method.
+ *
+ * Note that this state has no real bearing on whether the lock screen is showing or dismissed.
+ */
+ val isUnlocked: StateFlow<Boolean>
+
+ /**
+ * The currently-configured authentication method. This determines how the authentication
+ * challenge is completed in order to unlock an otherwise locked device.
+ */
+ val authenticationMethod: StateFlow<AuthenticationMethodModel>
+
+ /**
+ * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
+ * dismisses once the authentication challenge is completed. For example, completing a biometric
+ * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
+ * lock screen.
+ */
+ val isBypassEnabled: StateFlow<Boolean>
+
+ /** See [isUnlocked]. */
+ fun setUnlocked(isUnlocked: Boolean)
+
+ /** See [authenticationMethod]. */
+ fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel)
+
+ /** See [isBypassEnabled]. */
+ fun setBypassEnabled(isBypassEnabled: Boolean)
+}
+
+class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
+ // TODO(b/280883900): get data from real data sources in SysUI.
+
+ private val _isUnlocked = MutableStateFlow(false)
+ override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
+
+ private val _authenticationMethod =
+ MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.PIN(1234))
+ override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
+ _authenticationMethod.asStateFlow()
+
+ private val _isBypassEnabled = MutableStateFlow(false)
+ override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
+
+ override fun setUnlocked(isUnlocked: Boolean) {
+ _isUnlocked.value = isUnlocked
+ }
+
+ override fun setBypassEnabled(isBypassEnabled: Boolean) {
+ _isBypassEnabled.value = isBypassEnabled
+ }
+
+ override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
+ _authenticationMethod.value = authenticationMethod
+ }
+}
+
+@Module
+interface AuthenticationRepositoryModule {
+ @Binds fun repository(impl: AuthenticationRepositoryImpl): AuthenticationRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
new file mode 100644
index 000000000000..5aea930401d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication.domain.interactor
+
+import com.android.systemui.authentication.data.repository.AuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Hosts application business logic related to authentication. */
+@SysUISingleton
+class AuthenticationInteractor
+@Inject
+constructor(
+ @Application applicationScope: CoroutineScope,
+ private val repository: AuthenticationRepository,
+) {
+ /**
+ * The currently-configured authentication method. This determines how the authentication
+ * challenge is completed in order to unlock an otherwise locked device.
+ */
+ val authenticationMethod: StateFlow<AuthenticationMethodModel> = repository.authenticationMethod
+
+ /**
+ * Whether the device is unlocked.
+ *
+ * A device that is not yet unlocked requires unlocking by completing an authentication
+ * challenge according to the current authentication method.
+ *
+ * Note that this state has no real bearing on whether the lock screen is showing or dismissed.
+ */
+ val isUnlocked: StateFlow<Boolean> =
+ combine(authenticationMethod, repository.isUnlocked) { authMethod, isUnlocked ->
+ isUnlockedWithAuthMethod(
+ isUnlocked = isUnlocked,
+ authMethod = authMethod,
+ )
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue =
+ isUnlockedWithAuthMethod(
+ isUnlocked = repository.isUnlocked.value,
+ authMethod = repository.authenticationMethod.value,
+ )
+ )
+
+ /**
+ * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
+ * dismisses once the authentication challenge is completed. For example, completing a biometric
+ * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
+ * lock screen.
+ */
+ val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
+
+ init {
+ // UNLOCKS WHEN AUTH METHOD REMOVED.
+ //
+ // Unlocks the device if the auth method becomes None.
+ applicationScope.launch {
+ repository.authenticationMethod.collect {
+ if (it is AuthenticationMethodModel.None) {
+ unlockDevice()
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns `true` if the device currently requires authentication before content can be viewed;
+ * `false` if content can be displayed without unlocking first.
+ */
+ fun isAuthenticationRequired(): Boolean {
+ return !isUnlocked.value && authenticationMethod.value.isSecure
+ }
+
+ /**
+ * Unlocks the device, assuming that the authentication challenge has been completed
+ * successfully.
+ */
+ fun unlockDevice() {
+ repository.setUnlocked(true)
+ }
+
+ /**
+ * Locks the device. From now on, the device will remain locked until [authenticate] is called
+ * with the correct input.
+ */
+ fun lockDevice() {
+ repository.setUnlocked(false)
+ }
+
+ /**
+ * Attempts to authenticate the user and unlock the device.
+ *
+ * @param input The input from the user to try to authenticate with. This can be a list of
+ * different things, based on the current authentication method.
+ * @return `true` if the authentication succeeded and the device is now unlocked; `false`
+ * otherwise.
+ */
+ fun authenticate(input: List<Any>): Boolean {
+ val isSuccessful =
+ when (val authMethod = this.authenticationMethod.value) {
+ is AuthenticationMethodModel.PIN -> input.asCode() == authMethod.code
+ is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password
+ is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates
+ else -> true
+ }
+
+ if (isSuccessful) {
+ repository.setUnlocked(true)
+ }
+
+ return isSuccessful
+ }
+
+ /** Triggers a biometric-powered unlock of the device. */
+ fun biometricUnlock() {
+ // TODO(b/280883900): only allow this if the biometric is enabled and there's a match.
+ repository.setUnlocked(true)
+ }
+
+ /** See [authenticationMethod]. */
+ fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
+ repository.setAuthenticationMethod(authenticationMethod)
+ }
+
+ /** See [isBypassEnabled]. */
+ fun toggleBypassEnabled() {
+ repository.setBypassEnabled(!repository.isBypassEnabled.value)
+ }
+
+ companion object {
+ private fun isUnlockedWithAuthMethod(
+ isUnlocked: Boolean,
+ authMethod: AuthenticationMethodModel,
+ ): Boolean {
+ return if (authMethod is AuthenticationMethodModel.None) {
+ true
+ } else {
+ isUnlocked
+ }
+ }
+
+ /**
+ * Returns a PIN code from the given list. It's assumed the given list elements are all
+ * [Int].
+ */
+ private fun List<Any>.asCode(): Int? {
+ if (isEmpty()) {
+ return null
+ }
+
+ var code = 0
+ map { it as Int }.forEach { integer -> code = code * 10 + integer }
+
+ return code
+ }
+
+ /**
+ * Returns a password from the given list. It's assumed the given list elements are all
+ * [Char].
+ */
+ private fun List<Any>.asPassword(): String {
+ val anyList = this
+ return buildString { anyList.forEach { append(it as Char) } }
+ }
+
+ /**
+ * Returns a list of [AuthenticationMethodModel.Pattern.PatternCoordinate] from the given
+ * list. It's assumed the given list elements are all
+ * [AuthenticationMethodModel.Pattern.PatternCoordinate].
+ */
+ private fun List<Any>.asPattern():
+ List<AuthenticationMethodModel.Pattern.PatternCoordinate> {
+ val anyList = this
+ return buildList {
+ anyList.forEach { add(it as AuthenticationMethodModel.Pattern.PatternCoordinate) }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
new file mode 100644
index 000000000000..83250b638424
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication.shared.model
+
+/** Enumerates all known authentication methods. */
+sealed class AuthenticationMethodModel(
+ /**
+ * Whether the authentication method is considered to be "secure".
+ *
+ * "Secure" authentication methods require authentication to unlock the device. Non-secure auth
+ * methods simply require user dismissal.
+ */
+ open val isSecure: Boolean,
+) {
+ /** There is no authentication method on the device. We shouldn't even show the lock screen. */
+ object None : AuthenticationMethodModel(isSecure = false)
+
+ /** The most basic authentication method. The lock screen can be swiped away when displayed. */
+ object Swipe : AuthenticationMethodModel(isSecure = false)
+
+ data class PIN(val code: Int) : AuthenticationMethodModel(isSecure = true)
+
+ data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
+
+ data class Pattern(val coordinates: List<PatternCoordinate>) :
+ AuthenticationMethodModel(isSecure = true) {
+
+ data class PatternCoordinate(
+ val x: Int,
+ val y: Int,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index dff2c0e91f1f..b98b23e2e220 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -32,6 +32,7 @@ import com.android.systemui.accessibility.AccessibilityModule;
import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule;
import com.android.systemui.appops.dagger.AppOpsModule;
import com.android.systemui.assist.AssistModule;
+import com.android.systemui.authentication.AuthenticationModule;
import com.android.systemui.biometrics.AlternateUdfpsTouchProvider;
import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
@@ -146,6 +147,7 @@ import javax.inject.Named;
AccessibilityRepositoryModule.class,
AppOpsModule.class,
AssistModule.class,
+ AuthenticationModule.class,
BiometricsModule.class,
BouncerViewModule.class,
ClipboardOverlayModule.class,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
new file mode 100644
index 000000000000..2e62bebfe3f1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.authentication.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.AuthenticationRepository
+import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class AuthenticationInteractorTest : SysuiTestCase() {
+
+ private val testScope = TestScope()
+ private val repository: AuthenticationRepository = AuthenticationRepositoryImpl()
+ private val underTest =
+ AuthenticationInteractor(
+ applicationScope = testScope.backgroundScope,
+ repository = repository,
+ )
+
+ @Test
+ fun authMethod() =
+ testScope.runTest {
+ val authMethod by collectLastValue(underTest.authenticationMethod)
+ assertThat(authMethod).isEqualTo(AuthenticationMethodModel.PIN(1234))
+
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+ assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password("password"))
+ }
+
+ @Test
+ fun isUnlocked_whenAuthMethodIsNone_isTrue() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ assertThat(isUnlocked).isFalse()
+
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+ assertThat(isUnlocked).isTrue()
+ }
+
+ @Test
+ fun unlockDevice() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ assertThat(isUnlocked).isFalse()
+
+ underTest.unlockDevice()
+ runCurrent()
+
+ assertThat(isUnlocked).isTrue()
+ }
+
+ @Test
+ fun biometricUnlock() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ assertThat(isUnlocked).isFalse()
+
+ underTest.biometricUnlock()
+ runCurrent()
+
+ assertThat(isUnlocked).isTrue()
+ }
+
+ @Test
+ fun toggleBypassEnabled() =
+ testScope.runTest {
+ val isBypassEnabled by collectLastValue(underTest.isBypassEnabled)
+ assertThat(isBypassEnabled).isFalse()
+
+ underTest.toggleBypassEnabled()
+ assertThat(isBypassEnabled).isTrue()
+
+ underTest.toggleBypassEnabled()
+ assertThat(isBypassEnabled).isFalse()
+ }
+
+ @Test
+ fun isAuthenticationRequired_lockedAndSecured_true() =
+ testScope.runTest {
+ underTest.lockDevice()
+ runCurrent()
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+
+ assertThat(underTest.isAuthenticationRequired()).isTrue()
+ }
+
+ @Test
+ fun isAuthenticationRequired_lockedAndNotSecured_false() =
+ testScope.runTest {
+ underTest.lockDevice()
+ runCurrent()
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+ assertThat(underTest.isAuthenticationRequired()).isFalse()
+ }
+
+ @Test
+ fun isAuthenticationRequired_unlockedAndSecured_false() =
+ testScope.runTest {
+ underTest.unlockDevice()
+ runCurrent()
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+
+ assertThat(underTest.isAuthenticationRequired()).isFalse()
+ }
+
+ @Test
+ fun isAuthenticationRequired_unlockedAndNotSecured_false() =
+ testScope.runTest {
+ underTest.unlockDevice()
+ runCurrent()
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
+
+ assertThat(underTest.isAuthenticationRequired()).isFalse()
+ }
+
+ @Test
+ fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+ assertThat(isUnlocked).isFalse()
+
+ assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
+ assertThat(isUnlocked).isTrue()
+ }
+
+ @Test
+ fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+ assertThat(isUnlocked).isFalse()
+
+ assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
+ assertThat(isUnlocked).isFalse()
+ }
+
+ @Test
+ fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+ assertThat(isUnlocked).isFalse()
+
+ assertThat(underTest.authenticate("password".toList())).isTrue()
+ assertThat(isUnlocked).isTrue()
+ }
+
+ @Test
+ fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
+ assertThat(isUnlocked).isFalse()
+
+ assertThat(underTest.authenticate("alohomora".toList())).isFalse()
+ assertThat(isUnlocked).isFalse()
+ }
+
+ @Test
+ fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ underTest.setAuthenticationMethod(
+ AuthenticationMethodModel.Pattern(
+ listOf(
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 0,
+ y = 0,
+ ),
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 0,
+ y = 1,
+ ),
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 0,
+ y = 2,
+ ),
+ )
+ )
+ )
+ assertThat(isUnlocked).isFalse()
+
+ assertThat(
+ underTest.authenticate(
+ listOf(
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 0,
+ y = 0,
+ ),
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 0,
+ y = 1,
+ ),
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 0,
+ y = 2,
+ ),
+ )
+ )
+ )
+ .isTrue()
+ assertThat(isUnlocked).isTrue()
+ }
+
+ @Test
+ fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ underTest.setAuthenticationMethod(
+ AuthenticationMethodModel.Pattern(
+ listOf(
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 0,
+ y = 0,
+ ),
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 0,
+ y = 1,
+ ),
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 0,
+ y = 2,
+ ),
+ )
+ )
+ )
+ assertThat(isUnlocked).isFalse()
+
+ assertThat(
+ underTest.authenticate(
+ listOf(
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 2,
+ y = 0,
+ ),
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 2,
+ y = 1,
+ ),
+ AuthenticationMethodModel.Pattern.PatternCoordinate(
+ x = 2,
+ y = 2,
+ ),
+ )
+ )
+ )
+ .isFalse()
+ assertThat(isUnlocked).isFalse()
+ }
+
+ @Test
+ fun unlocksDevice_whenAuthMethodBecomesNone() =
+ testScope.runTest {
+ val isUnlocked by collectLastValue(underTest.isUnlocked)
+ assertThat(isUnlocked).isFalse()
+
+ repository.setAuthenticationMethod(AuthenticationMethodModel.None)
+
+ assertThat(isUnlocked).isTrue()
+ }
+}