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

Commit 56e0cbcd authored by Fabian Kozynski's avatar Fabian Kozynski Committed by Android (Google) Code Review
Browse files

Merge changes from topic "353253280" into main

* changes:
  Migrate some viewmodel to activatables
  Add media to QSFragmentCompose
parents cbf06aaf 20919631
Loading
Loading
Loading
Loading
+10 −2
Original line number Diff line number Diff line
@@ -24,12 +24,16 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.media.controls.domain.pipeline.legacyMediaDataManagerImpl
import com.android.systemui.media.controls.domain.pipeline.mediaDataManager
import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment
import com.android.systemui.testKosmos
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
@@ -39,7 +43,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() {
    protected val kosmos = testKosmos()
    protected val kosmos = testKosmos().apply { mediaDataManager = legacyMediaDataManagerImpl }

    protected val lifecycleOwner =
        TestLifecycleOwner(
@@ -62,11 +66,15 @@ abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() {
    }

    protected inline fun TestScope.testWithinLifecycle(
        crossinline block: suspend TestScope.() -> TestResult
        usingMedia: Boolean = true,
        crossinline block: suspend TestScope.() -> TestResult,
    ): TestResult {
        return runTest {
            kosmos.usingMediaInComposeFragment = usingMedia

            lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED)
            underTest.activateIn(kosmos.testScope)
            runCurrent()
            block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) }
        }
    }
+103 −0
Original line number Diff line number Diff line
@@ -25,6 +25,15 @@ import androidx.test.filters.SmallTest
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.domain.pipeline.legacyMediaDataManagerImpl
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.media.controls.ui.controller.mediaCarouselController
import com.android.systemui.media.controls.ui.view.MediaHostState
import com.android.systemui.media.controls.ui.view.qqsMediaHost
import com.android.systemui.media.controls.ui.view.qsMediaHost
import com.android.systemui.qs.composefragment.viewmodel.MediaState.ACTIVE_MEDIA
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.res.R
@@ -35,9 +44,11 @@ import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFl
import com.android.systemui.statusbar.sysuiStatusBarStateController
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -185,6 +196,92 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest()
            }
        }

    @Test
    fun qqsMediaHost_initializedCorrectly() =
        with(kosmos) {
            testScope.testWithinLifecycle {
                assertThat(underTest.qqsMediaHost.location)
                    .isEqualTo(MediaHierarchyManager.LOCATION_QQS)
                assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED)
                assertThat(underTest.qqsMediaHost.showsOnlyActiveMedia).isTrue()
                assertThat(underTest.qqsMediaHost.hostView).isNotNull()
            }
        }

    @Test
    fun qsMediaHost_initializedCorrectly() =
        with(kosmos) {
            testScope.testWithinLifecycle {
                assertThat(underTest.qsMediaHost.location)
                    .isEqualTo(MediaHierarchyManager.LOCATION_QS)
                assertThat(underTest.qsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED)
                assertThat(underTest.qsMediaHost.showsOnlyActiveMedia).isFalse()
                assertThat(underTest.qsMediaHost.hostView).isNotNull()
            }
        }

    @Test
    fun qqsMediaVisible_onlyWhenActiveMedia() =
        with(kosmos) {
            testScope.testWithinLifecycle {
                whenever(mediaCarouselController.isLockedAndHidden()).thenReturn(false)

                assertThat(underTest.qqsMediaVisible).isEqualTo(underTest.qqsMediaHost.visible)

                setMediaState(NO_MEDIA)
                assertThat(underTest.qqsMediaVisible).isFalse()

                setMediaState(ANY_MEDIA)
                assertThat(underTest.qqsMediaVisible).isFalse()

                setMediaState(ACTIVE_MEDIA)
                assertThat(underTest.qqsMediaVisible).isTrue()
            }
        }

    @Test
    fun qsMediaVisible_onAnyMedia() =
        with(kosmos) {
            testScope.testWithinLifecycle {
                whenever(mediaCarouselController.isLockedAndHidden()).thenReturn(false)

                assertThat(underTest.qsMediaVisible).isEqualTo(underTest.qsMediaHost.visible)

                setMediaState(NO_MEDIA)
                assertThat(underTest.qsMediaVisible).isFalse()

                setMediaState(ANY_MEDIA)
                assertThat(underTest.qsMediaVisible).isTrue()

                setMediaState(ACTIVE_MEDIA)
                assertThat(underTest.qsMediaVisible).isTrue()
            }
        }

    @Test
    fun notUsingMedia_mediaNotVisible() =
        with(kosmos) {
            testScope.testWithinLifecycle(usingMedia = false) {
                setMediaState(ACTIVE_MEDIA)

                assertThat(underTest.qqsMediaVisible).isFalse()
                assertThat(underTest.qsMediaVisible).isFalse()
            }
        }

    private fun TestScope.setMediaState(state: MediaState) {
        with(kosmos) {
            val activeMedia = state == ACTIVE_MEDIA
            val anyMedia = state != NO_MEDIA
            whenever(legacyMediaDataManagerImpl.hasActiveMediaOrRecommendation())
                .thenReturn(activeMedia)
            whenever(legacyMediaDataManagerImpl.hasAnyMediaOrRecommendation()).thenReturn(anyMedia)
            qqsMediaHost.updateViewVisibility()
            qsMediaHost.updateViewVisibility()
        }
        runCurrent()
    }

    companion object {
        private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS

@@ -195,3 +292,9 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest()
        private const val epsilon = 0.001f
    }
}

