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

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

Merge "[flexiglass] QS scene view-model is a SysUiViewModel" into main

parents cd6cccb5 c1b875b5
Loading
Loading
Loading
Loading
+16 −5
Original line number Diff line number Diff line
@@ -92,7 +92,8 @@ import com.android.systemui.notifications.ui.composable.NotificationStackCutoffG
import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.InQS
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneActionsViewModel
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneContentViewModel
import com.android.systemui.res.R
import com.android.systemui.scene.session.ui.composable.SaveableSession
import com.android.systemui.scene.shared.model.Scenes
@@ -120,8 +121,9 @@ class QuickSettingsScene
constructor(
    private val shadeSession: SaveableSession,
    private val notificationStackScrollView: Lazy<NotificationScrollView>,
    private val viewModel: QuickSettingsSceneViewModel,
    private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
    private val actionsViewModelFactory: QuickSettingsSceneActionsViewModel.Factory,
    private val contentViewModelFactory: QuickSettingsSceneContentViewModel.Factory,
    private val tintedIconManagerFactory: TintedIconManager.Factory,
    private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
    private val statusBarIconController: StatusBarIconController,
@@ -130,8 +132,16 @@ constructor(
) : ComposableScene {
    override val key = Scenes.QuickSettings

    private val actionsViewModel: QuickSettingsSceneActionsViewModel by lazy {
        actionsViewModelFactory.create()
    }

    override val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
        viewModel.destinationScenes
        actionsViewModel.actions

    override suspend fun activate() {
        actionsViewModel.activate()
    }

    @Composable
    override fun SceneScope.Content(
@@ -139,7 +149,7 @@ constructor(
    ) {
        QuickSettingsScene(
            notificationStackScrollView = notificationStackScrollView.get(),
            viewModel = viewModel,
            viewModelFactory = contentViewModelFactory,
            notificationsPlaceholderViewModel =
                rememberViewModel { notificationsPlaceholderViewModelFactory.create() },
            createTintedIconManager = tintedIconManagerFactory::create,
@@ -156,7 +166,7 @@ constructor(
@Composable
private fun SceneScope.QuickSettingsScene(
    notificationStackScrollView: NotificationScrollView,
    viewModel: QuickSettingsSceneViewModel,
    viewModelFactory: QuickSettingsSceneContentViewModel.Factory,
    notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
    createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
    createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
@@ -168,6 +178,7 @@ private fun SceneScope.QuickSettingsScene(
) {
    val cutoutLocation = LocalDisplayCutout.current.location

    val viewModel = rememberViewModel { viewModelFactory.create() }
    val brightnessMirrorViewModel = rememberViewModel {
        viewModel.brightnessMirrorViewModelFactory.create()
    }
+19 −79
Original line number Diff line number Diff line
@@ -36,11 +36,7 @@ import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintA
import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.qs.FooterActionsController
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneBackInteractor
@@ -49,41 +45,29 @@ import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModelFactory
import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.times
import org.mockito.Mockito.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
@EnableSceneContainer
class QuickSettingsSceneViewModelTest : SysuiTestCase() {
class QuickSettingsSceneActionsViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
    private val footerActionsViewModel = mock<FooterActionsViewModel>()
    private val footerActionsViewModelFactory =
        mock<FooterActionsViewModel.Factory> {
            whenever(create(any())).thenReturn(footerActionsViewModel)
        }
    private val footerActionsController = mock<FooterActionsController>()

    private val sceneInteractor = kosmos.sceneInteractor
    private val sceneBackInteractor = kosmos.sceneBackInteractor
    private val sceneContainerStartable = kosmos.sceneContainerStartable

    private lateinit var underTest: QuickSettingsSceneViewModel
    private lateinit var underTest: QuickSettingsSceneActionsViewModel

    @Before
    fun setUp() {
@@ -91,22 +75,18 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {

        sceneContainerStartable.start()
        underTest =
            QuickSettingsSceneViewModel(
                brightnessMirrorViewModelFactory = kosmos.brightnessMirrorViewModelFactory,
                shadeHeaderViewModelFactory = kosmos.shadeHeaderViewModelFactory,
            QuickSettingsSceneActionsViewModel(
                qsSceneAdapter = qsFlexiglassAdapter,
                footerActionsViewModelFactory = footerActionsViewModelFactory,
                footerActionsController = footerActionsController,
                sceneBackInteractor = sceneBackInteractor,
                mediaCarouselInteractor = kosmos.mediaCarouselInteractor,
            )
        underTest.activateIn(testScope)
    }

    @Test
    fun destinations_whenNotCustomizing_unlocked() =
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, false)
            val destinations by collectLastValue(underTest.destinationScenes)
            val actions by collectLastValue(underTest.actions)
            val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
            qsFlexiglassAdapter.setCustomizing(false)
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
@@ -116,7 +96,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
                SuccessFingerprintAuthenticationStatus(0, true)
            )

            assertThat(destinations)
            assertThat(actions)
                .isEqualTo(
                    mapOf(
                        Back to UserActionResult(Scenes.Shade),
@@ -135,7 +115,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, false)
            qsFlexiglassAdapter.setCustomizing(false)
            val destinations by collectLastValue(underTest.destinationScenes)
            val actions by collectLastValue(underTest.actions)

            val currentScene by collectLastValue(sceneInteractor.currentScene)
            val backScene by collectLastValue(sceneBackInteractor.backScene)
@@ -145,7 +125,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
            assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
            assertThat(backScene).isEqualTo(Scenes.Lockscreen)

            assertThat(destinations)
            assertThat(actions)
                .isEqualTo(
                    mapOf(
                        Back to UserActionResult(Scenes.Lockscreen),
@@ -164,7 +144,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, false)
            qsFlexiglassAdapter.setCustomizing(false)
            val destinations by collectLastValue(underTest.destinationScenes)
            val actions by collectLastValue(underTest.actions)

            val currentScene by collectLastValue(sceneInteractor.currentScene)
            val backScene by collectLastValue(sceneBackInteractor.backScene)
@@ -176,7 +156,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {

            assertThat(currentScene).isEqualTo(Scenes.Gone)
            assertThat(backScene).isNull()
            assertThat(destinations)
            assertThat(actions)
                .isEqualTo(
                    mapOf(
                        Back to UserActionResult(Scenes.Shade),
@@ -194,7 +174,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
    fun destinations_whenNotCustomizing_authMethodSwipe_lockscreenNotDismissed() =
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, false)
            val destinations by collectLastValue(underTest.destinationScenes)
            val actions by collectLastValue(underTest.actions)
            val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
            qsFlexiglassAdapter.setCustomizing(false)
            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
@@ -202,7 +182,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
                AuthenticationMethodModel.None
            )

            assertThat(destinations)
            assertThat(actions)
                .isEqualTo(
                    mapOf(
                        Back to UserActionResult(Scenes.Shade),
@@ -220,17 +200,17 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
    fun destinations_whenCustomizing_noDestinations() =
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, false)
            val destinations by collectLastValue(underTest.destinationScenes)
            val actions by collectLastValue(underTest.actions)
            qsFlexiglassAdapter.setCustomizing(true)

            assertThat(destinations).isEmpty()
            assertThat(actions).isEmpty()
        }

    @Test
    fun destinations_whenNotCustomizing_inSplitShade_unlocked() =
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, true)
            val destinations by collectLastValue(underTest.destinationScenes)
            val actions by collectLastValue(underTest.actions)
            val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
            qsFlexiglassAdapter.setCustomizing(false)
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
@@ -240,7 +220,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
                SuccessFingerprintAuthenticationStatus(0, true)
            )

            assertThat(destinations)
            assertThat(actions)
                .isEqualTo(
                    mapOf(
                        Back to UserActionResult(Scenes.Shade),
@@ -258,49 +238,9 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
    fun destinations_whenCustomizing_inSplitShade_noDestinations() =
        testScope.runTest {
            overrideResource(R.bool.config_use_split_notification_shade, true)
            val destinations by collectLastValue(underTest.destinationScenes)
            val actions by collectLastValue(underTest.actions)
            qsFlexiglassAdapter.setCustomizing(true)

            assertThat(destinations).isEmpty()
        }

    @Test
    fun gettingViewModelInitializesControllerOnlyOnce() {
        underTest.getFooterActionsViewModel(mock())
        underTest.getFooterActionsViewModel(mock())

        verify(footerActionsController, times(1)).init()
    }

    @Test
    fun addAndRemoveMedia_mediaVisibilityIsUpdated() =
        testScope.runTest {
            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
            val isMediaVisible by collectLastValue(underTest.isMediaVisible)
            val userMedia = MediaData(active = true)

            assertThat(isMediaVisible).isFalse()

            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)

            assertThat(isMediaVisible).isTrue()

            kosmos.mediaFilterRepository.removeSelectedUserMediaEntry(userMedia.instanceId)

            assertThat(isMediaVisible).isFalse()
        }

    @Test
    fun addInactiveMedia_mediaVisibilityIsUpdated() =
        testScope.runTest {
            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
            val isMediaVisible by collectLastValue(underTest.isMediaVisible)
            val userMedia = MediaData(active = false)

            assertThat(isMediaVisible).isFalse()

            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)

            assertThat(isMediaVisible).isTrue()
            assertThat(actions).isEmpty()
        }
}
+126 −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.qs.ui.viewmodel

import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.qs.FooterActionsController
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModelFactory
import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.times
import org.mockito.Mockito.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
@EnableSceneContainer
class QuickSettingsSceneContentViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
    private val footerActionsViewModel = mock<FooterActionsViewModel>()
    private val footerActionsViewModelFactory =
        mock<FooterActionsViewModel.Factory> {
            whenever(create(any())).thenReturn(footerActionsViewModel)
        }
    private val footerActionsController = mock<FooterActionsController>()

    private val sceneContainerStartable = kosmos.sceneContainerStartable

    private lateinit var underTest: QuickSettingsSceneContentViewModel

    @Before
    fun setUp() {
        kosmos.fakeFeatureFlagsClassic.set(Flags.NEW_NETWORK_SLICE_UI, false)

        sceneContainerStartable.start()
        underTest =
            QuickSettingsSceneContentViewModel(
                brightnessMirrorViewModelFactory = kosmos.brightnessMirrorViewModelFactory,
                shadeHeaderViewModelFactory = kosmos.shadeHeaderViewModelFactory,
                qsSceneAdapter = qsFlexiglassAdapter,
                footerActionsViewModelFactory = footerActionsViewModelFactory,
                footerActionsController = footerActionsController,
                mediaCarouselInteractor = kosmos.mediaCarouselInteractor,
            )
        underTest.activateIn(testScope)
    }

    @Test
    fun gettingViewModelInitializesControllerOnlyOnce() {
        underTest.getFooterActionsViewModel(mock())
        underTest.getFooterActionsViewModel(mock())

        verify(footerActionsController, times(1)).init()
    }

    @Test
    fun addAndRemoveMedia_mediaVisibilityIsUpdated() =
        testScope.runTest {
            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
            val isMediaVisible by collectLastValue(underTest.isMediaVisible)
            val userMedia = MediaData(active = true)

            assertThat(isMediaVisible).isFalse()

            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)

            assertThat(isMediaVisible).isTrue()

            kosmos.mediaFilterRepository.removeSelectedUserMediaEntry(userMedia.instanceId)

            assertThat(isMediaVisible).isFalse()
        }

    @Test
    fun addInactiveMedia_mediaVisibilityIsUpdated() =
        testScope.runTest {
            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
            val isMediaVisible by collectLastValue(underTest.isMediaVisible)
            val userMedia = MediaData(active = false)

            assertThat(isMediaVisible).isFalse()

            kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)

            assertThat(isMediaVisible).isTrue()
        }
}
+88 −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.qs.ui.viewmodel

import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.scene.domain.interactor.SceneBackInteractor
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map

/**
 * Models the UI state needed to figure out which user actions can trigger navigation from the quick
 * settings scene to other scenes.
 *
 * Different from [QuickSettingsSceneContentViewModel] that models UI state needed for rendering the
 * content of the quick settings scene.
 */
class QuickSettingsSceneActionsViewModel
@AssistedInject
constructor(
    private val qsSceneAdapter: QSSceneAdapter,
    sceneBackInteractor: SceneBackInteractor,
) : SceneActionsViewModel() {

    private val backScene: Flow<SceneKey> =
        sceneBackInteractor.backScene
            .filter { it != Scenes.QuickSettings }
            .map { it ?: Scenes.Shade }

    override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
        combine(
                qsSceneAdapter.isCustomizerShowing,
                backScene,
            ) { isCustomizing, backScene ->
                buildMap<UserAction, UserActionResult> {
                    if (isCustomizing) {
                        // TODO(b/332749288) Empty map so there are no back handlers and back can
                        // close
                        // customizer

                        // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade
                        // while customizing
                    } else {
                        put(Back, UserActionResult(backScene))
                        put(Swipe(SwipeDirection.Up), UserActionResult(backScene))
                        put(
                            Swipe(fromSource = Edge.Bottom, direction = SwipeDirection.Up),
                            UserActionResult(SceneFamilies.Home),
                        )
                    }
                }
            }
            .collectLatest { actions -> setActions(actions) }
    }

    @AssistedFactory
    interface Factory {
        fun create(): QuickSettingsSceneActionsViewModel
    }
}
+17 −49

File changed and moved.

Preview size limit exceeded, changes collapsed.