diff options
14 files changed, 440 insertions, 63 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 2d58c8cad2b1..a266e7eb44a1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -49,7 +49,6 @@ import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import javax.inject.Provider -import kotlinx.coroutines.flow.collectLatest /** * Renders a container of a collection of "scenes" that the user can switch between using certain @@ -117,7 +116,7 @@ fun SceneContainer( ) { "invalid ContentKey: $actionableContentKey" } - actionableContent.userActions.collectLatest { userActions -> + viewModel.filteredUserActions(actionableContent.userActions).collect { userActions -> userActionsByContentKey[actionableContentKey] = viewModel.resolveSceneFamilies(userActions) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 05a0119d68e4..bfcde7dab6d2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -130,10 +130,6 @@ fun SceneScope.CollapsedShadeHeader( modifier: Modifier = Modifier, ) { val viewModel = rememberViewModel("CollapsedShadeHeader") { viewModelFactory.create() } - val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle() - if (isDisabled) { - return - } val cutoutWidth = LocalDisplayCutout.current.width() val cutoutHeight = LocalDisplayCutout.current.height() @@ -196,7 +192,7 @@ fun SceneScope.CollapsedShadeHeader( horizontalArrangement = Arrangement.End, modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentEnd) - .padding(horizontal = horizontalPadding) + .padding(horizontal = horizontalPadding), ) { if (isLargeScreenLayout) { ShadeCarrierGroup( @@ -207,7 +203,7 @@ fun SceneScope.CollapsedShadeHeader( SystemIconContainer( viewModel = viewModel, isClickable = isLargeScreenLayout, - modifier = Modifier.align(Alignment.CenterVertically) + modifier = Modifier.align(Alignment.CenterVertically), ) { StatusIcons( viewModel = viewModel, @@ -217,7 +213,7 @@ fun SceneScope.CollapsedShadeHeader( modifier = Modifier.align(Alignment.CenterVertically) .padding(end = 6.dp) - .weight(1f, fill = false) + .weight(1f, fill = false), ) BatteryIcon( createBatteryMeterViewController = @@ -252,27 +248,15 @@ fun SceneScope.CollapsedShadeHeader( CutoutLocation.NONE, CutoutLocation.RIGHT -> { startPlaceable.placeRelative(x = 0, y = 0) - endPlaceable.placeRelative( - x = startPlaceable.width, - y = 0, - ) + endPlaceable.placeRelative(x = startPlaceable.width, y = 0) } CutoutLocation.CENTER -> { startPlaceable.placeRelative(x = 0, y = 0) - endPlaceable.placeRelative( - x = startPlaceable.width + cutoutWidthPx, - y = 0, - ) + endPlaceable.placeRelative(x = startPlaceable.width + cutoutWidthPx, y = 0) } CutoutLocation.LEFT -> { - startPlaceable.placeRelative( - x = cutoutWidthPx, - y = 0, - ) - endPlaceable.placeRelative( - x = startPlaceable.width + cutoutWidthPx, - y = 0, - ) + startPlaceable.placeRelative(x = cutoutWidthPx, y = 0) + endPlaceable.placeRelative(x = startPlaceable.width + cutoutWidthPx, y = 0) } } } @@ -288,10 +272,6 @@ fun SceneScope.ExpandedShadeHeader( modifier: Modifier = Modifier, ) { val viewModel = rememberViewModel("ExpandedShadeHeader") { viewModelFactory.create() } - val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle() - if (isDisabled) { - return - } val useExpandedFormat by remember { derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) } @@ -302,17 +282,14 @@ fun SceneScope.ExpandedShadeHeader( Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) { if (isPrivacyChipVisible) { Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) { - PrivacyChip( - viewModel = viewModel, - modifier = Modifier.align(Alignment.CenterEnd), - ) + PrivacyChip(viewModel = viewModel, modifier = Modifier.align(Alignment.CenterEnd)) } } Column( verticalArrangement = Arrangement.Bottom, modifier = Modifier.fillMaxWidth() - .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight) + .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight), ) { Box(modifier = Modifier.fillMaxWidth()) { Box { @@ -362,11 +339,7 @@ fun SceneScope.ExpandedShadeHeader( } @Composable -private fun SceneScope.Clock( - scale: Float, - viewModel: ShadeHeaderViewModel, - modifier: Modifier, -) { +private fun SceneScope.Clock(scale: Float, viewModel: ShadeHeaderViewModel, modifier: Modifier) { val layoutDirection = LocalLayoutDirection.current Element(key = ShadeHeader.Elements.Clock, modifier = modifier) { @@ -391,10 +364,10 @@ private fun SceneScope.Clock( LayoutDirection.Ltr -> 0f LayoutDirection.Rtl -> 1f }, - 0.5f + 0.5f, ) } - .clickable { viewModel.onClockClicked() } + .clickable { viewModel.onClockClicked() }, ) } } @@ -447,10 +420,7 @@ private fun BatteryIcon( } @Composable -private fun ShadeCarrierGroup( - viewModel: ShadeHeaderViewModel, - modifier: Modifier = Modifier, -) { +private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) { Row(modifier = modifier) { val subIds by viewModel.mobileSubIds.collectAsStateWithLifecycle() @@ -465,11 +435,11 @@ private fun ShadeCarrierGroup( viewModel = (viewModel.mobileIconsViewModel.viewModelForSub( subId, - StatusBarLocation.SHADE_CARRIER_GROUP + StatusBarLocation.SHADE_CARRIER_GROUP, ) as ShadeCarrierGroupMobileIconViewModel), ) .also { it.setOnClickListener { viewModel.onShadeCarrierGroupClicked() } } - }, + } ) } } @@ -506,7 +476,7 @@ private fun SceneScope.StatusIcons( Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimary), Utils.getColorAttrDefaultColor( themedContext, - android.R.attr.textColorPrimaryInverse + android.R.attr.textColorPrimaryInverse, ), ) statusBarIconController.addIconGroup(iconManager) @@ -551,7 +521,7 @@ private fun SystemIconContainer( viewModel: ShadeHeaderViewModel, isClickable: Boolean, modifier: Modifier = Modifier, - content: @Composable RowScope.() -> Unit + content: @Composable RowScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() @@ -578,10 +548,7 @@ private fun SystemIconContainer( } @Composable -private fun SceneScope.PrivacyChip( - viewModel: ShadeHeaderViewModel, - modifier: Modifier = Modifier, -) { +private fun SceneScope.PrivacyChip(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) { val privacyList by viewModel.privacyItems.collectAsStateWithLifecycle() AndroidView( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/DisabledContentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/DisabledContentInteractorTest.kt new file mode 100644 index 000000000000..08225a7770d2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/DisabledContentInteractorTest.kt @@ -0,0 +1,145 @@ +/* + * 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.scene.domain.interactor + +import android.app.StatusBarManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DisabledContentInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val underTest = kosmos.disabledContentInteractor + + @Test + fun isDisabled_notificationsShade() = + kosmos.runTest { + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NONE) + assertThat(underTest.isDisabled(Overlays.NotificationsShade)).isFalse() + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NOTIFICATION_SHADE) + assertThat(underTest.isDisabled(Overlays.NotificationsShade)).isTrue() + } + + @Test + fun isDisabled_qsShade() = + kosmos.runTest { + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NONE) + assertThat(underTest.isDisabled(Overlays.QuickSettingsShade)).isFalse() + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_QUICK_SETTINGS) + assertThat(underTest.isDisabled(Overlays.QuickSettingsShade)).isTrue() + } + + @Test + fun repeatWhenDisabled() = + kosmos.runTest { + var notificationDisabledCount = 0 + applicationCoroutineScope.launch { + underTest.repeatWhenDisabled(Overlays.NotificationsShade) { + notificationDisabledCount++ + } + } + var qsDisabledCount = 0 + applicationCoroutineScope.launch { + underTest.repeatWhenDisabled(Overlays.QuickSettingsShade) { qsDisabledCount++ } + } + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_QUICK_SETTINGS) + assertThat(notificationDisabledCount).isEqualTo(0) + assertThat(qsDisabledCount).isEqualTo(1) + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel( + disable2 = + StatusBarManager.DISABLE2_NOTIFICATION_SHADE or + StatusBarManager.DISABLE2_QUICK_SETTINGS + ) + assertThat(notificationDisabledCount).isEqualTo(1) + assertThat(qsDisabledCount).isEqualTo(1) + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NOTIFICATION_SHADE) + assertThat(notificationDisabledCount).isEqualTo(1) + assertThat(qsDisabledCount).isEqualTo(1) + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_QUICK_SETTINGS) + assertThat(notificationDisabledCount).isEqualTo(1) + assertThat(qsDisabledCount).isEqualTo(2) + } + + @Test + fun filteredUserActions() = + kosmos.runTest { + val map = + mapOf<UserAction, UserActionResult>( + Swipe.Up to UserActionResult.ShowOverlay(Overlays.NotificationsShade), + Swipe.Down to UserActionResult.ShowOverlay(Overlays.QuickSettingsShade), + ) + val unfiltered = MutableStateFlow(map) + val filtered by collectLastValue(underTest.filteredUserActions(unfiltered)) + assertThat(filtered).isEqualTo(map) + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NOTIFICATION_SHADE) + assertThat(filtered) + .isEqualTo( + mapOf(Swipe.Down to UserActionResult.ShowOverlay(Overlays.QuickSettingsShade)) + ) + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_QUICK_SETTINGS) + assertThat(filtered) + .isEqualTo( + mapOf(Swipe.Up to UserActionResult.ShowOverlay(Overlays.NotificationsShade)) + ) + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel( + disable2 = + StatusBarManager.DISABLE2_NOTIFICATION_SHADE or + StatusBarManager.DISABLE2_QUICK_SETTINGS + ) + assertThat(filtered).isEqualTo(emptyMap<UserAction, UserActionResult>()) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 7fe3d8d08afa..48edded5df18 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.scene.domain.interactor +import android.app.StatusBarManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState @@ -30,6 +31,8 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.Transition @@ -43,6 +46,8 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -523,4 +528,51 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(Scenes.Gone) } + + @Test + fun showOverlay_overlayDisabled_doesNothing() = + kosmos.runTest { + val currentOverlays by collectLastValue(underTest.currentOverlays) + val disabledOverlay = Overlays.QuickSettingsShade + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_QUICK_SETTINGS) + assertThat(disabledContentInteractor.isDisabled(disabledOverlay)).isTrue() + assertThat(currentOverlays).doesNotContain(disabledOverlay) + + underTest.showOverlay(disabledOverlay, "reason") + + assertThat(currentOverlays).doesNotContain(disabledOverlay) + } + + @Test + fun replaceOverlay_withDisabledOverlay_doesNothing() = + kosmos.runTest { + val currentOverlays by collectLastValue(underTest.currentOverlays) + val showingOverlay = Overlays.NotificationsShade + underTest.showOverlay(showingOverlay, "reason") + assertThat(currentOverlays).isEqualTo(setOf(showingOverlay)) + val disabledOverlay = Overlays.QuickSettingsShade + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_QUICK_SETTINGS) + assertThat(disabledContentInteractor.isDisabled(disabledOverlay)).isTrue() + + underTest.replaceOverlay(showingOverlay, disabledOverlay, "reason") + + assertThat(currentOverlays).isEqualTo(setOf(showingOverlay)) + } + + @Test + fun changeScene_toDisabledScene_doesNothing() = + kosmos.runTest { + val currentScene by collectLastValue(underTest.currentScene) + val disabledScene = Scenes.Shade + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NOTIFICATION_SHADE) + assertThat(disabledContentInteractor.isDisabled(disabledScene)).isTrue() + assertThat(currentScene).isNotEqualTo(disabledScene) + + underTest.changeScene(disabledScene, "reason") + + assertThat(currentScene).isNotEqualTo(disabledScene) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index cca847effe94..5d49c113a539 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -81,6 +81,9 @@ import com.android.systemui.keyguard.domain.interactor.scenetransition.lockscree import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +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.model.sysUiState import com.android.systemui.power.data.repository.fakePowerRepository @@ -101,6 +104,8 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shared.system.QuickStepContract import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository @@ -2673,6 +2678,25 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(isAlternateBouncerVisible).isFalse() } + @Test + fun handleDisableFlags() = + kosmos.runTest { + underTest.start() + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + sceneInteractor.changeScene(Scenes.Shade, "reason") + sceneInteractor.showOverlay(Overlays.NotificationsShade, "reason") + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(currentOverlays).contains(Overlays.NotificationsShade) + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NOTIFICATION_SHADE) + runCurrent() + + assertThat(currentScene).isNotEqualTo(Scenes.Shade) + assertThat(currentOverlays).isEmpty() + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/DisabledContentInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/DisabledContentInteractor.kt new file mode 100644 index 000000000000..d7c3b6b43c71 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/DisabledContentInteractor.kt @@ -0,0 +1,93 @@ +/* + * 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.scene.domain.interactor + +import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class DisabledContentInteractor +@Inject +constructor(private val disableFlagsInteractor: DisableFlagsInteractor) { + + /** Returns `true` if the given [key] is disabled; `false` if it's enabled */ + fun isDisabled( + key: ContentKey, + disabledFlags: DisableFlagsModel = disableFlagsInteractor.disableFlags.value, + ): Boolean { + return with(disabledFlags) { + when (key) { + Scenes.Shade, + Overlays.NotificationsShade -> !isShadeEnabled() + Scenes.QuickSettings, + Overlays.QuickSettingsShade -> !isQuickSettingsEnabled() + else -> false + } + } + } + + /** Runs the given [block] each time that [key] becomes disabled. */ + suspend fun repeatWhenDisabled(key: ContentKey, block: suspend (disabled: ContentKey) -> Unit) { + disableFlagsInteractor.disableFlags + .map { isDisabled(key) } + .distinctUntilChanged() + .collectLatest { isDisabled -> + if (isDisabled) { + block(key) + } + } + } + + /** + * Returns a filtered version of [unfiltered], without action-result entries that would navigate + * to disabled scenes. + */ + fun filteredUserActions( + unfiltered: Flow<Map<UserAction, UserActionResult>> + ): Flow<Map<UserAction, UserActionResult>> { + return combine(disableFlagsInteractor.disableFlags, unfiltered) { + disabledFlags, + unfilteredMap -> + unfilteredMap.filterValues { actionResult -> + val destination = + when (actionResult) { + is UserActionResult.ChangeScene -> actionResult.toScene + is UserActionResult.ShowOverlay -> actionResult.overlay + is UserActionResult.ReplaceByOverlay -> actionResult.overlay + else -> null + } + if (destination != null) { + // results that lead to a disabled destination get filtered out. + !isDisabled(key = destination, disabledFlags = disabledFlags) + } else { + // Action results that don't lead to a destination are never filtered out. + true + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index f20e5a54f6ed..d83d74e4e538 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -21,6 +21,8 @@ import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor @@ -64,6 +66,7 @@ constructor( private val sceneFamilyResolvers: Lazy<Map<SceneKey, @JvmSuppressWildcards SceneResolver>>, private val deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>, private val keyguardEnabledInteractor: Lazy<KeyguardEnabledInteractor>, + private val disabledContentInteractor: DisabledContentInteractor, ) { interface OnSceneAboutToChangeListener { @@ -465,6 +468,10 @@ constructor( return false } + if (disabledContentInteractor.isDisabled(to)) { + return false + } + val inMidTransitionFromGone = (transitionState.value as? ObservableTransitionState.Transition)?.fromContent == Scenes.Gone @@ -503,6 +510,10 @@ constructor( " Logging reason for overlay change was: $loggingReason" } + if (to != null && disabledContentInteractor.isDisabled(to)) { + return false + } + val isFromValid = (from == null) || (from in currentOverlays.value) val isToValid = (to == null) || (to !in currentOverlays.value && to in repository.allContentKeys) @@ -517,4 +528,14 @@ constructor( /** Returns `true` if [scene] can be resolved from [family]. */ fun isSceneInFamily(scene: SceneKey, family: SceneKey): Boolean = sceneFamilyResolvers.get()[family]?.includesScene(scene) == true + + /** + * Returns a filtered version of [unfiltered], without action-result entries that would navigate + * to disabled scenes. + */ + fun filteredUserActions( + unfiltered: Flow<Map<UserAction, UserActionResult>> + ): Flow<Map<UserAction, UserActionResult>> { + return disabledContentInteractor.filteredUserActions(unfiltered) + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index aece5c65ce12..8d8c24eae9e2 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -19,7 +19,6 @@ package com.android.systemui.scene.domain.startable import android.app.StatusBarManager -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.internal.logging.UiEventLogger @@ -56,6 +55,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.scene.data.model.asIterable import com.android.systemui.scene.data.model.sceneStackOf +import com.android.systemui.scene.domain.interactor.DisabledContentInteractor import com.android.systemui.scene.domain.interactor.SceneBackInteractor import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor @@ -103,6 +103,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** * Hooks up business logic that manipulates the state of the [SceneInteractor] for the system UI @@ -144,6 +145,7 @@ constructor( private val alternateBouncerInteractor: AlternateBouncerInteractor, private val vibratorHelper: VibratorHelper, private val msdlPlayer: MSDLPlayer, + private val disabledContentInteractor: DisabledContentInteractor, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -281,6 +283,7 @@ constructor( handlePowerState() handleDreamState() handleShadeTouchability() + handleDisableFlags() } private fun handleBouncerImeVisibility() { @@ -565,6 +568,38 @@ constructor( } } + private fun handleDisableFlags() { + applicationScope.launch { + launch { + sceneInteractor.currentScene.collectLatest { currentScene -> + disabledContentInteractor.repeatWhenDisabled(currentScene) { + switchToScene( + targetSceneKey = SceneFamilies.Home, + loggingReason = + "Current scene ${currentScene.debugName} became" + " disabled", + ) + } + } + } + + launch { + sceneInteractor.currentOverlays.collectLatest { overlays -> + overlays.forEach { overlay -> + launch { + disabledContentInteractor.repeatWhenDisabled(overlay) { + sceneInteractor.hideOverlay( + overlay = overlay, + loggingReason = + "Overlay ${overlay.debugName} became" + " disabled", + ) + } + } + } + } + } + } + } + private fun handleDeviceEntryHapticsWhileDeviceLocked() { applicationScope.launch { deviceEntryInteractor.isDeviceEntered.collectLatest { isDeviceEntered -> diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 32d5cb460cd8..c1e8032aa9e5 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -280,6 +280,16 @@ constructor( } } + /** + * Returns a filtered version of [unfiltered], without action-result entries that would navigate + * to disabled scenes. + */ + fun filteredUserActions( + unfiltered: Flow<Map<UserAction, UserActionResult>> + ): Flow<Map<UserAction, UserActionResult>> { + return sceneInteractor.filteredUserActions(unfiltered) + } + /** Defines interface for classes that can handle externally-reported [MotionEvent]s. */ interface MotionEventHandler { /** Notifies that a [MotionEvent] has occurred. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index 45516aa69cd7..0d847d84c820 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -23,6 +23,7 @@ import android.icu.text.DateFormat import android.icu.text.DisplayContext import android.os.UserHandle import android.provider.Settings +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.plugins.ActivityStarter @@ -48,7 +49,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import com.android.app.tracing.coroutines.launchTraced as launch /** Models UI state for the shade header. */ class ShadeHeaderViewModel @@ -87,10 +87,6 @@ constructor( /** Whether or not the privacy chip is enabled in the device privacy config. */ val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled - private val _isDisabled = MutableStateFlow(false) - /** Whether or not the Shade Header should be disabled based on disableFlags. */ - val isDisabled: StateFlow<Boolean> = _isDisabled.asStateFlow() - private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm) private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year) private val longerDateFormat = MutableStateFlow(getFormatFromPattern(longerPattern)) @@ -132,8 +128,6 @@ constructor( .collect { _mobileSubIds.value = it } } - launch { shadeInteractor.isQsEnabled.map { !it }.collect { _isDisabled.value = it } } - awaitCancellation() } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index 72cb1dfe38db..1556058d51ba 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -1,8 +1,12 @@ package com.android.systemui.kosmos import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.settings.brightness.ui.BrightnessWarningToast +import com.android.systemui.util.mockito.mock import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.test.StandardTestDispatcher @@ -32,12 +36,15 @@ fun Kosmos.useStandardTestDispatcher() = apply { testDispatcher = StandardTestDi fun Kosmos.useUnconfinedTestDispatcher() = apply { testDispatcher = UnconfinedTestDispatcher() } var Kosmos.testScope by Fixture { TestScope(testDispatcher) } -var Kosmos.applicationCoroutineScope by Fixture { testScope.backgroundScope } +var Kosmos.backgroundScope by Fixture { testScope.backgroundScope } +var Kosmos.applicationCoroutineScope by Fixture { backgroundScope } var Kosmos.testCase: SysuiTestCase by Fixture() var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { - testScope.backgroundScope.coroutineContext + backgroundScope.coroutineContext } var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testScope.coroutineContext } +var Kosmos.brightnessWarningToast: BrightnessWarningToast by + Kosmos.Fixture { mock<BrightnessWarningToast>() } /** * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in @@ -49,3 +56,5 @@ fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = fun Kosmos.runCurrent() = testScope.runCurrent() fun <T> Kosmos.collectLastValue(flow: Flow<T>) = testScope.collectLastValue(flow) + +fun <T> Kosmos.collectValues(flow: Flow<T>): FlowValue<List<T>> = testScope.collectValues(flow) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/DisabledContentInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/DisabledContentInteractorKosmos.kt new file mode 100644 index 000000000000..12d4e90dc469 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/DisabledContentInteractorKosmos.kt @@ -0,0 +1,25 @@ +/* + * 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.scene.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor + +val Kosmos.disabledContentInteractor by Fixture { + DisabledContentInteractor(disableFlagsInteractor = disableFlagsInteractor) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt index f84c3bdfdaf1..eb352baab0e4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt @@ -33,5 +33,6 @@ val Kosmos.sceneInteractor: SceneInteractor by sceneFamilyResolvers = { sceneFamilyResolvers }, deviceUnlockedInteractor = { deviceUnlockedInteractor }, keyguardEnabledInteractor = { keyguardEnabledInteractor }, + disabledContentInteractor = disabledContentInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 7e6a7271c561..82b5f6332b23 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -37,6 +37,7 @@ import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testScope import com.android.systemui.model.sysUiState import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.disabledContentInteractor import com.android.systemui.scene.domain.interactor.sceneBackInteractor import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -83,5 +84,6 @@ val Kosmos.sceneContainerStartable by Fixture { alternateBouncerInteractor = alternateBouncerInteractor, vibratorHelper = vibratorHelper, msdlPlayer = msdlPlayer, + disabledContentInteractor = disabledContentInteractor, ) } |