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

Commit 6b944264 authored by Ioana Alexandru's avatar Ioana Alexandru
Browse files

Visual stability for modes dialog

If a tile should disappear after being pressed, keep it in the grid
while the dialog is visible.

Bug: 346519570
Test: ModesDialogViewModelTest + manually tested with bedtime mode, which currently doesn't support manual invocation
Flag: android.app.modes_ui
Change-Id: I7658ddd52d7f349829a991a1b414efc7158cc03e
parent 8a735c3f
Loading
Loading
Loading
Loading
+15 −7
Original line number Diff line number Diff line
@@ -73,15 +73,22 @@ class FakeZenModeRepository : ZenModeRepository {
    }

    fun activateMode(id: String) {
        val oldMode = mutableModesFlow.value.find { it.id == id } ?: return
        removeMode(id)
        mutableModesFlow.value += TestModeBuilder(oldMode).setActive(true).build()
        updateModeActiveState(id = id, isActive = true)
    }

    fun deactivateMode(id: String) {
        val oldMode = mutableModesFlow.value.find { it.id == id } ?: return
        removeMode(id)
        mutableModesFlow.value += TestModeBuilder(oldMode).setActive(false).build()
        updateModeActiveState(id = id, isActive = false)
    }

    // Update the active state while maintaining the mode's position in the list
    private fun updateModeActiveState(id: String, isActive: Boolean) {
        val modes = mutableModesFlow.value.toMutableList()
        val index = modes.indexOfFirst { it.id == id }
        if (index < 0) {
            throw IllegalArgumentException("mode $id not found")
        }
        modes[index] = TestModeBuilder(modes[index]).setActive(isActive).build()
        mutableModesFlow.value = modes
    }
}

@@ -101,7 +108,8 @@ fun FakeZenModeRepository.updateNotificationPolicy(
            suppressedVisualEffects,
            state,
            priorityConversationSenders,
        ))
        )
    )

private fun newMode(id: String, active: Boolean = false): ZenMode {
    return TestModeBuilder().setId(id).setName("Mode $id").setActive(active).build()
+112 −0
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -139,6 +140,117 @@ class ModesDialogViewModelTest : SysuiTestCase() {
            }
        }

    @Test
    fun tiles_stableWhileCollecting() =
        testScope.runTest {
            val job = Job()
            val tiles by collectLastValue(underTest.tiles, context = job)

            repository.addModes(
                listOf(
                    TestModeBuilder()
                        .setName("Active without manual")
                        .setActive(true)
                        .setManualInvocationAllowed(false)
                        .build(),
                    TestModeBuilder()
                        .setName("Active with manual")
                        .setActive(true)
                        .setManualInvocationAllowed(true)
                        .build(),
                    TestModeBuilder()
                        .setName("Inactive with manual")
                        .setActive(false)
                        .setManualInvocationAllowed(true)
                        .build(),
                    TestModeBuilder()
                        .setName("Inactive without manual")
                        .setActive(false)
                        .setManualInvocationAllowed(false)
                        .build(),
                )
            )
            runCurrent()

            assertThat(tiles?.size).isEqualTo(3)

            // Check that tile is initially present
            with(tiles?.elementAt(0)!!) {
                assertThat(this.text).isEqualTo("Active without manual")
                assertThat(this.subtext).isEqualTo("On")
                assertThat(this.enabled).isEqualTo(true)

                // Click tile to toggle it
                this.onClick()
                runCurrent()
            }
            // Check that tile is still present at the same location, but turned off
            assertThat(tiles?.size).isEqualTo(3)
            with(tiles?.elementAt(0)!!) {
                assertThat(this.text).isEqualTo("Active without manual")
                assertThat(this.subtext).isEqualTo("Off")
                assertThat(this.enabled).isEqualTo(false)
            }

            // Stop collecting, then start again
            job.cancel()
            val tiles2 by collectLastValue(underTest.tiles)
            runCurrent()

            // Check that tile is now gone
            assertThat(tiles2?.size).isEqualTo(2)
            assertThat(tiles2?.elementAt(0)!!.text).isEqualTo("Active with manual")
            assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Inactive with manual")
        }

    @Test
    fun tiles_filtersOutRemovedModes() =
        testScope.runTest {
            val job = Job()
            val tiles by collectLastValue(underTest.tiles, context = job)

            repository.addModes(
                listOf(
                    TestModeBuilder()
                        .setId("A")
                        .setName("Active without manual")
                        .setActive(true)
                        .setManualInvocationAllowed(false)
                        .build(),
                    TestModeBuilder()
                        .setId("B")
                        .setName("Active with manual")
                        .setActive(true)
                        .setManualInvocationAllowed(true)
                        .build(),
                    TestModeBuilder()
                        .setId("C")
                        .setName("Inactive with manual")
                        .setActive(false)
                        .setManualInvocationAllowed(true)
                        .build(),
                )
            )
            runCurrent()

            assertThat(tiles?.size).isEqualTo(3)

            repository.removeMode("A")
            runCurrent()

            assertThat(tiles?.size).isEqualTo(2)

            repository.removeMode("B")
            runCurrent()

            assertThat(tiles?.size).isEqualTo(1)

            repository.removeMode("C")
            runCurrent()

            assertThat(tiles?.size).isEqualTo(0)
        }

    @Test
    fun onClick_togglesTileState() =
        testScope.runTest {
+23 −5
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.scan

/**
 * Viewmodel for the priority ("zen") modes dialog that can be opened from quick settings. It allows
@@ -46,11 +47,28 @@ constructor(
    private val dialogDelegate: ModesDialogDelegate,
) {
    // Modes that should be displayed in the dialog
    // TODO(b/346519570): Include modes that have not been set up yet.
    private val visibleModes: Flow<List<ZenMode>> =
        zenModeInteractor.modes.map {
            it.filter { mode ->
                mode.rule.isEnabled && (mode.isActive || mode.rule.isManualInvocationAllowed)
        zenModeInteractor.modes
            // While this is being collected (or in other words, while the dialog is open), we don't
            // want a mode to disappear from the list if, for instance, the user deactivates it,
            // since that can be confusing (similar to how we have visual stability for
            // notifications while the shade is open).
            // This ensures new modes are added to the list, and updates to modes already in the
            // list are registered correctly.
            .scan(listOf()) { prev, modes ->
                val prevIds = prev.map { it.id }.toSet()

                modes.filter { mode ->
                    when {
                        // Mode appeared previously -> keep it even if otherwise we may have
                        // filtered it
                        mode.id in prevIds -> true
                        // Mode is enabled -> show if active (so user can toggle off), or if it
                        // can be manually toggled on
                        mode.rule.isEnabled -> mode.isActive || mode.rule.isManualInvocationAllowed
                        // TODO(b/346519570): Include modes that have not been set up yet.
                        else -> false
                    }
                }
            }