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