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:
Ale Nijamkin
2023-05-14 23:07:08 +00:00
committed by Android (Google) Code Review
25 changed files with 2479 additions and 372 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = ""
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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