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

Commit 4f45c38f authored by Matías Hernández's avatar Matías Hernández
Browse files

Add back standalone DND QS tile

Bug: 401217520
Test: atest + manual
Flag: android.app.modes_ui_dnd_tile
Change-Id: I89142afd355ce3f70ed68a6c8873c32e0aecc5a1
parent bb13b093
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -62,6 +62,16 @@ flag {
  }
}

flag {
  name: "modes_ui_dnd_tile"
  namespace: "systemui"
  description: "Shows a dedicated tile for the DND mode; dependent on modes_ui"
  bug: "401217520"
  metadata {
    purpose: PURPOSE_BUGFIX
  }
}

flag {
  name: "modes_hsum"
  namespace: "systemui"
+211 −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.tiles

import android.app.Flags
import android.os.Handler
import android.platform.test.annotations.EnableFlags
import android.service.quicksettings.Tile
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.kosmos.mainCoroutineContext
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.qs.QSTile.BooleanState
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.shared.QSSettingsPackageRepository
import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileDataInteractor
import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
import com.android.systemui.qs.tiles.impl.modes.ui.ModesDndTileMapper
import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
import com.android.systemui.statusbar.policy.ui.dialog.modesDialogEventLogger
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever

@EnableFlags(Flags.FLAG_MODES_UI)
@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper(setAsMainLooper = true)
class ModesDndTileTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val testDispatcher = kosmos.testDispatcher

    @Mock private lateinit var qsHost: QSHost

    @Mock private lateinit var metricsLogger: MetricsLogger

    @Mock private lateinit var statusBarStateController: StatusBarStateController

    @Mock private lateinit var activityStarter: ActivityStarter

    @Mock private lateinit var qsLogger: QSLogger

    @Mock private lateinit var uiEventLogger: QsEventLogger

    @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider

    @Mock private lateinit var dialogDelegate: ModesDialogDelegate

    @Mock private lateinit var settingsPackageRepository: QSSettingsPackageRepository

    private val inputHandler = FakeQSTileIntentUserInputHandler()
    private val zenModeRepository = kosmos.zenModeRepository
    private val tileDataInteractor =
        ModesDndTileDataInteractor(context, kosmos.zenModeInteractor, testDispatcher)
    private val mapper = ModesDndTileMapper(context.resources, context.theme)

    private lateinit var userActionInteractor: ModesDndTileUserActionInteractor
    private lateinit var secureSettings: SecureSettings
    private lateinit var testableLooper: TestableLooper
    private lateinit var underTest: ModesDndTile

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        testableLooper = TestableLooper.get(this)
        secureSettings = FakeSettings()

        // Allow the tile to load resources
        whenever(qsHost.context).thenReturn(context)
        whenever(qsHost.userContext).thenReturn(context)

        whenever(qsTileConfigProvider.getConfig(any()))
            .thenReturn(
                QSTileConfigTestBuilder.build {
                    uiConfig =
                        QSTileUIConfig.Resource(
                            iconRes = R.drawable.qs_dnd_icon_off,
                            labelRes = R.string.quick_settings_dnd_label,
                        )
                }
            )

        userActionInteractor =
            ModesDndTileUserActionInteractor(
                kosmos.mainCoroutineContext,
                inputHandler,
                dialogDelegate,
                kosmos.zenModeInteractor,
                kosmos.modesDialogEventLogger,
                settingsPackageRepository,
            )

        underTest =
            ModesDndTile(
                qsHost,
                uiEventLogger,
                testableLooper.looper,
                Handler(testableLooper.looper),
                FalsingManagerFake(),
                metricsLogger,
                statusBarStateController,
                activityStarter,
                qsLogger,
                qsTileConfigProvider,
                tileDataInteractor,
                mapper,
                userActionInteractor,
            )

        underTest.initialize()
        underTest.setListening(Object(), true)

        testableLooper.processAllMessages()
    }

    @After
    fun tearDown() {
        underTest.destroy()
        testableLooper.processAllMessages()
    }

    @Test
    fun stateUpdatesOnChange() =
        testScope.runTest {
            assertThat(underTest.state.state).isEqualTo(Tile.STATE_INACTIVE)

            zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND)
            runCurrent()
            testableLooper.processAllMessages()

            assertThat(underTest.state.state).isEqualTo(Tile.STATE_ACTIVE)
        }

    @Test
    fun handleUpdateState_withModel_updatesState() =
        testScope.runTest {
            val tileState =
                BooleanState().apply {
                    state = Tile.STATE_INACTIVE
                    secondaryLabel = "Old secondary label"
                }
            val model = ModesDndTileModel(isActivated = true)

            underTest.handleUpdateState(tileState, model)

            assertThat(tileState.state).isEqualTo(Tile.STATE_ACTIVE)
            assertThat(tileState.secondaryLabel).isEqualTo("On")
        }

    @Test
    fun handleUpdateState_withNull_updatesState() =
        testScope.runTest {
            val tileState =
                BooleanState().apply {
                    state = Tile.STATE_INACTIVE
                    secondaryLabel = "Old secondary label"
                }
            zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND)
            runCurrent()

            underTest.handleUpdateState(tileState, null)

            assertThat(tileState.state).isEqualTo(Tile.STATE_ACTIVE)
            assertThat(tileState.secondaryLabel).isEqualTo("On")
        }
}
+118 −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.tiles.impl.modes.domain.interactor

