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