summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt70
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt85
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt113
5 files changed, 210 insertions, 66 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 63a3eca4695d..64227b8c5f2a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -228,43 +228,45 @@ internal fun PatternBouncer(
}
}
) {
- // Draw lines between dots.
- selectedDots.forEachIndexed { index, dot ->
- if (index > 0) {
- val previousDot = selectedDots[index - 1]
- val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
- val startLerp = 1 - lineFadeOutAnimationProgress
- val from = pixelOffset(previousDot, spacing, verticalOffset)
- val to = pixelOffset(dot, spacing, verticalOffset)
- val lerpedFrom =
- Offset(
- x = from.x + (to.x - from.x) * startLerp,
- y = from.y + (to.y - from.y) * startLerp,
+ if (isAnimationEnabled) {
+ // Draw lines between dots.
+ selectedDots.forEachIndexed { index, dot ->
+ if (index > 0) {
+ val previousDot = selectedDots[index - 1]
+ val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
+ val startLerp = 1 - lineFadeOutAnimationProgress
+ val from = pixelOffset(previousDot, spacing, verticalOffset)
+ val to = pixelOffset(dot, spacing, verticalOffset)
+ val lerpedFrom =
+ Offset(
+ x = from.x + (to.x - from.x) * startLerp,
+ y = from.y + (to.y - from.y) * startLerp,
+ )
+ drawLine(
+ start = lerpedFrom,
+ end = to,
+ cap = StrokeCap.Round,
+ alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
+ color = lineColor,
+ strokeWidth = lineStrokeWidth,
)
- drawLine(
- start = lerpedFrom,
- end = to,
- cap = StrokeCap.Round,
- alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
- color = lineColor,
- strokeWidth = lineStrokeWidth,
- )
+ }
}
- }
- // Draw the line between the most recently-selected dot and the input pointer position.
- inputPosition?.let { lineEnd ->
- currentDot?.let { dot ->
- val from = pixelOffset(dot, spacing, verticalOffset)
- val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
- drawLine(
- start = from,
- end = lineEnd,
- cap = StrokeCap.Round,
- alpha = lineAlpha(spacing, lineLength),
- color = lineColor,
- strokeWidth = lineStrokeWidth,
- )
+ // Draw the line between the most recently-selected dot and the input pointer position.
+ inputPosition?.let { lineEnd ->
+ currentDot?.let { dot ->
+ val from = pixelOffset(dot, spacing, verticalOffset)
+ val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
+ drawLine(
+ start = from,
+ end = lineEnd,
+ cap = StrokeCap.Round,
+ alpha = lineAlpha(spacing, lineLength),
+ color = lineColor,
+ strokeWidth = lineStrokeWidth,
+ )
+ }
}
}
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
index a9779663cc7c..deb3d035d753 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -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
@@ -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
@@ -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. */
@@ -156,32 +153,18 @@ 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(),
- initialValue = false,
- )
+ 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(),
- initialValue = true,
- )
+ refreshingFlow(
+ initialValue = true,
+ getFreshValue = lockPatternUtils::isVisiblePatternEnabled,
+ )
private val _throttling = MutableStateFlow(AuthenticationThrottlingModel())
override val throttling: StateFlow<AuthenticationThrottlingModel> = _throttling.asStateFlow()
@@ -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
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
index b482977bde67..d4371bf30e0e 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -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,
)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 1b14acc7fabc..38fb8b968775 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -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,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
new file mode 100644
index 000000000000..005697044c0f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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,
+ ),
+ )
+ }
+}