import android.app.Flags
import android.os.UserHandle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@EnableFlags(Flags.FLAG_MODES_UI)
@RunWith(AndroidJUnit4::class)
class ModesDndTileDataInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val dispatcher = kosmos.testDispatcher
    private val zenModeRepository = kosmos.fakeZenModeRepository

    private val underTest by lazy {
        ModesDndTileDataInteractor(context, kosmos.zenModeInteractor, dispatcher)
    }

    @Test
    @EnableFlags(Flags.FLAG_MODES_UI_DND_TILE)
    fun availability_flagOn_isTrue() =
        testScope.runTest {
            val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())

            assertThat(availability).containsExactly(true)
        }

    @Test
    @DisableFlags(Flags.FLAG_MODES_UI_DND_TILE)
    fun availability_flagOff_isFalse() =
        testScope.runTest {
            val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())

            assertThat(availability).containsExactly(false)
        }

    @Test
    fun tileData_dndChanges_updateActivated() =
        testScope.runTest {
            val model by
                collectLastValue(
                    underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
                )

            runCurrent()
            assertThat(model!!.isActivated).isFalse()

            zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND)
            runCurrent()
            assertThat(model!!.isActivated).isTrue()

            zenModeRepository.deactivateMode(TestModeBuilder.MANUAL_DND)
            runCurrent()
            assertThat(model!!.isActivated).isFalse()
        }

    @Test
    fun tileData_otherModeChanges_notActivated() =
        testScope.runTest {
            val model by
                collectLastValue(
                    underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
                )

            runCurrent()
            assertThat(model!!.isActivated).isFalse()

            zenModeRepository.addMode("Other mode")
            runCurrent()
            assertThat(model!!.isActivated).isFalse()

            zenModeRepository.activateMode("Other mode")
            runCurrent()
            assertThat(model!!.isActivated).isFalse()
        }

    private companion object {
        val TEST_USER = UserHandle.of(1)!!
    }
}
+134 −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.tiles.impl.modes.domain.interactor