private enum class MediaState {
    ACTIVE_MEDIA,
    ANY_MEDIA,
    NO_MEDIA,
}
+24 −21
Original line number Diff line number Diff line
@@ -20,21 +20,25 @@ 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.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.qs.panels.domain.interactor.qsPreferencesInteractor
import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class QuickQuickSettingsViewModelTest : SysuiTestCase() {
@@ -65,7 +69,8 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() {
            fakeConfigurationRepository.onConfigurationChange()
        }

    private val underTest = kosmos.quickQuickSettingsViewModel
    private val underTest =
        kosmos.quickQuickSettingsViewModelFactory.create().apply { activateIn(kosmos.testScope) }

    @Before
    fun setUp() {
@@ -77,17 +82,15 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() {
        with(kosmos) {
            testScope.runTest {
                setRows(2)
                val columns by collectLastValue(underTest.columns)
                val tileViewModels by collectLastValue(underTest.tileViewModels)

                assertThat(columns).isEqualTo(4)
                assertThat(underTest.columns).isEqualTo(4)
                // All tiles in 4 columns
                // [1] [2] [3 3]
                // [4] [5 5]
                // [6 6] [7] [8]
                // [9 9]

                assertThat(tileViewModels!!.map { it.tile.spec }).isEqualTo(tiles.take(5))
                assertThat(underTest.tileViewModels.map { it.tile.spec }).isEqualTo(tiles.take(5))
            }
        }

@@ -96,10 +99,8 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() {
        with(kosmos) {
            testScope.runTest {
                setRows(2)
                val columns by collectLastValue(underTest.columns)
                val tileViewModels by collectLastValue(underTest.tileViewModels)

                assertThat(columns).isEqualTo(4)
                assertThat(underTest.columns).isEqualTo(4)
                // All tiles in 4 columns
                // [1] [2] [3 3]
                // [4] [5 5]
@@ -107,9 +108,9 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() {
                // [9 9]

                setRows(3)
                assertThat(tileViewModels!!.map { it.tile.spec }).isEqualTo(tiles.take(8))
                assertThat(underTest.tileViewModels.map { it.tile.spec }).isEqualTo(tiles.take(8))
                setRows(1)
                assertThat(tileViewModels!!.map { it.tile.spec }).isEqualTo(tiles.take(3))
                assertThat(underTest.tileViewModels.map { it.tile.spec }).isEqualTo(tiles.take(3))
            }
        }

@@ -118,10 +119,8 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() {
        with(kosmos) {
            testScope.runTest {
                setRows(2)
                val columns by collectLastValue(underTest.columns)
                val tileViewModels by collectLastValue(underTest.tileViewModels)

                assertThat(columns).isEqualTo(4)
                assertThat(underTest.columns).isEqualTo(4)
                // All tiles in 4 columns
                // [1] [2] [3 3]
                // [4] [5 5]
@@ -130,8 +129,9 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() {

                // Remove tile small:4
                currentTilesInteractor.removeTiles(setOf(tiles[3]))
                runCurrent()

                assertThat(tileViewModels!!.map { it.tile.spec })
                assertThat(underTest.tileViewModels.map { it.tile.spec })
                    .isEqualTo(
                        listOf(
                                "$PREFIX_SMALL:1",
@@ -149,13 +149,16 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() {
        currentTilesInteractor.setTiles(tiles)
    }

    private fun Kosmos.setRows(rows: Int) {
    private fun TestScope.setRows(rows: Int) {
        with(kosmos) {
            testCase.context.orCreateTestableResources.addOverride(
                R.integer.quick_qs_paginated_grid_num_rows,
                rows,
            )
            fakeConfigurationRepository.onConfigurationChange()
        }
        runCurrent()
    }

    private companion object {
        const val PREFIX_SMALL = "small"
+65 −16
Original line number Diff line number Diff line
@@ -38,12 +38,14 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.windowInsetsPadding
@@ -73,13 +75,14 @@ import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastRoundToInt
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
@@ -98,10 +101,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dump.DumpManager
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.lifecycle.setSnapshotBinding
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.media.dagger.MediaModule.QS_PANEL
import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
import com.android.systemui.plugins.qs.QS
import com.android.systemui.plugins.qs.QSContainerController
import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings
@@ -127,7 +127,6 @@ import com.android.systemui.util.println
import java.io.PrintWriter
import java.util.function.Consumer
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
@@ -135,6 +134,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

@SuppressLint("ValidFragment")
class QSFragmentCompose
@@ -142,11 +142,11 @@ class QSFragmentCompose
constructor(
    private val qsFragmentComposeViewModelFactory: QSFragmentComposeViewModel.Factory,
    private val dumpManager: DumpManager,
    @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost,
    @Named(QS_PANEL) private val qsMediaHost: MediaHost,
) : LifecycleFragment(), QS, Dumpable {

    private val scrollListener = MutableStateFlow<QS.ScrollListener?>(null)
    private val collapsedMediaVisibilityChangedListener =
        MutableStateFlow<(Consumer<Boolean>)?>(null)
    private val heightListener = MutableStateFlow<QS.HeightListener?>(null)
    private val qsContainerController = MutableStateFlow<QSContainerController?>(null)

@@ -183,8 +183,6 @@ constructor(
        QSComposeFragment.isUnexpectedlyInLegacyMode()
        viewModel = qsFragmentComposeViewModelFactory.create(lifecycleScope)

        qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS)
        qsMediaHost.init(MediaHierarchyManager.LOCATION_QS)
        setListenerCollections()
        lifecycleScope.launch { viewModel.activate() }
    }
@@ -491,7 +489,7 @@ constructor(
    }

    override fun setCollapsedMediaVisibilityChangedListener(listener: Consumer<Boolean>?) {
        // TODO (b/353253280)
        collapsedMediaVisibilityChangedListener.value = listener
    }

    override fun setScrollListener(scrollListener: QS.ScrollListener?) {
@@ -534,6 +532,7 @@ constructor(
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                this@QSFragmentCompose.view?.setSnapshotBinding {
                    scrollListener.value?.onQsPanelScrollChanged(scrollState.value)
                    collapsedMediaVisibilityChangedListener.value?.accept(viewModel.qqsMediaVisible)
                }
                launch {
                    setListenerJob(
@@ -569,7 +568,7 @@ constructor(
                .squishiness
                .collectAsStateWithLifecycle()

        Column(modifier = Modifier.sysuiResTag("quick_qs_panel")) {
        Column(modifier = Modifier.sysuiResTag(ResIdTags.quickQsPanel)) {
            Box(
                modifier =
                    Modifier.fillMaxWidth()
@@ -581,6 +580,9 @@ constructor(
                                leftFromRoot + coordinates.size.width,
                                topFromRoot + coordinates.size.height,
                            )
                            if (squishiness == 1f) {
                                viewModel.qqsHeight = coordinates.size.height
                            }
                        }
                        // Use an approach layout to determien the height without squishiness, as
                        // that's the value that NPVC and QuickSettingsController care about
@@ -595,8 +597,7 @@ constructor(
                        .padding(top = { qqsPadding }, bottom = { bottomPadding })
            ) {
                if (viewModel.isQsEnabled) {
                    QuickQuickSettings(
                        viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel,
                    Column(
                        modifier =
                            Modifier.collapseExpandSemanticAction(
                                    stringResource(
@@ -608,7 +609,16 @@ constructor(
                                        QuickSettingsShade.Dimensions.Padding.roundToPx()
                                    }
                                ),
                        verticalArrangement =
                            spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
                    ) {
                        QuickQuickSettings(
                            viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel
                        )
                        if (viewModel.qqsMediaVisible) {
                            MediaObject(mediaHost = viewModel.qqsMediaHost)
                        }
                    }
                }
            }
            Spacer(modifier = Modifier.weight(1f))
@@ -645,22 +655,35 @@ constructor(
                                }
                                .onSizeChanged { viewModel.qsScrollHeight = it.height }
                                .verticalScroll(scrollState)
                                .sysuiResTag(ResIdTags.qsScroll)
                    ) {
                        Spacer(
                            modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() }
                        )
                        QuickSettingsLayout(
                            viewModel = viewModel.containerViewModel,
                            modifier = Modifier.sysuiResTag("quick_settings_panel"),
                            modifier = Modifier.sysuiResTag(ResIdTags.quickSettingsPanel),
                        )
                        Spacer(modifier = Modifier.height(8.dp))
                        if (viewModel.qsMediaVisible) {
                            MediaObject(
                                mediaHost = viewModel.qsMediaHost,
                                modifier =
                                    Modifier.padding(
                                        horizontal = {
                                            QuickSettingsShade.Dimensions.Padding.roundToPx()
                                        }
                                    ),
                            )
                        }
                    }
                }
                QuickSettingsTheme {
                    FooterActions(
                        viewModel = viewModel.footerActionsViewModel,
                        qsVisibilityLifecycleOwner = this@QSFragmentCompose,
                        modifier =
                            Modifier.sysuiResTag("qs_footer_actions")
                            Modifier.sysuiResTag(ResIdTags.qsFooterActions)
                                .element(ElementKeys.FooterActions),
                    )
                }
@@ -914,3 +937,29 @@ private fun Modifier.gesturesDisabled(disabled: Boolean) =
    } else {
        this
    }

@Composable
private fun MediaObject(mediaHost: MediaHost, modifier: Modifier = Modifier) {
    Box {
        AndroidView(
            modifier = modifier,
            factory = {
                mediaHost.hostView.apply {
                    layoutParams =
                        FrameLayout.LayoutParams(
                            FrameLayout.LayoutParams.MATCH_PARENT,
                            FrameLayout.LayoutParams.WRAP_CONTENT,
                        )
                }
            },
            onReset = {},
        )
    }
}

private object ResIdTags {
    const val quickSettingsPanel = "quick_settings_panel"
    const val quickQsPanel = "quick_qs_panel"
    const val qsScroll = "expanded_qs_scroll_view"
    const val qsFooterActions = "qs_footer_actions"
}
+40 −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.composefragment.dagger

import android.content.Context
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.util.Utils
import dagger.Module
import dagger.Provides
import javax.inject.Named

@Module
interface QSFragmentComposeModule {

    companion object {
        const val QS_USING_MEDIA_PLAYER = "compose_fragment_using_media_player"

        @Provides
        @SysUISingleton
        @Named(QS_USING_MEDIA_PLAYER)
        fun providesUsingMedia(@Application context: Context): Boolean {
            return Utils.useQsMediaPlayer(context)
        }
    }
}
Loading