Merge changes Icb51a009,I1dff6dc1,I5effca8f into udc-dev
* changes: [flexiglass] Introduces SceneTestUtils. [flexiglass] Multiple input method bouncer scene. Adds horizontal/vertical grids.
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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.compose.grid
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.constrainWidth
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Renders a grid with [columns] columns.
|
||||
*
|
||||
* Child composables will be arranged row by row.
|
||||
*
|
||||
* Each column is spaced from the columns to its left and right by [horizontalSpacing]. Each cell
|
||||
* inside a column is spaced from the cells above and below it with [verticalSpacing].
|
||||
*/
|
||||
@Composable
|
||||
fun VerticalGrid(
|
||||
columns: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
verticalSpacing: Dp = 0.dp,
|
||||
horizontalSpacing: Dp = 0.dp,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Grid(
|
||||
primarySpaces = columns,
|
||||
isVertical = true,
|
||||
modifier = modifier,
|
||||
verticalSpacing = verticalSpacing,
|
||||
horizontalSpacing = horizontalSpacing,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a grid with [rows] rows.
|
||||
*
|
||||
* Child composables will be arranged column by column.
|
||||
*
|
||||
* Each column is spaced from the columns to its left and right by [horizontalSpacing]. Each cell
|
||||
* inside a column is spaced from the cells above and below it with [verticalSpacing].
|
||||
*/
|
||||
@Composable
|
||||
fun HorizontalGrid(
|
||||
rows: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
verticalSpacing: Dp = 0.dp,
|
||||
horizontalSpacing: Dp = 0.dp,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Grid(
|
||||
primarySpaces = rows,
|
||||
isVertical = false,
|
||||
modifier = modifier,
|
||||
verticalSpacing = verticalSpacing,
|
||||
horizontalSpacing = horizontalSpacing,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Grid(
|
||||
primarySpaces: Int,
|
||||
isVertical: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
verticalSpacing: Dp,
|
||||
horizontalSpacing: Dp,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
check(primarySpaces > 0) {
|
||||
"Must provide a positive number of ${if (isVertical) "columns" else "rows"}"
|
||||
}
|
||||
|
||||
val sizeCache = remember {
|
||||
object {
|
||||
var rowHeights = intArrayOf()
|
||||
var columnWidths = intArrayOf()
|
||||
}
|
||||
}
|
||||
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
content = content,
|
||||
) { measurables, constraints ->
|
||||
val cells = measurables.size
|
||||
val columns: Int
|
||||
val rows: Int
|
||||
if (isVertical) {
|
||||
columns = primarySpaces
|
||||
rows = ceil(cells.toFloat() / primarySpaces).toInt()
|
||||
} else {
|
||||
columns = ceil(cells.toFloat() / primarySpaces).toInt()
|
||||
rows = primarySpaces
|
||||
}
|
||||
|
||||
if (sizeCache.rowHeights.size != rows) {
|
||||
sizeCache.rowHeights = IntArray(rows) { 0 }
|
||||
}
|
||||
if (sizeCache.columnWidths.size != columns) {
|
||||
sizeCache.columnWidths = IntArray(columns) { 0 }
|
||||
}
|
||||
|
||||
val totalHorizontalSpacingBetweenChildren =
|
||||
((columns - 1) * horizontalSpacing.toPx()).roundToInt()
|
||||
val totalVerticalSpacingBetweenChildren = ((rows - 1) * verticalSpacing.toPx()).roundToInt()
|
||||
val childConstraints =
|
||||
Constraints().apply {
|
||||
if (constraints.maxWidth != Constraints.Infinity) {
|
||||
constrainWidth(
|
||||
(constraints.maxWidth - totalHorizontalSpacingBetweenChildren) / columns
|
||||
)
|
||||
}
|
||||
if (constraints.maxHeight != Constraints.Infinity) {
|
||||
constrainWidth(
|
||||
(constraints.maxHeight - totalVerticalSpacingBetweenChildren) / rows
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val placeables = buildList {
|
||||
for (cellIndex in measurables.indices) {
|
||||
val column: Int
|
||||
val row: Int
|
||||
if (isVertical) {
|
||||
column = cellIndex % columns
|
||||
row = cellIndex / columns
|
||||
} else {
|
||||
column = cellIndex / rows
|
||||
row = cellIndex % rows
|
||||
}
|
||||
|
||||
val placeable = measurables[cellIndex].measure(childConstraints)
|
||||
sizeCache.rowHeights[row] = max(sizeCache.rowHeights[row], placeable.height)
|
||||
sizeCache.columnWidths[column] =
|
||||
max(sizeCache.columnWidths[column], placeable.width)
|
||||
add(placeable)
|
||||
}
|
||||
}
|
||||
|
||||
var totalWidth = totalHorizontalSpacingBetweenChildren
|
||||
for (column in sizeCache.columnWidths.indices) {
|
||||
totalWidth += sizeCache.columnWidths[column]
|
||||
}
|
||||
|
||||
var totalHeight = totalVerticalSpacingBetweenChildren
|
||||
for (row in sizeCache.rowHeights.indices) {
|
||||
totalHeight += sizeCache.rowHeights[row]
|
||||
}
|
||||
|
||||
layout(totalWidth, totalHeight) {
|
||||
var y = 0
|
||||
repeat(rows) { row ->
|
||||
var x = 0
|
||||
var maxChildHeight = 0
|
||||
repeat(columns) { column ->
|
||||
val cellIndex = row * columns + column
|
||||
if (cellIndex < cells) {
|
||||
val placeable = placeables[cellIndex]
|
||||
placeable.placeRelative(x, y)
|
||||
x += placeable.width + horizontalSpacing.roundToPx()
|
||||
maxChildHeight = max(maxChildHeight, placeable.height)
|
||||
}
|
||||
}
|
||||
y += maxChildHeight + verticalSpacing.roundToPx()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,30 @@
|
||||
|
||||
package com.android.systemui.bouncer.ui.composable
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
|
||||
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
|
||||
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
|
||||
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
|
||||
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
@@ -55,26 +67,69 @@ constructor(
|
||||
)
|
||||
.asStateFlow()
|
||||
|
||||
@Composable
|
||||
override fun Content(
|
||||
modifier: Modifier,
|
||||
) {
|
||||
// TODO(b/280877228): implement the real UI.
|
||||
@Composable override fun Content(modifier: Modifier) = BouncerScene(viewModel, modifier)
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
Text("Bouncer", style = MaterialTheme.typography.headlineLarge)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Button(onClick = { viewModel.onAuthenticateButtonClicked() }) {
|
||||
Text("Authenticate")
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
private fun BouncerScene(
|
||||
viewModel: BouncerViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val message: String by viewModel.message.collectAsState()
|
||||
val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState()
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(60.dp),
|
||||
modifier =
|
||||
modifier.background(MaterialTheme.colorScheme.surface).fillMaxSize().padding(32.dp)
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = message,
|
||||
label = "Bouncer message",
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
|
||||
Box(Modifier.weight(1f)) {
|
||||
when (val nonNullViewModel = authMethodViewModel) {
|
||||
is PinBouncerViewModel ->
|
||||
PinBouncer(
|
||||
viewModel = nonNullViewModel,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
is PasswordBouncerViewModel ->
|
||||
PasswordBouncer(
|
||||
viewModel = nonNullViewModel,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
is PatternBouncerViewModel ->
|
||||
PatternBouncer(
|
||||
viewModel = nonNullViewModel,
|
||||
modifier =
|
||||
Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = viewModel::onEmergencyServicesButtonClicked,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(com.android.internal.R.string.lockscreen_emergency_call),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.bouncer.ui.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
|
||||
|
||||
/** UI for the input part of a password-requiring version of the bouncer. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun PasswordBouncer(
|
||||
viewModel: PasswordBouncerViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val password: String by viewModel.password.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// When the UI comes up, request focus on the TextField to bring up the software keyboard.
|
||||
focusRequester.requestFocus()
|
||||
// Also, report that the UI is shown to let the view-model runs some logic.
|
||||
viewModel.onShown()
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier,
|
||||
) {
|
||||
val color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val lineWidthPx = with(LocalDensity.current) { 2.dp.toPx() }
|
||||
|
||||
TextField(
|
||||
value = password,
|
||||
onValueChange = viewModel::onPasswordInputChanged,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onDone = { viewModel.onAuthenticateKeyPressed() },
|
||||
),
|
||||
modifier =
|
||||
Modifier.focusRequester(focusRequester).drawBehind {
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(x = 0f, y = size.height - lineWidthPx),
|
||||
end = Offset(size.width, y = size.height - lineWidthPx),
|
||||
strokeWidth = lineWidthPx,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(100.dp))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* 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.bouncer.ui.composable
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
|
||||
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* UI for the input part of a pattern-requiring version of the bouncer.
|
||||
*
|
||||
* The user can press, hold, and drag their pointer to select dots along a grid of dots.
|
||||
*/
|
||||
@Composable
|
||||
internal fun PatternBouncer(
|
||||
viewModel: PatternBouncerViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// Report that the UI is shown to let the view-model run some logic.
|
||||
LaunchedEffect(Unit) { viewModel.onShown() }
|
||||
|
||||
val colCount = viewModel.columnCount
|
||||
val rowCount = viewModel.rowCount
|
||||
|
||||
val dotColor = MaterialTheme.colorScheme.secondary
|
||||
val dotRadius = with(LocalDensity.current) { 8.dp.toPx() }
|
||||
val lineColor = MaterialTheme.colorScheme.primary
|
||||
val lineStrokeWidth = dotRadius * 2 + with(LocalDensity.current) { 4.dp.toPx() }
|
||||
|
||||
var containerSize: IntSize by remember { mutableStateOf(IntSize(0, 0)) }
|
||||
val horizontalSpacing = containerSize.width / colCount
|
||||
val verticalSpacing = containerSize.height / rowCount
|
||||
val spacing = min(horizontalSpacing, verticalSpacing).toFloat()
|
||||
val verticalOffset = containerSize.height - spacing * rowCount
|
||||
|
||||
// All dots that should be rendered on the grid.
|
||||
val dots: List<PatternDotViewModel> by viewModel.dots.collectAsState()
|
||||
// The most recently selected dot, if the user is currently dragging.
|
||||
val currentDot: PatternDotViewModel? by viewModel.currentDot.collectAsState()
|
||||
// The dots selected so far, if the user is currently dragging.
|
||||
val selectedDots: List<PatternDotViewModel> by viewModel.selectedDots.collectAsState()
|
||||
|
||||
// Map of animatables for the scale of each dot, keyed by dot.
|
||||
val scales = remember(dots) { dots.associateWith { Animatable(1f) } }
|
||||
// Map of animatables for the lines that connect between selected dots, keyed by the destination
|
||||
// dot of the line.
|
||||
val lines = remember(dots) { dots.associateWith { Animatable(1f) } }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// When the current dot is changed, we need to update our animations.
|
||||
LaunchedEffect(currentDot) {
|
||||
// Make sure that the current dot is scaled up while the other dots are scaled back down.
|
||||
scales.entries.forEach { (dot, animatable) ->
|
||||
val isSelected = dot == currentDot
|
||||
launch {
|
||||
animatable.animateTo(if (isSelected) 2f else 1f)
|
||||
if (isSelected) {
|
||||
animatable.animateTo(1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that all dot-connecting lines are decaying, if they're not already animating.
|
||||
selectedDots.forEach {
|
||||
lines[it]?.let { line ->
|
||||
if (!line.isRunning) {
|
||||
scope.launch {
|
||||
line.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is the position of the input pointer.
|
||||
var inputPosition: Offset? by remember { mutableStateOf(null) }
|
||||
|
||||
Canvas(
|
||||
modifier
|
||||
// Need to clip to bounds to make sure that the lines don't follow the input pointer
|
||||
// when it leaves the bounds of the dot grid.
|
||||
.clipToBounds()
|
||||
.onSizeChanged { containerSize = it }
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
onDragStart = { start ->
|
||||
inputPosition = start
|
||||
viewModel.onDragStart()
|
||||
},
|
||||
onDragEnd = {
|
||||
inputPosition = null
|
||||
lines.values.forEach { animatable ->
|
||||
scope.launch { animatable.animateTo(1f) }
|
||||
}
|
||||
viewModel.onDragEnd()
|
||||
},
|
||||
) { change, _ ->
|
||||
inputPosition = change.position
|
||||
viewModel.onDrag(
|
||||
xPx = change.position.x,
|
||||
yPx = change.position.y,
|
||||
containerSizePx = containerSize.width,
|
||||
verticalOffsetPx = verticalOffset,
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
// Draw lines between dots.
|
||||
selectedDots.forEachIndexed { index, dot ->
|
||||
if (index > 0) {
|
||||
val previousDot = selectedDots[index - 1]
|
||||
drawLine(
|
||||
from = previousDot,
|
||||
to = dot,
|
||||
alpha = { distance -> lineAlpha(spacing, distance) },
|
||||
spacing = spacing,
|
||||
verticalOffset = verticalOffset,
|
||||
lineColor = lineColor,
|
||||
lineStrokeWidth = lineStrokeWidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the line between the most recently-selected dot and the input pointer position.
|
||||
inputPosition?.let { lineEnd ->
|
||||
currentDot?.let { dot ->
|
||||
drawLine(
|
||||
from = dot,
|
||||
to = lineEnd,
|
||||
alpha = { distance -> lineAlpha(spacing, distance) },
|
||||
spacing = spacing,
|
||||
verticalOffset = verticalOffset,
|
||||
lineColor = lineColor,
|
||||
lineStrokeWidth = lineStrokeWidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw each dot on the grid.
|
||||
dots.forEach { dot ->
|
||||
drawDot(
|
||||
dot = dot,
|
||||
scaleFactor = { scales[dot]?.value ?: 1f },
|
||||
spacing = spacing,
|
||||
verticalOffset = verticalOffset,
|
||||
dotColor = dotColor,
|
||||
dotRadius = dotRadius,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Draws the given [dot]. */
|
||||
private fun DrawScope.drawDot(
|
||||
dot: PatternDotViewModel,
|
||||
scaleFactor: () -> Float,
|
||||
spacing: Float,
|
||||
verticalOffset: Float,
|
||||
dotColor: Color,
|
||||
dotRadius: Float,
|
||||
) {
|
||||
drawCircle(
|
||||
color = dotColor,
|
||||
radius = dotRadius * scaleFactor.invoke(),
|
||||
center = pixelOffset(dot, spacing, verticalOffset),
|
||||
)
|
||||
}
|
||||
|
||||
/** Draws a line from the [from] origin dot to the [to] destination dot. */
|
||||
private fun DrawScope.drawLine(
|
||||
from: PatternDotViewModel,
|
||||
to: PatternDotViewModel,
|
||||
alpha: (distance: Float) -> Float,
|
||||
spacing: Float,
|
||||
verticalOffset: Float,
|
||||
lineColor: Color,
|
||||
lineStrokeWidth: Float,
|
||||
) {
|
||||
drawLine(
|
||||
from = from,
|
||||
to = pixelOffset(to, spacing, verticalOffset),
|
||||
alpha = alpha,
|
||||
spacing = spacing,
|
||||
verticalOffset = verticalOffset,
|
||||
lineColor = lineColor,
|
||||
lineStrokeWidth = lineStrokeWidth,
|
||||
)
|
||||
}
|
||||
|
||||
/** Draws a line from the [from] origin dot to the [to] destination. */
|
||||
private fun DrawScope.drawLine(
|
||||
from: PatternDotViewModel,
|
||||
to: Offset,
|
||||
alpha: (distance: Float) -> Float,
|
||||
spacing: Float,
|
||||
verticalOffset: Float,
|
||||
lineColor: Color,
|
||||
lineStrokeWidth: Float,
|
||||
) {
|
||||
val fromAsOffset = pixelOffset(from, spacing, verticalOffset)
|
||||
val distance = sqrt((to.y - fromAsOffset.y).pow(2) + (to.x - fromAsOffset.x).pow(2))
|
||||
|
||||
drawLine(
|
||||
color = lineColor,
|
||||
start = fromAsOffset,
|
||||
end = to,
|
||||
strokeWidth = lineStrokeWidth,
|
||||
cap = StrokeCap.Round,
|
||||
alpha = alpha.invoke(distance),
|
||||
)
|
||||
}
|
||||
|
||||
/** Returns an [Offset] representation of the given [dot] in pixel coordinates. */
|
||||
private fun pixelOffset(
|
||||
dot: PatternDotViewModel,
|
||||
spacing: Float,
|
||||
verticalOffset: Float,
|
||||
): Offset {
|
||||
return Offset(
|
||||
x = dot.x * spacing + spacing / 2,
|
||||
y = dot.y * spacing + spacing / 2 + verticalOffset,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alpha for a line between dots where dots are [spacing] apart from each other on the
|
||||
* dot grid and the line ends [distance] away from the origin dot.
|
||||
*
|
||||
* The reason [distance] can be different from [spacing] is that all lines originate in dots but one
|
||||
* line might end where the user input pointer is, which isn't always a dot position.
|
||||
*/
|
||||
private fun lineAlpha(spacing: Float, distance: Float): Float {
|
||||
// Custom curve for the alpha of a line as a function of its distance from its source dot. The
|
||||
// farther the user input pointer goes from the line, the more opaque the line gets.
|
||||
return ((distance / spacing - 0.3f) * 4f).coerceIn(0f, 1f)
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalAnimationApi::class)
|
||||
|
||||
package com.android.systemui.bouncer.ui.composable
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.android.compose.grid.VerticalGrid
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
|
||||
import com.android.systemui.common.shared.model.ContentDescription
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
import com.android.systemui.common.ui.compose.Icon
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
internal fun PinBouncer(
|
||||
viewModel: PinBouncerViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// Report that the UI is shown to let the view-model run some logic.
|
||||
LaunchedEffect(Unit) { viewModel.onShown() }
|
||||
|
||||
// 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()
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.heightIn(min = 16.dp).animateContentSize(),
|
||||
) {
|
||||
// TODO(b/281871687): add support for dot shapes.
|
||||
val (previousPinLength, currentPinLength) = pinLength
|
||||
val dotCount = max(previousPinLength, currentPinLength) + 1
|
||||
repeat(dotCount) { index ->
|
||||
AnimatedVisibility(
|
||||
visible = index < currentPinLength,
|
||||
enter = fadeIn() + scaleIn() + slideInHorizontally(),
|
||||
exit = fadeOut() + scaleOut() + slideOutHorizontally(),
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.size(16.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
CircleShape,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(100.dp))
|
||||
|
||||
VerticalGrid(
|
||||
columns = 3,
|
||||
verticalSpacing = 12.dp,
|
||||
horizontalSpacing = 20.dp,
|
||||
) {
|
||||
repeat(9) { index ->
|
||||
val digit = index + 1
|
||||
PinButton(
|
||||
onClicked = { viewModel.onPinButtonClicked(digit) },
|
||||
) { contentColor ->
|
||||
PinDigit(digit, contentColor)
|
||||
}
|
||||
}
|
||||
|
||||
PinButton(
|
||||
onClicked = { viewModel.onBackspaceButtonClicked() },
|
||||
onLongPressed = { viewModel.onBackspaceButtonLongPressed() },
|
||||
isHighlighted = true,
|
||||
) { contentColor ->
|
||||
PinIcon(
|
||||
Icon.Resource(
|
||||
res = R.drawable.ic_backspace_24dp,
|
||||
contentDescription =
|
||||
ContentDescription.Resource(R.string.keyboardview_keycode_delete),
|
||||
),
|
||||
contentColor,
|
||||
)
|
||||
}
|
||||
|
||||
PinButton(
|
||||
onClicked = { viewModel.onPinButtonClicked(0) },
|
||||
) { contentColor ->
|
||||
PinDigit(0, contentColor)
|
||||
}
|
||||
|
||||
PinButton(
|
||||
onClicked = { viewModel.onAuthenticateButtonClicked() },
|
||||
isHighlighted = true,
|
||||
) { contentColor ->
|
||||
PinIcon(
|
||||
Icon.Resource(
|
||||
res = R.drawable.ic_keyboard_tab_36dp,
|
||||
contentDescription =
|
||||
ContentDescription.Resource(R.string.keyboardview_keycode_enter),
|
||||
),
|
||||
contentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinDigit(
|
||||
digit: Int,
|
||||
contentColor: Color,
|
||||
) {
|
||||
// TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes it
|
||||
// into Text, use that here, to animate more efficiently.
|
||||
Text(
|
||||
text = digit.toString(),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = contentColor,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinIcon(
|
||||
icon: Icon,
|
||||
contentColor: Color,
|
||||
) {
|
||||
Icon(
|
||||
icon = icon,
|
||||
tint = contentColor,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PinButton(
|
||||
onClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onLongPressed: (() -> Unit)? = null,
|
||||
isHighlighted: Boolean = false,
|
||||
content: @Composable (contentColor: Color) -> Unit,
|
||||
) {
|
||||
var isPressed: Boolean by remember { mutableStateOf(false) }
|
||||
val cornerRadius: Dp by
|
||||
animateDpAsState(
|
||||
if (isPressed) 24.dp else PinButtonSize / 2,
|
||||
label = "PinButton round corners",
|
||||
)
|
||||
val containerColor: Color by
|
||||
animateColorAsState(
|
||||
when {
|
||||
isPressed -> MaterialTheme.colorScheme.primaryContainer
|
||||
isHighlighted -> MaterialTheme.colorScheme.secondaryContainer
|
||||
else -> MaterialTheme.colorScheme.surface
|
||||
},
|
||||
label = "Pin button container color",
|
||||
)
|
||||
val contentColor: Color by
|
||||
animateColorAsState(
|
||||
when {
|
||||
isPressed -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||
isHighlighted -> MaterialTheme.colorScheme.onSecondaryContainer
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
},
|
||||
label = "Pin button container color",
|
||||
)
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
modifier
|
||||
.size(PinButtonSize)
|
||||
.drawBehind {
|
||||
drawRoundRect(
|
||||
color = containerColor,
|
||||
cornerRadius = CornerRadius(cornerRadius.toPx()),
|
||||
)
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
isPressed = true
|
||||
tryAwaitRelease()
|
||||
isPressed = false
|
||||
},
|
||||
onTap = { onClicked() },
|
||||
onLongPress = onLongPressed?.let { { onLongPressed() } },
|
||||
)
|
||||
},
|
||||
) {
|
||||
content(contentColor)
|
||||
}
|
||||
}
|
||||
|
||||
private val PinButtonSize = 84.dp
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.bouncer.ui.viewmodel
|
||||
|
||||
sealed interface AuthMethodBouncerViewModel
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.systemui.bouncer.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
@@ -30,12 +31,44 @@ import kotlinx.coroutines.flow.stateIn
|
||||
class BouncerViewModel
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Application private val applicationContext: Context,
|
||||
@Application private val applicationScope: CoroutineScope,
|
||||
interactorFactory: BouncerInteractor.Factory,
|
||||
containerName: String,
|
||||
) {
|
||||
private val interactor: BouncerInteractor = interactorFactory.create(containerName)
|
||||
|
||||
private val pin: PinBouncerViewModel by lazy {
|
||||
PinBouncerViewModel(
|
||||
applicationScope = applicationScope,
|
||||
interactor = interactor,
|
||||
)
|
||||
}
|
||||
|
||||
private val password: PasswordBouncerViewModel by lazy {
|
||||
PasswordBouncerViewModel(
|
||||
interactor = interactor,
|
||||
)
|
||||
}
|
||||
|
||||
private val pattern: PatternBouncerViewModel by lazy {
|
||||
PatternBouncerViewModel(
|
||||
applicationContext = applicationContext,
|
||||
applicationScope = applicationScope,
|
||||
interactor = interactor,
|
||||
)
|
||||
}
|
||||
|
||||
/** View-model for the current UI, based on the current authentication method. */
|
||||
val authMethod: StateFlow<AuthMethodBouncerViewModel?> =
|
||||
interactor.authenticationMethod
|
||||
.map { authMethod -> toViewModel(authMethod) }
|
||||
.stateIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = toViewModel(interactor.authenticationMethod.value),
|
||||
)
|
||||
|
||||
/** The user-facing message to show in the bouncer. */
|
||||
val message: StateFlow<String> =
|
||||
interactor.message
|
||||
@@ -46,30 +79,19 @@ constructor(
|
||||
initialValue = interactor.message.value ?: "",
|
||||
)
|
||||
|
||||
/** Notifies that the authenticate button was clicked. */
|
||||
fun onAuthenticateButtonClicked() {
|
||||
// TODO(b/280877228): remove this and send the real input.
|
||||
interactor.authenticate(
|
||||
when (interactor.authenticationMethod.value) {
|
||||
is AuthenticationMethodModel.PIN -> listOf(1, 2, 3, 4)
|
||||
is AuthenticationMethodModel.Password -> "password".toList()
|
||||
is AuthenticationMethodModel.Pattern ->
|
||||
listOf(
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(2, 0),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(2, 1),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(2, 2),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(0, 0),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(0, 1),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(0, 2),
|
||||
)
|
||||
else -> emptyList()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** Notifies that the emergency services button was clicked. */
|
||||
fun onEmergencyServicesButtonClicked() {
|
||||
// TODO(b/280877228): implement this.
|
||||
// TODO(b/280877228): implement this
|
||||
}
|
||||
|
||||
private fun toViewModel(
|
||||
authMethod: AuthenticationMethodModel,
|
||||
): AuthMethodBouncerViewModel? {
|
||||
return when (authMethod) {
|
||||
is AuthenticationMethodModel.PIN -> pin
|
||||
is AuthenticationMethodModel.Password -> password
|
||||
is AuthenticationMethodModel.Pattern -> pattern
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.bouncer.ui.viewmodel
|
||||
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/** Holds UI state and handles user input for the password bouncer UI. */
|
||||
class PasswordBouncerViewModel(
|
||||
private val interactor: BouncerInteractor,
|
||||
) : AuthMethodBouncerViewModel {
|
||||
|
||||
private val _password = MutableStateFlow("")
|
||||
/** The password entered so far. */
|
||||
val password: StateFlow<String> = _password.asStateFlow()
|
||||
|
||||
/** Notifies that the UI has been shown to the user. */
|
||||
fun onShown() {
|
||||
interactor.resetMessage()
|
||||
}
|
||||
|
||||
/** Notifies that the user has changed the password input. */
|
||||
fun onPasswordInputChanged(password: String) {
|
||||
if (this.password.value.isEmpty() && password.isNotEmpty()) {
|
||||
interactor.clearMessage()
|
||||
}
|
||||
|
||||
_password.value = password
|
||||
}
|
||||
|
||||
/** Notifies that the user has pressed the key for attempting to authenticate the password. */
|
||||
fun onAuthenticateKeyPressed() {
|
||||
interactor.authenticate(password.value.toCharArray().toList())
|
||||
_password.value = ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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.bouncer.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/** Holds UI state and handles user input for the pattern bouncer UI. */
|
||||
class PatternBouncerViewModel(
|
||||
private val applicationContext: Context,
|
||||
applicationScope: CoroutineScope,
|
||||
private val interactor: BouncerInteractor,
|
||||
) : AuthMethodBouncerViewModel {
|
||||
|
||||
/** The number of columns in the dot grid. */
|
||||
val columnCount = 3
|
||||
/** The number of rows in the dot grid. */
|
||||
val rowCount = 3
|
||||
|
||||
private val _selectedDots = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf())
|
||||
/** The dots that were selected by the user, in the order of selection. */
|
||||
val selectedDots: StateFlow<List<PatternDotViewModel>> =
|
||||
_selectedDots
|
||||
.map { it.toList() }
|
||||
.stateIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
private val _currentDot = MutableStateFlow<PatternDotViewModel?>(null)
|
||||
/** The most-recently selected dot that the user selected. */
|
||||
val currentDot: StateFlow<PatternDotViewModel?> = _currentDot.asStateFlow()
|
||||
|
||||
private val _dots = MutableStateFlow(defaultDots())
|
||||
/** All dots on the grid. */
|
||||
val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow()
|
||||
|
||||
/** Notifies that the UI has been shown to the user. */
|
||||
fun onShown() {
|
||||
interactor.resetMessage()
|
||||
}
|
||||
|
||||
/** Notifies that the user has started a drag gesture across the dot grid. */
|
||||
fun onDragStart() {
|
||||
interactor.clearMessage()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that the user is dragging across the dot grid.
|
||||
*
|
||||
* @param xPx The horizontal coordinate of the position of the user's pointer, in pixels.
|
||||
* @param yPx The vertical coordinate of the position of the user's pointer, in pixels.
|
||||
* @param containerSizePx The size of the container of the dot grid, in pixels. It's assumed
|
||||
* that the dot grid is perfectly square such that width and height are equal.
|
||||
* @param verticalOffsetPx How far down from `0` does the dot grid start on the display.
|
||||
*/
|
||||
fun onDrag(xPx: Float, yPx: Float, containerSizePx: Int, verticalOffsetPx: Float) {
|
||||
val cellWidthPx = containerSizePx / columnCount
|
||||
val cellHeightPx = containerSizePx / rowCount
|
||||
|
||||
if (xPx < 0 || yPx < verticalOffsetPx) {
|
||||
return
|
||||
}
|
||||
|
||||
val dotColumn = (xPx / cellWidthPx).toInt()
|
||||
val dotRow = ((yPx - verticalOffsetPx) / cellHeightPx).toInt()
|
||||
if (dotColumn > columnCount - 1 || dotRow > rowCount - 1) {
|
||||
return
|
||||
}
|
||||
|
||||
val dotPixelX = dotColumn * cellWidthPx + cellWidthPx / 2
|
||||
val dotPixelY = dotRow * cellHeightPx + cellHeightPx / 2 + verticalOffsetPx
|
||||
|
||||
val distance = sqrt((xPx - dotPixelX).pow(2) + (yPx - dotPixelY).pow(2))
|
||||
val hitRadius = hitFactor * min(cellWidthPx, cellHeightPx) / 2
|
||||
if (distance > hitRadius) {
|
||||
return
|
||||
}
|
||||
|
||||
val hitDot = dots.value.firstOrNull { dot -> dot.x == dotColumn && dot.y == dotRow }
|
||||
if (hitDot != null && !_selectedDots.value.contains(hitDot)) {
|
||||
val skippedOverDots =
|
||||
currentDot.value?.let { previousDot ->
|
||||
buildList {
|
||||
var dot = previousDot
|
||||
while (dot != hitDot) {
|
||||
add(dot)
|
||||
dot =
|
||||
PatternDotViewModel(
|
||||
x =
|
||||
if (hitDot.x > dot.x) dot.x + 1
|
||||
else if (hitDot.x < dot.x) dot.x - 1 else dot.x,
|
||||
y =
|
||||
if (hitDot.y > dot.y) dot.y + 1
|
||||
else if (hitDot.y < dot.y) dot.y - 1 else dot.y,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
?: emptyList()
|
||||
|
||||
_selectedDots.value =
|
||||
linkedSetOf<PatternDotViewModel>().apply {
|
||||
addAll(_selectedDots.value)
|
||||
addAll(skippedOverDots)
|
||||
add(hitDot)
|
||||
}
|
||||
_currentDot.value = hitDot
|
||||
}
|
||||
}
|
||||
|
||||
/** Notifies that the user has ended the drag gesture across the dot grid. */
|
||||
fun onDragEnd() {
|
||||
interactor.authenticate(_selectedDots.value.map { it.toCoordinate() })
|
||||
|
||||
_dots.value = defaultDots()
|
||||
_currentDot.value = null
|
||||
_selectedDots.value = linkedSetOf()
|
||||
}
|
||||
|
||||
private fun defaultDots(): List<PatternDotViewModel> {
|
||||
return buildList {
|
||||
(0 until columnCount).forEach { x ->
|
||||
(0 until rowCount).forEach { y ->
|
||||
add(
|
||||
PatternDotViewModel(
|
||||
x = x,
|
||||
y = y,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val hitFactor: Float by lazy {
|
||||
val outValue = TypedValue()
|
||||
applicationContext.resources.getValue(
|
||||
com.android.internal.R.dimen.lock_pattern_dot_hit_factor,
|
||||
outValue,
|
||||
true
|
||||
)
|
||||
max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_DOT_HIT_FACTOR = 0.2f
|
||||
}
|
||||
}
|
||||
|
||||
data class PatternDotViewModel(
|
||||
val x: Int,
|
||||
val y: Int,
|
||||
) {
|
||||
fun toCoordinate(): AuthenticationMethodModel.Pattern.PatternCoordinate {
|
||||
return AuthenticationMethodModel.Pattern.PatternCoordinate(
|
||||
x = x,
|
||||
y = y,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.bouncer.ui.viewmodel
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import com.android.systemui.util.kotlin.pairwise
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Holds UI state and handles user input for the PIN code bouncer UI. */
|
||||
class PinBouncerViewModel(
|
||||
private val applicationScope: CoroutineScope,
|
||||
private val interactor: BouncerInteractor,
|
||||
) : AuthMethodBouncerViewModel {
|
||||
|
||||
private val entered = MutableStateFlow<List<Int>>(emptyList())
|
||||
/**
|
||||
* The length of the PIN digits that were input so far, two values are supplied the previous and
|
||||
* the current.
|
||||
*/
|
||||
val pinLengths: StateFlow<Pair<Int, Int>> =
|
||||
entered
|
||||
.pairwise()
|
||||
.map { it.previousValue.size to it.newValue.size }
|
||||
.stateIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = 0 to 0,
|
||||
)
|
||||
private var resetPinJob: Job? = null
|
||||
|
||||
/** Notifies that the UI has been shown to the user. */
|
||||
fun onShown() {
|
||||
interactor.resetMessage()
|
||||
}
|
||||
|
||||
/** Notifies that the user clicked on a PIN button with the given digit value. */
|
||||
fun onPinButtonClicked(input: Int) {
|
||||
resetPinJob?.cancel()
|
||||
resetPinJob = null
|
||||
|
||||
if (entered.value.isEmpty()) {
|
||||
interactor.clearMessage()
|
||||
}
|
||||
|
||||
entered.value += input
|
||||
}
|
||||
|
||||
/** Notifies that the user clicked the backspace button. */
|
||||
fun onBackspaceButtonClicked() {
|
||||
if (entered.value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
entered.value = entered.value.toMutableList().apply { removeLast() }
|
||||
}
|
||||
|
||||
/** Notifies that the user long-pressed the backspace button. */
|
||||
fun onBackspaceButtonLongPressed() {
|
||||
resetPinJob?.cancel()
|
||||
resetPinJob =
|
||||
applicationScope.launch {
|
||||
while (entered.value.isNotEmpty()) {
|
||||
onBackspaceButtonClicked()
|
||||
delay(BACKSPACE_LONG_PRESS_DELAY_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Notifies that the user clicked the "enter" button. */
|
||||
fun onAuthenticateButtonClicked() {
|
||||
interactor.authenticate(entered.value)
|
||||
entered.value = emptyList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting const val BACKSPACE_LONG_PRESS_DELAY_MS = 80L
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,9 @@ 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.android.systemui.scene.SceneTestUtils
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
@@ -37,10 +37,10 @@ import org.junit.runners.JUnit4
|
||||
class AuthenticationInteractorTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val repository: AuthenticationRepository = AuthenticationRepositoryImpl()
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val repository: AuthenticationRepository = utils.authenticationRepository()
|
||||
private val underTest =
|
||||
AuthenticationInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
utils.authenticationInteractor(
|
||||
repository = repository,
|
||||
)
|
||||
|
||||
|
||||
@@ -19,13 +19,9 @@ package com.android.systemui.bouncer.domain.interactor
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
|
||||
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.bouncer.data.repo.BouncerRepository
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
|
||||
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -44,23 +40,16 @@ import org.junit.runners.JUnit4
|
||||
class BouncerInteractorTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val authenticationInteractor =
|
||||
AuthenticationInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
repository = AuthenticationRepositoryImpl(),
|
||||
)
|
||||
private val sceneInteractor =
|
||||
SceneInteractor(
|
||||
repository = fakeSceneContainerRepository(),
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
private val sceneInteractor = utils.sceneInteractor()
|
||||
private val underTest =
|
||||
BouncerInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
applicationContext = context,
|
||||
repository = BouncerRepository(),
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = "container1",
|
||||
)
|
||||
|
||||
@Before
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.bouncer.ui.viewmodel
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
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 BouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val authenticationInteractor =
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
private val underTest =
|
||||
utils.bouncerViewModel(
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = utils.sceneInteractor(),
|
||||
)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() =
|
||||
testScope.runTest {
|
||||
val authMethodViewModel: AuthMethodBouncerViewModel? by
|
||||
collectLastValue(underTest.authMethod)
|
||||
authMethodsToTest().forEach { authMethod ->
|
||||
authenticationInteractor.setAuthenticationMethod(authMethod)
|
||||
|
||||
if (authMethod.isSecure) {
|
||||
assertThat(authMethodViewModel).isNotNull()
|
||||
} else {
|
||||
assertThat(authMethodViewModel).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authMethod_reusesInstances() =
|
||||
testScope.runTest {
|
||||
val seen = mutableMapOf<AuthenticationMethodModel, AuthMethodBouncerViewModel>()
|
||||
val authMethodViewModel: AuthMethodBouncerViewModel? by
|
||||
collectLastValue(underTest.authMethod)
|
||||
// First pass, populate our "seen" map:
|
||||
authMethodsToTest().forEach { authMethod ->
|
||||
authenticationInteractor.setAuthenticationMethod(authMethod)
|
||||
authMethodViewModel?.let { seen[authMethod] = it }
|
||||
}
|
||||
|
||||
// Second pass, assert same instances are reused:
|
||||
authMethodsToTest().forEach { authMethod ->
|
||||
authenticationInteractor.setAuthenticationMethod(authMethod)
|
||||
authMethodViewModel?.let { assertThat(it).isSameInstanceAs(seen[authMethod]) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authMethodsToTest_returnsCompleteSampleOfAllAuthMethodTypes() {
|
||||
assertThat(authMethodsToTest().map { it::class }.toSet())
|
||||
.isEqualTo(AuthenticationMethodModel::class.sealedSubclasses.toSet())
|
||||
}
|
||||
|
||||
private fun authMethodsToTest(): List<AuthenticationMethodModel> {
|
||||
return listOf(
|
||||
AuthenticationMethodModel.None,
|
||||
AuthenticationMethodModel.Swipe,
|
||||
AuthenticationMethodModel.PIN(1234),
|
||||
AuthenticationMethodModel.Password("password"),
|
||||
AuthenticationMethodModel.Pattern(
|
||||
listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1))
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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.bouncer.ui.viewmodel
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class PasswordBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val authenticationInteractor =
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
private val sceneInteractor = utils.sceneInteractor()
|
||||
private val bouncerInteractor =
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
)
|
||||
private val bouncerViewModel =
|
||||
utils.bouncerViewModel(
|
||||
bouncerInteractor = bouncerInteractor,
|
||||
)
|
||||
private val underTest =
|
||||
PasswordBouncerViewModel(
|
||||
interactor = bouncerInteractor,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
overrideResource(R.string.keyguard_enter_your_password, ENTER_YOUR_PASSWORD)
|
||||
overrideResource(R.string.kg_wrong_password, WRONG_PASSWORD)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onShown() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val password by collectLastValue(underTest.password)
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Password("password")
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
|
||||
underTest.onShown()
|
||||
|
||||
assertThat(message).isEqualTo(ENTER_YOUR_PASSWORD)
|
||||
assertThat(password).isEqualTo("")
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onPasswordInputChanged() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val password by collectLastValue(underTest.password)
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Password("password")
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
|
||||
underTest.onPasswordInputChanged("password")
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(password).isEqualTo("password")
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onAuthenticateKeyPressed_whenCorrect() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Password("password")
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onPasswordInputChanged("password")
|
||||
|
||||
underTest.onAuthenticateKeyPressed()
|
||||
|
||||
assertThat(isUnlocked).isTrue()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onAuthenticateKeyPressed_whenWrong() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val password by collectLastValue(underTest.password)
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Password("password")
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onPasswordInputChanged("wrong")
|
||||
|
||||
underTest.onAuthenticateKeyPressed()
|
||||
|
||||
assertThat(password).isEqualTo("")
|
||||
assertThat(message).isEqualTo(WRONG_PASSWORD)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onAuthenticateKeyPressed_correctAfterWrong() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val password by collectLastValue(underTest.password)
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Password("password")
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onPasswordInputChanged("wrong")
|
||||
underTest.onAuthenticateKeyPressed()
|
||||
assertThat(password).isEqualTo("")
|
||||
assertThat(message).isEqualTo(WRONG_PASSWORD)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
|
||||
// Enter the correct password:
|
||||
underTest.onPasswordInputChanged("password")
|
||||
assertThat(message).isEmpty()
|
||||
|
||||
underTest.onAuthenticateKeyPressed()
|
||||
|
||||
assertThat(isUnlocked).isTrue()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTAINER_NAME = "container1"
|
||||
private const val ENTER_YOUR_PASSWORD = "Enter your password"
|
||||
private const val WRONG_PASSWORD = "Wrong password"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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.bouncer.ui.viewmodel
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.truth.Truth.assertWithMessage
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class PatternBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val authenticationInteractor =
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
private val sceneInteractor = utils.sceneInteractor()
|
||||
private val bouncerInteractor =
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
)
|
||||
private val bouncerViewModel =
|
||||
utils.bouncerViewModel(
|
||||
bouncerInteractor = bouncerInteractor,
|
||||
)
|
||||
private val underTest =
|
||||
PatternBouncerViewModel(
|
||||
applicationContext = context,
|
||||
applicationScope = testScope.backgroundScope,
|
||||
interactor = bouncerInteractor,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
overrideResource(R.string.keyguard_enter_your_pattern, ENTER_YOUR_PATTERN)
|
||||
overrideResource(R.string.kg_wrong_pattern, WRONG_PATTERN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onShown() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val selectedDots by collectLastValue(underTest.selectedDots)
|
||||
val currentDot by collectLastValue(underTest.currentDot)
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
|
||||
underTest.onShown()
|
||||
|
||||
assertThat(message).isEqualTo(ENTER_YOUR_PATTERN)
|
||||
assertThat(selectedDots).isEmpty()
|
||||
assertThat(currentDot).isNull()
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDragStart() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val selectedDots by collectLastValue(underTest.selectedDots)
|
||||
val currentDot by collectLastValue(underTest.currentDot)
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
|
||||
underTest.onDragStart()
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(selectedDots).isEmpty()
|
||||
assertThat(currentDot).isNull()
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDragEnd_whenCorrect() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val selectedDots by collectLastValue(underTest.selectedDots)
|
||||
val currentDot by collectLastValue(underTest.currentDot)
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onDragStart()
|
||||
assertThat(currentDot).isNull()
|
||||
CORRECT_PATTERN.forEachIndexed { index, coordinate ->
|
||||
underTest.onDrag(
|
||||
xPx = 30f * coordinate.x + 15,
|
||||
yPx = 30f * coordinate.y + 15,
|
||||
containerSizePx = 90,
|
||||
verticalOffsetPx = 0f,
|
||||
)
|
||||
assertWithMessage("Wrong selected dots for index $index")
|
||||
.that(selectedDots)
|
||||
.isEqualTo(
|
||||
CORRECT_PATTERN.subList(0, index + 1).map {
|
||||
PatternDotViewModel(
|
||||
x = it.x,
|
||||
y = it.y,
|
||||
)
|
||||
}
|
||||
)
|
||||
assertWithMessage("Wrong current dot for index $index")
|
||||
.that(currentDot)
|
||||
.isEqualTo(
|
||||
PatternDotViewModel(
|
||||
x = CORRECT_PATTERN.subList(0, index + 1).last().x,
|
||||
y = CORRECT_PATTERN.subList(0, index + 1).last().y,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
underTest.onDragEnd()
|
||||
|
||||
assertThat(isUnlocked).isTrue()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDragEnd_whenWrong() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val selectedDots by collectLastValue(underTest.selectedDots)
|
||||
val currentDot by collectLastValue(underTest.currentDot)
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onDragStart()
|
||||
CORRECT_PATTERN.subList(0, 3).forEach { coordinate ->
|
||||
underTest.onDrag(
|
||||
xPx = 30f * coordinate.x + 15,
|
||||
yPx = 30f * coordinate.y + 15,
|
||||
containerSizePx = 90,
|
||||
verticalOffsetPx = 0f,
|
||||
)
|
||||
}
|
||||
|
||||
underTest.onDragEnd()
|
||||
|
||||
assertThat(selectedDots).isEmpty()
|
||||
assertThat(currentDot).isNull()
|
||||
assertThat(message).isEqualTo(WRONG_PATTERN)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDragEnd_correctAfterWrong() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val selectedDots by collectLastValue(underTest.selectedDots)
|
||||
val currentDot by collectLastValue(underTest.currentDot)
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Pattern(CORRECT_PATTERN)
|
||||
)
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onDragStart()
|
||||
CORRECT_PATTERN.subList(2, 7).forEach { coordinate ->
|
||||
underTest.onDrag(
|
||||
xPx = 30f * coordinate.x + 15,
|
||||
yPx = 30f * coordinate.y + 15,
|
||||
containerSizePx = 90,
|
||||
verticalOffsetPx = 0f,
|
||||
)
|
||||
}
|
||||
underTest.onDragEnd()
|
||||
assertThat(selectedDots).isEmpty()
|
||||
assertThat(currentDot).isNull()
|
||||
assertThat(message).isEqualTo(WRONG_PATTERN)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
|
||||
// Enter the correct pattern:
|
||||
CORRECT_PATTERN.forEach { coordinate ->
|
||||
underTest.onDrag(
|
||||
xPx = 30f * coordinate.x + 15,
|
||||
yPx = 30f * coordinate.y + 15,
|
||||
containerSizePx = 90,
|
||||
verticalOffsetPx = 0f,
|
||||
)
|
||||
}
|
||||
|
||||
underTest.onDragEnd()
|
||||
|
||||
assertThat(isUnlocked).isTrue()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTAINER_NAME = "container1"
|
||||
private const val ENTER_YOUR_PATTERN = "Enter your pattern"
|
||||
private const val WRONG_PATTERN = "Wrong pattern"
|
||||
private val CORRECT_PATTERN =
|
||||
listOf(
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(x = 1, y = 1),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(x = 0, y = 1),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(x = 0, y = 0),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(x = 1, y = 0),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(x = 2, y = 0),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(x = 2, y = 1),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(x = 2, y = 2),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(x = 1, y = 2),
|
||||
AuthenticationMethodModel.Pattern.PatternCoordinate(x = 0, y = 2),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* 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.bouncer.ui.viewmodel
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class PinBouncerViewModelTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val sceneInteractor = utils.sceneInteractor()
|
||||
private val authenticationInteractor =
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
private val bouncerInteractor =
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
)
|
||||
private val bouncerViewModel =
|
||||
BouncerViewModel(
|
||||
applicationContext = context,
|
||||
applicationScope = testScope.backgroundScope,
|
||||
interactorFactory =
|
||||
object : BouncerInteractor.Factory {
|
||||
override fun create(containerName: String): BouncerInteractor {
|
||||
return bouncerInteractor
|
||||
}
|
||||
},
|
||||
containerName = CONTAINER_NAME,
|
||||
)
|
||||
private val underTest =
|
||||
PinBouncerViewModel(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
interactor = bouncerInteractor,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN)
|
||||
overrideResource(R.string.kg_wrong_pin, WRONG_PIN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onShown() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val pinLengths by collectLastValue(underTest.pinLengths)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
|
||||
underTest.onShown()
|
||||
|
||||
assertThat(message).isEqualTo(ENTER_YOUR_PIN)
|
||||
assertThat(pinLengths).isEqualTo(0 to 0)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onPinButtonClicked() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val pinLengths by collectLastValue(underTest.pinLengths)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
|
||||
underTest.onPinButtonClicked(1)
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(pinLengths).isEqualTo(0 to 1)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBackspaceButtonClicked() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val pinLengths by collectLastValue(underTest.pinLengths)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onPinButtonClicked(1)
|
||||
assertThat(pinLengths).isEqualTo(0 to 1)
|
||||
|
||||
underTest.onBackspaceButtonClicked()
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(pinLengths).isEqualTo(1 to 0)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBackspaceButtonLongPressed() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val pinLengths by collectLastValue(underTest.pinLengths)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onPinButtonClicked(1)
|
||||
underTest.onPinButtonClicked(2)
|
||||
underTest.onPinButtonClicked(3)
|
||||
underTest.onPinButtonClicked(4)
|
||||
|
||||
underTest.onBackspaceButtonLongPressed()
|
||||
repeat(4) { index ->
|
||||
assertThat(pinLengths).isEqualTo(4 - index to 3 - index)
|
||||
advanceTimeBy(PinBouncerViewModel.BACKSPACE_LONG_PRESS_DELAY_MS)
|
||||
}
|
||||
|
||||
assertThat(message).isEmpty()
|
||||
assertThat(pinLengths).isEqualTo(1 to 0)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onAuthenticateButtonClicked_whenCorrect() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onPinButtonClicked(1)
|
||||
underTest.onPinButtonClicked(2)
|
||||
underTest.onPinButtonClicked(3)
|
||||
underTest.onPinButtonClicked(4)
|
||||
|
||||
underTest.onAuthenticateButtonClicked()
|
||||
|
||||
assertThat(isUnlocked).isTrue()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onAuthenticateButtonClicked_whenWrong() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val pinLengths by collectLastValue(underTest.pinLengths)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onPinButtonClicked(1)
|
||||
underTest.onPinButtonClicked(2)
|
||||
underTest.onPinButtonClicked(3)
|
||||
underTest.onPinButtonClicked(4)
|
||||
underTest.onPinButtonClicked(5) // PIN is now wrong!
|
||||
|
||||
underTest.onAuthenticateButtonClicked()
|
||||
|
||||
assertThat(pinLengths).isEqualTo(0 to 0)
|
||||
assertThat(message).isEqualTo(WRONG_PIN)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onAuthenticateButtonClicked_correctAfterWrong() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val message by collectLastValue(bouncerViewModel.message)
|
||||
val pinLengths by collectLastValue(underTest.pinLengths)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
underTest.onShown()
|
||||
underTest.onPinButtonClicked(1)
|
||||
underTest.onPinButtonClicked(2)
|
||||
underTest.onPinButtonClicked(3)
|
||||
underTest.onPinButtonClicked(4)
|
||||
underTest.onPinButtonClicked(5) // PIN is now wrong!
|
||||
underTest.onAuthenticateButtonClicked()
|
||||
assertThat(message).isEqualTo(WRONG_PIN)
|
||||
assertThat(pinLengths).isEqualTo(0 to 0)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
|
||||
// Enter the correct PIN:
|
||||
underTest.onPinButtonClicked(1)
|
||||
underTest.onPinButtonClicked(2)
|
||||
underTest.onPinButtonClicked(3)
|
||||
underTest.onPinButtonClicked(4)
|
||||
assertThat(message).isEmpty()
|
||||
|
||||
underTest.onAuthenticateButtonClicked()
|
||||
|
||||
assertThat(isUnlocked).isTrue()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTAINER_NAME = "container1"
|
||||
private const val ENTER_YOUR_PIN = "Enter your pin"
|
||||
private const val WRONG_PIN = "Wrong pin"
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,10 @@ package com.android.systemui.keyguard.domain.interactor
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
|
||||
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.bouncer.data.repo.BouncerRepository
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
|
||||
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -43,34 +39,21 @@ import org.junit.runners.JUnit4
|
||||
class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val sceneInteractor =
|
||||
SceneInteractor(
|
||||
repository = fakeSceneContainerRepository(),
|
||||
)
|
||||
private val mAuthenticationInteractor =
|
||||
AuthenticationInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
repository = AuthenticationRepositoryImpl(),
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val sceneInteractor = utils.sceneInteractor()
|
||||
private val authenticationInteractor =
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
private val underTest =
|
||||
LockScreenSceneInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
authenticationInteractor = mAuthenticationInteractor,
|
||||
bouncerInteractorFactory =
|
||||
object : BouncerInteractor.Factory {
|
||||
override fun create(containerName: String): BouncerInteractor {
|
||||
return BouncerInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
applicationContext = context,
|
||||
repository = BouncerRepository(),
|
||||
authenticationInteractor = mAuthenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = containerName,
|
||||
)
|
||||
}
|
||||
},
|
||||
utils.lockScreenSceneInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = CONTAINER_NAME,
|
||||
bouncerInteractor =
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -78,10 +61,10 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
testScope.runTest {
|
||||
val isDeviceLocked by collectLastValue(underTest.isDeviceLocked)
|
||||
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
authenticationInteractor.lockDevice()
|
||||
assertThat(isDeviceLocked).isTrue()
|
||||
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
authenticationInteractor.unlockDevice()
|
||||
assertThat(isDeviceLocked).isFalse()
|
||||
}
|
||||
|
||||
@@ -90,8 +73,8 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
testScope.runTest {
|
||||
val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
|
||||
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
authenticationInteractor.lockDevice()
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
|
||||
assertThat(isSwipeToDismissEnabled).isTrue()
|
||||
}
|
||||
@@ -101,8 +84,8 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
testScope.runTest {
|
||||
val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
|
||||
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
authenticationInteractor.unlockDevice()
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
|
||||
assertThat(isSwipeToDismissEnabled).isFalse()
|
||||
}
|
||||
@@ -110,9 +93,9 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun dismissLockScreen_deviceLockedWithSecureAuthMethod_switchesToBouncer() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.lockDevice()
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
|
||||
|
||||
underTest.dismissLockScreen()
|
||||
@@ -123,9 +106,9 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun dismissLockScreen_deviceUnlocked_switchesToGone() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.unlockDevice()
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
|
||||
|
||||
underTest.dismissLockScreen()
|
||||
@@ -136,9 +119,9 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun dismissLockScreen_deviceLockedWithInsecureAuthMethod_switchesToGone() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.lockDevice()
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
|
||||
|
||||
underTest.dismissLockScreen()
|
||||
@@ -149,15 +132,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun deviceLockedInNonLockScreenScene_switchesToLockScreenScene() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
runCurrent()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone))
|
||||
runCurrent()
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
authenticationInteractor.unlockDevice()
|
||||
runCurrent()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
authenticationInteractor.lockDevice()
|
||||
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
|
||||
}
|
||||
@@ -165,15 +148,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun deviceBiometricUnlockedInLockScreen_bypassEnabled_switchesToGone() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
|
||||
if (!mAuthenticationInteractor.isBypassEnabled.value) {
|
||||
mAuthenticationInteractor.toggleBypassEnabled()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen))
|
||||
if (!authenticationInteractor.isBypassEnabled.value) {
|
||||
authenticationInteractor.toggleBypassEnabled()
|
||||
}
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
|
||||
|
||||
mAuthenticationInteractor.biometricUnlock()
|
||||
authenticationInteractor.biometricUnlock()
|
||||
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
}
|
||||
@@ -181,15 +164,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun deviceBiometricUnlockedInLockScreen_bypassNotEnabled_doesNotSwitch() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
|
||||
if (mAuthenticationInteractor.isBypassEnabled.value) {
|
||||
mAuthenticationInteractor.toggleBypassEnabled()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.lockDevice()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen))
|
||||
if (authenticationInteractor.isBypassEnabled.value) {
|
||||
authenticationInteractor.toggleBypassEnabled()
|
||||
}
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
|
||||
|
||||
mAuthenticationInteractor.biometricUnlock()
|
||||
authenticationInteractor.biometricUnlock()
|
||||
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
|
||||
}
|
||||
@@ -197,12 +180,12 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun switchFromLockScreenToGone_authMethodSwipe_unlocksDevice() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
assertThat(isUnlocked).isFalse()
|
||||
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone))
|
||||
|
||||
assertThat(isUnlocked).isTrue()
|
||||
}
|
||||
@@ -210,12 +193,12 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
assertThat(isUnlocked).isFalse()
|
||||
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone))
|
||||
|
||||
assertThat(isUnlocked).isFalse()
|
||||
}
|
||||
@@ -223,15 +206,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun switchFromNonLockScreenToGone_authMethodSwipe_doesNotUnlockDevice() =
|
||||
testScope.runTest {
|
||||
val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
|
||||
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
|
||||
runCurrent()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Shade))
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Shade))
|
||||
runCurrent()
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
runCurrent()
|
||||
assertThat(isUnlocked).isFalse()
|
||||
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone))
|
||||
|
||||
assertThat(isUnlocked).isFalse()
|
||||
}
|
||||
@@ -239,12 +222,12 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun authMethodChangedToNone_onLockScreenScene_dismissesLockScreen() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
|
||||
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
|
||||
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
}
|
||||
@@ -252,19 +235,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun authMethodChangedToNone_notOnLockScreenScene_doesNotDismissLockScreen() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
runCurrent()
|
||||
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.QuickSettings))
|
||||
sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.QuickSettings))
|
||||
runCurrent()
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
|
||||
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
|
||||
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTAINER_NAME = "container1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,12 @@ package com.android.systemui.keyguard.ui.viewmodel
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
|
||||
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.bouncer.data.repo.BouncerRepository
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
|
||||
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
|
||||
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -46,14 +42,11 @@ import org.junit.runners.JUnit4
|
||||
class LockScreenSceneViewModelTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val sceneInteractor =
|
||||
SceneInteractor(
|
||||
repository = fakeSceneContainerRepository(),
|
||||
)
|
||||
private val mAuthenticationInteractor =
|
||||
AuthenticationInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
repository = AuthenticationRepositoryImpl(),
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val sceneInteractor = utils.sceneInteractor()
|
||||
private val authenticationInteractor =
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
|
||||
private val underTest =
|
||||
@@ -62,38 +55,28 @@ class LockScreenSceneViewModelTest : SysuiTestCase() {
|
||||
interactorFactory =
|
||||
object : LockScreenSceneInteractor.Factory {
|
||||
override fun create(containerName: String): LockScreenSceneInteractor {
|
||||
return LockScreenSceneInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
authenticationInteractor = mAuthenticationInteractor,
|
||||
bouncerInteractorFactory =
|
||||
object : BouncerInteractor.Factory {
|
||||
override fun create(containerName: String): BouncerInteractor {
|
||||
return BouncerInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
applicationContext = context,
|
||||
repository = BouncerRepository(),
|
||||
authenticationInteractor = mAuthenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = containerName,
|
||||
)
|
||||
}
|
||||
},
|
||||
return utils.lockScreenSceneInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = CONTAINER_NAME,
|
||||
bouncerInteractor =
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
containerName = CONTAINER_NAME
|
||||
containerName = CONTAINER_1
|
||||
)
|
||||
|
||||
@Test
|
||||
fun lockButtonIcon_whenLocked() =
|
||||
testScope.runTest {
|
||||
val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
|
||||
mAuthenticationInteractor.setAuthenticationMethod(
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Password("password")
|
||||
)
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
authenticationInteractor.lockDevice()
|
||||
|
||||
assertThat((lockButtonIcon as? Icon.Resource)?.res)
|
||||
.isEqualTo(R.drawable.ic_device_lock_on)
|
||||
@@ -103,10 +86,10 @@ class LockScreenSceneViewModelTest : SysuiTestCase() {
|
||||
fun lockButtonIcon_whenUnlocked() =
|
||||
testScope.runTest {
|
||||
val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
|
||||
mAuthenticationInteractor.setAuthenticationMethod(
|
||||
authenticationInteractor.setAuthenticationMethod(
|
||||
AuthenticationMethodModel.Password("password")
|
||||
)
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
authenticationInteractor.unlockDevice()
|
||||
|
||||
assertThat((lockButtonIcon as? Icon.Resource)?.res)
|
||||
.isEqualTo(R.drawable.ic_device_lock_off)
|
||||
@@ -116,8 +99,8 @@ class LockScreenSceneViewModelTest : SysuiTestCase() {
|
||||
fun upTransitionSceneKey_swipeToUnlockedEnabled_gone() =
|
||||
testScope.runTest {
|
||||
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
|
||||
authenticationInteractor.lockDevice()
|
||||
|
||||
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
|
||||
}
|
||||
@@ -126,8 +109,8 @@ class LockScreenSceneViewModelTest : SysuiTestCase() {
|
||||
fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() =
|
||||
testScope.runTest {
|
||||
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
|
||||
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer)
|
||||
}
|
||||
@@ -135,9 +118,9 @@ class LockScreenSceneViewModelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
runCurrent()
|
||||
|
||||
underTest.onLockButtonClicked()
|
||||
@@ -148,9 +131,9 @@ class LockScreenSceneViewModelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun onContentClicked_deviceUnlocked_switchesToGone() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.unlockDevice()
|
||||
runCurrent()
|
||||
|
||||
underTest.onContentClicked()
|
||||
@@ -161,9 +144,9 @@ class LockScreenSceneViewModelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
runCurrent()
|
||||
|
||||
underTest.onContentClicked()
|
||||
@@ -174,17 +157,13 @@ class LockScreenSceneViewModelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun onLockButtonClicked_deviceUnlocked_switchesToGone() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.unlockDevice()
|
||||
runCurrent()
|
||||
|
||||
underTest.onLockButtonClicked()
|
||||
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTAINER_NAME = "container1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,11 @@ package com.android.systemui.qs.ui.viewmodel
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
|
||||
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.bouncer.data.repo.BouncerRepository
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
|
||||
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
|
||||
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -44,14 +40,11 @@ import org.junit.runners.JUnit4
|
||||
class QuickSettingsSceneViewModelTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val sceneInteractor =
|
||||
SceneInteractor(
|
||||
repository = fakeSceneContainerRepository(),
|
||||
)
|
||||
private val mAuthenticationInteractor =
|
||||
AuthenticationInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
repository = AuthenticationRepositoryImpl(),
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val sceneInteractor = utils.sceneInteractor()
|
||||
private val authenticationInteractor =
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
|
||||
private val underTest =
|
||||
@@ -59,36 +52,26 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
|
||||
lockScreenSceneInteractorFactory =
|
||||
object : LockScreenSceneInteractor.Factory {
|
||||
override fun create(containerName: String): LockScreenSceneInteractor {
|
||||
return LockScreenSceneInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
authenticationInteractor = mAuthenticationInteractor,
|
||||
bouncerInteractorFactory =
|
||||
object : BouncerInteractor.Factory {
|
||||
override fun create(containerName: String): BouncerInteractor {
|
||||
return BouncerInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
applicationContext = context,
|
||||
repository = BouncerRepository(),
|
||||
authenticationInteractor = mAuthenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = containerName,
|
||||
)
|
||||
}
|
||||
},
|
||||
return utils.lockScreenSceneInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = CONTAINER_NAME,
|
||||
bouncerInteractor =
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
containerName = CONTAINER_NAME
|
||||
containerName = CONTAINER_1
|
||||
)
|
||||
|
||||
@Test
|
||||
fun onContentClicked_deviceUnlocked_switchesToGone() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.unlockDevice()
|
||||
runCurrent()
|
||||
|
||||
underTest.onContentClicked()
|
||||
@@ -99,17 +82,13 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
runCurrent()
|
||||
|
||||
underTest.onContentClicked()
|
||||
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTAINER_NAME = "container1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* 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.scene.data.repository
|
||||
|
||||
import com.android.systemui.scene.data.model.SceneContainerConfig
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
|
||||
fun fakeSceneContainerRepository(
|
||||
containerConfigurations: Set<SceneContainerConfig> =
|
||||
setOf(
|
||||
fakeSceneContainerConfig("container1"),
|
||||
fakeSceneContainerConfig("container2"),
|
||||
)
|
||||
): SceneContainerRepository {
|
||||
return SceneContainerRepository(containerConfigurations)
|
||||
}
|
||||
|
||||
fun fakeSceneKeys(): List<SceneKey> {
|
||||
return listOf(
|
||||
SceneKey.QuickSettings,
|
||||
SceneKey.Shade,
|
||||
SceneKey.LockScreen,
|
||||
SceneKey.Bouncer,
|
||||
SceneKey.Gone,
|
||||
)
|
||||
}
|
||||
|
||||
fun fakeSceneContainerConfig(
|
||||
name: String,
|
||||
sceneKeys: List<SceneKey> = fakeSceneKeys(),
|
||||
): SceneContainerConfig {
|
||||
return SceneContainerConfig(
|
||||
name = name,
|
||||
sceneKeys = sceneKeys,
|
||||
initialSceneKey = SceneKey.LockScreen,
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,7 @@ package com.android.systemui.scene.data.repository
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -34,9 +35,11 @@ import org.junit.runners.JUnit4
|
||||
@RunWith(JUnit4::class)
|
||||
class SceneContainerRepositoryTest : SysuiTestCase() {
|
||||
|
||||
private val utils = SceneTestUtils(this)
|
||||
|
||||
@Test
|
||||
fun allSceneKeys() {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
assertThat(underTest.allSceneKeys("container1"))
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
@@ -51,13 +54,13 @@ class SceneContainerRepositoryTest : SysuiTestCase() {
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun allSceneKeys_noSuchContainer_throws() {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
underTest.allSceneKeys("nonExistingContainer")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun currentScene() = runTest {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
val currentScene by collectLastValue(underTest.currentScene("container1"))
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
|
||||
|
||||
@@ -67,23 +70,23 @@ class SceneContainerRepositoryTest : SysuiTestCase() {
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun currentScene_noSuchContainer_throws() {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
underTest.currentScene("nonExistingContainer")
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun setCurrentScene_noSuchContainer_throws() {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
underTest.setCurrentScene("nonExistingContainer", SceneModel(SceneKey.Shade))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun setCurrentScene_noSuchSceneInContainer_throws() {
|
||||
val underTest =
|
||||
fakeSceneContainerRepository(
|
||||
utils.fakeSceneContainerRepository(
|
||||
setOf(
|
||||
fakeSceneContainerConfig("container1"),
|
||||
fakeSceneContainerConfig(
|
||||
utils.fakeSceneContainerConfig("container1"),
|
||||
utils.fakeSceneContainerConfig(
|
||||
"container2",
|
||||
listOf(SceneKey.QuickSettings, SceneKey.LockScreen)
|
||||
),
|
||||
@@ -94,7 +97,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun isVisible() = runTest {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
val isVisible by collectLastValue(underTest.isVisible("container1"))
|
||||
assertThat(isVisible).isTrue()
|
||||
|
||||
@@ -107,19 +110,19 @@ class SceneContainerRepositoryTest : SysuiTestCase() {
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun isVisible_noSuchContainer_throws() {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
underTest.isVisible("nonExistingContainer")
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun setVisible_noSuchContainer_throws() {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
underTest.setVisible("nonExistingContainer", false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sceneTransitionProgress() = runTest {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
val sceneTransitionProgress by
|
||||
collectLastValue(underTest.sceneTransitionProgress("container1"))
|
||||
assertThat(sceneTransitionProgress).isEqualTo(1f)
|
||||
@@ -133,7 +136,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() {
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun sceneTransitionProgress_noSuchContainer_throws() {
|
||||
val underTest = fakeSceneContainerRepository()
|
||||
val underTest = utils.fakeSceneContainerRepository()
|
||||
underTest.sceneTransitionProgress("nonExistingContainer")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,7 @@ package com.android.systemui.scene.domain.interactor
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
|
||||
import com.android.systemui.scene.data.repository.fakeSceneKeys
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -36,14 +35,12 @@ import org.junit.runners.JUnit4
|
||||
@RunWith(JUnit4::class)
|
||||
class SceneInteractorTest : SysuiTestCase() {
|
||||
|
||||
private val underTest =
|
||||
SceneInteractor(
|
||||
repository = fakeSceneContainerRepository(),
|
||||
)
|
||||
private val utils = SceneTestUtils(this)
|
||||
private val underTest = utils.sceneInteractor()
|
||||
|
||||
@Test
|
||||
fun allSceneKeys() {
|
||||
assertThat(underTest.allSceneKeys("container1")).isEqualTo(fakeSceneKeys())
|
||||
assertThat(underTest.allSceneKeys("container1")).isEqualTo(utils.fakeSceneKeys())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -21,9 +21,7 @@ package com.android.systemui.scene.ui.viewmodel
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
|
||||
import com.android.systemui.scene.data.repository.fakeSceneKeys
|
||||
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -36,10 +34,9 @@ import org.junit.runners.JUnit4
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class SceneContainerViewModelTest : SysuiTestCase() {
|
||||
private val interactor =
|
||||
SceneInteractor(
|
||||
repository = fakeSceneContainerRepository(),
|
||||
)
|
||||
|
||||
private val utils = SceneTestUtils(this)
|
||||
private val interactor = utils.sceneInteractor()
|
||||
private val underTest =
|
||||
SceneContainerViewModel(
|
||||
interactor = interactor,
|
||||
@@ -60,7 +57,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun allSceneKeys() {
|
||||
assertThat(underTest.allSceneKeys).isEqualTo(fakeSceneKeys())
|
||||
assertThat(underTest.allSceneKeys).isEqualTo(utils.fakeSceneKeys())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,15 +18,11 @@ package com.android.systemui.shade.ui.viewmodel
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
|
||||
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
|
||||
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
|
||||
import com.android.systemui.bouncer.data.repo.BouncerRepository
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
|
||||
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
|
||||
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.shared.model.SceneModel
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -44,14 +40,11 @@ import org.junit.runners.JUnit4
|
||||
class ShadeSceneViewModelTest : SysuiTestCase() {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val sceneInteractor =
|
||||
SceneInteractor(
|
||||
repository = fakeSceneContainerRepository(),
|
||||
)
|
||||
private val mAuthenticationInteractor =
|
||||
AuthenticationInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
repository = AuthenticationRepositoryImpl(),
|
||||
private val utils = SceneTestUtils(this, testScope)
|
||||
private val sceneInteractor = utils.sceneInteractor()
|
||||
private val authenticationInteractor =
|
||||
utils.authenticationInteractor(
|
||||
repository = utils.authenticationRepository(),
|
||||
)
|
||||
|
||||
private val underTest =
|
||||
@@ -60,36 +53,26 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
|
||||
lockScreenSceneInteractorFactory =
|
||||
object : LockScreenSceneInteractor.Factory {
|
||||
override fun create(containerName: String): LockScreenSceneInteractor {
|
||||
return LockScreenSceneInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
authenticationInteractor = mAuthenticationInteractor,
|
||||
bouncerInteractorFactory =
|
||||
object : BouncerInteractor.Factory {
|
||||
override fun create(containerName: String): BouncerInteractor {
|
||||
return BouncerInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
applicationContext = context,
|
||||
repository = BouncerRepository(),
|
||||
authenticationInteractor = mAuthenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = containerName,
|
||||
)
|
||||
}
|
||||
},
|
||||
return utils.lockScreenSceneInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = CONTAINER_NAME,
|
||||
bouncerInteractor =
|
||||
utils.bouncerInteractor(
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
containerName = CONTAINER_NAME
|
||||
containerName = SceneTestUtils.CONTAINER_1
|
||||
)
|
||||
|
||||
@Test
|
||||
fun upTransitionSceneKey_deviceLocked_lockScreen() =
|
||||
testScope.runTest {
|
||||
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
|
||||
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.LockScreen)
|
||||
}
|
||||
@@ -98,8 +81,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
|
||||
fun upTransitionSceneKey_deviceUnlocked_gone() =
|
||||
testScope.runTest {
|
||||
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.unlockDevice()
|
||||
|
||||
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
|
||||
}
|
||||
@@ -107,9 +90,9 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun onContentClicked_deviceUnlocked_switchesToGone() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.unlockDevice()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.unlockDevice()
|
||||
runCurrent()
|
||||
|
||||
underTest.onContentClicked()
|
||||
@@ -120,17 +103,13 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
|
||||
testScope.runTest {
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
|
||||
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
mAuthenticationInteractor.lockDevice()
|
||||
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1))
|
||||
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
|
||||
authenticationInteractor.lockDevice()
|
||||
runCurrent()
|
||||
|
||||
underTest.onContentClicked()
|
||||
|
||||
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTAINER_NAME = "container1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.scene
|
||||
|
||||
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.domain.interactor.AuthenticationInteractor
|
||||
import com.android.systemui.bouncer.data.repo.BouncerRepository
|
||||
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
|
||||
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
|
||||
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
|
||||
import com.android.systemui.scene.data.model.SceneContainerConfig
|
||||
import com.android.systemui.scene.data.repository.SceneContainerRepository
|
||||
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
||||
/**
|
||||
* Utilities for creating scene container framework related repositories, interactors, and
|
||||
* view-models for tests.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SceneTestUtils(
|
||||
test: SysuiTestCase,
|
||||
private val testScope: TestScope? = null,
|
||||
) {
|
||||
|
||||
private val context = test.context
|
||||
|
||||
fun fakeSceneContainerRepository(
|
||||
containerConfigurations: Set<SceneContainerConfig> =
|
||||
setOf(
|
||||
fakeSceneContainerConfig(CONTAINER_1),
|
||||
fakeSceneContainerConfig(CONTAINER_2),
|
||||
)
|
||||
): SceneContainerRepository {
|
||||
return SceneContainerRepository(containerConfigurations)
|
||||
}
|
||||
|
||||
fun fakeSceneKeys(): List<SceneKey> {
|
||||
return listOf(
|
||||
SceneKey.QuickSettings,
|
||||
SceneKey.Shade,
|
||||
SceneKey.LockScreen,
|
||||
SceneKey.Bouncer,
|
||||
SceneKey.Gone,
|
||||
)
|
||||
}
|
||||
|
||||
fun fakeSceneContainerConfig(
|
||||
name: String,
|
||||
sceneKeys: List<SceneKey> = fakeSceneKeys(),
|
||||
): SceneContainerConfig {
|
||||
return SceneContainerConfig(
|
||||
name = name,
|
||||
sceneKeys = sceneKeys,
|
||||
initialSceneKey = SceneKey.LockScreen,
|
||||
)
|
||||
}
|
||||
|
||||
fun sceneInteractor(): SceneInteractor {
|
||||
return SceneInteractor(
|
||||
repository = fakeSceneContainerRepository(),
|
||||
)
|
||||
}
|
||||
|
||||
fun authenticationRepository(): AuthenticationRepository {
|
||||
return AuthenticationRepositoryImpl()
|
||||
}
|
||||
|
||||
fun authenticationInteractor(
|
||||
repository: AuthenticationRepository,
|
||||
): AuthenticationInteractor {
|
||||
return AuthenticationInteractor(
|
||||
applicationScope = applicationScope(),
|
||||
repository = repository,
|
||||
)
|
||||
}
|
||||
|
||||
private fun applicationScope(): CoroutineScope {
|
||||
return checkNotNull(testScope) {
|
||||
"""
|
||||
TestScope not initialized, please create a TestScope and inject it into
|
||||
SceneTestUtils.
|
||||
"""
|
||||
.trimIndent()
|
||||
}
|
||||
.backgroundScope
|
||||
}
|
||||
|
||||
fun bouncerInteractor(
|
||||
authenticationInteractor: AuthenticationInteractor,
|
||||
sceneInteractor: SceneInteractor,
|
||||
): BouncerInteractor {
|
||||
return BouncerInteractor(
|
||||
applicationScope = applicationScope(),
|
||||
applicationContext = context,
|
||||
repository = BouncerRepository(),
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = CONTAINER_1,
|
||||
)
|
||||
}
|
||||
|
||||
fun bouncerViewModel(
|
||||
bouncerInteractor: BouncerInteractor,
|
||||
): BouncerViewModel {
|
||||
return BouncerViewModel(
|
||||
applicationContext = context,
|
||||
applicationScope = applicationScope(),
|
||||
interactorFactory =
|
||||
object : BouncerInteractor.Factory {
|
||||
override fun create(containerName: String): BouncerInteractor {
|
||||
return bouncerInteractor
|
||||
}
|
||||
},
|
||||
containerName = CONTAINER_1,
|
||||
)
|
||||
}
|
||||
|
||||
fun lockScreenSceneInteractor(
|
||||
authenticationInteractor: AuthenticationInteractor,
|
||||
sceneInteractor: SceneInteractor,
|
||||
bouncerInteractor: BouncerInteractor,
|
||||
): LockScreenSceneInteractor {
|
||||
return LockScreenSceneInteractor(
|
||||
applicationScope = applicationScope(),
|
||||
authenticationInteractor = authenticationInteractor,
|
||||
bouncerInteractorFactory =
|
||||
object : BouncerInteractor.Factory {
|
||||
override fun create(containerName: String): BouncerInteractor {
|
||||
return bouncerInteractor
|
||||
}
|
||||
},
|
||||
sceneInteractor = sceneInteractor,
|
||||
containerName = CONTAINER_1,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CONTAINER_1 = "container1"
|
||||
const val CONTAINER_2 = "container2"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user