import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.mainCoroutineContext
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.shared.QSSettingsPackageRepository
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate
import com.android.systemui.statusbar.policy.ui.dialog.modesDialogEventLogger
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(android.app.Flags.FLAG_MODES_UI)
class ModesDndTileUserActionInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val inputHandler = kosmos.qsTileIntentUserInputHandler
    private val mockDialogDelegate = kosmos.mockModesDialogDelegate
    private val zenModeRepository = kosmos.zenModeRepository
    private val zenModeInteractor = kosmos.zenModeInteractor
    private val settingsPackageRepository = mock<QSSettingsPackageRepository>()

    private val underTest =
        ModesDndTileUserActionInteractor(
            kosmos.mainCoroutineContext,
            inputHandler,
            mockDialogDelegate,
            zenModeInteractor,
            kosmos.modesDialogEventLogger,
            settingsPackageRepository,
        )

    @Before
    fun setUp() {
        whenever(settingsPackageRepository.getSettingsPackageName()).thenReturn(SETTINGS_PACKAGE)
    }

    @Test
    fun handleClick_dndActive_deactivatesDnd() =
        testScope.runTest {
            val dndMode by collectLastValue(zenModeInteractor.dndMode)
            zenModeRepository.activateMode(MANUAL_DND)
            assertThat(dndMode?.isActive).isTrue()

            underTest.handleInput(QSTileInputTestKtx.click(data = ModesDndTileModel(true)))

            assertThat(dndMode?.isActive).isFalse()
        }

    @Test
    fun handleClick_dndInactive_activatesDnd() =
        testScope.runTest {
            val dndMode by collectLastValue(zenModeInteractor.dndMode)
            assertThat(dndMode?.isActive).isFalse()

            underTest.handleInput(QSTileInputTestKtx.click(data = ModesDndTileModel(false)))

            assertThat(dndMode?.isActive).isTrue()
        }

    @Test
    fun handleLongClick_active_opensSettings() =
        testScope.runTest {
            zenModeRepository.activateMode(MANUAL_DND)
            runCurrent()

            underTest.handleInput(QSTileInputTestKtx.longClick(ModesDndTileModel(true)))

            QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
                assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE)
                assertThat(it.intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
                assertThat(it.intent.getStringExtra(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID))
                    .isEqualTo(MANUAL_DND.id)
            }
        }

    @Test
    fun handleLongClick_inactive_opensSettings() =
        testScope.runTest {
            zenModeRepository.activateMode(MANUAL_DND)
            zenModeRepository.deactivateMode(MANUAL_DND)
            runCurrent()

            underTest.handleInput(QSTileInputTestKtx.longClick(ModesDndTileModel(false)))

            QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
                assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE)
                assertThat(it.intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
                assertThat(it.intent.getStringExtra(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID))
                    .isEqualTo(MANUAL_DND.id)
            }
        }

    companion object {
        private const val SETTINGS_PACKAGE = "the.settings.package"
    }
}
+80 −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.tiles.impl.modes.ui

import android.app.Flags
import android.graphics.drawable.TestStubDrawable
import android.platform.test.annotations.EnableFlags
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.qs.tiles.impl.modes.domain.model.ModesDndTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
import com.android.systemui.res.R
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(Flags.FLAG_MODES_UI)
class ModesDndTileMapperTest : SysuiTestCase() {
    val config =
        QSTileConfigTestBuilder.build {
            uiConfig =
                QSTileUIConfig.Resource(
                    iconRes = R.drawable.qs_dnd_icon_off,
                    labelRes = R.string.quick_settings_modes_label,
                )
        }

    val underTest =
        ModesDndTileMapper(
            context.orCreateTestableResources
                .apply {
                    addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable())
                    addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable())
                }
                .resources,
            context.theme,
        )

    @Test
    fun map_inactiveState() {
        val model = ModesDndTileModel(isActivated = false)

        val state = underTest.map(config, model)

        assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE)
        assertThat((state.icon as Icon.Loaded).res).isEqualTo(R.drawable.qs_dnd_icon_off)
        assertThat(state.secondaryLabel).isEqualTo("Off")
    }

    @Test
    fun map_activeState() {
        val model = ModesDndTileModel(isActivated = true)

        val state = underTest.map(config, model)

        assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
        assertThat((state.icon as Icon.Loaded).res).isEqualTo(R.drawable.qs_dnd_icon_on)
        assertThat(state.secondaryLabel).isEqualTo("On")
    }
}
Loading