Merge "[flexiglass] Auto-confirm, isPatternVisible are "refreshing flow"" into udc-qpr-dev

This commit is contained in:
Ale Nijamkin
2023-07-22 02:05:50 +00:00
committed by Android (Google) Code Review
5 changed files with 210 additions and 66 deletions

View File

@@ -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,
)
}
}
}

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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,
),
)
}
}