Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt +159 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -86,6 +92,7 @@ class EditModeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { qsFlashlightTileConfig, qsBatterySaverTileConfig, qsAlarmTileConfig, qsAirplaneModeTileConfig, qsCameraSensorPrivacyToggleTileConfig, qsMicrophoneSensorPrivacyToggleTileConfig, ) Loading Loading @@ -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) } } Loading Loading @@ -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) Loading Loading @@ -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" Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt +18 −0 Original line number Diff line number Diff line Loading @@ -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(" } Loading packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +8 −2 Original line number Diff line number Diff line Loading @@ -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() } } } Loading packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +11 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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() { Loading Loading @@ -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 { Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt +94 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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>, ) { Loading Loading @@ -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 } Loading @@ -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 Loading @@ -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 Loading @@ -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
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt +159 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -86,6 +92,7 @@ class EditModeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { qsFlashlightTileConfig, qsBatterySaverTileConfig, qsAlarmTileConfig, qsAirplaneModeTileConfig, qsCameraSensorPrivacyToggleTileConfig, qsMicrophoneSensorPrivacyToggleTileConfig, ) Loading Loading @@ -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) } } Loading Loading @@ -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) Loading Loading @@ -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" Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt +18 −0 Original line number Diff line number Diff line Loading @@ -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(" } Loading
packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +8 −2 Original line number Diff line number Diff line Loading @@ -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() } } } Loading
packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +11 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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() { Loading Loading @@ -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 { Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt +94 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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>, ) { Loading Loading @@ -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 } Loading @@ -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 Loading @@ -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 Loading @@ -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) } }