[flexiglass] Bouncer throttling - composables. am: 33ddd9ab17 am: 690c782e22
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23303130 Change-Id: I44fd23b56b52a9ca58b070e53f6d3d3deea8bfa6 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
committed by
Automerger Merge Worker
commit
e84f9e3ef9
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package com.android.systemui.scene.ui.composable
|
package com.android.systemui.scene.ui.composable
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.android.systemui.bouncer.ui.composable.BouncerScene
|
import com.android.systemui.bouncer.ui.composable.BouncerScene
|
||||||
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
|
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
@@ -28,6 +29,7 @@ import com.android.systemui.scene.shared.model.Scene
|
|||||||
import com.android.systemui.scene.shared.model.SceneContainerNames
|
import com.android.systemui.scene.shared.model.SceneContainerNames
|
||||||
import com.android.systemui.shade.ui.composable.ShadeScene
|
import com.android.systemui.shade.ui.composable.ShadeScene
|
||||||
import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
|
import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
|
||||||
|
import com.android.systemui.statusbar.phone.SystemUIDialog
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
@@ -57,6 +59,7 @@ object SceneModule {
|
|||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
@Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
|
@Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
|
||||||
fun bouncerScene(
|
fun bouncerScene(
|
||||||
|
@Application context: Context,
|
||||||
viewModelFactory: BouncerViewModel.Factory,
|
viewModelFactory: BouncerViewModel.Factory,
|
||||||
): BouncerScene {
|
): BouncerScene {
|
||||||
return BouncerScene(
|
return BouncerScene(
|
||||||
@@ -64,6 +67,7 @@ object SceneModule {
|
|||||||
viewModelFactory.create(
|
viewModelFactory.create(
|
||||||
containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
|
containerName = SceneContainerNames.SYSTEM_UI_DEFAULT,
|
||||||
),
|
),
|
||||||
|
dialogFactory = { SystemUIDialog(context) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,16 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
package com.android.systemui.bouncer.ui.composable
|
package com.android.systemui.bouncer.ui.composable
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -26,15 +33,20 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.android.systemui.R
|
||||||
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
|
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
|
||||||
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
|
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
|
||||||
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
|
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
|
||||||
@@ -51,6 +63,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
/** The bouncer scene displays authentication challenges like PIN, password, or pattern. */
|
/** The bouncer scene displays authentication challenges like PIN, password, or pattern. */
|
||||||
class BouncerScene(
|
class BouncerScene(
|
||||||
private val viewModel: BouncerViewModel,
|
private val viewModel: BouncerViewModel,
|
||||||
|
private val dialogFactory: () -> AlertDialog,
|
||||||
) : ComposableScene {
|
) : ComposableScene {
|
||||||
override val key = SceneKey.Bouncer
|
override val key = SceneKey.Bouncer
|
||||||
|
|
||||||
@@ -68,16 +81,19 @@ class BouncerScene(
|
|||||||
override fun Content(
|
override fun Content(
|
||||||
containerName: String,
|
containerName: String,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) = BouncerScene(viewModel, modifier)
|
) = BouncerScene(viewModel, dialogFactory, modifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BouncerScene(
|
private fun BouncerScene(
|
||||||
viewModel: BouncerViewModel,
|
viewModel: BouncerViewModel,
|
||||||
|
dialogFactory: () -> AlertDialog,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val message: String by viewModel.message.collectAsState()
|
val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
|
||||||
val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState()
|
val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState()
|
||||||
|
val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
|
||||||
|
var dialog: Dialog? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -88,9 +104,10 @@ private fun BouncerScene(
|
|||||||
Crossfade(
|
Crossfade(
|
||||||
targetState = message,
|
targetState = message,
|
||||||
label = "Bouncer message",
|
label = "Bouncer message",
|
||||||
) {
|
animationSpec = if (message.isUpdateAnimated) tween() else snap(),
|
||||||
|
) { message ->
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = message.text,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
@@ -132,5 +149,26 @@ private fun BouncerScene(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dialogMessage != null) {
|
||||||
|
if (dialog == null) {
|
||||||
|
dialog =
|
||||||
|
dialogFactory().apply {
|
||||||
|
setMessage(dialogMessage)
|
||||||
|
setButton(
|
||||||
|
DialogInterface.BUTTON_NEUTRAL,
|
||||||
|
context.getString(R.string.ok),
|
||||||
|
) { _, _ ->
|
||||||
|
viewModel.onThrottlingDialogDismissed()
|
||||||
|
}
|
||||||
|
setCancelable(false)
|
||||||
|
setCanceledOnTouchOutside(false)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialog?.dismiss()
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ internal fun PasswordBouncer(
|
|||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val password: String by viewModel.password.collectAsState()
|
val password: String by viewModel.password.collectAsState()
|
||||||
|
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// When the UI comes up, request focus on the TextField to bring up the software keyboard.
|
// When the UI comes up, request focus on the TextField to bring up the software keyboard.
|
||||||
@@ -71,6 +72,7 @@ internal fun PasswordBouncer(
|
|||||||
TextField(
|
TextField(
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = viewModel::onPasswordInputChanged,
|
onValueChange = viewModel::onPasswordInputChanged,
|
||||||
|
enabled = isInputEnabled,
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
|
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.android.internal.R
|
import com.android.internal.R
|
||||||
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
|
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
|
||||||
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
|
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
|
||||||
|
import com.android.systemui.compose.modifiers.thenIf
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
@@ -82,6 +83,8 @@ internal fun PatternBouncer(
|
|||||||
val currentDot: PatternDotViewModel? by viewModel.currentDot.collectAsState()
|
val currentDot: PatternDotViewModel? by viewModel.currentDot.collectAsState()
|
||||||
// The dots selected so far, if the user is currently dragging.
|
// The dots selected so far, if the user is currently dragging.
|
||||||
val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState()
|
val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState()
|
||||||
|
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
|
||||||
|
val isAnimationEnabled: Boolean by viewModel.isPatternVisible.collectAsState()
|
||||||
|
|
||||||
// Map of animatables for the scale of each dot, keyed by dot.
|
// Map of animatables for the scale of each dot, keyed by dot.
|
||||||
val dotScalingAnimatables = remember(dots) { dots.associateWith { Animatable(1f) } }
|
val dotScalingAnimatables = remember(dots) { dots.associateWith { Animatable(1f) } }
|
||||||
@@ -96,16 +99,24 @@ internal fun PatternBouncer(
|
|||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|
||||||
// When the current dot is changed, we need to update our animations.
|
// When the current dot is changed, we need to update our animations.
|
||||||
LaunchedEffect(currentDot) {
|
LaunchedEffect(currentDot, isAnimationEnabled) {
|
||||||
view.performHapticFeedback(
|
view.performHapticFeedback(
|
||||||
HapticFeedbackConstants.VIRTUAL_KEY,
|
HapticFeedbackConstants.VIRTUAL_KEY,
|
||||||
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
|
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Make sure that the current dot is scaled up while the other dots are scaled back down.
|
if (!isAnimationEnabled) {
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that the current dot is scaled up while the other dots are scaled back
|
||||||
|
// down.
|
||||||
dotScalingAnimatables.entries.forEach { (dot, animatable) ->
|
dotScalingAnimatables.entries.forEach { (dot, animatable) ->
|
||||||
val isSelected = dot == currentDot
|
val isSelected = dot == currentDot
|
||||||
launch {
|
// Launch using the longer-lived scope because we want these animations to proceed to
|
||||||
|
// completion even if the LaunchedEffect is canceled because its key objects have
|
||||||
|
// changed.
|
||||||
|
scope.launch {
|
||||||
animatable.animateTo(if (isSelected) 2f else 1f)
|
animatable.animateTo(if (isSelected) 2f else 1f)
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
animatable.animateTo(1f)
|
animatable.animateTo(1f)
|
||||||
@@ -116,14 +127,18 @@ internal fun PatternBouncer(
|
|||||||
selectedDots.forEach { dot ->
|
selectedDots.forEach { dot ->
|
||||||
lineFadeOutAnimatables[dot]?.let { line ->
|
lineFadeOutAnimatables[dot]?.let { line ->
|
||||||
if (!line.isRunning) {
|
if (!line.isRunning) {
|
||||||
|
// Launch using the longer-lived scope because we want these animations to
|
||||||
|
// proceed to completion even if the LaunchedEffect is canceled because its key
|
||||||
|
// objects have changed.
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (dot == currentDot) {
|
if (dot == currentDot) {
|
||||||
// Reset the fade-out animation for the current dot. When the current
|
// Reset the fade-out animation for the current dot. When the
|
||||||
// dot is switched, this entire code block runs again for the newly
|
// current dot is switched, this entire code block runs again for
|
||||||
// selected dot.
|
// the newly selected dot.
|
||||||
line.snapTo(1f)
|
line.snapTo(1f)
|
||||||
} else {
|
} else {
|
||||||
// For all non-current dots, make sure that the lines are fading out.
|
// For all non-current dots, make sure that the lines are fading
|
||||||
|
// out.
|
||||||
line.animateTo(
|
line.animateTo(
|
||||||
targetValue = 0f,
|
targetValue = 0f,
|
||||||
animationSpec =
|
animationSpec =
|
||||||
@@ -148,27 +163,34 @@ internal fun PatternBouncer(
|
|||||||
// when it leaves the bounds of the dot grid.
|
// when it leaves the bounds of the dot grid.
|
||||||
.clipToBounds()
|
.clipToBounds()
|
||||||
.onSizeChanged { containerSize = it }
|
.onSizeChanged { containerSize = it }
|
||||||
.pointerInput(Unit) {
|
.thenIf(isInputEnabled) {
|
||||||
detectDragGestures(
|
Modifier.pointerInput(Unit) {
|
||||||
onDragStart = { start ->
|
detectDragGestures(
|
||||||
inputPosition = start
|
onDragStart = { start ->
|
||||||
viewModel.onDragStart()
|
inputPosition = start
|
||||||
},
|
viewModel.onDragStart()
|
||||||
onDragEnd = {
|
},
|
||||||
inputPosition = null
|
onDragEnd = {
|
||||||
lineFadeOutAnimatables.values.forEach { animatable ->
|
inputPosition = null
|
||||||
scope.launch { animatable.animateTo(1f) }
|
if (isAnimationEnabled) {
|
||||||
}
|
lineFadeOutAnimatables.values.forEach { animatable ->
|
||||||
viewModel.onDragEnd()
|
// Launch using the longer-lived scope because we want these
|
||||||
},
|
// animations to proceed to completion even if the surrounding
|
||||||
) { change, _ ->
|
// scope is canceled.
|
||||||
inputPosition = change.position
|
scope.launch { animatable.animateTo(1f) }
|
||||||
viewModel.onDrag(
|
}
|
||||||
xPx = change.position.x,
|
}
|
||||||
yPx = change.position.y,
|
viewModel.onDragEnd()
|
||||||
containerSizePx = containerSize.width,
|
},
|
||||||
verticalOffsetPx = verticalOffset,
|
) { change, _ ->
|
||||||
)
|
inputPosition = change.position
|
||||||
|
viewModel.onDrag(
|
||||||
|
xPx = change.position.x,
|
||||||
|
yPx = change.position.y,
|
||||||
|
containerSizePx = containerSize.width,
|
||||||
|
verticalOffsetPx = verticalOffset,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
|
|||||||
import com.android.systemui.common.shared.model.ContentDescription
|
import com.android.systemui.common.shared.model.ContentDescription
|
||||||
import com.android.systemui.common.shared.model.Icon
|
import com.android.systemui.common.shared.model.Icon
|
||||||
import com.android.systemui.common.ui.compose.Icon
|
import com.android.systemui.common.ui.compose.Icon
|
||||||
|
import com.android.systemui.compose.modifiers.thenIf
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -75,6 +76,7 @@ internal fun PinBouncer(
|
|||||||
|
|
||||||
// The length of the PIN input received so far, so we know how many dots to render.
|
// The length of the PIN input received so far, so we know how many dots to render.
|
||||||
val pinLength: Pair<Int, Int> by viewModel.pinLengths.collectAsState()
|
val pinLength: Pair<Int, Int> by viewModel.pinLengths.collectAsState()
|
||||||
|
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -116,6 +118,7 @@ internal fun PinBouncer(
|
|||||||
val digit = index + 1
|
val digit = index + 1
|
||||||
PinButton(
|
PinButton(
|
||||||
onClicked = { viewModel.onPinButtonClicked(digit) },
|
onClicked = { viewModel.onPinButtonClicked(digit) },
|
||||||
|
isEnabled = isInputEnabled,
|
||||||
) { contentColor ->
|
) { contentColor ->
|
||||||
PinDigit(digit, contentColor)
|
PinDigit(digit, contentColor)
|
||||||
}
|
}
|
||||||
@@ -124,6 +127,7 @@ internal fun PinBouncer(
|
|||||||
PinButton(
|
PinButton(
|
||||||
onClicked = { viewModel.onBackspaceButtonClicked() },
|
onClicked = { viewModel.onBackspaceButtonClicked() },
|
||||||
onLongPressed = { viewModel.onBackspaceButtonLongPressed() },
|
onLongPressed = { viewModel.onBackspaceButtonLongPressed() },
|
||||||
|
isEnabled = isInputEnabled,
|
||||||
isHighlighted = true,
|
isHighlighted = true,
|
||||||
) { contentColor ->
|
) { contentColor ->
|
||||||
PinIcon(
|
PinIcon(
|
||||||
@@ -138,6 +142,7 @@ internal fun PinBouncer(
|
|||||||
|
|
||||||
PinButton(
|
PinButton(
|
||||||
onClicked = { viewModel.onPinButtonClicked(0) },
|
onClicked = { viewModel.onPinButtonClicked(0) },
|
||||||
|
isEnabled = isInputEnabled,
|
||||||
) { contentColor ->
|
) { contentColor ->
|
||||||
PinDigit(0, contentColor)
|
PinDigit(0, contentColor)
|
||||||
}
|
}
|
||||||
@@ -145,6 +150,7 @@ internal fun PinBouncer(
|
|||||||
PinButton(
|
PinButton(
|
||||||
onClicked = { viewModel.onAuthenticateButtonClicked() },
|
onClicked = { viewModel.onAuthenticateButtonClicked() },
|
||||||
isHighlighted = true,
|
isHighlighted = true,
|
||||||
|
isEnabled = isInputEnabled,
|
||||||
) { contentColor ->
|
) { contentColor ->
|
||||||
PinIcon(
|
PinIcon(
|
||||||
Icon.Resource(
|
Icon.Resource(
|
||||||
@@ -187,6 +193,7 @@ private fun PinIcon(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PinButton(
|
private fun PinButton(
|
||||||
onClicked: () -> Unit,
|
onClicked: () -> Unit,
|
||||||
|
isEnabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onLongPressed: (() -> Unit)? = null,
|
onLongPressed: (() -> Unit)? = null,
|
||||||
isHighlighted: Boolean = false,
|
isHighlighted: Boolean = false,
|
||||||
@@ -228,16 +235,18 @@ private fun PinButton(
|
|||||||
cornerRadius = CornerRadius(cornerRadius.toPx()),
|
cornerRadius = CornerRadius(cornerRadius.toPx()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.pointerInput(Unit) {
|
.thenIf(isEnabled) {
|
||||||
detectTapGestures(
|
Modifier.pointerInput(Unit) {
|
||||||
onPress = {
|
detectTapGestures(
|
||||||
isPressed = true
|
onPress = {
|
||||||
tryAwaitRelease()
|
isPressed = true
|
||||||
isPressed = false
|
tryAwaitRelease()
|
||||||
},
|
isPressed = false
|
||||||
onTap = { onClicked() },
|
},
|
||||||
onLongPress = onLongPressed?.let { { onLongPressed() } },
|
onTap = { onClicked() },
|
||||||
)
|
onLongPress = onLongPressed?.let { { onLongPressed() } },
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
content(contentColor)
|
content(contentColor)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import android.content.Context
|
|||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||||
|
import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
|
||||||
import com.android.systemui.dagger.qualifiers.Application
|
import com.android.systemui.dagger.qualifiers.Application
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
@@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -45,21 +47,6 @@ constructor(
|
|||||||
) {
|
) {
|
||||||
private val interactor: BouncerInteractor = interactorFactory.create(containerName)
|
private val interactor: BouncerInteractor = interactorFactory.create(containerName)
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether updates to the message should be cross-animated from one message to another.
|
|
||||||
*
|
|
||||||
* If `false`, no animation should be applied, the message text should just be replaced
|
|
||||||
* instantly.
|
|
||||||
*/
|
|
||||||
val isMessageUpdateAnimationsEnabled: StateFlow<Boolean> =
|
|
||||||
interactor.throttling
|
|
||||||
.map { it == null }
|
|
||||||
.stateIn(
|
|
||||||
scope = applicationScope,
|
|
||||||
started = SharingStarted.WhileSubscribed(),
|
|
||||||
initialValue = interactor.throttling.value == null,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val isInputEnabled: StateFlow<Boolean> =
|
private val isInputEnabled: StateFlow<Boolean> =
|
||||||
interactor.throttling
|
interactor.throttling
|
||||||
.map { it == null }
|
.map { it == null }
|
||||||
@@ -104,13 +91,21 @@ constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** The user-facing message to show in the bouncer. */
|
/** The user-facing message to show in the bouncer. */
|
||||||
val message: StateFlow<String> =
|
val message: StateFlow<MessageViewModel> =
|
||||||
interactor.message
|
combine(
|
||||||
.map { it ?: "" }
|
interactor.message,
|
||||||
|
interactor.throttling,
|
||||||
|
) { message, throttling ->
|
||||||
|
toMessageViewModel(message, throttling)
|
||||||
|
}
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = applicationScope,
|
scope = applicationScope,
|
||||||
started = SharingStarted.WhileSubscribed(),
|
started = SharingStarted.WhileSubscribed(),
|
||||||
initialValue = interactor.message.value ?: "",
|
initialValue =
|
||||||
|
toMessageViewModel(
|
||||||
|
message = interactor.message.value,
|
||||||
|
throttling = interactor.throttling.value,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val _throttlingDialogMessage = MutableStateFlow<String?>(null)
|
private val _throttlingDialogMessage = MutableStateFlow<String?>(null)
|
||||||
@@ -177,6 +172,28 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun toMessageViewModel(
|
||||||
|
message: String?,
|
||||||
|
throttling: AuthenticationThrottledModel?,
|
||||||
|
): MessageViewModel {
|
||||||
|
return MessageViewModel(
|
||||||
|
text = message ?: "",
|
||||||
|
isUpdateAnimated = throttling == null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MessageViewModel(
|
||||||
|
val text: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether updates to the message should be cross-animated from one message to another.
|
||||||
|
*
|
||||||
|
* If `false`, no animation should be applied, the message text should just be replaced
|
||||||
|
* instantly.
|
||||||
|
*/
|
||||||
|
val isUpdateAnimated: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|||||||
@@ -93,22 +93,21 @@ class BouncerViewModelTest : SysuiTestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun isMessageUpdateAnimationsEnabled() =
|
fun message() =
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
val isMessageUpdateAnimationsEnabled by
|
val message by collectLastValue(underTest.message)
|
||||||
collectLastValue(underTest.isMessageUpdateAnimationsEnabled)
|
|
||||||
val throttling by collectLastValue(bouncerInteractor.throttling)
|
val throttling by collectLastValue(bouncerInteractor.throttling)
|
||||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||||
assertThat(isMessageUpdateAnimationsEnabled).isTrue()
|
assertThat(message?.isUpdateAnimated).isTrue()
|
||||||
|
|
||||||
repeat(BouncerInteractor.THROTTLE_EVERY) {
|
repeat(BouncerInteractor.THROTTLE_EVERY) {
|
||||||
// Wrong PIN.
|
// Wrong PIN.
|
||||||
bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
|
bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
|
||||||
}
|
}
|
||||||
assertThat(isMessageUpdateAnimationsEnabled).isFalse()
|
assertThat(message?.isUpdateAnimated).isFalse()
|
||||||
|
|
||||||
throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
|
throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
|
||||||
assertThat(isMessageUpdateAnimationsEnabled).isTrue()
|
assertThat(message?.isUpdateAnimated).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
underTest.onShown()
|
underTest.onShown()
|
||||||
|
|
||||||
assertThat(message).isEqualTo(ENTER_YOUR_PASSWORD)
|
assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
|
||||||
assertThat(password).isEqualTo("")
|
assertThat(password).isEqualTo("")
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
@@ -109,7 +109,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
underTest.onPasswordInputChanged("password")
|
underTest.onPasswordInputChanged("password")
|
||||||
|
|
||||||
assertThat(message).isEmpty()
|
assertThat(message?.text).isEmpty()
|
||||||
assertThat(password).isEqualTo("password")
|
assertThat(password).isEqualTo("password")
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
@@ -156,7 +156,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
|
|||||||
underTest.onAuthenticateKeyPressed()
|
underTest.onAuthenticateKeyPressed()
|
||||||
|
|
||||||
assertThat(password).isEqualTo("")
|
assertThat(password).isEqualTo("")
|
||||||
assertThat(message).isEqualTo(WRONG_PASSWORD)
|
assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
}
|
}
|
||||||
@@ -179,13 +179,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
|
|||||||
underTest.onPasswordInputChanged("wrong")
|
underTest.onPasswordInputChanged("wrong")
|
||||||
underTest.onAuthenticateKeyPressed()
|
underTest.onAuthenticateKeyPressed()
|
||||||
assertThat(password).isEqualTo("")
|
assertThat(password).isEqualTo("")
|
||||||
assertThat(message).isEqualTo(WRONG_PASSWORD)
|
assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
|
|
||||||
// Enter the correct password:
|
// Enter the correct password:
|
||||||
underTest.onPasswordInputChanged("password")
|
underTest.onPasswordInputChanged("password")
|
||||||
assertThat(message).isEmpty()
|
assertThat(message?.text).isEmpty()
|
||||||
|
|
||||||
underTest.onAuthenticateKeyPressed()
|
underTest.onAuthenticateKeyPressed()
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
underTest.onShown()
|
underTest.onShown()
|
||||||
|
|
||||||
assertThat(message).isEqualTo(ENTER_YOUR_PATTERN)
|
assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
|
||||||
assertThat(selectedDots).isEmpty()
|
assertThat(selectedDots).isEmpty()
|
||||||
assertThat(currentDot).isNull()
|
assertThat(currentDot).isNull()
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
@@ -115,7 +115,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
underTest.onDragStart()
|
underTest.onDragStart()
|
||||||
|
|
||||||
assertThat(message).isEmpty()
|
assertThat(message?.text).isEmpty()
|
||||||
assertThat(selectedDots).isEmpty()
|
assertThat(selectedDots).isEmpty()
|
||||||
assertThat(currentDot).isNull()
|
assertThat(currentDot).isNull()
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
@@ -202,7 +202,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
assertThat(selectedDots).isEmpty()
|
assertThat(selectedDots).isEmpty()
|
||||||
assertThat(currentDot).isNull()
|
assertThat(currentDot).isNull()
|
||||||
assertThat(message).isEqualTo(WRONG_PATTERN)
|
assertThat(message?.text).isEqualTo(WRONG_PATTERN)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
}
|
}
|
||||||
@@ -235,7 +235,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
|
|||||||
underTest.onDragEnd()
|
underTest.onDragEnd()
|
||||||
assertThat(selectedDots).isEmpty()
|
assertThat(selectedDots).isEmpty()
|
||||||
assertThat(currentDot).isNull()
|
assertThat(currentDot).isNull()
|
||||||
assertThat(message).isEqualTo(WRONG_PATTERN)
|
assertThat(message?.text).isEqualTo(WRONG_PATTERN)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
underTest.onShown()
|
underTest.onShown()
|
||||||
|
|
||||||
assertThat(message).isEqualTo(ENTER_YOUR_PIN)
|
assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN)
|
||||||
assertThat(pinLengths).isEqualTo(0 to 0)
|
assertThat(pinLengths).isEqualTo(0 to 0)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
@@ -116,7 +116,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
underTest.onPinButtonClicked(1)
|
underTest.onPinButtonClicked(1)
|
||||||
|
|
||||||
assertThat(message).isEmpty()
|
assertThat(message?.text).isEmpty()
|
||||||
assertThat(pinLengths).isEqualTo(0 to 1)
|
assertThat(pinLengths).isEqualTo(0 to 1)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
@@ -140,7 +140,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
underTest.onBackspaceButtonClicked()
|
underTest.onBackspaceButtonClicked()
|
||||||
|
|
||||||
assertThat(message).isEmpty()
|
assertThat(message?.text).isEmpty()
|
||||||
assertThat(pinLengths).isEqualTo(1 to 0)
|
assertThat(pinLengths).isEqualTo(1 to 0)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
@@ -170,7 +170,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
|||||||
advanceTimeBy(PinBouncerViewModel.BACKSPACE_LONG_PRESS_DELAY_MS)
|
advanceTimeBy(PinBouncerViewModel.BACKSPACE_LONG_PRESS_DELAY_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(message).isEmpty()
|
assertThat(message?.text).isEmpty()
|
||||||
assertThat(pinLengths).isEqualTo(1 to 0)
|
assertThat(pinLengths).isEqualTo(1 to 0)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
@@ -220,7 +220,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
|||||||
underTest.onAuthenticateButtonClicked()
|
underTest.onAuthenticateButtonClicked()
|
||||||
|
|
||||||
assertThat(pinLengths).isEqualTo(0 to 0)
|
assertThat(pinLengths).isEqualTo(0 to 0)
|
||||||
assertThat(message).isEqualTo(WRONG_PIN)
|
assertThat(message?.text).isEqualTo(WRONG_PIN)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
|||||||
underTest.onPinButtonClicked(4)
|
underTest.onPinButtonClicked(4)
|
||||||
underTest.onPinButtonClicked(5) // PIN is now wrong!
|
underTest.onPinButtonClicked(5) // PIN is now wrong!
|
||||||
underTest.onAuthenticateButtonClicked()
|
underTest.onAuthenticateButtonClicked()
|
||||||
assertThat(message).isEqualTo(WRONG_PIN)
|
assertThat(message?.text).isEqualTo(WRONG_PIN)
|
||||||
assertThat(pinLengths).isEqualTo(0 to 0)
|
assertThat(pinLengths).isEqualTo(0 to 0)
|
||||||
assertThat(isUnlocked).isFalse()
|
assertThat(isUnlocked).isFalse()
|
||||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||||
@@ -254,7 +254,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
|
|||||||
underTest.onPinButtonClicked(2)
|
underTest.onPinButtonClicked(2)
|
||||||
underTest.onPinButtonClicked(3)
|
underTest.onPinButtonClicked(3)
|
||||||
underTest.onPinButtonClicked(4)
|
underTest.onPinButtonClicked(4)
|
||||||
assertThat(message).isEmpty()
|
assertThat(message?.text).isEmpty()
|
||||||
|
|
||||||
underTest.onAuthenticateButtonClicked()
|
underTest.onAuthenticateButtonClicked()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user