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

Commit 60d986c9 authored by Fabián Kozynski's avatar Fabián Kozynski
Browse files

Add edit mode events and QQS/QS open events

Test: atest TileSpecTest EditModeViewModelTest
Test: dump atoms
Bug: 339262836
Bug: 331600035
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Change-Id: Id3e756823ee314dc1b008d678cd0cbfaa80c36fd
parent a424fedc
Loading
Loading
Loading
Loading
+159 −5
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.content.ComponentName
import android.graphics.drawable.TestStubDrawable
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.internal.logging.uiEventLoggerFake
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.ContentDescription
@@ -29,9 +30,12 @@ import com.android.systemui.common.shared.model.Text
import com.android.systemui.common.ui.compose.toAnnotatedString
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.FakeQSFactory
import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.QSEditEvent
import com.android.systemui.qs.panels.data.repository.stockTilesRepository
import com.android.systemui.qs.panels.domain.interactor.FakeTileAvailabilityInteractor
import com.android.systemui.qs.panels.domain.interactor.tileAvailabilityInteractorsMap
@@ -42,8 +46,10 @@ import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesReposi
import com.android.systemui.qs.pipeline.data.repository.fakeMinimumTilesRepository
import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.metricSpec
import com.android.systemui.qs.qsTileFactory
import com.android.systemui.qs.shared.model.TileCategory
import com.android.systemui.qs.tiles.impl.airplane.qsAirplaneModeTileConfig
import com.android.systemui.qs.tiles.impl.alarm.qsAlarmTileConfig
import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
@@ -86,6 +92,7 @@ class EditModeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
                qsFlashlightTileConfig,
                qsBatterySaverTileConfig,
                qsAlarmTileConfig,
                qsAirplaneModeTileConfig,
                qsCameraSensorPrivacyToggleTileConfig,
                qsMicrophoneSensorPrivacyToggleTileConfig,
            )
