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

Commit 93889333 authored by Fabián Kozynski's avatar Fabián Kozynski
Browse files

Handle media in landscape

Adds a viewmodel that indicates if media should be in a row with tiles.
This is used to determine number of columns, and number of rows in QQS.

Media in landscape only exists in SingleShade, in other cases, the
viewmodel will indicate that media should not be in a row and the
behaviors would not be affected. The viewmodel is tied to the media
location.

As in the previous media CL, some animations may not work correctly and
will be handled in future CLs.

Test: atest com.android.systemui.qs
Test: manual, media on and off, portrait, landscape and split screen
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Bug: 353253280
Change-Id: I04d0c3f7bfc127e32293290ca63c7281d1f623c9
parent b046e9be
Loading
Loading
Loading
Loading
+49 −0
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import com.android.systemui.qs.composefragment.viewmodel.MediaState.ANY_MEDIA
import com.android.systemui.qs.composefragment.viewmodel.MediaState.NO_MEDIA
import com.android.systemui.qs.fgsManagerController
import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
import com.android.systemui.qs.panels.ui.viewmodel.setConfigurationForMediaInRow
import com.android.systemui.res.R
import com.android.systemui.shade.largeScreenHeaderHelper
import com.android.systemui.statusbar.StatusBarState
@@ -269,6 +270,54 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest()
            }
        }

    @Test
    fun mediaNotInRow() =
        with(kosmos) {
            testScope.testWithinLifecycle {
                setConfigurationForMediaInRow(mediaInRow = false)
                setMediaState(ACTIVE_MEDIA)

                assertThat(underTest.qqsMediaInRow).isFalse()
                assertThat(underTest.qsMediaInRow).isFalse()
            }
        }

    @Test
    fun mediaInRow_mediaActive_bothInRow() =
        with(kosmos) {
            testScope.testWithinLifecycle {
                setConfigurationForMediaInRow(mediaInRow = true)
                setMediaState(ACTIVE_MEDIA)

                assertThat(underTest.qqsMediaInRow).isTrue()
                assertThat(underTest.qsMediaInRow).isTrue()
            }
        }

    @Test
    fun mediaInRow_mediaRecommendation_onlyQSInRow() =
        with(kosmos) {
            testScope.testWithinLifecycle {
                setConfigurationForMediaInRow(mediaInRow = true)
                setMediaState(ANY_MEDIA)

                assertThat(underTest.qqsMediaInRow).isFalse()
                assertThat(underTest.qsMediaInRow).isTrue()
            }
        }

    @Test
    fun mediaInRow_correctConfig_noMediaVisible_noMediaInRow() =
        with(kosmos) {
            testScope.testWithinLifecycle {
                setConfigurationForMediaInRow(mediaInRow = true)
                setMediaState(NO_MEDIA)

                assertThat(underTest.qqsMediaInRow).isFalse()
                assertThat(underTest.qsMediaInRow).isFalse()
            }
        }

    private fun TestScope.setMediaState(state: MediaState) {
        with(kosmos) {
            val activeMedia = state == ACTIVE_MEDIA
+182 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.qs.panels.ui

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.getBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.width
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.qs.composefragment.QuickQuickSettingsLayout
import com.android.systemui.qs.composefragment.QuickSettingsLayout
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@SmallTest
class QSFragmentComposeTest : SysuiTestCase() {

    @get:Rule val composeTestRule = createComposeRule()

    @Test
    fun portraitLayout_qqs() {
        composeTestRule.setContent {
            QuickQuickSettingsLayout(
                tiles = { Tiles(TILES_HEIGHT_PORTRAIT) },
                media = { Media() },
                mediaInRow = false,
            )
        }

        composeTestRule.waitForIdle()

        val tilesBounds = composeTestRule.onNodeWithTag(TILES).getBoundsInRoot()
        val mediaBounds = composeTestRule.onNodeWithTag(MEDIA).getBoundsInRoot()

        // All nodes aligned in a column
        assertThat(tilesBounds.left).isEqualTo(mediaBounds.left)
        assertThat(tilesBounds.right).isEqualTo(mediaBounds.right)
        assertThat(tilesBounds.bottom).isLessThan(mediaBounds.top)
    }

    @Test
    fun landscapeLayout_qqs() {
        composeTestRule.setContent {
            QuickQuickSettingsLayout(
                tiles = { Tiles(TILES_HEIGHT_LANDSCAPE) },
                media = { Media() },
                mediaInRow = true,
            )
        }

        composeTestRule.waitForIdle()

        val tilesBounds = composeTestRule.onNodeWithTag(TILES).getBoundsInRoot()
        val mediaBounds = composeTestRule.onNodeWithTag(MEDIA).getBoundsInRoot()

        // Media to the right of tiles
        assertThat(tilesBounds.right).isLessThan(mediaBounds.left)
        // "Same" width
        assertThat((tilesBounds.width - mediaBounds.width).abs()).isAtMost(1.dp)
        // Vertically centered
        assertThat((tilesBounds.centerY - mediaBounds.centerY).abs()).isAtMost(1.dp)
    }

    @Test
    fun portraitLayout_qs() {
        composeTestRule.setContent {
            QuickSettingsLayout(
                brightness = { Brightness() },
                tiles = { Tiles(TILES_HEIGHT_PORTRAIT) },
                media = { Media() },
                mediaInRow = false,
            )
        }

        composeTestRule.waitForIdle()

        val brightnessBounds = composeTestRule.onNodeWithTag(BRIGHTNESS).getBoundsInRoot()
        val tilesBounds = composeTestRule.onNodeWithTag(TILES).getBoundsInRoot()
        val mediaBounds = composeTestRule.onNodeWithTag(MEDIA).getBoundsInRoot()

        assertThat(brightnessBounds.left).isEqualTo(tilesBounds.left)
        assertThat(tilesBounds.left).isEqualTo(mediaBounds.left)

        assertThat(brightnessBounds.right).isEqualTo(tilesBounds.right)
        assertThat(tilesBounds.right).isEqualTo(mediaBounds.right)

        assertThat(brightnessBounds.bottom).isLessThan(tilesBounds.top)
        assertThat(tilesBounds.bottom).isLessThan(mediaBounds.top)
    }

    @Test
    fun landscapeLayout_qs() {
        composeTestRule.setContent {
            QuickSettingsLayout(
                brightness = { Brightness() },
                tiles = { Tiles(TILES_HEIGHT_PORTRAIT) },
                media = { Media() },
                mediaInRow = true,
            )
        }

        composeTestRule.waitForIdle()

        val brightnessBounds = composeTestRule.onNodeWithTag(BRIGHTNESS).getBoundsInRoot()
        val tilesBounds = composeTestRule.onNodeWithTag(TILES).getBoundsInRoot()
        val mediaBounds = composeTestRule.onNodeWithTag(MEDIA).getBoundsInRoot()

        // Brightness takes full width, with left end aligned with tiles and right end aligned with
        // media
        assertThat(brightnessBounds.left).isEqualTo(tilesBounds.left)
        assertThat(brightnessBounds.right).isEqualTo(mediaBounds.right)

        // Brightness above tiles and media
        assertThat(brightnessBounds.bottom).isLessThan(tilesBounds.top)
        assertThat(brightnessBounds.bottom).isLessThan(mediaBounds.top)

        // Media to the right of tiles
        assertThat(tilesBounds.right).isLessThan(mediaBounds.left)
        // "Same" width
        assertThat((tilesBounds.width - mediaBounds.width).abs()).isAtMost(1.dp)
        // Vertically centered
        assertThat((tilesBounds.centerY - mediaBounds.centerY).abs()).isAtMost(1.dp)
    }

    private companion object {
        const val BRIGHTNESS = "brightness"
        const val TILES = "tiles"
        const val MEDIA = "media"
        val TILES_HEIGHT_PORTRAIT = 300.dp
        val TILES_HEIGHT_LANDSCAPE = 150.dp
        val MEDIA_HEIGHT = 100.dp
        val BRIGHTNESS_HEIGHT = 64.dp

        @Composable
        fun Brightness() {
            Box(modifier = Modifier.testTag(BRIGHTNESS).height(BRIGHTNESS_HEIGHT).fillMaxWidth())
        }

        @Composable
        fun Tiles(height: Dp) {
            Box(modifier = Modifier.testTag(TILES).height(height).fillMaxWidth())
        }

        @Composable
        fun Media() {
            Box(modifier = Modifier.testTag(MEDIA).height(MEDIA_HEIGHT).fillMaxWidth())
        }

        val DpRect.centerY: Dp
            get() = (top + bottom) / 2

        fun Dp.abs() = if (this > 0.dp) this else -this
    }
}
+142 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.qs.panels.ui.viewmodel

import android.content.res.Configuration
import android.content.res.mainResources
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.flags.setFlagValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
import com.android.systemui.media.controls.ui.controller.MediaLocation
import com.android.systemui.media.controls.ui.controller.mediaHostStatesManager
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.runner.RunWith
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(ParameterizedAndroidJunit4::class)
@SmallTest
class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : SysuiTestCase() {

    private val kosmos = testKosmos().apply { usingMediaInComposeFragment = testData.usingMedia }

    private val underTest by lazy {
        kosmos.mediaInRowInLandscapeViewModelFactory.create(TESTED_MEDIA_LOCATION)
    }

    @Before
    fun setUp() {
        mSetFlagsRule.setFlagValue(DualShade.FLAG_NAME, testData.shadeMode == ShadeMode.Dual)
    }

    @Test
    fun shouldMediaShowInRow() =
        with(kosmos) {
            testScope.runTest {
                underTest.activateIn(testScope)

                shadeRepository.setShadeLayoutWide(testData.shadeMode != ShadeMode.Single)
                val config =
                    Configuration(mainResources.configuration).apply {
                        orientation = testData.orientation
                        screenLayout = testData.screenLayoutLong
                    }
                fakeConfigurationRepository.onConfigurationChange(config)
                mainResources.configuration.updateFrom(config)
                mediaHostStatesManager.updateHostState(
                    testData.mediaLocation,
                    MediaHost.MediaHostStateHolder().apply { visible = testData.mediaVisible },
                )
                runCurrent()

                assertThat(underTest.shouldMediaShowInRow).isEqualTo(testData.mediaInRowExpected)
            }
        }

    data class TestData(
        val usingMedia: Boolean,
        val shadeMode: ShadeMode,
        val orientation: Int,
        val screenLayoutLong: Int,
        val mediaVisible: Boolean,
        @MediaLocation val mediaLocation: Int,
    ) {
        val mediaInRowExpected: Boolean
            get() =
                usingMedia &&
                    shadeMode == ShadeMode.Single &&
                    orientation == Configuration.ORIENTATION_LANDSCAPE &&
                    screenLayoutLong == Configuration.SCREENLAYOUT_LONG_YES &&
                    mediaVisible &&
                    mediaLocation == TESTED_MEDIA_LOCATION
    }

    companion object {
        private const val TESTED_MEDIA_LOCATION = LOCATION_QS

        @JvmStatic
        @Parameters(name = "{0}")
        fun data(): Collection<TestData> {
            val usingMediaValues = setOf(true, false)
            val shadeModeValues = setOf(ShadeMode.Single, ShadeMode.Split, ShadeMode.Dual)
            val orientationValues =
                setOf(Configuration.ORIENTATION_LANDSCAPE, Configuration.ORIENTATION_PORTRAIT)
            val screenLayoutLongValues =
                setOf(Configuration.SCREENLAYOUT_LONG_YES, Configuration.SCREENLAYOUT_LONG_NO)
            val mediaVisibleValues = setOf(true, false)
            val mediaLocationsValues = setOf(LOCATION_QS, LOCATION_QQS)

            return usingMediaValues.flatMap { usingMedia ->
                shadeModeValues.flatMap { shadeMode ->
                    orientationValues.flatMap { orientation ->
                        screenLayoutLongValues.flatMap { screenLayoutLong ->
                            mediaVisibleValues.flatMap { mediaVisible ->
                                mediaLocationsValues.map { mediaLocation ->
                                    TestData(
                                        usingMedia,
                                        shadeMode,
                                        orientation,
                                        screenLayoutLong,
                                        mediaVisible,
                                        mediaLocation,
                                    )
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
+219 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.qs.panels.ui.viewmodel

import android.content.res.mainResources
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.configurationRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
import com.android.systemui.media.controls.ui.controller.MediaLocation
import com.android.systemui.media.controls.ui.controller.mediaHostStatesManager
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment
import com.android.systemui.qs.panels.data.repository.QSColumnsRepository
import com.android.systemui.qs.panels.data.repository.qsColumnsRepository
import com.android.systemui.res.R
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@SmallTest
class QSColumnsViewModelTest : SysuiTestCase() {
    private val kosmos =
        testKosmos().apply {
            usingMediaInComposeFragment = true
            testCase.context.orCreateTestableResources.addOverride(
                R.integer.quick_settings_infinite_grid_num_columns,
                SINGLE_SPLIT_SHADE_COLUMNS,
            )
            testCase.context.orCreateTestableResources.addOverride(
                R.integer.quick_settings_dual_shade_num_columns,
                DUAL_SHADE_COLUMNS,
            )
            testCase.context.orCreateTestableResources.addOverride(
                R.integer.quick_settings_split_shade_num_columns,
                SINGLE_SPLIT_SHADE_COLUMNS,
            )
            qsColumnsRepository = QSColumnsRepository(mainResources, configurationRepository)
        }

    @Test
    @DisableFlags(DualShade.FLAG_NAME)
    fun mediaLocationNull_singleOrSplit_alwaysSingleShadeColumns() =
        with(kosmos) {
            testScope.runTest {
                val underTest = qsColumnsViewModelFactory.create(null)
                underTest.activateIn(testScope)

                setConfigurationForMediaInRow(mediaInRow = false)

                makeMediaVisible(LOCATION_QQS, visible = true)
                makeMediaVisible(LOCATION_QS, visible = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(SINGLE_SPLIT_SHADE_COLUMNS)

                setConfigurationForMediaInRow(mediaInRow = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(SINGLE_SPLIT_SHADE_COLUMNS)
            }
        }

    @Test
    @EnableFlags(DualShade.FLAG_NAME)
    fun mediaLocationNull_dualShade_alwaysDualShadeColumns() =
        with(kosmos) {
            testScope.runTest {
                val underTest = qsColumnsViewModelFactory.create(null)
                underTest.activateIn(testScope)

                setConfigurationForMediaInRow(mediaInRow = false)

                makeMediaVisible(LOCATION_QQS, visible = true)
                makeMediaVisible(LOCATION_QS, visible = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(DUAL_SHADE_COLUMNS)

                setConfigurationForMediaInRow(mediaInRow = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(DUAL_SHADE_COLUMNS)
            }
        }

    @Test
    @EnableFlags(DualShade.FLAG_NAME)
    fun mediaLocationQS_dualShade_alwaysDualShadeColumns() =
        with(kosmos) {
            testScope.runTest {
                val underTest = qsColumnsViewModelFactory.create(LOCATION_QS)
                underTest.activateIn(testScope)

                setConfigurationForMediaInRow(mediaInRow = false)

                makeMediaVisible(LOCATION_QS, visible = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(DUAL_SHADE_COLUMNS)

                setConfigurationForMediaInRow(mediaInRow = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(DUAL_SHADE_COLUMNS)
            }
        }

    @Test
    @EnableFlags(DualShade.FLAG_NAME)
    fun mediaLocationQQS_dualShade_alwaysDualShadeColumns() =
        with(kosmos) {
            testScope.runTest {
                val underTest = qsColumnsViewModelFactory.create(LOCATION_QQS)
                underTest.activateIn(testScope)

                setConfigurationForMediaInRow(mediaInRow = false)

                makeMediaVisible(LOCATION_QQS, visible = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(DUAL_SHADE_COLUMNS)

                setConfigurationForMediaInRow(mediaInRow = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(DUAL_SHADE_COLUMNS)
            }
        }

    @Test
    @DisableFlags(DualShade.FLAG_NAME)
    fun mediaLocationQS_singleOrSplit_halfColumnsOnCorrectConfigurationAndVisible() =
        with(kosmos) {
            testScope.runTest {
                val underTest = qsColumnsViewModelFactory.create(LOCATION_QS)
                underTest.activateIn(testScope)

                setConfigurationForMediaInRow(mediaInRow = false)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(SINGLE_SPLIT_SHADE_COLUMNS)

                setConfigurationForMediaInRow(mediaInRow = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(SINGLE_SPLIT_SHADE_COLUMNS)

                makeMediaVisible(LOCATION_QS, visible = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(SINGLE_SPLIT_SHADE_COLUMNS / 2)
            }
        }

    @Test
    @DisableFlags(DualShade.FLAG_NAME)
    fun mediaLocationQQS_singleOrSplit_halfColumnsOnCorrectConfigurationAndVisible() =
        with(kosmos) {
            testScope.runTest {
                val underTest = qsColumnsViewModelFactory.create(LOCATION_QQS)
                underTest.activateIn(testScope)

                setConfigurationForMediaInRow(mediaInRow = false)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(SINGLE_SPLIT_SHADE_COLUMNS)

                setConfigurationForMediaInRow(mediaInRow = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(SINGLE_SPLIT_SHADE_COLUMNS)

                makeMediaVisible(LOCATION_QQS, visible = true)
                runCurrent()

                assertThat(underTest.columns).isEqualTo(SINGLE_SPLIT_SHADE_COLUMNS / 2)
            }
        }

    companion object {
        private const val SINGLE_SPLIT_SHADE_COLUMNS = 4
        private const val DUAL_SHADE_COLUMNS = 2

        private fun Kosmos.makeMediaVisible(@MediaLocation location: Int, visible: Boolean) {
            mediaHostStatesManager.updateHostState(
                location,
                MediaHost.MediaHostStateHolder().apply { this.visible = visible },
            )
        }
    }
}
+40 −0

File changed.

Preview size limit exceeded, changes collapsed.

Loading