Loading packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt +33 −2 Original line number Diff line number Diff line Loading @@ -87,6 +87,8 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.panels.ui.compose.toolbar.TextFeedback.tag import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackViewModel import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.qs.ui.composable.QuickSettingsTheme import com.android.systemui.qs.ui.compose.borderOnFocus Loading Loading @@ -145,6 +147,10 @@ fun FooterActions( var userSwitcher by remember { mutableStateOf<FooterActionsButtonViewModel?>(null) } var power by remember { mutableStateOf(viewModel.initialPower()) } var textFeedback by remember { mutableStateOf<TextFeedbackViewModel>(TextFeedbackViewModel.NoFeedback) } LaunchedEffect( context, qsVisibilityLifecycleOwner, Loading @@ -152,6 +158,7 @@ fun FooterActions( viewModel.security, viewModel.foregroundServices, viewModel.userSwitcher, viewModel.textFeedback, ) { launch { // Listen for dialog requests as soon as we are composed, even when not visible. Loading @@ -164,6 +171,7 @@ fun FooterActions( launch { viewModel.foregroundServices.collect { foregroundServices = it } } launch { viewModel.userSwitcher.collect { userSwitcher = it } } launch { viewModel.power.collect { power = it } } launch { viewModel.textFeedback.collect { textFeedback = it } } } } Loading Loading @@ -215,12 +223,20 @@ fun FooterActions( verticalAlignment = Alignment.CenterVertically, ) { CompositionLocalProvider(LocalContentColor provides contentColor) { if (security == null && foregroundServices == null) { if ( security == null && foregroundServices == null && textFeedback == TextFeedbackViewModel.NoFeedback ) { Spacer(Modifier.weight(1f)) } val useModifierBasedExpandable = remember { QSComposeFragment.isEnabled } if (textFeedback != TextFeedbackViewModel.NoFeedback) { TextFeedback({ textFeedback }, Modifier.weight(1f)) } else { SecurityButton({ security }, useModifierBasedExpandable, Modifier.weight(1f)) } ForegroundServicesButton({ foregroundServices }, useModifierBasedExpandable) IconButton( { userSwitcher }, Loading Loading @@ -261,6 +277,21 @@ private fun SecurityButton( ) } @Composable private fun TextFeedback(model: () -> TextFeedbackViewModel, modifier: Modifier = Modifier) { val model = model() if (model is TextFeedbackViewModel.LoadedTextFeedback) { TextButton( model.icon, model.label, showNewDot = false, useModifierBasedExpandable = false, onClick = null, modifier = modifier.tag(), ) } } /** The foreground services button. */ @Composable private fun RowScope.ForegroundServicesButton( Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +11 −2 Original line number Diff line number Diff line Loading @@ -39,7 +39,8 @@ import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import org.junit.Before import org.junit.Test import org.junit.runner.RunWith Loading @@ -53,11 +54,19 @@ import org.mockito.kotlin.eq @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper class FooterActionsInteractorTest : SysuiTestCase() { private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) private lateinit var utils: FooterActionsTestUtils @Before fun setUp() { utils = FooterActionsTestUtils(context, TestableLooper.get(this), TestCoroutineScheduler()) utils = FooterActionsTestUtils( context, TestableLooper.get(this), testScope.testScheduler, testScope.backgroundScope, ) } @Test Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +121 −1 Original line number Diff line number Diff line Loading @@ -18,25 +18,38 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.graphics.drawable.Drawable import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import android.view.ContextThemeWrapper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.settingslib.Utils import com.android.settingslib.drawable.UserIconDrawable import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.qs.FakeFgsManagerController import com.android.systemui.qs.QSSecurityFooterUtils import com.android.systemui.qs.QsEventLoggerFake import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.qs.footer.FooterActionsTestUtils import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.base.shared.model.FakeQSTileConfigProvider import com.android.systemui.qs.tiles.base.shared.model.QSTileConfigProvider import com.android.systemui.res.R import com.android.systemui.security.data.model.SecurityModel import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.connectivity.ConnectivityModule import com.android.systemui.statusbar.policy.FakeSecurityController import com.android.systemui.statusbar.policy.FakeUserInfoController import com.android.systemui.statusbar.policy.FakeUserInfoController.FakeInfo Loading Loading @@ -72,7 +85,13 @@ class FooterActionsViewModelTest : SysuiTestCase() { @Before fun setUp() { utils = FooterActionsTestUtils(context, TestableLooper.get(this), testScope.testScheduler) utils = FooterActionsTestUtils( context, TestableLooper.get(this), testScope.testScheduler, testScope.backgroundScope, ) } private fun runTest(block: suspend TestScope.() -> Unit) { Loading Loading @@ -440,4 +459,105 @@ class FooterActionsViewModelTest : SysuiTestCase() { underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = false) assertThat(underTest.backgroundAlpha.value).isEqualTo(1f) } @Test @DisableFlags(QSComposeFragment.FLAG_NAME) @DisableSceneContainer fun textFeedback_neverFeedback() = runTest { val qsTileConfigProvider = createAndPopulateQsTileConfigProvider() val textFeedbackInteractor = utils.textFeedbackInteractor(qsTileConfigProvider = qsTileConfigProvider) val underTest = utils.footerActionsViewModel( textFeedbackInteractor = textFeedbackInteractor, shadeMode = ShadeMode.Single, ) val textFeedback by collectLastValue(underTest.textFeedback) assertThat(textFeedback).isEqualTo(TextFeedbackViewModel.NoFeedback) textFeedbackInteractor.requestShowFeedback(AIRPLANE_MODE_TILE_SPEC) assertThat(textFeedback).isEqualTo(TextFeedbackViewModel.NoFeedback) } @Test @EnableFlags(QSComposeFragment.FLAG_NAME) fun textFeedback_composeFragmentEnabled() = runTest { textFeedback_newComposeUI() } @Test @EnableSceneContainer fun textFeedback_sceneContainerEnabled() = runTest { textFeedback_newComposeUI() } private fun TestScope.textFeedback_newComposeUI() { val qsTileConfigProvider = createAndPopulateQsTileConfigProvider() val textFeedbackInteractor = utils.textFeedbackInteractor(qsTileConfigProvider = qsTileConfigProvider) val securityController = FakeSecurityController() // We need Security so the combined flow will have at least one value val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() // Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the // logic in securityToConfig. val securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null } whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer { securityToConfig(it.arguments.first() as SecurityModel) } val fgsManagerController = FakeFgsManagerController(showFooterDot = false, numRunningPackages = 1) val underTest = utils.footerActionsViewModel( textFeedbackInteractor = textFeedbackInteractor, footerActionsInteractor = utils.footerActionsInteractor( foregroundServicesRepository = utils.foregroundServicesRepository(fgsManagerController), securityRepository = utils.securityRepository(securityController), qsSecurityFooterUtils = qsSecurityFooterUtils, ), shadeMode = ShadeMode.Single, ) val textFeedback by collectLastValue(underTest.textFeedback) val foregroundServices by collectLastValue(underTest.foregroundServices) assertThat(textFeedback).isEqualTo(TextFeedbackViewModel.NoFeedback) assertThat(foregroundServices!!.displayText).isTrue() textFeedbackInteractor.requestShowFeedback(AIRPLANE_MODE_TILE_SPEC) val config = qsTileConfigProvider.getConfig(AIRPLANE_MODE_TILE_SPEC.spec) assertThat(textFeedback) .isEqualTo( TextFeedbackViewModel.LoadedTextFeedback( label = context.getString(config.uiConfig.labelRes), icon = Icon.Loaded( drawable = context.getDrawable(config.uiConfig.iconRes)!!, contentDescription = null, res = config.uiConfig.iconRes, ), ) ) assertThat(foregroundServices!!.displayText).isFalse() } companion object { val AIRPLANE_MODE_TILE_SPEC = TileSpec.create(ConnectivityModule.AIRPLANE_MODE_TILE_SPEC) private fun createAndPopulateQsTileConfigProvider(): QSTileConfigProvider { val logger = QsEventLoggerFake(UiEventLoggerFake(), InstanceIdSequenceFake(Int.MAX_VALUE)) return FakeQSTileConfigProvider().apply { putConfig( AIRPLANE_MODE_TILE_SPEC, ConnectivityModule.provideAirplaneModeTileConfig(logger), ) } } } } packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/ToggleTextFeedbackRepositoryTest.kt 0 → 100644 +96 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.data.repository import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.qs.panels.data.model.TextFeedbackRequestModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.junit.Before import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class ToggleTextFeedbackRepositoryTest : SysuiTestCase() { private val kosmos = testKosmos() private val underTest = kosmos.toggleTextFeedbackRepository @Before fun setUp() { underTest.activateIn(kosmos.testScope) } @Test fun noFeedbackOnStart() = with(kosmos) { runTest { val feedbackRequest by collectLastValue(underTest.textFeedback) assertThat(feedbackRequest).isEqualTo(TextFeedbackRequestModel.NoFeedback) } } @Test fun feedbackRequestShown() = with(kosmos) { runTest { val feedbackRequest by collectLastValue(underTest.textFeedback) underTest.setTextFeedback(TILE_SPEC) assertThat(feedbackRequest) .isEqualTo(TextFeedbackRequestModel.FeedbackForTile(TILE_SPEC)) } } @Test fun feedbackRequest_noFeedbackIfNoCollectors_firstIsAlwaysNoFeedback() = with(kosmos) { runTest { val collectionJob = testScope.backgroundScope.launch { underTest.textFeedback.collect {} } underTest.setTextFeedback(TILE_SPEC) runCurrent() collectionJob.cancelAndJoin() runCurrent() assertThat(underTest.textFeedback.first()) .isEqualTo(TextFeedbackRequestModel.NoFeedback) } } companion object { private val TILE_SPEC = TileSpec.create("a") private val OTHER_TILE_SPEC = TileSpec.create("b") } } packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TextFeedbackInteractorTest.kt 0 → 100644 +254 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.domain.interactor import android.content.ComponentName import android.graphics.drawable.TestStubDrawable import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.qs.panels.domain.model.TextFeedbackModel import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.base.shared.model.QSTileConfig import com.android.systemui.qs.tiles.base.shared.model.populateQsTileConfigProvider import com.android.systemui.qs.tiles.impl.airplane.qsAirplaneModeTileConfig import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig import com.android.systemui.settings.fakeUserTracker import com.android.systemui.settings.userTracker import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import org.junit.runner.RunWith import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class TextFeedbackInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().apply { populateQsTileConfigProvider() whenever(fakeUserTracker.userContext.packageManager).thenReturn(mock()) } private val underTest = kosmos.textFeedbackInteractor @Test fun noFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun knownTileTextFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() assertThat(textFeedback).isEqualTo(qsAirplaneModeTileConfig.toTextFeedbackModel()) } } @Test fun unknownTileTextFeedback_noFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(TileSpec.create("unknown")) runCurrent() assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun customTile_notInstalled_NoFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(CUSTOM_TILE_SPEC) runCurrent() assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun customTile_installed_loadedIconAndLabel() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) fakeInstalledTilesRepository.setInstalledServicesForUser( userTracker.userId, listOf( FakeInstalledTilesComponentRepository.ServiceInfo( CUSTOM_TILE_SPEC.componentName, serviceName = SERVICE_NAME, serviceIcon = SERVICE_ICON, ) ), ) runCurrent() underTest.requestShowFeedback(CUSTOM_TILE_SPEC) runCurrent() assertThat(textFeedback) .isEqualTo( TextFeedbackModel.LoadedTextFeedback( label = SERVICE_NAME, icon = Icon.Loaded(SERVICE_ICON, null), ) ) } } @Test fun customTile_installed_differentUser_noFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) fakeInstalledTilesRepository.setInstalledServicesForUser( userTracker.userId + 1, listOf( FakeInstalledTilesComponentRepository.ServiceInfo( CUSTOM_TILE_SPEC.componentName, serviceName = SERVICE_NAME, serviceIcon = SERVICE_ICON, ) ), ) runCurrent() underTest.requestShowFeedback(CUSTOM_TILE_SPEC) runCurrent() assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun feedbackRequestRemainsVisibleForSomeTime() = with(kosmos) { runTest { val feedbackRequest by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) assertThat(feedbackRequest) .isEqualTo(qsAirplaneModeTileConfig.toTextFeedbackModel()) } } @Test fun feedbackRequest_afterClearDelay_noFeedback() = with(kosmos) { runTest { val feedbackRequest by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY + 1.milliseconds) runCurrent() assertThat(feedbackRequest).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun feedbackRequest_thenAnotherFeedbackRequest_timerRestarted() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) runCurrent() underTest.requestShowFeedback(qsFlashlightTileConfig.tileSpec) assertThat(textFeedback).isEqualTo(qsFlashlightTileConfig.toTextFeedbackModel()) testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) assertThat(textFeedback).isEqualTo(qsFlashlightTileConfig.toTextFeedbackModel()) testScope.advanceTimeBy(2.milliseconds) assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun feedbackRequest_thenSameTileRequest_timerRestarted() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) runCurrent() underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) assertThat(textFeedback).isEqualTo(qsAirplaneModeTileConfig.toTextFeedbackModel()) testScope.advanceTimeBy(2.milliseconds) assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } companion object { private fun QSTileConfig.toTextFeedbackModel(): TextFeedbackModel.TextFeedback { return TextFeedbackModel.TextFeedback(uiConfig.labelRes, uiConfig.iconRes) } private val CUSTOM_TILE_SPEC = TileSpec.create(ComponentName("pkg", "srv")) private val SERVICE_NAME = "TileService" private val SERVICE_ICON = TestStubDrawable("tile_service_icon") } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt +33 −2 Original line number Diff line number Diff line Loading @@ -87,6 +87,8 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.panels.ui.compose.toolbar.TextFeedback.tag import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackViewModel import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.qs.ui.composable.QuickSettingsTheme import com.android.systemui.qs.ui.compose.borderOnFocus Loading Loading @@ -145,6 +147,10 @@ fun FooterActions( var userSwitcher by remember { mutableStateOf<FooterActionsButtonViewModel?>(null) } var power by remember { mutableStateOf(viewModel.initialPower()) } var textFeedback by remember { mutableStateOf<TextFeedbackViewModel>(TextFeedbackViewModel.NoFeedback) } LaunchedEffect( context, qsVisibilityLifecycleOwner, Loading @@ -152,6 +158,7 @@ fun FooterActions( viewModel.security, viewModel.foregroundServices, viewModel.userSwitcher, viewModel.textFeedback, ) { launch { // Listen for dialog requests as soon as we are composed, even when not visible. Loading @@ -164,6 +171,7 @@ fun FooterActions( launch { viewModel.foregroundServices.collect { foregroundServices = it } } launch { viewModel.userSwitcher.collect { userSwitcher = it } } launch { viewModel.power.collect { power = it } } launch { viewModel.textFeedback.collect { textFeedback = it } } } } Loading Loading @@ -215,12 +223,20 @@ fun FooterActions( verticalAlignment = Alignment.CenterVertically, ) { CompositionLocalProvider(LocalContentColor provides contentColor) { if (security == null && foregroundServices == null) { if ( security == null && foregroundServices == null && textFeedback == TextFeedbackViewModel.NoFeedback ) { Spacer(Modifier.weight(1f)) } val useModifierBasedExpandable = remember { QSComposeFragment.isEnabled } if (textFeedback != TextFeedbackViewModel.NoFeedback) { TextFeedback({ textFeedback }, Modifier.weight(1f)) } else { SecurityButton({ security }, useModifierBasedExpandable, Modifier.weight(1f)) } ForegroundServicesButton({ foregroundServices }, useModifierBasedExpandable) IconButton( { userSwitcher }, Loading Loading @@ -261,6 +277,21 @@ private fun SecurityButton( ) } @Composable private fun TextFeedback(model: () -> TextFeedbackViewModel, modifier: Modifier = Modifier) { val model = model() if (model is TextFeedbackViewModel.LoadedTextFeedback) { TextButton( model.icon, model.label, showNewDot = false, useModifierBasedExpandable = false, onClick = null, modifier = modifier.tag(), ) } } /** The foreground services button. */ @Composable private fun RowScope.ForegroundServicesButton( Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt +11 −2 Original line number Diff line number Diff line Loading @@ -39,7 +39,8 @@ import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import org.junit.Before import org.junit.Test import org.junit.runner.RunWith Loading @@ -53,11 +54,19 @@ import org.mockito.kotlin.eq @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper class FooterActionsInteractorTest : SysuiTestCase() { private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) private lateinit var utils: FooterActionsTestUtils @Before fun setUp() { utils = FooterActionsTestUtils(context, TestableLooper.get(this), TestCoroutineScheduler()) utils = FooterActionsTestUtils( context, TestableLooper.get(this), testScope.testScheduler, testScope.backgroundScope, ) } @Test Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +121 −1 Original line number Diff line number Diff line Loading @@ -18,25 +18,38 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.graphics.drawable.Drawable import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import android.view.ContextThemeWrapper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.settingslib.Utils import com.android.settingslib.drawable.UserIconDrawable import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.qs.FakeFgsManagerController import com.android.systemui.qs.QSSecurityFooterUtils import com.android.systemui.qs.QsEventLoggerFake import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.qs.footer.FooterActionsTestUtils import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.base.shared.model.FakeQSTileConfigProvider import com.android.systemui.qs.tiles.base.shared.model.QSTileConfigProvider import com.android.systemui.res.R import com.android.systemui.security.data.model.SecurityModel import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.connectivity.ConnectivityModule import com.android.systemui.statusbar.policy.FakeSecurityController import com.android.systemui.statusbar.policy.FakeUserInfoController import com.android.systemui.statusbar.policy.FakeUserInfoController.FakeInfo Loading Loading @@ -72,7 +85,13 @@ class FooterActionsViewModelTest : SysuiTestCase() { @Before fun setUp() { utils = FooterActionsTestUtils(context, TestableLooper.get(this), testScope.testScheduler) utils = FooterActionsTestUtils( context, TestableLooper.get(this), testScope.testScheduler, testScope.backgroundScope, ) } private fun runTest(block: suspend TestScope.() -> Unit) { Loading Loading @@ -440,4 +459,105 @@ class FooterActionsViewModelTest : SysuiTestCase() { underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = false) assertThat(underTest.backgroundAlpha.value).isEqualTo(1f) } @Test @DisableFlags(QSComposeFragment.FLAG_NAME) @DisableSceneContainer fun textFeedback_neverFeedback() = runTest { val qsTileConfigProvider = createAndPopulateQsTileConfigProvider() val textFeedbackInteractor = utils.textFeedbackInteractor(qsTileConfigProvider = qsTileConfigProvider) val underTest = utils.footerActionsViewModel( textFeedbackInteractor = textFeedbackInteractor, shadeMode = ShadeMode.Single, ) val textFeedback by collectLastValue(underTest.textFeedback) assertThat(textFeedback).isEqualTo(TextFeedbackViewModel.NoFeedback) textFeedbackInteractor.requestShowFeedback(AIRPLANE_MODE_TILE_SPEC) assertThat(textFeedback).isEqualTo(TextFeedbackViewModel.NoFeedback) } @Test @EnableFlags(QSComposeFragment.FLAG_NAME) fun textFeedback_composeFragmentEnabled() = runTest { textFeedback_newComposeUI() } @Test @EnableSceneContainer fun textFeedback_sceneContainerEnabled() = runTest { textFeedback_newComposeUI() } private fun TestScope.textFeedback_newComposeUI() { val qsTileConfigProvider = createAndPopulateQsTileConfigProvider() val textFeedbackInteractor = utils.textFeedbackInteractor(qsTileConfigProvider = qsTileConfigProvider) val securityController = FakeSecurityController() // We need Security so the combined flow will have at least one value val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() // Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the // logic in securityToConfig. val securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null } whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer { securityToConfig(it.arguments.first() as SecurityModel) } val fgsManagerController = FakeFgsManagerController(showFooterDot = false, numRunningPackages = 1) val underTest = utils.footerActionsViewModel( textFeedbackInteractor = textFeedbackInteractor, footerActionsInteractor = utils.footerActionsInteractor( foregroundServicesRepository = utils.foregroundServicesRepository(fgsManagerController), securityRepository = utils.securityRepository(securityController), qsSecurityFooterUtils = qsSecurityFooterUtils, ), shadeMode = ShadeMode.Single, ) val textFeedback by collectLastValue(underTest.textFeedback) val foregroundServices by collectLastValue(underTest.foregroundServices) assertThat(textFeedback).isEqualTo(TextFeedbackViewModel.NoFeedback) assertThat(foregroundServices!!.displayText).isTrue() textFeedbackInteractor.requestShowFeedback(AIRPLANE_MODE_TILE_SPEC) val config = qsTileConfigProvider.getConfig(AIRPLANE_MODE_TILE_SPEC.spec) assertThat(textFeedback) .isEqualTo( TextFeedbackViewModel.LoadedTextFeedback( label = context.getString(config.uiConfig.labelRes), icon = Icon.Loaded( drawable = context.getDrawable(config.uiConfig.iconRes)!!, contentDescription = null, res = config.uiConfig.iconRes, ), ) ) assertThat(foregroundServices!!.displayText).isFalse() } companion object { val AIRPLANE_MODE_TILE_SPEC = TileSpec.create(ConnectivityModule.AIRPLANE_MODE_TILE_SPEC) private fun createAndPopulateQsTileConfigProvider(): QSTileConfigProvider { val logger = QsEventLoggerFake(UiEventLoggerFake(), InstanceIdSequenceFake(Int.MAX_VALUE)) return FakeQSTileConfigProvider().apply { putConfig( AIRPLANE_MODE_TILE_SPEC, ConnectivityModule.provideAirplaneModeTileConfig(logger), ) } } } }
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/ToggleTextFeedbackRepositoryTest.kt 0 → 100644 +96 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.data.repository import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.qs.panels.data.model.TextFeedbackRequestModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.junit.Before import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class ToggleTextFeedbackRepositoryTest : SysuiTestCase() { private val kosmos = testKosmos() private val underTest = kosmos.toggleTextFeedbackRepository @Before fun setUp() { underTest.activateIn(kosmos.testScope) } @Test fun noFeedbackOnStart() = with(kosmos) { runTest { val feedbackRequest by collectLastValue(underTest.textFeedback) assertThat(feedbackRequest).isEqualTo(TextFeedbackRequestModel.NoFeedback) } } @Test fun feedbackRequestShown() = with(kosmos) { runTest { val feedbackRequest by collectLastValue(underTest.textFeedback) underTest.setTextFeedback(TILE_SPEC) assertThat(feedbackRequest) .isEqualTo(TextFeedbackRequestModel.FeedbackForTile(TILE_SPEC)) } } @Test fun feedbackRequest_noFeedbackIfNoCollectors_firstIsAlwaysNoFeedback() = with(kosmos) { runTest { val collectionJob = testScope.backgroundScope.launch { underTest.textFeedback.collect {} } underTest.setTextFeedback(TILE_SPEC) runCurrent() collectionJob.cancelAndJoin() runCurrent() assertThat(underTest.textFeedback.first()) .isEqualTo(TextFeedbackRequestModel.NoFeedback) } } companion object { private val TILE_SPEC = TileSpec.create("a") private val OTHER_TILE_SPEC = TileSpec.create("b") } }
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TextFeedbackInteractorTest.kt 0 → 100644 +254 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.domain.interactor import android.content.ComponentName import android.graphics.drawable.TestStubDrawable import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.qs.panels.domain.model.TextFeedbackModel import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.base.shared.model.QSTileConfig import com.android.systemui.qs.tiles.base.shared.model.populateQsTileConfigProvider import com.android.systemui.qs.tiles.impl.airplane.qsAirplaneModeTileConfig import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig import com.android.systemui.settings.fakeUserTracker import com.android.systemui.settings.userTracker import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import org.junit.runner.RunWith import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class TextFeedbackInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().apply { populateQsTileConfigProvider() whenever(fakeUserTracker.userContext.packageManager).thenReturn(mock()) } private val underTest = kosmos.textFeedbackInteractor @Test fun noFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun knownTileTextFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() assertThat(textFeedback).isEqualTo(qsAirplaneModeTileConfig.toTextFeedbackModel()) } } @Test fun unknownTileTextFeedback_noFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(TileSpec.create("unknown")) runCurrent() assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun customTile_notInstalled_NoFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(CUSTOM_TILE_SPEC) runCurrent() assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun customTile_installed_loadedIconAndLabel() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) fakeInstalledTilesRepository.setInstalledServicesForUser( userTracker.userId, listOf( FakeInstalledTilesComponentRepository.ServiceInfo( CUSTOM_TILE_SPEC.componentName, serviceName = SERVICE_NAME, serviceIcon = SERVICE_ICON, ) ), ) runCurrent() underTest.requestShowFeedback(CUSTOM_TILE_SPEC) runCurrent() assertThat(textFeedback) .isEqualTo( TextFeedbackModel.LoadedTextFeedback( label = SERVICE_NAME, icon = Icon.Loaded(SERVICE_ICON, null), ) ) } } @Test fun customTile_installed_differentUser_noFeedback() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) fakeInstalledTilesRepository.setInstalledServicesForUser( userTracker.userId + 1, listOf( FakeInstalledTilesComponentRepository.ServiceInfo( CUSTOM_TILE_SPEC.componentName, serviceName = SERVICE_NAME, serviceIcon = SERVICE_ICON, ) ), ) runCurrent() underTest.requestShowFeedback(CUSTOM_TILE_SPEC) runCurrent() assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun feedbackRequestRemainsVisibleForSomeTime() = with(kosmos) { runTest { val feedbackRequest by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) assertThat(feedbackRequest) .isEqualTo(qsAirplaneModeTileConfig.toTextFeedbackModel()) } } @Test fun feedbackRequest_afterClearDelay_noFeedback() = with(kosmos) { runTest { val feedbackRequest by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY + 1.milliseconds) runCurrent() assertThat(feedbackRequest).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun feedbackRequest_thenAnotherFeedbackRequest_timerRestarted() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) runCurrent() underTest.requestShowFeedback(qsFlashlightTileConfig.tileSpec) assertThat(textFeedback).isEqualTo(qsFlashlightTileConfig.toTextFeedbackModel()) testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) assertThat(textFeedback).isEqualTo(qsFlashlightTileConfig.toTextFeedbackModel()) testScope.advanceTimeBy(2.milliseconds) assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } @Test fun feedbackRequest_thenSameTileRequest_timerRestarted() = with(kosmos) { runTest { val textFeedback by collectLastValue(underTest.textFeedback) underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) runCurrent() testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) runCurrent() underTest.requestShowFeedback(qsAirplaneModeTileConfig.tileSpec) testScope.advanceTimeBy(TextFeedbackInteractor.CLEAR_DELAY - 1.milliseconds) assertThat(textFeedback).isEqualTo(qsAirplaneModeTileConfig.toTextFeedbackModel()) testScope.advanceTimeBy(2.milliseconds) assertThat(textFeedback).isEqualTo(TextFeedbackModel.NoFeedback) } } companion object { private fun QSTileConfig.toTextFeedbackModel(): TextFeedbackModel.TextFeedback { return TextFeedbackModel.TextFeedback(uiConfig.labelRes, uiConfig.iconRes) } private val CUSTOM_TILE_SPEC = TileSpec.create(ComponentName("pkg", "srv")) private val SERVICE_NAME = "TileService" private val SERVICE_ICON = TestStubDrawable("tile_service_icon") } }