@@ -116,7 +123,7 @@ class EditModeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {

            fakeInstalledTilesRepository.setInstalledServicesForUser(
                userTracker.userId,
                listOf(serviceInfo1, serviceInfo2)
                listOf(serviceInfo1, serviceInfo2),
            )

            with(fakeQSTileConfigProvider) { configs.forEach { putConfig(it.tileSpec, it) } }
@@ -424,10 +431,7 @@ class EditModeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
            testScope.runTest {
                val tiles by collectLastValue(underTest.tiles)
                val currentTiles =
                    mutableListOf(
                        TileSpec.create("flashlight"),
                        TileSpec.create("airplane"),
                    )
                    mutableListOf(TileSpec.create("flashlight"), TileSpec.create("airplane"))
                currentTilesInteractor.setTiles(currentTiles)
                assertThat(currentTiles.size).isLessThan(minNumberOfTiles)

@@ -549,6 +553,156 @@ class EditModeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
            }
        }

    // UI EVENT TESTS

    @Test
    fun startEditing_onlyOneEvent() =
        kosmos.runTest {
            underTest.startEditing()
            underTest.startEditing()

            assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)

            assertThat(uiEventLoggerFake[0].eventId).isEqualTo(QSEditEvent.QS_EDIT_OPEN.id)
        }

    @Test
    fun stopEditing_notEditing_noEvent() =
        kosmos.runTest {
            underTest.stopEditing()

            assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0)
        }

    @Test
    fun stopEditing_whenEditing_correctEvent() =
        kosmos.runTest {
            underTest.startEditing()
            underTest.stopEditing()

            assertThat(uiEventLoggerFake[1].eventId).isEqualTo(QSEditEvent.QS_EDIT_CLOSED.id)
        }

    @Test
    fun addTile_correctPackageAndPosition() =
        kosmos.runTest {
            val flashlightTile = TileSpec.create("flashlight")
            val airplaneTile = TileSpec.create("airplane")
            val internetTile = TileSpec.create("internet")
            val customTile = TileSpec.create(component2)
            currentTilesInteractor.setTiles(listOf(flashlightTile))
            runCurrent()

            underTest.addTile(airplaneTile)
            underTest.addTile(internetTile, position = 0)
            underTest.addTile(customTile, position = 1)

            assertThat(uiEventLoggerFake.numLogs()).isEqualTo(3)

            with(uiEventLoggerFake[0]) {
                assertThat(eventId).isEqualTo(QSEditEvent.QS_EDIT_ADD.id)
                assertThat(packageName).isEqualTo(airplaneTile.metricSpec)
                assertThat(position).isEqualTo(-1)
            }
            with(uiEventLoggerFake[1]) {
                assertThat(eventId).isEqualTo(QSEditEvent.QS_EDIT_ADD.id)
                assertThat(packageName).isEqualTo(internetTile.metricSpec)
                assertThat(position).isEqualTo(0)
            }
            with(uiEventLoggerFake[2]) {
                assertThat(eventId).isEqualTo(QSEditEvent.QS_EDIT_ADD.id)
                assertThat(packageName).isEqualTo(customTile.metricSpec)
                assertThat(position).isEqualTo(1)
            }
        }

    @Test
    fun addTile_alreadyThere_usesMoveEvent() =
        kosmos.runTest {
            val flashlightTile = TileSpec.create("flashlight")
            val airplaneTile = TileSpec.create("airplane")
            val internetTile = TileSpec.create("internet")
            currentTilesInteractor.setTiles(listOf(flashlightTile, airplaneTile, internetTile))
            runCurrent()

            underTest.addTile(flashlightTile) // adding at the end, should use correct position
            underTest.addTile(internetTile, 0)

            assertThat(uiEventLoggerFake.numLogs()).isEqualTo(2)

            with(uiEventLoggerFake[0]) {
                assertThat(eventId).isEqualTo(QSEditEvent.QS_EDIT_MOVE.id)
                assertThat(packageName).isEqualTo(flashlightTile.metricSpec)
                // adding at the end, should use correct position
                assertThat(position).isEqualTo(2)
            }
            with(uiEventLoggerFake[1]) {
                assertThat(eventId).isEqualTo(QSEditEvent.QS_EDIT_MOVE.id)
                assertThat(packageName).isEqualTo(internetTile.metricSpec)
                assertThat(position).isEqualTo(0)
            }
        }

    @Test
    fun removeTileEvent() =
        kosmos.runTest {
            val flashlightTile = TileSpec.create("flashlight")
            val airplaneTile = TileSpec.create("airplane")
            val internetTile = TileSpec.create("internet")
            currentTilesInteractor.setTiles(listOf(flashlightTile, airplaneTile, internetTile))
            runCurrent()

            underTest.removeTile(airplaneTile)

            assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)

            with(uiEventLoggerFake[0]) {
                assertThat(eventId).isEqualTo(QSEditEvent.QS_EDIT_REMOVE.id)
                assertThat(packageName).isEqualTo(airplaneTile.metricSpec)
            }
        }

    @Test
    fun setTiles_emitsCorrectOperation_individualOperations() =
        kosmos.runTest {
            val flashlightTile = TileSpec.create("flashlight")
            val airplaneTile = TileSpec.create("airplane")
            val internetTile = TileSpec.create("internet")
            val alarmTile = TileSpec.create("alarm")

            currentTilesInteractor.setTiles(listOf(flashlightTile, airplaneTile, internetTile))
            runCurrent()

            // 0. Move flashlightTile to position 2
            underTest.setTiles(listOf(airplaneTile, internetTile, flashlightTile))
            runCurrent()

            // 1. Add alarm tile at position 1
            underTest.setTiles(listOf(airplaneTile, alarmTile, internetTile, flashlightTile))
            runCurrent()

            // 2. Remove internetTile
            underTest.setTiles(listOf(airplaneTile, alarmTile, flashlightTile))
            runCurrent()

            assertThat(uiEventLoggerFake.numLogs()).isEqualTo(3)

            with(uiEventLoggerFake[0]) {
                assertThat(eventId).isEqualTo(QSEditEvent.QS_EDIT_MOVE.id)
                assertThat(packageName).isEqualTo(flashlightTile.metricSpec)
                assertThat(position).isEqualTo(2)
            }
            with(uiEventLoggerFake[1]) {
                assertThat(eventId).isEqualTo(QSEditEvent.QS_EDIT_ADD.id)
                assertThat(packageName).isEqualTo(alarmTile.metricSpec)
                assertThat(position).isEqualTo(1)
            }
            with(uiEventLoggerFake[2]) {
                assertThat(eventId).isEqualTo(QSEditEvent.QS_EDIT_REMOVE.id)
                assertThat(packageName).isEqualTo(internetTile.metricSpec)
            }
        }

    companion object {
        private val drawable1 = TestStubDrawable("drawable1")
        private val appName1 = "App1"
+18 −0
Original line number Diff line number Diff line
@@ -85,6 +85,24 @@ class TileSpecTest : SysuiTestCase() {
        assertThat(TileSpec.create("")).isEqualTo(TileSpec.Invalid)
    }

    @Test
    fun metricSpec_invalid() {
        assertThat(TileSpec.Invalid.metricSpec).isEmpty()
    }

    @Test
    fun metricSpec_platform_specName() {
        val tile = "spec"
        assertThat(TileSpec.create(tile).metricSpec).isEqualTo(tile)
    }

    @Test
    fun metricSpec_custom_packageName() {
        val componentName = ComponentName("test_pkg", "test_cls")

        assertThat(TileSpec.create(componentName).metricSpec).isEqualTo(componentName.packageName)
    }

    companion object {
        private const val CUSTOM_TILE_PREFIX = "custom("
    }
+8 −2
Original line number Diff line number Diff line
@@ -325,9 +325,15 @@ constructor(
        }

        SceneTransitionLayout(state = sceneState, modifier = Modifier.fillMaxSize()) {
            scene(QuickSettings) { QuickSettingsElement() }
            scene(QuickSettings) {
                LaunchedEffect(Unit) { viewModel.onQSOpen() }
                QuickSettingsElement()
            }

            scene(QuickQuickSettings) { QuickQuickSettingsElement() }
            scene(QuickQuickSettings) {
                LaunchedEffect(Unit) { viewModel.onQQSOpen() }
                QuickQuickSettingsElement()
            }
        }
    }

