Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c99e2ad5 authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Android (Google) Code Review
Browse files

Merge changes from topic "flexi-scene-layout-calculation-test" into main

* changes:
  [flexiglass] Test for bouncer scene layout calculation.
  [flexiglass] Test for FoldPosture.
parents a7bd2f67 f6ad1063
Loading
Loading
Loading
Loading
+7 −62
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.systemui.bouncer.ui.composable
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.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.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.Modifier
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.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
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
@@ -93,8 +89,8 @@ import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
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 @@ private fun SceneScope.BouncerScene(
) {
    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 @@ private fun SceneScope.BouncerScene(
        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 @@ private fun StackedLayout(
    }
}

@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 {
    /**
+61 −0
Original line number Diff line number Diff line
/*
 * 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\"")
    }
}
+3 −39
Original line number Diff line number Diff line
@@ -23,19 +23,9 @@ import androidx.compose.runtime.getValue
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 @@ fun foldPosture(): State<FoldPosture> {
        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)
    }
}
+124 −0
Original line number Diff line number Diff line
/*
 * 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
                }
            )
        )
    }
}
+66 −0
Original line number Diff line number Diff line
/*
 * 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
}
Loading