From 6de6b038961054057281169d83c177c2ecd6d39e Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Wed, 10 May 2023 14:47:04 -0700 Subject: [PATCH 1/3] Adds horizontal/vertical grids. These are composable functions that can render fixed-size horizontal or vertical grids of cells. They are "fixed size" as in: they don't scroll like their "Lazy" counterparts. Bug: 280877228 Test: tested manually using the new screen added to the Compose Gallery app. Please see screen recording in the other CL in this topic. Change-Id: I5effca8fd12a0c9e77efb9b3438d2e40adb789f3 --- .../src/com/android/compose/grid/Grids.kt | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt diff --git a/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt new file mode 100644 index 0000000000000..5224c51bb7c34 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt @@ -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() + } + } + } +} From eebf1054030121dc78c3aa4b5a2c3bf68f23cd03 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Fri, 5 May 2023 16:23:25 -0700 Subject: [PATCH 2/3] [flexiglass] Multiple input method bouncer scene. Expanded the placeholder bouncer scene to correctly show different UIs based on the authentication method and call the correct auth APIs to check user input, unlock the device, etc. Bug: 280877228 Test: unit tests included. Manually tested with the entire relation chain in the Compose Gallery testdbed app. Change-Id: I1dff6dc1d849fd0b3d5d5dff300485e3ef770f08 --- .../bouncer/ui/composable/BouncerScene.kt | 93 ++++-- .../bouncer/ui/composable/PasswordBouncer.kt | 99 ++++++ .../bouncer/ui/composable/PatternBouncer.kt | 281 +++++++++++++++++ .../bouncer/ui/composable/PinBouncer.kt | 247 +++++++++++++++ .../viewmodel/AuthMethodBouncerViewModel.kt | 19 ++ .../bouncer/ui/viewmodel/BouncerViewModel.kt | 68 ++-- .../ui/viewmodel/PasswordBouncerViewModel.kt | 52 ++++ .../ui/viewmodel/PatternBouncerViewModel.kt | 189 ++++++++++++ .../ui/viewmodel/PinBouncerViewModel.kt | 101 ++++++ .../ui/viewmodel/BouncerViewModelTest.kt | 128 ++++++++ .../viewmodel/PasswordBouncerViewModelTest.kt | 218 +++++++++++++ .../viewmodel/PatternBouncerViewModelTest.kt | 292 ++++++++++++++++++ .../ui/viewmodel/PinBouncerViewModelTest.kt | 278 +++++++++++++++++ 13 files changed, 2023 insertions(+), 42 deletions(-) create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt create mode 100644 packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt create mode 100644 packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index 853300cf95081..c6c30199a2273 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -16,18 +16,30 @@ package com.android.systemui.bouncer.ui.composable +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel @@ -55,26 +67,69 @@ constructor( ) .asStateFlow() - @Composable - override fun Content( - modifier: Modifier, - ) { - // TODO(b/280877228): implement the real UI. + @Composable override fun Content(modifier: Modifier) = BouncerScene(viewModel, modifier) +} - Box(modifier = modifier) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.align(Alignment.Center) - ) { - Text("Bouncer", style = MaterialTheme.typography.headlineLarge) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button(onClick = { viewModel.onAuthenticateButtonClicked() }) { - Text("Authenticate") - } - } +@Composable +private fun BouncerScene( + viewModel: BouncerViewModel, + modifier: Modifier = Modifier, +) { + val message: String by viewModel.message.collectAsState() + val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState() + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(60.dp), + modifier = + modifier.background(MaterialTheme.colorScheme.surface).fillMaxSize().padding(32.dp) + ) { + Crossfade( + targetState = message, + label = "Bouncer message", + ) { + Text( + text = it, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) + } + + Box(Modifier.weight(1f)) { + when (val nonNullViewModel = authMethodViewModel) { + is PinBouncerViewModel -> + PinBouncer( + viewModel = nonNullViewModel, + modifier = Modifier.align(Alignment.Center), + ) + is PasswordBouncerViewModel -> + PasswordBouncer( + viewModel = nonNullViewModel, + modifier = Modifier.align(Alignment.Center), + ) + is PatternBouncerViewModel -> + PatternBouncer( + viewModel = nonNullViewModel, + modifier = + Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false) + .align(Alignment.BottomCenter), + ) + else -> Unit } } + + Button( + onClick = viewModel::onEmergencyServicesButtonClicked, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + ), + ) { + Text( + text = stringResource(com.android.internal.R.string.lockscreen_emergency_call), + style = MaterialTheme.typography.bodyMedium, + ) + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt new file mode 100644 index 0000000000000..4e85621e9e233 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt @@ -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)) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt new file mode 100644 index 0000000000000..383c748f5dd8f --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt @@ -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 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 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) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt new file mode 100644 index 0000000000000..9c210c225ab39 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt @@ -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 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 diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt new file mode 100644 index 0000000000000..ebefb78f0477d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index 8a183ae6ca7ce..eaa8ed5b358ef 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.bouncer.ui.viewmodel +import android.content.Context import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.dagger.qualifiers.Application @@ -30,12 +31,44 @@ import kotlinx.coroutines.flow.stateIn class BouncerViewModel @AssistedInject constructor( + @Application private val applicationContext: Context, @Application private val applicationScope: CoroutineScope, interactorFactory: BouncerInteractor.Factory, containerName: String, ) { private val interactor: BouncerInteractor = interactorFactory.create(containerName) + private val pin: PinBouncerViewModel by lazy { + PinBouncerViewModel( + applicationScope = applicationScope, + interactor = interactor, + ) + } + + private val password: PasswordBouncerViewModel by lazy { + PasswordBouncerViewModel( + interactor = interactor, + ) + } + + private val pattern: PatternBouncerViewModel by lazy { + PatternBouncerViewModel( + applicationContext = applicationContext, + applicationScope = applicationScope, + interactor = interactor, + ) + } + + /** View-model for the current UI, based on the current authentication method. */ + val authMethod: StateFlow = + interactor.authenticationMethod + .map { authMethod -> toViewModel(authMethod) } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = toViewModel(interactor.authenticationMethod.value), + ) + /** The user-facing message to show in the bouncer. */ val message: StateFlow = interactor.message @@ -46,30 +79,19 @@ constructor( initialValue = interactor.message.value ?: "", ) - /** Notifies that the authenticate button was clicked. */ - fun onAuthenticateButtonClicked() { - // TODO(b/280877228): remove this and send the real input. - interactor.authenticate( - when (interactor.authenticationMethod.value) { - is AuthenticationMethodModel.PIN -> listOf(1, 2, 3, 4) - is AuthenticationMethodModel.Password -> "password".toList() - is AuthenticationMethodModel.Pattern -> - listOf( - AuthenticationMethodModel.Pattern.PatternCoordinate(2, 0), - AuthenticationMethodModel.Pattern.PatternCoordinate(2, 1), - AuthenticationMethodModel.Pattern.PatternCoordinate(2, 2), - AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1), - AuthenticationMethodModel.Pattern.PatternCoordinate(0, 0), - AuthenticationMethodModel.Pattern.PatternCoordinate(0, 1), - AuthenticationMethodModel.Pattern.PatternCoordinate(0, 2), - ) - else -> emptyList() - } - ) - } - /** Notifies that the emergency services button was clicked. */ fun onEmergencyServicesButtonClicked() { - // TODO(b/280877228): implement this. + // TODO(b/280877228): implement this + } + + private fun toViewModel( + authMethod: AuthenticationMethodModel, + ): AuthMethodBouncerViewModel? { + return when (authMethod) { + is AuthenticationMethodModel.PIN -> pin + is AuthenticationMethodModel.Password -> password + is AuthenticationMethodModel.Pattern -> pattern + else -> null + } } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt new file mode 100644 index 0000000000000..730d4e8ba0503 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -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 = _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 = "" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt new file mode 100644 index 0000000000000..eb1b45771ad45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt @@ -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>(linkedSetOf()) + /** The dots that were selected by the user, in the order of selection. */ + val selectedDots: StateFlow> = + _selectedDots + .map { it.toList() } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList(), + ) + + private val _currentDot = MutableStateFlow(null) + /** The most-recently selected dot that the user selected. */ + val currentDot: StateFlow = _currentDot.asStateFlow() + + private val _dots = MutableStateFlow(defaultDots()) + /** All dots on the grid. */ + val dots: StateFlow> = _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().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 { + 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, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt new file mode 100644 index 0000000000000..f9223cb0872e9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -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>(emptyList()) + /** + * The length of the PIN digits that were input so far, two values are supplied the previous and + * the current. + */ + val pinLengths: StateFlow> = + 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 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt new file mode 100644 index 0000000000000..3c8c60368060f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -0,0 +1,128 @@ +/* + * 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.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.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 sceneInteractor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val mAuthenticationInteractor = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = AuthenticationRepositoryImpl(), + ) + private val underTest = + BouncerViewModel( + applicationContext = context, + applicationScope = testScope.backgroundScope, + interactorFactory = + object : BouncerInteractor.Factory { + override fun create(containerName: String): BouncerInteractor { + return BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = mAuthenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_NAME, + ) + } + }, + containerName = CONTAINER_NAME, + ) + + @Test + fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() = + testScope.runTest { + val authMethodViewModel: AuthMethodBouncerViewModel? by + collectLastValue(underTest.authMethod) + authMethodsToTest().forEach { authMethod -> + mAuthenticationInteractor.setAuthenticationMethod(authMethod) + + if (authMethod.isSecure) { + assertThat(authMethodViewModel).isNotNull() + } else { + assertThat(authMethodViewModel).isNull() + } + } + } + + @Test + fun authMethod_reusesInstances() = + testScope.runTest { + val seen = mutableMapOf() + val authMethodViewModel: AuthMethodBouncerViewModel? by + collectLastValue(underTest.authMethod) + // First pass, populate our "seen" map: + authMethodsToTest().forEach { authMethod -> + mAuthenticationInteractor.setAuthenticationMethod(authMethod) + authMethodViewModel?.let { seen[authMethod] = it } + } + + // Second pass, assert same instances are reused: + authMethodsToTest().forEach { authMethod -> + mAuthenticationInteractor.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 { + return listOf( + AuthenticationMethodModel.None, + AuthenticationMethodModel.Swipe, + AuthenticationMethodModel.PIN(1234), + AuthenticationMethodModel.Password("password"), + AuthenticationMethodModel.Pattern( + listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1)) + ), + ) + } + + companion object { + private const val CONTAINER_NAME = "container1" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt new file mode 100644 index 0000000000000..fe808f2f1d85b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -0,0 +1,218 @@ +/* + * 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.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.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 sceneInteractor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val mAuthenticationInteractor = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = AuthenticationRepositoryImpl(), + ) + private val bouncerInteractor = + BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = mAuthenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_NAME, + ) + 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 = + 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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val password by collectLastValue(underTest.password) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val password by collectLastValue(underTest.password) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val password by collectLastValue(underTest.password) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val password by collectLastValue(underTest.password) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Password("password") + ) + mAuthenticationInteractor.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" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt new file mode 100644 index 0000000000000..19d43fb89c251 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -0,0 +1,292 @@ +/* + * 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.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.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 sceneInteractor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val mAuthenticationInteractor = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = AuthenticationRepositoryImpl(), + ) + private val bouncerInteractor = + BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = mAuthenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_NAME, + ) + 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 = + 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(mAuthenticationInteractor.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) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + mAuthenticationInteractor.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(mAuthenticationInteractor.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) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val selectedDots by collectLastValue(underTest.selectedDots) + val currentDot by collectLastValue(underTest.currentDot) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + mAuthenticationInteractor.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(mAuthenticationInteractor.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) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + mAuthenticationInteractor.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(mAuthenticationInteractor.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) + mAuthenticationInteractor.setAuthenticationMethod( + AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + ) + mAuthenticationInteractor.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), + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt new file mode 100644 index 0000000000000..0b6868ffc9939 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -0,0 +1,278 @@ +/* + * 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.data.repository.AuthenticationRepositoryImpl +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repo.BouncerRepository +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.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 sceneInteractor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val mAuthenticationInteractor = + AuthenticationInteractor( + applicationScope = testScope.backgroundScope, + repository = AuthenticationRepositoryImpl(), + ) + private val bouncerInteractor = + BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = context, + repository = BouncerRepository(), + authenticationInteractor = mAuthenticationInteractor, + sceneInteractor = sceneInteractor, + containerName = CONTAINER_NAME, + ) + 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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.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(mAuthenticationInteractor.isUnlocked) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val message by collectLastValue(bouncerViewModel.message) + val pinLengths by collectLastValue(underTest.pinLengths) + mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + mAuthenticationInteractor.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" + } +} From d7438b7172de8352a9ef2e43fe3ea76e1017cd36 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Tue, 9 May 2023 16:50:04 -0700 Subject: [PATCH 3/3] [flexiglass] Introduces SceneTestUtils. Helps increase code reuse in tests. Bug: 279501596 Test: N/A Change-Id: Icb51a009da0e4fcdb6bae6f9a38663f7f843d08b --- .../AuthenticationInteractorTest.kt | 8 +- .../interactor/BouncerInteractorTest.kt | 23 +-- .../ui/viewmodel/BouncerViewModelTest.kt | 51 ++---- .../viewmodel/PasswordBouncerViewModelTest.kt | 70 +++----- .../viewmodel/PatternBouncerViewModelTest.kt | 70 +++----- .../ui/viewmodel/PinBouncerViewModelTest.kt | 69 ++++---- .../LockScreenSceneInteractorTest.kt | 151 +++++++--------- .../viewmodel/LockScreenSceneViewModelTest.kt | 91 ++++------ .../QuickSettingsSceneViewModelTest.kt | 63 +++---- .../systemui/scene/data/repository/Fakes.kt | 51 ------ .../SceneContainerRepositoryTest.kt | 29 ++-- .../domain/interactor/SceneInteractorTest.kt | 11 +- .../viewmodel/SceneContainerViewModelTest.kt | 13 +- .../ui/viewmodel/ShadeSceneViewModelTest.kt | 71 +++----- .../android/systemui/scene/SceneTestUtils.kt | 161 ++++++++++++++++++ 15 files changed, 434 insertions(+), 498 deletions(-) delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index 2e62bebfe3f1a..44c99053eb475 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -19,9 +19,9 @@ package com.android.systemui.authentication.domain.interactor import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.AuthenticationRepository -import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -37,10 +37,10 @@ import org.junit.runners.JUnit4 class AuthenticationInteractorTest : SysuiTestCase() { private val testScope = TestScope() - private val repository: AuthenticationRepository = AuthenticationRepositoryImpl() + private val utils = SceneTestUtils(this, testScope) + private val repository: AuthenticationRepository = utils.authenticationRepository() private val underTest = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, + utils.authenticationInteractor( repository = repository, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index 7dd376ede3617..730f89dd76ba8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -19,13 +19,9 @@ package com.android.systemui.bouncer.domain.interactor import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.data.repo.BouncerRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -44,23 +40,16 @@ import org.junit.runners.JUnit4 class BouncerInteractorTest : SysuiTestCase() { private val testScope = TestScope() + private val utils = SceneTestUtils(this, testScope) private val authenticationInteractor = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, - repository = AuthenticationRepositoryImpl(), - ) - private val sceneInteractor = - SceneInteractor( - repository = fakeSceneContainerRepository(), + utils.authenticationInteractor( + repository = utils.authenticationRepository(), ) + private val sceneInteractor = utils.sceneInteractor() private val underTest = - BouncerInteractor( - applicationScope = testScope.backgroundScope, - applicationContext = context, - repository = BouncerRepository(), + utils.bouncerInteractor( authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - containerName = "container1", ) @Before diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index 3c8c60368060f..954e67d77181b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -18,14 +18,9 @@ package com.android.systemui.bouncer.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.data.repo.BouncerRepository -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -40,33 +35,17 @@ import org.junit.runners.JUnit4 class BouncerViewModelTest : SysuiTestCase() { private val testScope = TestScope() - private val sceneInteractor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) - private val mAuthenticationInteractor = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, - repository = AuthenticationRepositoryImpl(), + private val utils = SceneTestUtils(this, testScope) + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), ) private val underTest = - BouncerViewModel( - applicationContext = context, - applicationScope = testScope.backgroundScope, - interactorFactory = - object : BouncerInteractor.Factory { - override fun create(containerName: String): BouncerInteractor { - return BouncerInteractor( - applicationScope = testScope.backgroundScope, - applicationContext = context, - repository = BouncerRepository(), - authenticationInteractor = mAuthenticationInteractor, - sceneInteractor = sceneInteractor, - containerName = CONTAINER_NAME, - ) - } - }, - containerName = CONTAINER_NAME, + utils.bouncerViewModel( + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = utils.sceneInteractor(), + ) ) @Test @@ -75,7 +54,7 @@ class BouncerViewModelTest : SysuiTestCase() { val authMethodViewModel: AuthMethodBouncerViewModel? by collectLastValue(underTest.authMethod) authMethodsToTest().forEach { authMethod -> - mAuthenticationInteractor.setAuthenticationMethod(authMethod) + authenticationInteractor.setAuthenticationMethod(authMethod) if (authMethod.isSecure) { assertThat(authMethodViewModel).isNotNull() @@ -93,13 +72,13 @@ class BouncerViewModelTest : SysuiTestCase() { collectLastValue(underTest.authMethod) // First pass, populate our "seen" map: authMethodsToTest().forEach { authMethod -> - mAuthenticationInteractor.setAuthenticationMethod(authMethod) + authenticationInteractor.setAuthenticationMethod(authMethod) authMethodViewModel?.let { seen[authMethod] = it } } // Second pass, assert same instances are reused: authMethodsToTest().forEach { authMethod -> - mAuthenticationInteractor.setAuthenticationMethod(authMethod) + authenticationInteractor.setAuthenticationMethod(authMethod) authMethodViewModel?.let { assertThat(it).isSameInstanceAs(seen[authMethod]) } } } @@ -121,8 +100,4 @@ class BouncerViewModelTest : SysuiTestCase() { ), ) } - - companion object { - private const val CONTAINER_NAME = "container1" - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index fe808f2f1d85b..e48b6386c7395 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -19,14 +19,9 @@ 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.data.repository.AuthenticationRepositoryImpl -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.data.repo.BouncerRepository -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -44,35 +39,20 @@ import org.junit.runners.JUnit4 class PasswordBouncerViewModelTest : SysuiTestCase() { private val testScope = TestScope() - private val sceneInteractor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) - private val mAuthenticationInteractor = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, - repository = AuthenticationRepositoryImpl(), + private val utils = SceneTestUtils(this, testScope) + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), ) + private val sceneInteractor = utils.sceneInteractor() private val bouncerInteractor = - BouncerInteractor( - applicationScope = testScope.backgroundScope, - applicationContext = context, - repository = BouncerRepository(), - authenticationInteractor = mAuthenticationInteractor, + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - containerName = CONTAINER_NAME, ) private val bouncerViewModel = - BouncerViewModel( - applicationContext = context, - applicationScope = testScope.backgroundScope, - interactorFactory = - object : BouncerInteractor.Factory { - override fun create(containerName: String): BouncerInteractor { - return bouncerInteractor - } - }, - containerName = CONTAINER_NAME, + utils.bouncerViewModel( + bouncerInteractor = bouncerInteractor, ) private val underTest = PasswordBouncerViewModel( @@ -88,14 +68,14 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Password("password") ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -111,14 +91,14 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onPasswordInputChanged() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Password("password") ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -135,12 +115,12 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateKeyPressed_whenCorrect() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Password("password") ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -156,14 +136,14 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateKeyPressed_whenWrong() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Password("password") ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -181,14 +161,14 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateKeyPressed_correctAfterWrong() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Password("password") ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 19d43fb89c251..6ce29e67982c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -19,14 +19,9 @@ 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.data.repository.AuthenticationRepositoryImpl -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.data.repo.BouncerRepository -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -45,35 +40,20 @@ import org.junit.runners.JUnit4 class PatternBouncerViewModelTest : SysuiTestCase() { private val testScope = TestScope() - private val sceneInteractor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) - private val mAuthenticationInteractor = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, - repository = AuthenticationRepositoryImpl(), + private val utils = SceneTestUtils(this, testScope) + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), ) + private val sceneInteractor = utils.sceneInteractor() private val bouncerInteractor = - BouncerInteractor( - applicationScope = testScope.backgroundScope, - applicationContext = context, - repository = BouncerRepository(), - authenticationInteractor = mAuthenticationInteractor, + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - containerName = CONTAINER_NAME, ) private val bouncerViewModel = - BouncerViewModel( - applicationContext = context, - applicationScope = testScope.backgroundScope, - interactorFactory = - object : BouncerInteractor.Factory { - override fun create(containerName: String): BouncerInteractor { - return bouncerInteractor - } - }, - containerName = CONTAINER_NAME, + utils.bouncerViewModel( + bouncerInteractor = bouncerInteractor, ) private val underTest = PatternBouncerViewModel( @@ -91,15 +71,15 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Pattern(CORRECT_PATTERN) ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -116,15 +96,15 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragStart() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Pattern(CORRECT_PATTERN) ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -142,14 +122,14 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_whenCorrect() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Pattern(CORRECT_PATTERN) ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -192,15 +172,15 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_whenWrong() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Pattern(CORRECT_PATTERN) ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -227,15 +207,15 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_correctAfterWrong() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Pattern(CORRECT_PATTERN) ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 0b6868ffc9939..bb28520ad8a0e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -19,14 +19,10 @@ 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.data.repository.AuthenticationRepositoryImpl -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.data.repo.BouncerRepository import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -45,23 +41,16 @@ import org.junit.runners.JUnit4 class PinBouncerViewModelTest : SysuiTestCase() { private val testScope = TestScope() - private val sceneInteractor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) - private val mAuthenticationInteractor = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, - repository = AuthenticationRepositoryImpl(), + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), ) private val bouncerInteractor = - BouncerInteractor( - applicationScope = testScope.backgroundScope, - applicationContext = context, - repository = BouncerRepository(), - authenticationInteractor = mAuthenticationInteractor, + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - containerName = CONTAINER_NAME, ) private val bouncerViewModel = BouncerViewModel( @@ -90,12 +79,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -111,12 +100,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onPinButtonClicked() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -133,12 +122,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onBackspaceButtonClicked() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -157,12 +146,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onBackspaceButtonLongPressed() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -187,10 +176,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateButtonClicked_whenCorrect() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -209,12 +198,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateButtonClicked_whenWrong() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) @@ -236,12 +225,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateButtonClicked_correctAfterWrong() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + 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) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) assertThat(isUnlocked).isFalse() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt index 749e7a0481eb5..c2c528a9babfc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt @@ -18,14 +18,10 @@ package com.android.systemui.keyguard.domain.interactor import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.data.repo.BouncerRepository -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1 import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -43,34 +39,21 @@ import org.junit.runners.JUnit4 class LockScreenSceneInteractorTest : SysuiTestCase() { private val testScope = TestScope() - private val sceneInteractor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) - private val mAuthenticationInteractor = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, - repository = AuthenticationRepositoryImpl(), + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), ) private val underTest = - LockScreenSceneInteractor( - applicationScope = testScope.backgroundScope, - authenticationInteractor = mAuthenticationInteractor, - bouncerInteractorFactory = - object : BouncerInteractor.Factory { - override fun create(containerName: String): BouncerInteractor { - return BouncerInteractor( - applicationScope = testScope.backgroundScope, - applicationContext = context, - repository = BouncerRepository(), - authenticationInteractor = mAuthenticationInteractor, - sceneInteractor = sceneInteractor, - containerName = containerName, - ) - } - }, + utils.lockScreenSceneInteractor( + authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - containerName = CONTAINER_NAME, + bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ), ) @Test @@ -78,10 +61,10 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { testScope.runTest { val isDeviceLocked by collectLastValue(underTest.isDeviceLocked) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() assertThat(isDeviceLocked).isTrue() - mAuthenticationInteractor.unlockDevice() + authenticationInteractor.unlockDevice() assertThat(isDeviceLocked).isFalse() } @@ -90,8 +73,8 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { testScope.runTest { val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled) - mAuthenticationInteractor.lockDevice() - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + authenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) assertThat(isSwipeToDismissEnabled).isTrue() } @@ -101,8 +84,8 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { testScope.runTest { val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled) - mAuthenticationInteractor.unlockDevice() - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + authenticationInteractor.unlockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) assertThat(isSwipeToDismissEnabled).isFalse() } @@ -110,9 +93,9 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun dismissLockScreen_deviceLockedWithSecureAuthMethod_switchesToBouncer() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.lockDevice() - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) underTest.dismissLockScreen() @@ -123,9 +106,9 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun dismissLockScreen_deviceUnlocked_switchesToGone() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.unlockDevice() - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.unlockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) underTest.dismissLockScreen() @@ -136,9 +119,9 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun dismissLockScreen_deviceLockedWithInsecureAuthMethod_switchesToGone() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.lockDevice() - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) underTest.dismissLockScreen() @@ -149,15 +132,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun deviceLockedInNonLockScreenScene_switchesToLockScreenScene() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) runCurrent() - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone)) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) runCurrent() - mAuthenticationInteractor.unlockDevice() + authenticationInteractor.unlockDevice() runCurrent() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) } @@ -165,15 +148,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun deviceBiometricUnlockedInLockScreen_bypassEnabled_switchesToGone() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.lockDevice() - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) - if (!mAuthenticationInteractor.isBypassEnabled.value) { - mAuthenticationInteractor.toggleBypassEnabled() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen)) + if (!authenticationInteractor.isBypassEnabled.value) { + authenticationInteractor.toggleBypassEnabled() } assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) - mAuthenticationInteractor.biometricUnlock() + authenticationInteractor.biometricUnlock() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } @@ -181,15 +164,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun deviceBiometricUnlockedInLockScreen_bypassNotEnabled_doesNotSwitch() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.lockDevice() - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) - if (mAuthenticationInteractor.isBypassEnabled.value) { - mAuthenticationInteractor.toggleBypassEnabled() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.lockDevice() + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen)) + if (authenticationInteractor.isBypassEnabled.value) { + authenticationInteractor.toggleBypassEnabled() } assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) - mAuthenticationInteractor.biometricUnlock() + authenticationInteractor.biometricUnlock() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) } @@ -197,12 +180,12 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun switchFromLockScreenToGone_authMethodSwipe_unlocksDevice() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) assertThat(isUnlocked).isFalse() - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone)) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) assertThat(isUnlocked).isTrue() } @@ -210,12 +193,12 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) assertThat(isUnlocked).isFalse() - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone)) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) assertThat(isUnlocked).isFalse() } @@ -223,15 +206,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun switchFromNonLockScreenToGone_authMethodSwipe_doesNotUnlockDevice() = testScope.runTest { - val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked) + val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) runCurrent() - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Shade)) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Shade)) runCurrent() - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) runCurrent() assertThat(isUnlocked).isFalse() - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone)) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) assertThat(isUnlocked).isFalse() } @@ -239,12 +222,12 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun authMethodChangedToNone_onLockScreenScene_dismissesLockScreen() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.LockScreen)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } @@ -252,19 +235,15 @@ class LockScreenSceneInteractorTest : SysuiTestCase() { @Test fun authMethodChangedToNone_notOnLockScreenScene_doesNotDismissLockScreen() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) runCurrent() - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.QuickSettings)) + sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.QuickSettings)) runCurrent() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings)) } - - companion object { - private const val CONTAINER_NAME = "container1" - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt index d335b09b196a1..9e8be3e7608f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt @@ -19,16 +19,12 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.data.repo.BouncerRepository -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1 import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -46,14 +42,11 @@ import org.junit.runners.JUnit4 class LockScreenSceneViewModelTest : SysuiTestCase() { private val testScope = TestScope() - private val sceneInteractor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) - private val mAuthenticationInteractor = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, - repository = AuthenticationRepositoryImpl(), + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), ) private val underTest = @@ -62,38 +55,28 @@ class LockScreenSceneViewModelTest : SysuiTestCase() { interactorFactory = object : LockScreenSceneInteractor.Factory { override fun create(containerName: String): LockScreenSceneInteractor { - return LockScreenSceneInteractor( - applicationScope = testScope.backgroundScope, - authenticationInteractor = mAuthenticationInteractor, - bouncerInteractorFactory = - object : BouncerInteractor.Factory { - override fun create(containerName: String): BouncerInteractor { - return BouncerInteractor( - applicationScope = testScope.backgroundScope, - applicationContext = context, - repository = BouncerRepository(), - authenticationInteractor = mAuthenticationInteractor, - sceneInteractor = sceneInteractor, - containerName = containerName, - ) - } - }, + return utils.lockScreenSceneInteractor( + authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - containerName = CONTAINER_NAME, + bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ), ) } }, - containerName = CONTAINER_NAME + containerName = CONTAINER_1 ) @Test fun lockButtonIcon_whenLocked() = testScope.runTest { val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Password("password") ) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.lockDevice() assertThat((lockButtonIcon as? Icon.Resource)?.res) .isEqualTo(R.drawable.ic_device_lock_on) @@ -103,10 +86,10 @@ class LockScreenSceneViewModelTest : SysuiTestCase() { fun lockButtonIcon_whenUnlocked() = testScope.runTest { val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) - mAuthenticationInteractor.setAuthenticationMethod( + authenticationInteractor.setAuthenticationMethod( AuthenticationMethodModel.Password("password") ) - mAuthenticationInteractor.unlockDevice() + authenticationInteractor.unlockDevice() assertThat((lockButtonIcon as? Icon.Resource)?.res) .isEqualTo(R.drawable.ic_device_lock_off) @@ -116,8 +99,8 @@ class LockScreenSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_swipeToUnlockedEnabled_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe) + authenticationInteractor.lockDevice() assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) } @@ -126,8 +109,8 @@ class LockScreenSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) } @@ -135,9 +118,9 @@ class LockScreenSceneViewModelTest : SysuiTestCase() { @Test fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() runCurrent() underTest.onLockButtonClicked() @@ -148,9 +131,9 @@ class LockScreenSceneViewModelTest : SysuiTestCase() { @Test fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.unlockDevice() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() runCurrent() underTest.onContentClicked() @@ -161,9 +144,9 @@ class LockScreenSceneViewModelTest : SysuiTestCase() { @Test fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() runCurrent() underTest.onContentClicked() @@ -174,17 +157,13 @@ class LockScreenSceneViewModelTest : SysuiTestCase() { @Test fun onLockButtonClicked_deviceUnlocked_switchesToGone() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.unlockDevice() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() runCurrent() underTest.onLockButtonClicked() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } - - companion object { - private const val CONTAINER_NAME = "container1" - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index e8875bee5276d..3f838e6516f16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -18,15 +18,11 @@ package com.android.systemui.qs.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.data.repo.BouncerRepository -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1 import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -44,14 +40,11 @@ import org.junit.runners.JUnit4 class QuickSettingsSceneViewModelTest : SysuiTestCase() { private val testScope = TestScope() - private val sceneInteractor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) - private val mAuthenticationInteractor = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, - repository = AuthenticationRepositoryImpl(), + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), ) private val underTest = @@ -59,36 +52,26 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { lockScreenSceneInteractorFactory = object : LockScreenSceneInteractor.Factory { override fun create(containerName: String): LockScreenSceneInteractor { - return LockScreenSceneInteractor( - applicationScope = testScope.backgroundScope, - authenticationInteractor = mAuthenticationInteractor, - bouncerInteractorFactory = - object : BouncerInteractor.Factory { - override fun create(containerName: String): BouncerInteractor { - return BouncerInteractor( - applicationScope = testScope.backgroundScope, - applicationContext = context, - repository = BouncerRepository(), - authenticationInteractor = mAuthenticationInteractor, - sceneInteractor = sceneInteractor, - containerName = containerName, - ) - } - }, + return utils.lockScreenSceneInteractor( + authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - containerName = CONTAINER_NAME, + bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ), ) } }, - containerName = CONTAINER_NAME + containerName = CONTAINER_1 ) @Test fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.unlockDevice() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() runCurrent() underTest.onContentClicked() @@ -99,17 +82,13 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { @Test fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() runCurrent() underTest.onContentClicked() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } - - companion object { - private const val CONTAINER_NAME = "container1" - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt deleted file mode 100644 index 1cdaec0c6581c..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt +++ /dev/null @@ -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 = - setOf( - fakeSceneContainerConfig("container1"), - fakeSceneContainerConfig("container2"), - ) -): SceneContainerRepository { - return SceneContainerRepository(containerConfigurations) -} - -fun fakeSceneKeys(): List { - return listOf( - SceneKey.QuickSettings, - SceneKey.Shade, - SceneKey.LockScreen, - SceneKey.Bouncer, - SceneKey.Gone, - ) -} - -fun fakeSceneContainerConfig( - name: String, - sceneKeys: List = fakeSceneKeys(), -): SceneContainerConfig { - return SceneContainerConfig( - name = name, - sceneKeys = sceneKeys, - initialSceneKey = SceneKey.LockScreen, - ) -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index 9e264db845e1c..6c7017bac68e7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -21,6 +21,7 @@ package com.android.systemui.scene.data.repository import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -34,9 +35,11 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class SceneContainerRepositoryTest : SysuiTestCase() { + private val utils = SceneTestUtils(this) + @Test fun allSceneKeys() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() assertThat(underTest.allSceneKeys("container1")) .isEqualTo( listOf( @@ -51,13 +54,13 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun allSceneKeys_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.allSceneKeys("nonExistingContainer") } @Test fun currentScene() = runTest { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() val currentScene by collectLastValue(underTest.currentScene("container1")) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) @@ -67,23 +70,23 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun currentScene_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.currentScene("nonExistingContainer") } @Test(expected = IllegalStateException::class) fun setCurrentScene_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.setCurrentScene("nonExistingContainer", SceneModel(SceneKey.Shade)) } @Test(expected = IllegalStateException::class) fun setCurrentScene_noSuchSceneInContainer_throws() { val underTest = - fakeSceneContainerRepository( + utils.fakeSceneContainerRepository( setOf( - fakeSceneContainerConfig("container1"), - fakeSceneContainerConfig( + utils.fakeSceneContainerConfig("container1"), + utils.fakeSceneContainerConfig( "container2", listOf(SceneKey.QuickSettings, SceneKey.LockScreen) ), @@ -94,7 +97,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test fun isVisible() = runTest { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() val isVisible by collectLastValue(underTest.isVisible("container1")) assertThat(isVisible).isTrue() @@ -107,19 +110,19 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun isVisible_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.isVisible("nonExistingContainer") } @Test(expected = IllegalStateException::class) fun setVisible_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.setVisible("nonExistingContainer", false) } @Test fun sceneTransitionProgress() = runTest { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() val sceneTransitionProgress by collectLastValue(underTest.sceneTransitionProgress("container1")) assertThat(sceneTransitionProgress).isEqualTo(1f) @@ -133,7 +136,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun sceneTransitionProgress_noSuchContainer_throws() { - val underTest = fakeSceneContainerRepository() + val underTest = utils.fakeSceneContainerRepository() underTest.sceneTransitionProgress("nonExistingContainer") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index c5ce092468627..cf99e3b09b32b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -21,8 +21,7 @@ package com.android.systemui.scene.domain.interactor import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.data.repository.fakeSceneKeys +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -36,14 +35,12 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class SceneInteractorTest : SysuiTestCase() { - private val underTest = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) + private val utils = SceneTestUtils(this) + private val underTest = utils.sceneInteractor() @Test fun allSceneKeys() { - assertThat(underTest.allSceneKeys("container1")).isEqualTo(fakeSceneKeys()) + assertThat(underTest.allSceneKeys("container1")).isEqualTo(utils.fakeSceneKeys()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index ab61ddddaeab5..6105c87357149 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -21,9 +21,7 @@ package com.android.systemui.scene.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.data.repository.fakeSceneKeys -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -36,10 +34,9 @@ import org.junit.runners.JUnit4 @SmallTest @RunWith(JUnit4::class) class SceneContainerViewModelTest : SysuiTestCase() { - private val interactor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) + + private val utils = SceneTestUtils(this) + private val interactor = utils.sceneInteractor() private val underTest = SceneContainerViewModel( interactor = interactor, @@ -60,7 +57,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun allSceneKeys() { - assertThat(underTest.allSceneKeys).isEqualTo(fakeSceneKeys()) + assertThat(underTest.allSceneKeys).isEqualTo(utils.fakeSceneKeys()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 688cce83a55d3..2e7f83d597f2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -18,15 +18,11 @@ package com.android.systemui.shade.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.data.repo.BouncerRepository -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor -import com.android.systemui.scene.data.repository.fakeSceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1 import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -44,14 +40,11 @@ import org.junit.runners.JUnit4 class ShadeSceneViewModelTest : SysuiTestCase() { private val testScope = TestScope() - private val sceneInteractor = - SceneInteractor( - repository = fakeSceneContainerRepository(), - ) - private val mAuthenticationInteractor = - AuthenticationInteractor( - applicationScope = testScope.backgroundScope, - repository = AuthenticationRepositoryImpl(), + private val utils = SceneTestUtils(this, testScope) + private val sceneInteractor = utils.sceneInteractor() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = utils.authenticationRepository(), ) private val underTest = @@ -60,36 +53,26 @@ class ShadeSceneViewModelTest : SysuiTestCase() { lockScreenSceneInteractorFactory = object : LockScreenSceneInteractor.Factory { override fun create(containerName: String): LockScreenSceneInteractor { - return LockScreenSceneInteractor( - applicationScope = testScope.backgroundScope, - authenticationInteractor = mAuthenticationInteractor, - bouncerInteractorFactory = - object : BouncerInteractor.Factory { - override fun create(containerName: String): BouncerInteractor { - return BouncerInteractor( - applicationScope = testScope.backgroundScope, - applicationContext = context, - repository = BouncerRepository(), - authenticationInteractor = mAuthenticationInteractor, - sceneInteractor = sceneInteractor, - containerName = containerName, - ) - } - }, + return utils.lockScreenSceneInteractor( + authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, - containerName = CONTAINER_NAME, + bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ), ) } }, - containerName = CONTAINER_NAME + containerName = SceneTestUtils.CONTAINER_1 ) @Test fun upTransitionSceneKey_deviceLocked_lockScreen() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() assertThat(upTransitionSceneKey).isEqualTo(SceneKey.LockScreen) } @@ -98,8 +81,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_deviceUnlocked_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.unlockDevice() + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) } @@ -107,9 +90,9 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.unlockDevice() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.unlockDevice() runCurrent() underTest.onContentClicked() @@ -120,17 +103,13 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) - mAuthenticationInteractor.lockDevice() + val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234)) + authenticationInteractor.lockDevice() runCurrent() underTest.onContentClicked() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } - - companion object { - private const val CONTAINER_NAME = "container1" - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt new file mode 100644 index 0000000000000..5a350bb540b75 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -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 = + setOf( + fakeSceneContainerConfig(CONTAINER_1), + fakeSceneContainerConfig(CONTAINER_2), + ) + ): SceneContainerRepository { + return SceneContainerRepository(containerConfigurations) + } + + fun fakeSceneKeys(): List { + return listOf( + SceneKey.QuickSettings, + SceneKey.Shade, + SceneKey.LockScreen, + SceneKey.Bouncer, + SceneKey.Gone, + ) + } + + fun fakeSceneContainerConfig( + name: String, + sceneKeys: List = 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" + } +}