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

Commit e3f4692e authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Disabled content support.

When content (scene or overlay) becomes disabled due to disable-flags,
the logic changes away from the scene or hides that overlay.

The logic also prevents changing into a disabled scene or showing a
disabled overlay.

Fix: 371545613
Test: unit test coverage expanded
Test: manually verified, with dual shade that both changing into a
scene/overlay that's disabled is not possible and that if the content is
disabled while it's being displayed, the content is switched away from
Test: to disable the QS scene / shade overlay, the "adb shell cmd
statusbar send-disable-flag quick-settings" command was used, to reset
it: "adb shell cmd statusbar send-disable-flag none"
Flag: com.android.systemui.scene_container

Change-Id: I399cbf3aabb04491dd7616126520d2f887fbb82c
parent 13e05e63
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -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)
            }
+18 −51
Original line number Diff line number Diff line
@@ -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(
+145 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.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>())
        }
}
+52 −0
Original line number Diff line number Diff line
@@ -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)
        }
}
+24 −0
Original line number Diff line number Diff line
@@ -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,
Loading