Loading packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +36 −34 Original line number Diff line number Diff line Loading @@ -228,6 +228,7 @@ internal fun PatternBouncer( } } ) { if (isAnimationEnabled) { // Draw lines between dots. selectedDots.forEachIndexed { index, dot -> if (index > 0) { Loading Loading @@ -267,6 +268,7 @@ internal fun PatternBouncer( ) } } } // Draw each dot on the grid. dots.forEach { dot -> Loading packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +55 −30 Original line number Diff line number Diff line Loading @@ -14,8 +14,6 @@ * limitations under the License. */ @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.authentication.data.repository import com.android.internal.widget.LockPatternChecker Loading @@ -29,6 +27,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock import dagger.Binds import dagger.Module Loading @@ -38,16 +37,14 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** Defines interface for classes that can access authentication-related application state. */ Loading Loading @@ -156,31 +153,17 @@ constructor( } override val isAutoConfirmEnabled: StateFlow<Boolean> = userRepository.selectedUserInfo .map { it.id } .flatMapLatest { userId -> flow { emit(lockPatternUtils.isAutoPinConfirmEnabled(userId)) } .flowOn(backgroundDispatcher) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), refreshingFlow( initialValue = false, getFreshValue = lockPatternUtils::isAutoPinConfirmEnabled, ) override val hintedPinLength: Int = 6 override val isPatternVisible: StateFlow<Boolean> = userRepository.selectedUserInfo .map { it.id } .flatMapLatest { userId -> flow { emit(lockPatternUtils.isVisiblePatternEnabled(userId)) } .flowOn(backgroundDispatcher) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), refreshingFlow( initialValue = true, getFreshValue = lockPatternUtils::isVisiblePatternEnabled, ) private val _throttling = MutableStateFlow(AuthenticationThrottlingModel()) Loading Loading @@ -276,6 +259,48 @@ constructor( ) } } /** * Returns a [StateFlow] that's automatically kept fresh. The passed-in [getFreshValue] is * invoked on a background thread every time the selected user is changed and every time a new * downstream subscriber is added to the flow. * * Initially, the flow will emit [initialValue] while it refreshes itself in the background by * invoking the [getFreshValue] function and emitting the fresh value when that's done. * * Every time the selected user is changed, the flow will re-invoke [getFreshValue] and emit the * new value. * * Every time a new downstream subscriber is added to the flow it first receives the latest * cached value that's either the [initialValue] or the latest previously fetched value. In * addition, adding a new downstream subscriber also triggers another [getFreshValue] call and a * subsequent emission of that newest value. */ private fun <T> refreshingFlow( initialValue: T, getFreshValue: suspend (selectedUserId: Int) -> T, ): StateFlow<T> { val flow = MutableStateFlow(initialValue) applicationScope.launch { combine( // Emits a value initially and every time the selected user is changed. userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(), // Emits a value only when the number of downstream subscribers of this flow // increases. flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current) -> current > previous }, ) { selectedUserId, _ -> selectedUserId } .collect { selectedUserId -> flow.value = withContext(backgroundDispatcher) { getFreshValue(selectedUserId) } } } return flow.asStateFlow() } } @Module Loading packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +3 −1 Original line number Diff line number Diff line Loading @@ -104,7 +104,9 @@ constructor( } .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, // Make sure this is kept as WhileSubscribed or we can run into a bug where the // downstream continues to receive old/stale/cached values. started = SharingStarted.WhileSubscribed(), initialValue = null, ) Loading packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +3 −1 Original line number Diff line number Diff line Loading @@ -60,7 +60,9 @@ class PinBouncerViewModel( } .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, // Make sure this is kept as WhileSubscribed or we can run into a bug where the // downstream continues to receive old/stale/cached values. started = SharingStarted.WhileSubscribed(), initialValue = ActionButtonAppearance.Hidden, ) Loading packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt 0 → 100644 +113 −0 Original line number Diff line number Diff line /* * Copyright 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. */ @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.authentication.data.repository import android.content.pm.UserInfo import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.keyguard.KeyguardSecurityModel import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.scene.SceneTestUtils import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.MockitoAnnotations @SmallTest @RunWith(JUnit4::class) class AuthenticationRepositoryTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils private val testUtils = SceneTestUtils(this) private val testScope = testUtils.testScope private val userRepository = FakeUserRepository() private lateinit var underTest: AuthenticationRepository @Before fun setUp() { MockitoAnnotations.initMocks(this) userRepository.setUserInfos(USER_INFOS) runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) } underTest = AuthenticationRepositoryImpl( applicationScope = testScope.backgroundScope, getSecurityMode = { KeyguardSecurityModel.SecurityMode.PIN }, backgroundDispatcher = testUtils.testDispatcher, userRepository = userRepository, keyguardRepository = testUtils.keyguardRepository, lockPatternUtils = lockPatternUtils, ) } @Test fun isAutoConfirmEnabled() = testScope.runTest { whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[0].id)).thenReturn(true) whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[1].id)).thenReturn(false) val values by collectValues(underTest.isAutoConfirmEnabled) assertThat(values.first()).isFalse() assertThat(values.last()).isTrue() userRepository.setSelectedUserInfo(USER_INFOS[1]) assertThat(values.last()).isFalse() } @Test fun isPatternVisible() = testScope.runTest { whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[0].id)).thenReturn(false) whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[1].id)).thenReturn(true) val values by collectValues(underTest.isPatternVisible) assertThat(values.first()).isTrue() assertThat(values.last()).isFalse() userRepository.setSelectedUserInfo(USER_INFOS[1]) assertThat(values.last()).isTrue() } companion object { private val USER_INFOS = listOf( UserInfo( /* id= */ 100, /* name= */ "First user", /* flags= */ 0, ), UserInfo( /* id= */ 101, /* name= */ "Second user", /* flags= */ 0, ), ) } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +36 −34 Original line number Diff line number Diff line Loading @@ -228,6 +228,7 @@ internal fun PatternBouncer( } } ) { if (isAnimationEnabled) { // Draw lines between dots. selectedDots.forEachIndexed { index, dot -> if (index > 0) { Loading Loading @@ -267,6 +268,7 @@ internal fun PatternBouncer( ) } } } // Draw each dot on the grid. dots.forEach { dot -> Loading
packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +55 −30 Original line number Diff line number Diff line Loading @@ -14,8 +14,6 @@ * limitations under the License. */ @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.authentication.data.repository import com.android.internal.widget.LockPatternChecker Loading @@ -29,6 +27,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock import dagger.Binds import dagger.Module Loading @@ -38,16 +37,14 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** Defines interface for classes that can access authentication-related application state. */ Loading Loading @@ -156,31 +153,17 @@ constructor( } override val isAutoConfirmEnabled: StateFlow<Boolean> = userRepository.selectedUserInfo .map { it.id } .flatMapLatest { userId -> flow { emit(lockPatternUtils.isAutoPinConfirmEnabled(userId)) } .flowOn(backgroundDispatcher) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), refreshingFlow( initialValue = false, getFreshValue = lockPatternUtils::isAutoPinConfirmEnabled, ) override val hintedPinLength: Int = 6 override val isPatternVisible: StateFlow<Boolean> = userRepository.selectedUserInfo .map { it.id } .flatMapLatest { userId -> flow { emit(lockPatternUtils.isVisiblePatternEnabled(userId)) } .flowOn(backgroundDispatcher) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), refreshingFlow( initialValue = true, getFreshValue = lockPatternUtils::isVisiblePatternEnabled, ) private val _throttling = MutableStateFlow(AuthenticationThrottlingModel()) Loading Loading @@ -276,6 +259,48 @@ constructor( ) } } /** * Returns a [StateFlow] that's automatically kept fresh. The passed-in [getFreshValue] is * invoked on a background thread every time the selected user is changed and every time a new * downstream subscriber is added to the flow. * * Initially, the flow will emit [initialValue] while it refreshes itself in the background by * invoking the [getFreshValue] function and emitting the fresh value when that's done. * * Every time the selected user is changed, the flow will re-invoke [getFreshValue] and emit the * new value. * * Every time a new downstream subscriber is added to the flow it first receives the latest * cached value that's either the [initialValue] or the latest previously fetched value. In * addition, adding a new downstream subscriber also triggers another [getFreshValue] call and a * subsequent emission of that newest value. */ private fun <T> refreshingFlow( initialValue: T, getFreshValue: suspend (selectedUserId: Int) -> T, ): StateFlow<T> { val flow = MutableStateFlow(initialValue) applicationScope.launch { combine( // Emits a value initially and every time the selected user is changed. userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(), // Emits a value only when the number of downstream subscribers of this flow // increases. flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current) -> current > previous }, ) { selectedUserId, _ -> selectedUserId } .collect { selectedUserId -> flow.value = withContext(backgroundDispatcher) { getFreshValue(selectedUserId) } } } return flow.asStateFlow() } } @Module Loading
packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +3 −1 Original line number Diff line number Diff line Loading @@ -104,7 +104,9 @@ constructor( } .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, // Make sure this is kept as WhileSubscribed or we can run into a bug where the // downstream continues to receive old/stale/cached values. started = SharingStarted.WhileSubscribed(), initialValue = null, ) Loading
packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +3 −1 Original line number Diff line number Diff line Loading @@ -60,7 +60,9 @@ class PinBouncerViewModel( } .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, // Make sure this is kept as WhileSubscribed or we can run into a bug where the // downstream continues to receive old/stale/cached values. started = SharingStarted.WhileSubscribed(), initialValue = ActionButtonAppearance.Hidden, ) Loading
packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt 0 → 100644 +113 −0 Original line number Diff line number Diff line /* * Copyright 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. */ @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.authentication.data.repository import android.content.pm.UserInfo import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.keyguard.KeyguardSecurityModel import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.scene.SceneTestUtils import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.MockitoAnnotations @SmallTest @RunWith(JUnit4::class) class AuthenticationRepositoryTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils private val testUtils = SceneTestUtils(this) private val testScope = testUtils.testScope private val userRepository = FakeUserRepository() private lateinit var underTest: AuthenticationRepository @Before fun setUp() { MockitoAnnotations.initMocks(this) userRepository.setUserInfos(USER_INFOS) runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) } underTest = AuthenticationRepositoryImpl( applicationScope = testScope.backgroundScope, getSecurityMode = { KeyguardSecurityModel.SecurityMode.PIN }, backgroundDispatcher = testUtils.testDispatcher, userRepository = userRepository, keyguardRepository = testUtils.keyguardRepository, lockPatternUtils = lockPatternUtils, ) } @Test fun isAutoConfirmEnabled() = testScope.runTest { whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[0].id)).thenReturn(true) whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[1].id)).thenReturn(false) val values by collectValues(underTest.isAutoConfirmEnabled) assertThat(values.first()).isFalse() assertThat(values.last()).isTrue() userRepository.setSelectedUserInfo(USER_INFOS[1]) assertThat(values.last()).isFalse() } @Test fun isPatternVisible() = testScope.runTest { whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[0].id)).thenReturn(false) whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[1].id)).thenReturn(true) val values by collectValues(underTest.isPatternVisible) assertThat(values.first()).isTrue() assertThat(values.last()).isFalse() userRepository.setSelectedUserInfo(USER_INFOS[1]) assertThat(values.last()).isTrue() } companion object { private val USER_INFOS = listOf( UserInfo( /* id= */ 100, /* name= */ "First user", /* flags= */ 0, ), UserInfo( /* id= */ 101, /* name= */ "Second user", /* flags= */ 0, ), ) } }