+11 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.LifecycleCoroutineScope
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.UiEventLogger
import com.android.keyguard.BouncerPanelExpansionCalculator
import com.android.systemui.Dumpable
import com.android.systemui.animation.ShadeInterpolation
@@ -51,6 +52,7 @@ import com.android.systemui.media.dagger.MediaModule.QS_PANEL
import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.FooterActionsController
import com.android.systemui.qs.QSEvent
import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeLog
import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeModule
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
@@ -113,6 +115,7 @@ constructor(
    @Named(QUICK_QS_PANEL) val qqsMediaHost: MediaHost,
    @Named(QS_PANEL) val qsMediaHost: MediaHost,
    @Named(QSFragmentComposeModule.QS_USING_MEDIA_PLAYER) private val usingMedia: Boolean,
    private val uiEventLogger: UiEventLogger,
    @Assisted private val lifecycleScope: LifecycleCoroutineScope,
) : Dumpable, ExclusiveActivatable() {

@@ -455,6 +458,14 @@ constructor(
        falsingInteractor.isFalseTouch(Classifier.QS_SWIPE_NESTED)
    }

    fun onQQSOpen() {
        uiEventLogger.log(QSEvent.QQS_PANEL_EXPANDED)
    }

    fun onQSOpen() {
        uiEventLogger.log(QSEvent.QS_PANEL_EXPANDED)
    }

    override suspend fun onActivated(): Nothing {
        initMediaHosts() // init regardless of using media (same as current QS).
        coroutineScope {
+94 −1
Original line number Diff line number Diff line
@@ -18,9 +18,14 @@ package com.android.systemui.qs.panels.ui.viewmodel

import android.content.Context
import androidx.compose.ui.util.fastMap
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import com.android.internal.logging.UiEventLogger
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.QSEditEvent
import com.android.systemui.qs.panels.domain.interactor.EditTilesListInteractor
import com.android.systemui.qs.panels.domain.interactor.GridLayoutTypeInteractor
import com.android.systemui.qs.panels.domain.interactor.TilesAvailabilityInteractor
@@ -30,10 +35,12 @@ import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
import com.android.systemui.qs.pipeline.domain.interactor.MinimumTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.metricSpec
import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.util.kotlin.emitOnStart
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -45,6 +52,7 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@SysUISingleton
@OptIn(ExperimentalCoroutinesApi::class)
@@ -55,10 +63,12 @@ constructor(
    private val currentTilesInteractor: CurrentTilesInteractor,
    private val tilesAvailabilityInteractor: TilesAvailabilityInteractor,
    private val minTilesInteractor: MinimumTilesInteractor,
    private val uiEventLogger: UiEventLogger,
    @ShadeDisplayAware private val configurationInteractor: ConfigurationInteractor,
    @ShadeDisplayAware private val context: Context,
    @Named("Default") private val defaultGridLayout: GridLayout,
    @Application private val applicationScope: CoroutineScope,
    @Background private val bgDispatcher: CoroutineDispatcher,
    gridLayoutTypeInteractor: GridLayoutTypeInteractor,
    gridLayoutMap: Map<GridLayoutType, @JvmSuppressWildcards GridLayout>,
) {
@@ -149,11 +159,17 @@ constructor(

    /** @see isEditing */
    fun startEditing() {
        if (!isEditing.value) {
            uiEventLogger.log(QSEditEvent.QS_EDIT_OPEN)
        }
        _isEditing.value = true
    }

    /** @see isEditing */
    fun stopEditing() {
        if (isEditing.value) {
            uiEventLogger.log(QSEditEvent.QS_EDIT_CLOSED)
        }
        _isEditing.value = false
    }

@@ -164,6 +180,7 @@ constructor(
    fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) {
        val specs = currentTilesInteractor.currentTilesSpecs.toMutableList()
        val currentPosition = specs.indexOf(tileSpec)
        val moved = currentPosition != -1

        if (currentPosition != -1) {
            // No operation needed if the element is already in the list at the right position
@@ -179,6 +196,12 @@ constructor(
        } else {
            specs.add(tileSpec)
        }
        uiEventLogger.logWithPosition(
            if (moved) QSEditEvent.QS_EDIT_MOVE else QSEditEvent.QS_EDIT_ADD,
            /* uid= */ 0,
            /* packageName= */ tileSpec.metricSpec,
            if (moved && position == POSITION_AT_END) specs.size - 1 else position,
        )

        // Setting the new tiles as one operation to avoid UI jank with tiles disappearing and
        // reappearing
@@ -187,10 +210,80 @@ constructor(

    /** Immediately removes [tileSpec] from the current tiles. */
    fun removeTile(tileSpec: TileSpec) {
        uiEventLogger.log(
            QSEditEvent.QS_EDIT_REMOVE,
            /* uid= */ 0,
            /* packageName= */ tileSpec.metricSpec,
        )
        currentTilesInteractor.removeTiles(listOf(tileSpec))
    }

    fun setTiles(tileSpecs: List<TileSpec>) {
        val currentTiles = currentTilesInteractor.currentTilesSpecs
        currentTilesInteractor.setTiles(tileSpecs)
        applicationScope.launch(bgDispatcher) {
            calculateDiffsAndEmitUiEvents(currentTiles, tileSpecs)
        }
    }

    private fun calculateDiffsAndEmitUiEvents(
        currentTiles: List<TileSpec>,
        newTiles: List<TileSpec>,
    ) {
        val listDiff = DiffUtil.calculateDiff(DiffCallback(currentTiles, newTiles))
        listDiff.dispatchUpdatesTo(
            object : ListUpdateCallback {
                override fun onInserted(position: Int, count: Int) {
                    newTiles.getOrNull(position)?.let {
                        uiEventLogger.logWithPosition(
                            QSEditEvent.QS_EDIT_ADD,
                            /* uid= */ 0,
                            /* packageName= */ it.metricSpec,
                            position,
                        )
                    }
                }

                override fun onRemoved(position: Int, count: Int) {
                    currentTiles.getOrNull(position)?.let {
                        uiEventLogger.log(QSEditEvent.QS_EDIT_REMOVE, 0, it.metricSpec)
                    }
                }

                override fun onMoved(fromPosition: Int, toPosition: Int) {
                    currentTiles.getOrNull(fromPosition)?.let {
                        uiEventLogger.logWithPosition(
                            QSEditEvent.QS_EDIT_MOVE,
                            /* uid= */ 0,
                            /* packageName= */ it.metricSpec,
                            toPosition,
                        )
                    }
                }

                override fun onChanged(position: Int, count: Int, payload: Any?) {}
            }
        )
    }
}

private class DiffCallback(
    private val currentList: List<TileSpec>,
    private val newList: List<TileSpec>,
) : DiffUtil.Callback() {
    override fun getOldListSize(): Int {
        return currentList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return currentList[oldItemPosition] == newList[newItemPosition]
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return areItemsTheSame(oldItemPosition, newItemPosition)
    }
}
Loading