summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt948
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt68
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt22
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt40
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java6
11 files changed, 616 insertions, 538 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index 87c28862f6f6..47f5663f25a7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalFoundationApi::class)
+
package com.android.systemui.bouncer.ui.composable
import android.app.AlertDialog
@@ -29,18 +31,18 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
@@ -51,6 +53,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
+import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -78,6 +81,7 @@ import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.SceneTransitionLayout
import com.android.compose.animation.scene.transitions
import com.android.compose.modifiers.thenIf
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
@@ -100,37 +104,41 @@ fun BouncerContent(
dialogFactory: BouncerDialogFactory,
modifier: Modifier = Modifier,
) {
- val isFullScreenUserSwitcherEnabled = viewModel.isUserSwitcherVisible
val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsState()
val layout = calculateLayout(isSideBySideSupported = isSideBySideSupported)
- when (layout) {
- BouncerSceneLayout.STANDARD ->
- StandardLayout(
- viewModel = viewModel,
- dialogFactory = dialogFactory,
- modifier = modifier,
- )
- BouncerSceneLayout.SIDE_BY_SIDE ->
- SideBySideLayout(
- viewModel = viewModel,
- dialogFactory = dialogFactory,
- isUserSwitcherVisible = isFullScreenUserSwitcherEnabled,
- modifier = modifier,
- )
- BouncerSceneLayout.STACKED ->
- StackedLayout(
- viewModel = viewModel,
- dialogFactory = dialogFactory,
- isUserSwitcherVisible = isFullScreenUserSwitcherEnabled,
- modifier = modifier,
- )
- BouncerSceneLayout.SPLIT ->
- SplitLayout(
- viewModel = viewModel,
- dialogFactory = dialogFactory,
- modifier = modifier,
- )
+ Box(
+ // Allows the content within each of the layouts to react to the appearance and
+ // disappearance of the IME, which is also known as the software keyboard.
+ //
+ // Despite the keyboard only being part of the password bouncer, adding it at this level is
+ // both necessary to properly handle the keyboard in all layouts and harmless in cases when
+ // the keyboard isn't used (like the PIN or pattern auth methods).
+ modifier = modifier.imePadding(),
+ ) {
+ when (layout) {
+ BouncerSceneLayout.STANDARD_BOUNCER ->
+ StandardLayout(
+ viewModel = viewModel,
+ )
+ BouncerSceneLayout.BESIDE_USER_SWITCHER ->
+ BesideUserSwitcherLayout(
+ viewModel = viewModel,
+ )
+ BouncerSceneLayout.BELOW_USER_SWITCHER ->
+ BelowUserSwitcherLayout(
+ viewModel = viewModel,
+ )
+ BouncerSceneLayout.SPLIT_BOUNCER ->
+ SplitLayout(
+ viewModel = viewModel,
+ )
+ }
+
+ Dialog(
+ viewModel = viewModel,
+ dialogFactory = dialogFactory,
+ )
}
}
@@ -141,15 +149,333 @@ fun BouncerContent(
@Composable
private fun StandardLayout(
viewModel: BouncerViewModel,
- dialogFactory: BouncerDialogFactory,
modifier: Modifier = Modifier,
- layout: BouncerSceneLayout = BouncerSceneLayout.STANDARD,
- outputOnly: Boolean = false,
+) {
+ val isHeightExpanded =
+ LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Expanded
+
+ FoldAware(
+ modifier =
+ modifier.padding(
+ top = 92.dp,
+ bottom = 48.dp,
+ ),
+ viewModel = viewModel,
+ aboveFold = {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ StatusMessage(
+ viewModel = viewModel,
+ modifier = Modifier,
+ )
+
+ OutputArea(
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = if (isHeightExpanded) 96.dp else 64.dp),
+ )
+ }
+ },
+ belowFold = {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Box(
+ modifier = Modifier.weight(1f),
+ ) {
+ InputArea(
+ viewModel = viewModel,
+ pinButtonRowVerticalSpacing = 12.dp,
+ centerPatternDotsVertically = false,
+ modifier = Modifier.align(Alignment.BottomCenter),
+ )
+ }
+
+ ActionArea(
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = 48.dp),
+ )
+ }
+ },
+ )
+}
+
+/**
+ * Renders the bouncer UI in split mode, with half on one side and half on the other side, swappable
+ * by double-tapping on the side.
+ */
+@Composable
+private fun SplitLayout(
+ viewModel: BouncerViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val authMethod by viewModel.authMethodViewModel.collectAsState()
+
+ Row(
+ modifier =
+ modifier
+ .fillMaxHeight()
+ .padding(
+ horizontal = 24.dp,
+ vertical = if (authMethod is PasswordBouncerViewModel) 24.dp else 48.dp,
+ ),
+ ) {
+ // Left side (in left-to-right locales).
+ Box(
+ modifier = Modifier.fillMaxHeight().weight(1f),
+ ) {
+ when (authMethod) {
+ is PinBouncerViewModel -> {
+ StatusMessage(
+ viewModel = viewModel,
+ modifier = Modifier.align(Alignment.TopCenter),
+ )
+
+ OutputArea(viewModel = viewModel, modifier = Modifier.align(Alignment.Center))
+
+ ActionArea(
+ viewModel = viewModel,
+ modifier = Modifier.align(Alignment.BottomCenter).padding(top = 48.dp),
+ )
+ }
+ is PatternBouncerViewModel -> {
+ StatusMessage(
+ viewModel = viewModel,
+ modifier = Modifier.align(Alignment.TopCenter),
+ )
+
+ ActionArea(
+ viewModel = viewModel,
+ modifier = Modifier.align(Alignment.BottomCenter).padding(vertical = 48.dp),
+ )
+ }
+ is PasswordBouncerViewModel -> {
+ ActionArea(
+ viewModel = viewModel,
+ modifier = Modifier.align(Alignment.BottomCenter),
+ )
+ }
+ else -> Unit
+ }
+ }
+
+ // Right side (in right-to-left locales).
+ Box(
+ modifier = Modifier.fillMaxHeight().weight(1f),
+ ) {
+ when (authMethod) {
+ is PinBouncerViewModel,
+ is PatternBouncerViewModel -> {
+ InputArea(
+ viewModel = viewModel,
+ pinButtonRowVerticalSpacing = 8.dp,
+ centerPatternDotsVertically = true,
+ modifier = Modifier.align(Alignment.Center),
+ )
+ }
+ is PasswordBouncerViewModel -> {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth().align(Alignment.Center),
+ ) {
+ StatusMessage(
+ viewModel = viewModel,
+ )
+
+ OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
+ }
+ }
+ else -> Unit
+ }
+ }
+ }
+}
+
+/**
+ * Arranges the bouncer contents and user switcher contents side-by-side, supporting a double tap
+ * anywhere on the background to flip their positions.
+ */
+@Composable
+private fun BesideUserSwitcherLayout(
+ viewModel: BouncerViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val layoutDirection = LocalLayoutDirection.current
+ val isLeftToRight = layoutDirection == LayoutDirection.Ltr
+ val (isSwapped, setSwapped) = rememberSaveable(isLeftToRight) { mutableStateOf(!isLeftToRight) }
+ val isHeightExpanded =
+ LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Expanded
+ val authMethod by viewModel.authMethodViewModel.collectAsState()
+
+ Row(
+ modifier =
+ modifier.pointerInput(Unit) {
+ detectTapGestures(
+ onDoubleTap = { offset ->
+ // Depending on where the user double tapped, switch the elements such that
+ // the endContent is closer to the side that was double tapped.
+ setSwapped(offset.x < size.width / 2)
+ }
+ )
+ },
+ ) {
+ val animatedOffset by
+ animateFloatAsState(
+ targetValue =
+ if (!isSwapped) {
+ // A non-swapped element has its natural placement so it's not offset.
+ 0f
+ } else if (isLeftToRight) {
+ // A swapped element has its elements offset horizontally. In the case of
+ // LTR locales, this means pushing the element to the right, hence the
+ // positive number.
+ 1f
+ } else {
+ // A swapped element has its elements offset horizontally. In the case of
+ // RTL locales, this means pushing the element to the left, hence the
+ // negative number.
+ -1f
+ },
+ label = "offset",
+ )
+
+ fun Modifier.swappable(inversed: Boolean = false): Modifier {
+ return graphicsLayer {
+ translationX =
+ size.width *
+ animatedOffset *
+ if (inversed) {
+ // A negative sign is used to make sure this is offset in the direction
+ // that's opposite to the direction that the user switcher is pushed in.
+ -1
+ } else {
+ 1
+ }
+ alpha = animatedAlpha(animatedOffset)
+ }
+ }
+
+ UserSwitcher(
+ viewModel = viewModel,
+ modifier = Modifier.weight(1f).align(Alignment.CenterVertically).swappable(),
+ )
+
+ FoldAware(
+ modifier =
+ Modifier.weight(1f)
+ .padding(
+ if (isHeightExpanded) {
+ PaddingValues(vertical = 128.dp)
+ } else {
+ PaddingValues(top = 94.dp, bottom = 48.dp)
+ }
+ )
+ .swappable(inversed = true),
+ viewModel = viewModel,
+ aboveFold = {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ StatusMessage(
+ viewModel = viewModel,
+ )
+
+ OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
+ }
+ },
+ belowFold = {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ val isOutputAreaVisible = authMethod !is PatternBouncerViewModel
+ // If there is an output area and the window is not tall enough, spacing needs
+ // to be added between the input and the output areas (otherwise the two get
+ // very squished together).
+ val addSpacingBetweenOutputAndInput = isOutputAreaVisible && !isHeightExpanded
+
+ Box(
+ modifier =
+ Modifier.weight(1f)
+ .padding(top = (if (addSpacingBetweenOutputAndInput) 24 else 0).dp),
+ ) {
+ InputArea(
+ viewModel = viewModel,
+ pinButtonRowVerticalSpacing = 12.dp,
+ centerPatternDotsVertically = true,
+ modifier = Modifier.align(Alignment.BottomCenter),
+ )
+ }
+
+ ActionArea(
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = 48.dp),
+ )
+ }
+ },
+ )
+ }
+}
+
+/** Arranges the bouncer contents and user switcher contents one on top of the other, vertically. */
+@Composable
+private fun BelowUserSwitcherLayout(
+ viewModel: BouncerViewModel,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier =
+ modifier.padding(
+ vertical = 128.dp,
+ )
+ ) {
+ UserSwitcher(
+ viewModel = viewModel,
+ modifier = Modifier.fillMaxWidth(),
+ )
+
+ Spacer(Modifier.weight(1f))
+
+ Box(modifier = Modifier.fillMaxWidth()) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ StatusMessage(
+ viewModel = viewModel,
+ )
+
+ OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
+
+ InputArea(
+ viewModel = viewModel,
+ pinButtonRowVerticalSpacing = 12.dp,
+ centerPatternDotsVertically = true,
+ modifier = Modifier.padding(top = 128.dp),
+ )
+
+ ActionArea(
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = 48.dp),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun FoldAware(
+ viewModel: BouncerViewModel,
+ aboveFold: @Composable BoxScope.() -> Unit,
+ belowFold: @Composable BoxScope.() -> Unit,
+ modifier: Modifier = Modifier,
) {
val foldPosture: FoldPosture by foldPosture()
val isSplitAroundTheFoldRequired by viewModel.isFoldSplitRequired.collectAsState()
- val isSplitAroundTheFold =
- foldPosture == FoldPosture.Tabletop && !outputOnly && isSplitAroundTheFoldRequired
+ val isSplitAroundTheFold = foldPosture == FoldPosture.Tabletop && isSplitAroundTheFoldRequired
val currentSceneKey =
if (isSplitAroundTheFold) SceneKeys.SplitSceneKey else SceneKeys.ContiguousSceneKey
@@ -160,115 +486,57 @@ private fun StandardLayout(
modifier = modifier,
) {
scene(SceneKeys.ContiguousSceneKey) {
- FoldSplittable(
- viewModel = viewModel,
- dialogFactory = dialogFactory,
- layout = layout,
- outputOnly = outputOnly,
+ FoldableScene(
+ aboveFold = aboveFold,
+ belowFold = belowFold,
isSplit = false,
)
}
scene(SceneKeys.SplitSceneKey) {
- FoldSplittable(
- viewModel = viewModel,
- dialogFactory = dialogFactory,
- layout = layout,
- outputOnly = outputOnly,
+ FoldableScene(
+ aboveFold = aboveFold,
+ belowFold = belowFold,
isSplit = true,
)
}
}
}
-/**
- * Renders the "standard" layout of the bouncer, where the bouncer is rendered on its own (no user
- * switcher UI) and laid out vertically, centered horizontally.
- *
- * If [isSplit] is `true`, the top and bottom parts of the bouncer are split such that they don't
- * render across the location of the fold hardware when the device is fully or part-way unfolded
- * with the fold hinge in a horizontal position.
- *
- * If [outputOnly] is `true`, only the "output" part of the UI is shown (where the entered PIN
- * "shapes" appear), if `false`, the entire UI is shown, including the area where the user can enter
- * their PIN or pattern.
- */
@Composable
-private fun SceneScope.FoldSplittable(
- viewModel: BouncerViewModel,
- dialogFactory: BouncerDialogFactory,
- layout: BouncerSceneLayout,
- outputOnly: Boolean,
+private fun SceneScope.FoldableScene(
+ aboveFold: @Composable BoxScope.() -> Unit,
+ belowFold: @Composable BoxScope.() -> Unit,
isSplit: Boolean,
modifier: Modifier = Modifier,
) {
- val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
- val dialogMessage: String? by viewModel.dialogMessage.collectAsState()
- var dialog: Dialog? by remember { mutableStateOf(null) }
- val actionButton: BouncerActionButtonModel? by viewModel.actionButton.collectAsState()
val splitRatio =
LocalContext.current.resources.getFloat(
R.dimen.motion_layout_half_fold_bouncer_height_ratio
)
- Column(modifier = modifier.padding(horizontal = 32.dp)) {
+ Column(
+ modifier = modifier.fillMaxHeight(),
+ ) {
// Content above the fold, when split on a foldable device in a "table top" posture:
Box(
modifier =
Modifier.element(SceneElements.AboveFold)
- .fillMaxWidth()
.then(
if (isSplit) {
Modifier.weight(splitRatio)
- } else if (outputOnly) {
- Modifier.fillMaxHeight()
} else {
Modifier
}
),
) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier.fillMaxWidth().padding(top = layout.topPadding),
- ) {
- Crossfade(
- targetState = message,
- label = "Bouncer message",
- animationSpec = if (message.isUpdateAnimated) tween() else snap(),
- ) { message ->
- Text(
- text = message.text,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.bodyLarge,
- )
- }
-
- if (!outputOnly) {
- Spacer(Modifier.height(layout.spacingBetweenMessageAndEnteredInput))
-
- UserInputArea(
- viewModel = viewModel,
- visibility = UserInputAreaVisibility.OUTPUT_ONLY,
- layout = layout,
- )
- }
- }
-
- if (outputOnly) {
- UserInputArea(
- viewModel = viewModel,
- visibility = UserInputAreaVisibility.OUTPUT_ONLY,
- layout = layout,
- modifier = Modifier.align(Alignment.Center),
- )
- }
+ aboveFold()
}
// Content below the fold, when split on a foldable device in a "table top" posture:
Box(
modifier =
Modifier.element(SceneElements.BelowFold)
- .fillMaxWidth()
.weight(
if (isSplit) {
1 - splitRatio
@@ -277,73 +545,40 @@ private fun SceneScope.FoldSplittable(
}
),
) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier.fillMaxSize()
- ) {
- if (!outputOnly) {
- Box(Modifier.weight(1f)) {
- UserInputArea(
- viewModel = viewModel,
- visibility = UserInputAreaVisibility.INPUT_ONLY,
- layout = layout,
- modifier = Modifier.align(Alignment.BottomCenter),
- )
- }
- }
-
- Spacer(Modifier.height(48.dp))
-
- val actionButtonModifier = Modifier.height(56.dp)
-
- actionButton.let { actionButtonViewModel ->
- if (actionButtonViewModel != null) {
- BouncerActionButton(
- viewModel = actionButtonViewModel,
- modifier = actionButtonModifier,
- )
- } else {
- Spacer(modifier = actionButtonModifier)
- }
- }
-
- Spacer(Modifier.height(layout.bottomPadding))
- }
+ belowFold()
}
+ }
+}
- if (dialogMessage != null) {
- if (dialog == null) {
- dialog =
- dialogFactory().apply {
- setMessage(dialogMessage)
- setButton(
- DialogInterface.BUTTON_NEUTRAL,
- context.getString(R.string.ok),
- ) { _, _ ->
- viewModel.onDialogDismissed()
- }
- setCancelable(false)
- setCanceledOnTouchOutside(false)
- show()
- }
- }
- } else {
- dialog?.dismiss()
- dialog = null
- }
+@Composable
+private fun StatusMessage(
+ viewModel: BouncerViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
+
+ Crossfade(
+ targetState = message,
+ label = "Bouncer message",
+ animationSpec = if (message.isUpdateAnimated) tween() else snap(),
+ modifier = modifier,
+ ) {
+ Text(
+ text = it.text,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.bodyLarge,
+ )
}
}
/**
- * Renders the user input area, where the user interacts with the UI to enter their credentials.
+ * Renders the user output area, where the user sees what they entered.
*
- * For example, this can be the pattern input area, the password text box, or pin pad.
+ * For example, this can be the PIN shapes or password text field.
*/
@Composable
-private fun UserInputArea(
+private fun OutputArea(
viewModel: BouncerViewModel,
- visibility: UserInputAreaVisibility,
- layout: BouncerSceneLayout,
modifier: Modifier = Modifier,
) {
val authMethodViewModel: AuthMethodBouncerViewModel? by
@@ -351,66 +586,115 @@ private fun UserInputArea(
when (val nonNullViewModel = authMethodViewModel) {
is PinBouncerViewModel ->
- when (visibility) {
- UserInputAreaVisibility.OUTPUT_ONLY ->
- PinInputDisplay(
- viewModel = nonNullViewModel,
- modifier = modifier,
- )
- UserInputAreaVisibility.INPUT_ONLY ->
- PinPad(
- viewModel = nonNullViewModel,
- layout = layout,
- modifier = modifier,
- )
- }
+ PinInputDisplay(
+ viewModel = nonNullViewModel,
+ modifier = modifier,
+ )
is PasswordBouncerViewModel ->
- if (visibility == UserInputAreaVisibility.INPUT_ONLY) {
- PasswordBouncer(
- viewModel = nonNullViewModel,
- modifier = modifier,
- )
- }
- is PatternBouncerViewModel ->
- if (visibility == UserInputAreaVisibility.INPUT_ONLY) {
- PatternBouncer(
- viewModel = nonNullViewModel,
- layout = layout,
- modifier = modifier.aspectRatio(1f, matchHeightConstraintsFirst = false),
- )
- }
+ PasswordBouncer(
+ viewModel = nonNullViewModel,
+ modifier = modifier,
+ )
else -> Unit
}
}
/**
- * Renders the action button on the bouncer, which triggers either Return to Call or Emergency Call.
+ * Renders the user input area, where the user enters their credentials.
+ *
+ * For example, this can be the pattern input area or the PIN pad.
*/
-@OptIn(ExperimentalFoundationApi::class)
@Composable
-private fun BouncerActionButton(
- viewModel: BouncerActionButtonModel,
+private fun InputArea(
+ viewModel: BouncerViewModel,
+ pinButtonRowVerticalSpacing: Dp,
+ centerPatternDotsVertically: Boolean,
modifier: Modifier = Modifier,
) {
- Button(
- onClick = viewModel.onClick,
- modifier =
- modifier.thenIf(viewModel.onLongClick != null) {
- Modifier.combinedClickable(
- onClick = viewModel.onClick,
- onLongClick = viewModel.onLongClick,
+ val authMethodViewModel: AuthMethodBouncerViewModel? by
+ viewModel.authMethodViewModel.collectAsState()
+
+ when (val nonNullViewModel = authMethodViewModel) {
+ is PinBouncerViewModel -> {
+ PinPad(
+ viewModel = nonNullViewModel,
+ verticalSpacing = pinButtonRowVerticalSpacing,
+ modifier = modifier,
+ )
+ }
+ is PatternBouncerViewModel -> {
+ PatternBouncer(
+ viewModel = nonNullViewModel,
+ centerDotsVertically = centerPatternDotsVertically,
+ modifier = modifier,
+ )
+ }
+ else -> Unit
+ }
+}
+
+@Composable
+private fun ActionArea(
+ viewModel: BouncerViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val actionButton: BouncerActionButtonModel? by viewModel.actionButton.collectAsState()
+
+ actionButton?.let { actionButtonViewModel ->
+ Box(
+ modifier = modifier,
+ ) {
+ Button(
+ onClick = actionButtonViewModel.onClick,
+ modifier =
+ Modifier.height(56.dp).thenIf(actionButtonViewModel.onLongClick != null) {
+ Modifier.combinedClickable(
+ onClick = actionButtonViewModel.onClick,
+ onLongClick = actionButtonViewModel.onLongClick,
+ )
+ },
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer,
+ contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
+ ),
+ ) {
+ Text(
+ text = actionButtonViewModel.label,
+ style = MaterialTheme.typography.bodyMedium,
)
- },
- colors =
- ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.tertiaryContainer,
- contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
- ),
- ) {
- Text(
- text = viewModel.label,
- style = MaterialTheme.typography.bodyMedium,
- )
+ }
+ }
+ }
+}
+
+@Composable
+private fun Dialog(
+ viewModel: BouncerViewModel,
+ dialogFactory: BouncerDialogFactory,
+) {
+ val dialogMessage: String? by viewModel.dialogMessage.collectAsState()
+ var dialog: Dialog? by remember { mutableStateOf(null) }
+
+ if (dialogMessage != null) {
+ if (dialog == null) {
+ dialog =
+ dialogFactory().apply {
+ setMessage(dialogMessage)
+ setButton(
+ DialogInterface.BUTTON_NEUTRAL,
+ context.getString(R.string.ok),
+ ) { _, _ ->
+ viewModel.onDialogDismissed()
+ }
+ setCancelable(false)
+ setCanceledOnTouchOutside(false)
+ show()
+ }
+ }
+ } else {
+ dialog?.dismiss()
+ dialog = null
}
}
@@ -420,6 +704,14 @@ private fun UserSwitcher(
viewModel: BouncerViewModel,
modifier: Modifier = Modifier,
) {
+ if (!viewModel.isUserSwitcherVisible) {
+ // Take up the same space as the user switcher normally would, but with nothing inside it.
+ Box(
+ modifier = modifier,
+ )
+ return
+ }
+
val selectedUserImage by viewModel.selectedUserImage.collectAsState(null)
val dropdownItems by viewModel.userSwitcherDropdown.collectAsState(emptyList())
@@ -539,195 +831,10 @@ private fun UserSwitcherDropdownMenu(
}
}
-/**
- * Renders the bouncer UI in split mode, with half on one side and half on the other side, swappable
- * by double-tapping on the side.
- */
-@Composable
-private fun SplitLayout(
- viewModel: BouncerViewModel,
- dialogFactory: BouncerDialogFactory,
- modifier: Modifier = Modifier,
-) {
- SwappableLayout(
- startContent = { startContentModifier ->
- StandardLayout(
- viewModel = viewModel,
- dialogFactory = dialogFactory,
- layout = BouncerSceneLayout.SPLIT,
- outputOnly = true,
- modifier = startContentModifier,
- )
- },
- endContent = { endContentModifier ->
- UserInputArea(
- viewModel = viewModel,
- visibility = UserInputAreaVisibility.INPUT_ONLY,
- layout = BouncerSceneLayout.SPLIT,
- modifier = endContentModifier,
- )
- },
- layout = BouncerSceneLayout.SPLIT,
- modifier = modifier,
- )
-}
-
-/**
- * Arranges the given two contents side-by-side, supporting a double tap anywhere on the background
- * to flip their positions.
- */
-@Composable
-private fun SwappableLayout(
- startContent: @Composable (Modifier) -> Unit,
- endContent: @Composable (Modifier) -> Unit,
- layout: BouncerSceneLayout,
- modifier: Modifier = Modifier,
-) {
- val layoutDirection = LocalLayoutDirection.current
- val isLeftToRight = layoutDirection == LayoutDirection.Ltr
- val (isSwapped, setSwapped) = rememberSaveable(isLeftToRight) { mutableStateOf(!isLeftToRight) }
-
- Row(
- modifier =
- modifier.pointerInput(Unit) {
- detectTapGestures(
- onDoubleTap = { offset ->
- // Depending on where the user double tapped, switch the elements such that
- // the endContent is closer to the side that was double tapped.
- setSwapped(offset.x < size.width / 2)
- }
- )
- },
- ) {
- val animatedOffset by
- animateFloatAsState(
- targetValue =
- if (!isSwapped) {
- // When startContent is first, both elements have their natural placement so
- // they are not offset in any way.
- 0f
- } else if (isLeftToRight) {
- // Since startContent is not first, the elements have to be swapped
- // horizontally. In the case of LTR locales, this means pushing startContent
- // to the right, hence the positive number.
- 1f
- } else {
- // Since startContent is not first, the elements have to be swapped
- // horizontally. In the case of RTL locales, this means pushing startContent
- // to the left, hence the negative number.
- -1f
- },
- label = "offset",
- )
-
- startContent(
- Modifier.fillMaxHeight().weight(1f).graphicsLayer {
- translationX = size.width * animatedOffset
- alpha = animatedAlpha(animatedOffset)
- }
- )
-
- Box(
- modifier =
- Modifier.fillMaxHeight().weight(1f).graphicsLayer {
- // A negative sign is used to make sure this is offset in the direction that's
- // opposite of the direction that the user switcher is pushed in.
- translationX = -size.width * animatedOffset
- alpha = animatedAlpha(animatedOffset)
- }
- ) {
- endContent(Modifier.align(layout.swappableEndContentAlignment).widthIn(max = 400.dp))
- }
- }
-}
-
-/**
- * Arranges the bouncer contents and user switcher contents side-by-side, supporting a double tap
- * anywhere on the background to flip their positions.
- *
- * In situations when [isUserSwitcherVisible] is `false`, one of two things may happen: either the
- * UI for the bouncer will be shown on its own, taking up one side, with the other side just being
- * empty space or, if that kind of "stand-alone side-by-side" isn't supported, the standard
- * rendering of the bouncer will be used instead of the side-by-side layout.
- */
-@Composable
-private fun SideBySideLayout(
- viewModel: BouncerViewModel,
- dialogFactory: BouncerDialogFactory,
- isUserSwitcherVisible: Boolean,
- modifier: Modifier = Modifier,
-) {
- SwappableLayout(
- startContent = { startContentModifier ->
- if (isUserSwitcherVisible) {
- UserSwitcher(
- viewModel = viewModel,
- modifier = startContentModifier,
- )
- } else {
- Box(
- modifier = startContentModifier,
- )
- }
- },
- endContent = { endContentModifier ->
- StandardLayout(
- viewModel = viewModel,
- dialogFactory = dialogFactory,
- layout = BouncerSceneLayout.SIDE_BY_SIDE,
- modifier = endContentModifier,
- )
- },
- layout = BouncerSceneLayout.SIDE_BY_SIDE,
- modifier = modifier,
- )
-}
-
-/** Arranges the bouncer contents and user switcher contents one on top of the other, vertically. */
-@Composable
-private fun StackedLayout(
- viewModel: BouncerViewModel,
- dialogFactory: BouncerDialogFactory,
- isUserSwitcherVisible: Boolean,
- modifier: Modifier = Modifier,
-) {
- Column(
- modifier = modifier,
- ) {
- if (isUserSwitcherVisible) {
- UserSwitcher(
- viewModel = viewModel,
- modifier = Modifier.fillMaxWidth().weight(1f),
- )
- }
-
- StandardLayout(
- viewModel = viewModel,
- dialogFactory = dialogFactory,
- layout = BouncerSceneLayout.STACKED,
- modifier = Modifier.fillMaxWidth().weight(1f),
- )
- }
-}
-
interface BouncerDialogFactory {
operator fun invoke(): AlertDialog
}
-/** Enumerates all supported user-input area visibilities. */
-private enum class UserInputAreaVisibility {
- /**
- * Only the area where the user enters the input is shown; the area where the input is reflected
- * back to the user is not shown.
- */
- INPUT_ONLY,
- /**
- * Only the area where the input is reflected back to the user is shown; the area where the
- * input is entered by the user is not shown.
- */
- OUTPUT_ONLY,
-}
-
/**
* Calculates an alpha for the user switcher and bouncer such that it's at `1` when the offset of
* the two reaches a stopping point but `0` in the middle of the transition.
@@ -774,48 +881,3 @@ private object SceneElements {
private val SceneTransitions = transitions {
from(SceneKeys.ContiguousSceneKey, to = SceneKeys.SplitSceneKey) { spec = tween() }
}
-
-/** Whether a more compact size should be used for various spacing dimensions. */
-internal val BouncerSceneLayout.isUseCompactSize: Boolean
- get() =
- when (this) {
- BouncerSceneLayout.SIDE_BY_SIDE -> true
- BouncerSceneLayout.SPLIT -> true
- else -> false
- }
-
-/** Amount of space to place between the message and the entered input UI elements, in dips. */
-private val BouncerSceneLayout.spacingBetweenMessageAndEnteredInput: Dp
- get() =
- when {
- this == BouncerSceneLayout.STACKED -> 24.dp
- isUseCompactSize -> 96.dp
- else -> 128.dp
- }
-
-/** Amount of space to place above the topmost UI element, in dips. */
-private val BouncerSceneLayout.topPadding: Dp
- get() =
- if (this == BouncerSceneLayout.SPLIT) {
- 40.dp
- } else {
- 92.dp
- }
-
-/** Amount of space to place below the bottommost UI element, in dips. */
-private val BouncerSceneLayout.bottomPadding: Dp
- get() =
- if (this == BouncerSceneLayout.SPLIT) {
- 40.dp
- } else {
- 48.dp
- }
-
-/** The in-a-box alignment for the content on the "end" side of a swappable layout. */
-private val BouncerSceneLayout.swappableEndContentAlignment: Alignment
- get() =
- if (this == BouncerSceneLayout.SPLIT) {
- Alignment.Center
- } else {
- Alignment.BottomCenter
- }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
index 08b7559dcf97..1c3d93c3b7e9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
@@ -26,8 +26,8 @@ import com.android.systemui.bouncer.ui.helper.calculateLayoutInternal
/**
* Returns the [BouncerSceneLayout] that should be used by the bouncer scene. If
- * [isSideBySideSupported] is `false`, then [BouncerSceneLayout.SIDE_BY_SIDE] is replaced by
- * [BouncerSceneLayout.STANDARD].
+ * [isSideBySideSupported] is `false`, then [BouncerSceneLayout.BESIDE_USER_SWITCHER] is replaced by
+ * [BouncerSceneLayout.STANDARD_BOUNCER].
*/
@Composable
fun calculateLayout(
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
index 279995959da2..09608115c456 100644
--- 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
@@ -17,7 +17,6 @@
package com.android.systemui.bouncer.ui.composable
import android.view.ViewTreeObserver
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.LocalTextStyle
@@ -31,7 +30,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
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
@@ -81,42 +79,38 @@ internal fun PasswordBouncer(
}
}
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = modifier,
- ) {
- val color = MaterialTheme.colorScheme.onSurfaceVariant
- val lineWidthPx = with(LocalDensity.current) { 2.dp.toPx() }
+ val color = MaterialTheme.colorScheme.onSurfaceVariant
+ val lineWidthPx = with(LocalDensity.current) { 2.dp.toPx() }
- TextField(
- value = password,
- onValueChange = viewModel::onPasswordInputChanged,
- enabled = isInputEnabled,
- 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)
- .onFocusChanged { viewModel.onTextFieldFocusChanged(it.isFocused) }
- .drawBehind {
- drawLine(
- color = color,
- start = Offset(x = 0f, y = size.height - lineWidthPx),
- end = Offset(size.width, y = size.height - lineWidthPx),
- strokeWidth = lineWidthPx,
- )
- },
- )
- }
+ TextField(
+ value = password,
+ onValueChange = viewModel::onPasswordInputChanged,
+ enabled = isInputEnabled,
+ 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)
+ .onFocusChanged { viewModel.onTextFieldFocusChanged(it.isFocused) }
+ .drawBehind {
+ drawLine(
+ color = color,
+ start = Offset(x = 0f, y = size.height - lineWidthPx),
+ end = Offset(size.width, y = size.height - lineWidthPx),
+ strokeWidth = lineWidthPx,
+ )
+ },
+ )
}
/** Returns a [State] with `true` when the IME/keyboard is visible and `false` when it's not. */
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
index a4b195508b3d..0a5f5d281f83 100644
--- 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
@@ -24,6 +24,8 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -48,7 +50,6 @@ import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
import com.android.compose.modifiers.thenIf
import com.android.internal.R
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
import kotlin.math.min
@@ -61,11 +62,14 @@ 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.
+ *
+ * If [centerDotsVertically] is `true`, the dots should be centered along the axis of interest; if
+ * `false`, the dots will be pushed towards the end/bottom of the axis.
*/
@Composable
internal fun PatternBouncer(
viewModel: PatternBouncerViewModel,
- layout: BouncerSceneLayout,
+ centerDotsVertically: Boolean,
modifier: Modifier = Modifier,
) {
DisposableEffect(Unit) {
@@ -197,6 +201,14 @@ internal fun PatternBouncer(
Canvas(
modifier
+ // Because the width also includes spacing to the left and right of the leftmost and
+ // rightmost dots in the grid and because UX mocks specify the width without that
+ // spacing, the actual width needs to be defined slightly bigger than the UX mock width.
+ .width((262 * colCount / 2).dp)
+ // Because the height also includes spacing above and below the topmost and bottommost
+ // dots in the grid and because UX mocks specify the height without that spacing, the
+ // actual height needs to be defined slightly bigger than the UX mock height.
+ .height((262 * rowCount / 2).dp)
// 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()
@@ -260,7 +272,7 @@ internal fun PatternBouncer(
availableSize = containerSize.height,
spacingPerDot = spacing,
dotCount = rowCount,
- isCentered = layout.isCenteredVertically,
+ isCentered = centerDotsVertically,
)
offset = Offset(horizontalOffset, verticalOffset)
scale = (colCount * spacing) / containerSize.width
@@ -423,10 +435,6 @@ private fun offset(
}
}
-/** Whether the UI should be centered vertically. */
-private val BouncerSceneLayout.isCenteredVertically: Boolean
- get() = this == BouncerSceneLayout.SPLIT
-
private const val DOT_DIAMETER_DP = 16
private const val SELECTED_DOT_DIAMETER_DP = 24
private const val SELECTED_DOT_REACTION_ANIMATION_DURATION_MS = 83
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
index 8f5d9f4a1790..f505b9067140 100644
--- 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
@@ -52,7 +52,6 @@ import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
import com.android.compose.grid.VerticalGrid
import com.android.compose.modifiers.thenIf
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.common.shared.model.ContentDescription
@@ -70,7 +69,7 @@ import kotlinx.coroutines.launch
@Composable
fun PinPad(
viewModel: PinBouncerViewModel,
- layout: BouncerSceneLayout,
+ verticalSpacing: Dp,
modifier: Modifier = Modifier,
) {
DisposableEffect(Unit) {
@@ -96,8 +95,8 @@ fun PinPad(
VerticalGrid(
columns = columns,
- verticalSpacing = layout.verticalSpacing,
- horizontalSpacing = calculateHorizontalSpacingBetweenColumns(layout.gridWidth),
+ verticalSpacing = verticalSpacing,
+ horizontalSpacing = calculateHorizontalSpacingBetweenColumns(gridWidth = 300.dp),
modifier = modifier,
) {
repeat(9) { index ->
@@ -355,14 +354,6 @@ private fun calculateHorizontalSpacingBetweenColumns(
return (gridWidth - (pinButtonMaxSize * columns)) / (columns - 1)
}
-/** The width of the grid of PIN pad buttons, in dips. */
-private val BouncerSceneLayout.gridWidth: Dp
- get() = if (isUseCompactSize) 292.dp else 300.dp
-
-/** The spacing between rows of PIN pad buttons, in dips. */
-private val BouncerSceneLayout.verticalSpacing: Dp
- get() = if (isUseCompactSize) 8.dp else 12.dp
-
/** Number of columns in the PIN pad grid. */
private const val columns = 3
/** Maximum size (width and height) of each PIN pad button. */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
index 5385442092b9..7f97718cb623 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
@@ -21,13 +21,13 @@ import androidx.annotation.VisibleForTesting
/** Enumerates all known adaptive layout configurations. */
enum class BouncerSceneLayout {
/** The default UI with the bouncer laid out normally. */
- STANDARD,
+ STANDARD_BOUNCER,
/** The bouncer is displayed vertically stacked with the user switcher. */
- STACKED,
+ BELOW_USER_SWITCHER,
/** The bouncer is displayed side-by-side with the user switcher or an empty space. */
- SIDE_BY_SIDE,
+ BESIDE_USER_SWITCHER,
/** The bouncer is split in two with both sides shown side-by-side. */
- SPLIT,
+ SPLIT_BOUNCER,
}
/** Enumerates the supported window size classes. */
@@ -48,19 +48,19 @@ fun calculateLayoutInternal(
isSideBySideSupported: Boolean,
): BouncerSceneLayout {
return when (height) {
- SizeClass.COMPACT -> BouncerSceneLayout.SPLIT
+ SizeClass.COMPACT -> BouncerSceneLayout.SPLIT_BOUNCER
SizeClass.MEDIUM ->
when (width) {
- SizeClass.COMPACT -> BouncerSceneLayout.STANDARD
- SizeClass.MEDIUM -> BouncerSceneLayout.STANDARD
- SizeClass.EXPANDED -> BouncerSceneLayout.SIDE_BY_SIDE
+ SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER
+ SizeClass.MEDIUM -> BouncerSceneLayout.STANDARD_BOUNCER
+ SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER
}
SizeClass.EXPANDED ->
when (width) {
- SizeClass.COMPACT -> BouncerSceneLayout.STANDARD
- SizeClass.MEDIUM -> BouncerSceneLayout.STACKED
- SizeClass.EXPANDED -> BouncerSceneLayout.SIDE_BY_SIDE
+ SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER
+ SizeClass.MEDIUM -> BouncerSceneLayout.BELOW_USER_SWITCHER
+ SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER
}
- }.takeIf { it != BouncerSceneLayout.SIDE_BY_SIDE || isSideBySideSupported }
- ?: BouncerSceneLayout.STANDARD
+ }.takeIf { it != BouncerSceneLayout.BESIDE_USER_SWITCHER || isSideBySideSupported }
+ ?: BouncerSceneLayout.STANDARD_BOUNCER
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index e9779cd02760..5fbb60d76fbb 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -59,6 +59,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
import com.android.systemui.scene.ui.view.WindowRootViewComponent;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
@@ -116,6 +117,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
private final AuthController mAuthController;
private final Lazy<SelectedUserInteractor> mUserInteractor;
private final Lazy<ShadeInteractor> mShadeInteractorLazy;
+ private final SceneContainerFlags mSceneContainerFlags;
private ViewGroup mWindowRootView;
private LayoutParams mLp;
private boolean mHasTopUi;
@@ -162,7 +164,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
Lazy<ShadeInteractor> shadeInteractorLazy,
ShadeWindowLogger logger,
Lazy<SelectedUserInteractor> userInteractor,
- UserTracker userTracker) {
+ UserTracker userTracker,
+ SceneContainerFlags sceneContainerFlags) {
mContext = context;
mWindowRootViewComponentFactory = windowRootViewComponentFactory;
mWindowManager = windowManager;
@@ -180,6 +183,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
dumpManager.registerDumpable(this);
mAuthController = authController;
mUserInteractor = userInteractor;
+ mSceneContainerFlags = sceneContainerFlags;
mLastKeyguardRotationAllowed = mKeyguardStateController.isKeyguardScreenRotationAllowed();
mLockScreenDisplayTimeout = context.getResources()
.getInteger(R.integer.config_lockScreenDisplayTimeout);
@@ -287,6 +291,15 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
mLp.privateFlags |= PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
mLp.insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
+ if (mSceneContainerFlags.isEnabled()) {
+ // This prevents the appearance and disappearance of the software keyboard (also known
+ // as the "IME") from scrolling/panning the window to make room for the keyboard.
+ //
+ // The scene container logic does its own adjustment and animation when the IME appears
+ // or disappears.
+ mLp.softInputMode = LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+ }
+
mWindowManager.addView(mWindowRootView, mLp);
mLpChanged.copyFrom(mLp);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
index 395d7129bee3..ca9582240b93 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
@@ -18,10 +18,10 @@ package com.android.systemui.bouncer.ui.helper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.SIDE_BY_SIDE
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.SPLIT
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.STACKED
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.STANDARD
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.BELOW_USER_SWITCHER
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.BESIDE_USER_SWITCHER
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.SPLIT_BOUNCER
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.STANDARD_BOUNCER
import com.google.common.truth.Truth.assertThat
import java.util.Locale
import org.junit.Test
@@ -84,33 +84,33 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
listOf(
Phone to
Expected(
- whenNaturallyHeld = STANDARD,
- whenUnnaturallyHeld = SPLIT,
+ whenNaturallyHeld = STANDARD_BOUNCER,
+ whenUnnaturallyHeld = SPLIT_BOUNCER,
),
Tablet to
Expected(
- whenNaturallyHeld = SIDE_BY_SIDE,
- whenUnnaturallyHeld = STACKED,
+ whenNaturallyHeld = BESIDE_USER_SWITCHER,
+ whenUnnaturallyHeld = BELOW_USER_SWITCHER,
),
Folded to
Expected(
- whenNaturallyHeld = STANDARD,
- whenUnnaturallyHeld = SPLIT,
+ whenNaturallyHeld = STANDARD_BOUNCER,
+ whenUnnaturallyHeld = SPLIT_BOUNCER,
),
Unfolded to
Expected(
- whenNaturallyHeld = SIDE_BY_SIDE,
- whenUnnaturallyHeld = STANDARD,
+ whenNaturallyHeld = BESIDE_USER_SWITCHER,
+ whenUnnaturallyHeld = STANDARD_BOUNCER,
),
TallerFolded to
Expected(
- whenNaturallyHeld = STANDARD,
- whenUnnaturallyHeld = SPLIT,
+ whenNaturallyHeld = STANDARD_BOUNCER,
+ whenUnnaturallyHeld = SPLIT_BOUNCER,
),
TallerUnfolded to
Expected(
- whenNaturallyHeld = SIDE_BY_SIDE,
- whenUnnaturallyHeld = SIDE_BY_SIDE,
+ whenNaturallyHeld = BESIDE_USER_SWITCHER,
+ whenUnnaturallyHeld = BESIDE_USER_SWITCHER,
),
)
.flatMap { (device, expected) ->
@@ -124,13 +124,13 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
)
)
- if (expected.whenNaturallyHeld == SIDE_BY_SIDE) {
+ if (expected.whenNaturallyHeld == BESIDE_USER_SWITCHER) {
add(
TestCase(
device = device,
held = device.naturallyHeld,
isSideBySideSupported = false,
- expected = STANDARD,
+ expected = STANDARD_BOUNCER,
)
)
}
@@ -144,13 +144,13 @@ class BouncerSceneLayoutTest : SysuiTestCase() {
)
)
- if (expected.whenUnnaturallyHeld == SIDE_BY_SIDE) {
+ if (expected.whenUnnaturallyHeld == BESIDE_USER_SWITCHER) {
add(
TestCase(
device = device,
held = device.naturallyHeld.flip(),
isSideBySideSupported = false,
- expected = STANDARD,
+ expected = STANDARD_BOUNCER,
)
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index d246f0e49e1c..ae5f625b1c8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -97,6 +97,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransition
import com.android.systemui.log.SessionTracker;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.scene.FakeWindowRootViewComponent;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
@@ -214,6 +215,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
private @Mock CoroutineDispatcher mDispatcher;
private @Mock DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
private @Mock SystemPropertiesHelper mSystemPropertiesHelper;
+ private @Mock SceneContainerFlags mSceneContainerFlags;
private FakeFeatureFlags mFeatureFlags;
private final int mDefaultUserId = 100;
@@ -258,7 +260,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
() -> mShadeInteractor,
mShadeWindowLogger,
() -> mSelectedUserInteractor,
- mUserTracker);
+ mUserTracker,
+ mSceneContainerFlags);
mFeatureFlags = new FakeFeatureFlags();
mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false);
mFeatureFlags.set(Flags.REFACTOR_GETCURRENTUSER, true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 39739e7bb93d..5ffbe65d2c50 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -75,6 +75,7 @@ import com.android.systemui.scene.SceneTestUtils;
import com.android.systemui.scene.data.repository.SceneContainerRepository;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
import com.android.systemui.scene.shared.logger.SceneLogger;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.data.repository.FakeShadeRepository;
@@ -138,6 +139,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
@Mock private ShadeWindowLogger mShadeWindowLogger;
@Mock private SelectedUserInteractor mSelectedUserInteractor;
@Mock private UserTracker mUserTracker;
+ @Mock private SceneContainerFlags mSceneContainerFlags;
@Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
@Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
@@ -274,7 +276,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
() -> mShadeInteractor,
mShadeWindowLogger,
() -> mSelectedUserInteractor,
- mUserTracker) {
+ mUserTracker,
+ mSceneContainerFlags) {
@Override
protected boolean isDebuggable() {
return false;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 8585d46fa8a5..5b9b390eea2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -119,6 +119,7 @@ import com.android.systemui.scene.SceneTestUtils;
import com.android.systemui.scene.data.repository.SceneContainerRepository;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
import com.android.systemui.scene.shared.logger.SceneLogger;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.settings.UserTracker;
@@ -343,6 +344,8 @@ public class BubblesTest extends SysuiTestCase {
private Icon mAppBubbleIcon;
@Mock
private Display mDefaultDisplay;
+ @Mock
+ private SceneContainerFlags mSceneContainerFlags;
private final SceneTestUtils mUtils = new SceneTestUtils(this);
private final TestScope mTestScope = mUtils.getTestScope();
@@ -503,7 +506,8 @@ public class BubblesTest extends SysuiTestCase {
() -> mShadeInteractor,
mShadeWindowLogger,
() -> mSelectedUserInteractor,
- mUserTracker
+ mUserTracker,
+ mSceneContainerFlags
);
mNotificationShadeWindowController.fetchWindowRootView();
mNotificationShadeWindowController.attach();