Merge changes from topic "flexi-scene-layout-calculation-test" into main
* changes:
[flexiglass] Test for bouncer scene layout calculation.
[flexiglass] Test for FoldPosture.
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 7eb7dac..57af2ba 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
@@ -19,7 +19,6 @@
import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
-import android.content.res.Configuration
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.snap
@@ -54,8 +53,6 @@
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
-import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -68,7 +65,6 @@
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextOverflow
@@ -83,8 +79,8 @@
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
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
@@ -93,8 +89,8 @@
import com.android.systemui.common.shared.model.Text.Companion.loadText
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.fold.ui.composable.FoldPosture
import com.android.systemui.fold.ui.composable.foldPosture
+import com.android.systemui.fold.ui.helper.FoldPosture
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.SceneKey
@@ -149,10 +145,7 @@
) {
val backgroundColor = MaterialTheme.colorScheme.surface
val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsState()
- val layout =
- calculateLayout(
- isSideBySideSupported = isSideBySideSupported,
- )
+ val layout = calculateLayout(isSideBySideSupported = isSideBySideSupported)
Box(modifier) {
Canvas(Modifier.element(Bouncer.Elements.Background).fillMaxSize()) {
@@ -163,27 +156,27 @@
val isFullScreenUserSwitcherEnabled = viewModel.isUserSwitcherVisible
when (layout) {
- Layout.STANDARD ->
+ BouncerSceneLayout.STANDARD ->
StandardLayout(
viewModel = viewModel,
dialogFactory = dialogFactory,
modifier = childModifier,
)
- Layout.SIDE_BY_SIDE ->
+ BouncerSceneLayout.SIDE_BY_SIDE ->
SideBySideLayout(
viewModel = viewModel,
dialogFactory = dialogFactory,
isUserSwitcherVisible = isFullScreenUserSwitcherEnabled,
modifier = childModifier,
)
- Layout.STACKED ->
+ BouncerSceneLayout.STACKED ->
StackedLayout(
viewModel = viewModel,
dialogFactory = dialogFactory,
isUserSwitcherVisible = isFullScreenUserSwitcherEnabled,
modifier = childModifier,
)
- Layout.SPLIT ->
+ BouncerSceneLayout.SPLIT ->
SplitLayout(
viewModel = viewModel,
dialogFactory = dialogFactory,
@@ -728,58 +721,10 @@
}
}
-@Composable
-private fun calculateLayout(
- isSideBySideSupported: Boolean,
-): Layout {
- val windowSizeClass = LocalWindowSizeClass.current
- val width = windowSizeClass.widthSizeClass
- val height = windowSizeClass.heightSizeClass
- val isLarge = width > WindowWidthSizeClass.Compact && height > WindowHeightSizeClass.Compact
- val isTall =
- when (height) {
- WindowHeightSizeClass.Expanded -> width < WindowWidthSizeClass.Expanded
- WindowHeightSizeClass.Medium -> width < WindowWidthSizeClass.Medium
- else -> false
- }
- val isSquare =
- when (width) {
- WindowWidthSizeClass.Compact -> height == WindowHeightSizeClass.Compact
- WindowWidthSizeClass.Medium -> height == WindowHeightSizeClass.Medium
- WindowWidthSizeClass.Expanded -> height == WindowHeightSizeClass.Expanded
- else -> false
- }
- val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
-
- return when {
- // Small and tall devices (i.e. phone/folded in portrait) or square device not in landscape
- // mode (unfolded with hinge along horizontal plane).
- (!isLarge && isTall) || (isSquare && !isLandscape) -> Layout.STANDARD
- // Small and wide devices (i.e. phone/folded in landscape).
- !isLarge -> Layout.SPLIT
- // Large and tall devices (i.e. tablet in portrait).
- isTall -> Layout.STACKED
- // Large and wide/square devices (i.e. tablet in landscape, unfolded).
- else -> if (isSideBySideSupported) Layout.SIDE_BY_SIDE else Layout.STANDARD
- }
-}
-
interface BouncerSceneDialogFactory {
operator fun invoke(): AlertDialog
}
-/** Enumerates all known adaptive layout configurations. */
-private enum class Layout {
- /** The default UI with the bouncer laid out normally. */
- STANDARD,
- /** The bouncer is displayed vertically stacked with the user switcher. */
- STACKED,
- /** The bouncer is displayed side-by-side with the user switcher or an empty space. */
- SIDE_BY_SIDE,
- /** The bouncer is split in two with both sides shown side-by-side. */
- SPLIT,
-}
-
/** Enumerates all supported user-input area visibilities. */
private enum class UserInputAreaVisibility {
/**
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
new file mode 100644
index 0000000..08b7559
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.material3.windowsizeclass.WindowHeightSizeClass
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.runtime.Composable
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
+import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
+import com.android.systemui.bouncer.ui.helper.SizeClass
+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].
+ */
+@Composable
+fun calculateLayout(
+ isSideBySideSupported: Boolean,
+): BouncerSceneLayout {
+ val windowSizeClass = LocalWindowSizeClass.current
+
+ return calculateLayoutInternal(
+ width = windowSizeClass.widthSizeClass.toEnum(),
+ height = windowSizeClass.heightSizeClass.toEnum(),
+ isSideBySideSupported = isSideBySideSupported,
+ )
+}
+
+private fun WindowWidthSizeClass.toEnum(): SizeClass {
+ return when (this) {
+ WindowWidthSizeClass.Compact -> SizeClass.COMPACT
+ WindowWidthSizeClass.Medium -> SizeClass.MEDIUM
+ WindowWidthSizeClass.Expanded -> SizeClass.EXPANDED
+ else -> error("Unsupported WindowWidthSizeClass \"$this\"")
+ }
+}
+
+private fun WindowHeightSizeClass.toEnum(): SizeClass {
+ return when (this) {
+ WindowHeightSizeClass.Compact -> SizeClass.COMPACT
+ WindowHeightSizeClass.Medium -> SizeClass.MEDIUM
+ WindowHeightSizeClass.Expanded -> SizeClass.EXPANDED
+ else -> error("Unsupported WindowHeightSizeClass \"$this\"")
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt
index 1c993cf..e77ade9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/fold/ui/composable/FoldPosture.kt
@@ -23,19 +23,9 @@
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
-import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
-
-sealed interface FoldPosture {
- /** A foldable device that's fully closed/folded or a device that doesn't support folding. */
- data object Folded : FoldPosture
- /** A foldable that's halfway open with the hinge held vertically. */
- data object Book : FoldPosture
- /** A foldable that's halfway open with the hinge held horizontally. */
- data object Tabletop : FoldPosture
- /** A foldable that's fully unfolded / flat. */
- data object FullyUnfolded : FoldPosture
-}
+import com.android.systemui.fold.ui.helper.FoldPosture
+import com.android.systemui.fold.ui.helper.foldPostureInternal
/** Returns the [FoldPosture] of the device currently. */
@Composable
@@ -48,32 +38,6 @@
initialValue = FoldPosture.Folded,
key1 = layoutInfo,
) {
- value =
- layoutInfo
- ?.displayFeatures
- ?.firstNotNullOfOrNull { it as? FoldingFeature }
- .let { foldingFeature ->
- when (foldingFeature?.state) {
- null -> FoldPosture.Folded
- FoldingFeature.State.HALF_OPENED ->
- foldingFeature.orientation.toHalfwayPosture()
- FoldingFeature.State.FLAT ->
- if (foldingFeature.isSeparating) {
- // Dual screen device.
- foldingFeature.orientation.toHalfwayPosture()
- } else {
- FoldPosture.FullyUnfolded
- }
- else -> error("Unsupported state \"${foldingFeature.state}\"")
- }
- }
- }
-}
-
-private fun FoldingFeature.Orientation.toHalfwayPosture(): FoldPosture {
- return when (this) {
- FoldingFeature.Orientation.HORIZONTAL -> FoldPosture.Tabletop
- FoldingFeature.Orientation.VERTICAL -> FoldPosture.Book
- else -> error("Unsupported orientation \"$this\"")
+ value = foldPostureInternal(layoutInfo)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
new file mode 100644
index 0000000..61b2057
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.fold.ui.helper
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FoldPostureTest : SysuiTestCase() {
+
+ @Test
+ fun foldPosture_whenNull_returnsFolded() {
+ assertThat(foldPostureInternal(null)).isEqualTo(FoldPosture.Folded)
+ }
+
+ @Test
+ fun foldPosture_whenHalfOpenHorizontally_returnsTabletop() {
+ assertThat(
+ foldPostureInternal(
+ createWindowLayoutInfo(
+ state = FoldingFeature.State.HALF_OPENED,
+ orientation = FoldingFeature.Orientation.HORIZONTAL,
+ )
+ )
+ )
+ .isEqualTo(FoldPosture.Tabletop)
+ }
+
+ @Test
+ fun foldPosture_whenHalfOpenVertically_returnsBook() {
+ assertThat(
+ foldPostureInternal(
+ createWindowLayoutInfo(
+ state = FoldingFeature.State.HALF_OPENED,
+ orientation = FoldingFeature.Orientation.VERTICAL,
+ )
+ )
+ )
+ .isEqualTo(FoldPosture.Book)
+ }
+
+ @Test
+ fun foldPosture_whenFlatAndNotSeparating_returnsFullyUnfolded() {
+ assertThat(
+ foldPostureInternal(
+ createWindowLayoutInfo(
+ state = FoldingFeature.State.FLAT,
+ orientation = FoldingFeature.Orientation.HORIZONTAL,
+ isSeparating = false,
+ )
+ )
+ )
+ .isEqualTo(FoldPosture.FullyUnfolded)
+ }
+
+ @Test
+ fun foldPosture_whenFlatAndSeparatingHorizontally_returnsTabletop() {
+ assertThat(
+ foldPostureInternal(
+ createWindowLayoutInfo(
+ state = FoldingFeature.State.FLAT,
+ isSeparating = true,
+ orientation = FoldingFeature.Orientation.HORIZONTAL,
+ )
+ )
+ )
+ .isEqualTo(FoldPosture.Tabletop)
+ }
+
+ @Test
+ fun foldPosture_whenFlatAndSeparatingVertically_returnsBook() {
+ assertThat(
+ foldPostureInternal(
+ createWindowLayoutInfo(
+ state = FoldingFeature.State.FLAT,
+ isSeparating = true,
+ orientation = FoldingFeature.Orientation.VERTICAL,
+ )
+ )
+ )
+ .isEqualTo(FoldPosture.Book)
+ }
+
+ private fun createWindowLayoutInfo(
+ state: FoldingFeature.State,
+ orientation: FoldingFeature.Orientation = FoldingFeature.Orientation.VERTICAL,
+ isSeparating: Boolean = false,
+ occlusionType: FoldingFeature.OcclusionType = FoldingFeature.OcclusionType.NONE,
+ ): WindowLayoutInfo {
+ return WindowLayoutInfo(
+ listOf(
+ object : FoldingFeature {
+ override val bounds: Rect = Rect(0, 0, 100, 100)
+ override val isSeparating: Boolean = isSeparating
+ override val occlusionType: FoldingFeature.OcclusionType = occlusionType
+ override val orientation: FoldingFeature.Orientation = orientation
+ override val state: FoldingFeature.State = state
+ }
+ )
+ )
+ }
+}
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
new file mode 100644
index 0000000..5385442
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.helper
+
+import androidx.annotation.VisibleForTesting
+
+/** Enumerates all known adaptive layout configurations. */
+enum class BouncerSceneLayout {
+ /** The default UI with the bouncer laid out normally. */
+ STANDARD,
+ /** The bouncer is displayed vertically stacked with the user switcher. */
+ STACKED,
+ /** The bouncer is displayed side-by-side with the user switcher or an empty space. */
+ SIDE_BY_SIDE,
+ /** The bouncer is split in two with both sides shown side-by-side. */
+ SPLIT,
+}
+
+/** Enumerates the supported window size classes. */
+enum class SizeClass {
+ COMPACT,
+ MEDIUM,
+ EXPANDED,
+}
+
+/**
+ * Internal version of `calculateLayout` in the System UI Compose library, extracted here to allow
+ * for testing that's not dependent on Compose.
+ */
+@VisibleForTesting
+fun calculateLayoutInternal(
+ width: SizeClass,
+ height: SizeClass,
+ isSideBySideSupported: Boolean,
+): BouncerSceneLayout {
+ return when (height) {
+ SizeClass.COMPACT -> BouncerSceneLayout.SPLIT
+ SizeClass.MEDIUM ->
+ when (width) {
+ SizeClass.COMPACT -> BouncerSceneLayout.STANDARD
+ SizeClass.MEDIUM -> BouncerSceneLayout.STANDARD
+ SizeClass.EXPANDED -> BouncerSceneLayout.SIDE_BY_SIDE
+ }
+ SizeClass.EXPANDED ->
+ when (width) {
+ SizeClass.COMPACT -> BouncerSceneLayout.STANDARD
+ SizeClass.MEDIUM -> BouncerSceneLayout.STACKED
+ SizeClass.EXPANDED -> BouncerSceneLayout.SIDE_BY_SIDE
+ }
+ }.takeIf { it != BouncerSceneLayout.SIDE_BY_SIDE || isSideBySideSupported }
+ ?: BouncerSceneLayout.STANDARD
+}
diff --git a/packages/SystemUI/src/com/android/systemui/fold/ui/helper/FoldPosture.kt b/packages/SystemUI/src/com/android/systemui/fold/ui/helper/FoldPosture.kt
new file mode 100644
index 0000000..bc1cc4f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/fold/ui/helper/FoldPosture.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.fold.ui.helper
+
+import androidx.annotation.VisibleForTesting
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+
+sealed interface FoldPosture {
+ /** A foldable device that's fully closed/folded or a device that doesn't support folding. */
+ data object Folded : FoldPosture
+ /** A foldable that's halfway open with the hinge held vertically. */
+ data object Book : FoldPosture
+ /** A foldable that's halfway open with the hinge held horizontally. */
+ data object Tabletop : FoldPosture
+ /** A foldable that's fully unfolded / flat. */
+ data object FullyUnfolded : FoldPosture
+}
+
+/**
+ * Internal version of `foldPosture` in the System UI Compose library, extracted here to allow for
+ * testing that's not dependent on Compose.
+ */
+@VisibleForTesting
+fun foldPostureInternal(layoutInfo: WindowLayoutInfo?): FoldPosture {
+ return layoutInfo
+ ?.displayFeatures
+ ?.firstNotNullOfOrNull { it as? FoldingFeature }
+ .let { foldingFeature ->
+ when (foldingFeature?.state) {
+ null -> FoldPosture.Folded
+ FoldingFeature.State.HALF_OPENED -> foldingFeature.orientation.toHalfwayPosture()
+ FoldingFeature.State.FLAT ->
+ if (foldingFeature.isSeparating) {
+ // Dual screen device.
+ foldingFeature.orientation.toHalfwayPosture()
+ } else {
+ FoldPosture.FullyUnfolded
+ }
+ else -> error("Unsupported state \"${foldingFeature.state}\"")
+ }
+ }
+}
+
+private fun FoldingFeature.Orientation.toHalfwayPosture(): FoldPosture {
+ return when (this) {
+ FoldingFeature.Orientation.HORIZONTAL -> FoldPosture.Tabletop
+ FoldingFeature.Orientation.VERTICAL -> FoldPosture.Book
+ else -> error("Unsupported orientation \"$this\"")
+ }
+}
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
new file mode 100644
index 0000000..395d712
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
@@ -0,0 +1,253 @@
+/*
+ * 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.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.google.common.truth.Truth.assertThat
+import java.util.Locale
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SmallTest
+@RunWith(Parameterized::class)
+class BouncerSceneLayoutTest : SysuiTestCase() {
+
+ data object Phone :
+ Device(
+ name = "phone",
+ width = SizeClass.COMPACT,
+ height = SizeClass.EXPANDED,
+ naturallyHeld = Vertically,
+ )
+ data object Tablet :
+ Device(
+ name = "tablet",
+ width = SizeClass.EXPANDED,
+ height = SizeClass.MEDIUM,
+ naturallyHeld = Horizontally,
+ )
+ data object Folded :
+ Device(
+ name = "folded",
+ width = SizeClass.COMPACT,
+ height = SizeClass.MEDIUM,
+ naturallyHeld = Vertically,
+ )
+ data object Unfolded :
+ Device(
+ name = "unfolded",
+ width = SizeClass.EXPANDED,
+ height = SizeClass.MEDIUM,
+ naturallyHeld = Vertically,
+ widthWhenUnnaturallyHeld = SizeClass.MEDIUM,
+ heightWhenUnnaturallyHeld = SizeClass.MEDIUM,
+ )
+ data object TallerFolded :
+ Device(
+ name = "taller folded",
+ width = SizeClass.COMPACT,
+ height = SizeClass.EXPANDED,
+ naturallyHeld = Vertically,
+ )
+ data object TallerUnfolded :
+ Device(
+ name = "taller unfolded",
+ width = SizeClass.EXPANDED,
+ height = SizeClass.EXPANDED,
+ naturallyHeld = Vertically,
+ )
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun testCases() =
+ listOf(
+ Phone to
+ Expected(
+ whenNaturallyHeld = STANDARD,
+ whenUnnaturallyHeld = SPLIT,
+ ),
+ Tablet to
+ Expected(
+ whenNaturallyHeld = SIDE_BY_SIDE,
+ whenUnnaturallyHeld = STACKED,
+ ),
+ Folded to
+ Expected(
+ whenNaturallyHeld = STANDARD,
+ whenUnnaturallyHeld = SPLIT,
+ ),
+ Unfolded to
+ Expected(
+ whenNaturallyHeld = SIDE_BY_SIDE,
+ whenUnnaturallyHeld = STANDARD,
+ ),
+ TallerFolded to
+ Expected(
+ whenNaturallyHeld = STANDARD,
+ whenUnnaturallyHeld = SPLIT,
+ ),
+ TallerUnfolded to
+ Expected(
+ whenNaturallyHeld = SIDE_BY_SIDE,
+ whenUnnaturallyHeld = SIDE_BY_SIDE,
+ ),
+ )
+ .flatMap { (device, expected) ->
+ buildList {
+ // Holding the device in its natural orientation (vertical or horizontal):
+ add(
+ TestCase(
+ device = device,
+ held = device.naturallyHeld,
+ expected = expected.layout(heldNaturally = true),
+ )
+ )
+
+ if (expected.whenNaturallyHeld == SIDE_BY_SIDE) {
+ add(
+ TestCase(
+ device = device,
+ held = device.naturallyHeld,
+ isSideBySideSupported = false,
+ expected = STANDARD,
+ )
+ )
+ }
+
+ // Holding the device the other way:
+ add(
+ TestCase(
+ device = device,
+ held = device.naturallyHeld.flip(),
+ expected = expected.layout(heldNaturally = false),
+ )
+ )
+
+ if (expected.whenUnnaturallyHeld == SIDE_BY_SIDE) {
+ add(
+ TestCase(
+ device = device,
+ held = device.naturallyHeld.flip(),
+ isSideBySideSupported = false,
+ expected = STANDARD,
+ )
+ )
+ }
+ }
+ }
+ }
+
+ @Parameterized.Parameter @JvmField var testCase: TestCase? = null
+
+ @Test
+ fun calculateLayout() {
+ testCase?.let { nonNullTestCase ->
+ with(nonNullTestCase) {
+ assertThat(
+ calculateLayoutInternal(
+ width = device.width(whenHeld = held),
+ height = device.height(whenHeld = held),
+ isSideBySideSupported = isSideBySideSupported,
+ )
+ )
+ .isEqualTo(expected)
+ }
+ }
+ }
+
+ data class TestCase(
+ val device: Device,
+ val held: Held,
+ val expected: BouncerSceneLayout,
+ val isSideBySideSupported: Boolean = true,
+ ) {
+ override fun toString(): String {
+ return buildString {
+ append(device.name)
+ append(" width: ${device.width(held).name.lowercase(Locale.US)}")
+ append(" height: ${device.height(held).name.lowercase(Locale.US)}")
+ append(" when held $held")
+ if (!isSideBySideSupported) {
+ append(" (side-by-side not supported)")
+ }
+ }
+ }
+ }
+
+ data class Expected(
+ val whenNaturallyHeld: BouncerSceneLayout,
+ val whenUnnaturallyHeld: BouncerSceneLayout,
+ ) {
+ fun layout(heldNaturally: Boolean): BouncerSceneLayout {
+ return if (heldNaturally) {
+ whenNaturallyHeld
+ } else {
+ whenUnnaturallyHeld
+ }
+ }
+ }
+
+ sealed class Device(
+ val name: String,
+ private val width: SizeClass,
+ private val height: SizeClass,
+ val naturallyHeld: Held,
+ private val widthWhenUnnaturallyHeld: SizeClass = height,
+ private val heightWhenUnnaturallyHeld: SizeClass = width,
+ ) {
+ fun width(whenHeld: Held): SizeClass {
+ return if (isHeldNaturally(whenHeld)) {
+ width
+ } else {
+ widthWhenUnnaturallyHeld
+ }
+ }
+
+ fun height(whenHeld: Held): SizeClass {
+ return if (isHeldNaturally(whenHeld)) {
+ height
+ } else {
+ heightWhenUnnaturallyHeld
+ }
+ }
+
+ private fun isHeldNaturally(whenHeld: Held): Boolean {
+ return whenHeld == naturallyHeld
+ }
+ }
+
+ sealed class Held {
+ abstract fun flip(): Held
+ }
+ data object Vertically : Held() {
+ override fun flip(): Held {
+ return Horizontally
+ }
+ }
+ data object Horizontally : Held() {
+ override fun flip(): Held {
+ return Vertically
+ }
+ }
+}