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

Commit 880c2f66 authored by Olivier St-Onge's avatar Olivier St-Onge
Browse files

Refactor Paginated layouts to allow for setting page keys

This adds a viewmodel for paginated layouts. These provide a list of keys to invalidate pages.

- This fixes the issue where pages won't recompose when tiles are resized
- This creates and activates a single infinite grid view model instead of one per page

Test: manually
Test: InfiniteGridViewModelTest
Test: PaginatableGridViewModelTest
Test: PaginatedGridLayoutTest
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Bug: 406063281
Change-Id: I9a5d3335b1b9b5b097d77a7a4e21815ae4015747
parent 13601812
Loading
Loading
Loading
Loading
+14 −8
Original line number Diff line number Diff line
@@ -14,17 +14,20 @@
 * limitations under the License.
 */

package com.android.systemui.qs.panels.ui.compose
package com.android.systemui.qs.panels.ui.viewmodel

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment
import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
import com.android.systemui.qs.panels.ui.compose.infinitegrid.infiniteGridLayout
import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -33,23 +36,27 @@ import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class InfiniteGridLayoutTest : SysuiTestCase() {
class InfiniteGridViewModelTest : SysuiTestCase() {
    private val kosmos =
        testKosmos().apply {
            testCase.context.orCreateTestableResources.addOverride(
                R.integer.quick_settings_infinite_grid_num_columns,
                4,
            )
            defaultLargeTilesRepository =
                object : DefaultLargeTilesRepository {
                    override val defaultLargeTiles: Set<TileSpec> = setOf(TileSpec.create("large"))
                }
            usingMediaInComposeFragment = false
        }

    private val underTest = kosmos.infiniteGridLayout
    private val Kosmos.underTest by Kosmos.Fixture { infiniteGridLayout.viewModelFactory.create() }

    @Test
    fun correctPagination_underOnePage_sameOrder() =
        with(kosmos) {
            testScope.runTest {
                val rows = 3
                val columns = 4

                val tiles =
                    listOf(
@@ -61,7 +68,7 @@ class InfiniteGridLayoutTest : SysuiTestCase() {
                        smallTile(),
                    )

                val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
                val pages = underTest.splitIntoPages(tiles, rows = rows)

                assertThat(pages).hasSize(1)
                assertThat(pages[0]).isEqualTo(tiles)
@@ -73,7 +80,6 @@ class InfiniteGridLayoutTest : SysuiTestCase() {
        with(kosmos) {
            testScope.runTest {
                val rows = 3
                val columns = 4

                val tiles =
                    listOf(
@@ -98,7 +104,7 @@ class InfiniteGridLayoutTest : SysuiTestCase() {
                // [L L] [S] [S]
                // [L L]

                val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
                val pages = underTest.splitIntoPages(tiles, rows = rows)

                assertThat(pages).hasSize(2)
                assertThat(pages[0]).isEqualTo(tiles.take(8))
+18 −18
Original line number Diff line number Diff line
@@ -14,13 +14,12 @@
 * limitations under the License.
 */

package com.android.systemui.qs.panels.ui.compose
package com.android.systemui.qs.panels.ui.viewmodel

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -28,24 +27,30 @@ import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class PaginatableGridLayoutTest : SysuiTestCase() {
class PaginatableGridViewModelTest : SysuiTestCase() {

    // Empty implementation to test PaginatableGridViewModel#splitInRows
    private val underTest: PaginatableGridViewModel =
        object : PaginatableGridViewModel {
            override val pageKeys: Array<Any> = emptyArray()

            override fun splitIntoPages(
                tiles: List<TileViewModel>,
                rows: Int,
            ): List<List<TileViewModel>> = emptyList()
        }

    @Test
    fun correctRows_gapsAtEnd() {
        val columns = 6

        val sizedTiles =
            listOf(
                largeTile(),
                extraLargeTile(),
                largeTile(),
                smallTile(),
                largeTile(),
            )
            listOf(largeTile(), extraLargeTile(), largeTile(), smallTile(), largeTile())

        // [L L] [XL XL XL]
        // [L L] [S] [L L]

        val rows = PaginatableGridLayout.splitInRows(sizedTiles, columns)
        val rows = underTest.splitInRows(sizedTiles, columns)

        assertThat(rows).hasSize(2)
        assertThat(rows[0]).isEqualTo(sizedTiles.take(2))
@@ -56,16 +61,11 @@ class PaginatableGridLayoutTest : SysuiTestCase() {
    fun correctRows_fullLastRow_noEmptyRow() {
        val columns = 6

        val sizedTiles =
            listOf(
                largeTile(),
                extraLargeTile(),
                smallTile(),
            )
        val sizedTiles = listOf(largeTile(), extraLargeTile(), smallTile())

        // [L L] [XL XL XL] [S]

        val rows = PaginatableGridLayout.splitInRows(sizedTiles, columns)
        val rows = underTest.splitInRows(sizedTiles, columns)

        assertThat(rows).hasSize(1)
        assertThat(rows[0]).isEqualTo(sizedTiles)
+41 −8
Original line number Diff line number Diff line
@@ -17,9 +17,18 @@
package com.android.systemui.qs.panels.domain.interactor

import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.qs.panels.shared.model.PanelsLog
import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.sample
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.onEach

/** Interactor to resize QS tiles down to icons when removed from the current tiles. */
class DynamicIconTilesInteractor
@@ -27,21 +36,45 @@ class DynamicIconTilesInteractor
constructor(
    private val iconTilesInteractor: IconTilesInteractor,
    private val currentTilesInteractor: CurrentTilesInteractor,
    @PanelsLog private val logBuffer: LogBuffer,
) : ExclusiveActivatable() {

    override suspend fun onActivated(): Nothing {
        currentTilesInteractor.currentTiles.collect { currentTiles ->
            // Only current tiles can be resized, so observe the current tiles and find the
            // intersection between them and the large tiles.
            val newLargeTiles =
                iconTilesInteractor.largeTilesSpecs.value intersect
                    currentTiles.map { it.spec }.toSet()
        currentTilesInteractor.userAndTiles
            .pairwise()
            .filter { !it.newValue.userChange } // Only compare tile changes for the same user
            .sample(iconTilesInteractor.largeTilesSpecs) { tilesData, largeTilesSpecs ->
                // Only current tiles can be resized, so remove deleted tiles from the set of
                // large tiles
                val deletedTiles = tilesData.previousValue.tiles - tilesData.newValue.tiles.toSet()
                largeTilesSpecs to deletedTiles.toSet()
            }
            .onEach { logChange(it.first, it.second) }
            .collect { (currentLargeTiles, deletedTiles) ->
                val newLargeTiles = currentLargeTiles - deletedTiles
                iconTilesInteractor.setLargeTiles(newLargeTiles)
            }
        awaitCancellation()
    }

    @AssistedFactory
    interface Factory {
        fun create(): DynamicIconTilesInteractor
    }

    private fun logChange(largeSpecs: Set<TileSpec>, deletedSpecs: Set<TileSpec>) {
        logBuffer.log(
            LOG_BUFFER_DELETED_TILES_RESIZED_TAG,
            LogLevel.DEBUG,
            {
                str1 = deletedSpecs.toString()
                str2 = largeSpecs.toString()
            },
            { "Deleted tiles=$str1, current large tiles specs=$str2" },
        )
    }

    private companion object {
        const val LOG_BUFFER_DELETED_TILES_RESIZED_TAG = "DeletedTilesResized"
    }
}
+15 −39
Original line number Diff line number Diff line
@@ -19,9 +19,8 @@ package com.android.systemui.qs.panels.ui.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.compose.animation.scene.ContentScope
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.TileRow
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.PaginatableGridViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec

@@ -53,45 +52,22 @@ interface GridLayout {
/**
 * A type of [GridLayout] that can be paginated, to use together with [PaginatedGridLayout].
 *
 * [splitIntoPages] determines how to split a list of tiles based on the number of rows and columns
 * available.
 * [splitIntoPages] determines how to split a list of tiles based on the number of rows available.
 */
interface PaginatableGridLayout : GridLayout {
    fun splitIntoPages(
        tiles: List<TileViewModel>,
        rows: Int,
        columns: Int,
    ): List<List<TileViewModel>>

    companion object {
    /** The factory to use when creating the grid layout view model. */
    val viewModelFactory: PaginatableGridViewModel.Factory

    /**
         * Splits a list of [SizedTile] into rows, each with at most [columns] occupied.
         *
         * It will leave gaps at the end of a row if the next [SizedTile] has [SizedTile.width] that
         * is larger than the space remaining in the row.
     * A single page from this layout. The viewmodel will be created and provided and will be the
     * same for all pages.
     */
        fun splitInRows(
            tiles: List<SizedTile<TileViewModel>>,
            columns: Int,
        ): List<List<SizedTile<TileViewModel>>> {
            val row = TileRow<TileViewModel>(columns)

            return buildList {
                for (tile in tiles) {
                    check(tile.width <= columns)
                    if (!row.maybeAddTile(tile)) {
                        // Couldn't add tile to previous row, create a row with the current tiles
                        // and start a new one
                        add(row.tiles)
                        row.clear()
                        row.maybeAddTile(tile)
                    }
                }
                if (row.tiles.isNotEmpty()) {
                    add(row.tiles)
                }
            }
        }
    }
    @Composable
    fun ContentScope.TileGridPage(
        viewModel: PaginatableGridViewModel,
        tiles: List<TileViewModel>,
        modifier: Modifier,
        listening: () -> Boolean,
    )
}
+14 −5
Original line number Diff line number Diff line
@@ -75,14 +75,16 @@ constructor(
            rememberViewModel(traceName = "PaginatedGridLayout-TileGrid") {
                viewModelFactory.create()
            }
        val delegateGridViewModel =
            rememberViewModel(traceName = "PaginatedGridLayout-TileGrid") {
                delegateGridLayout.viewModelFactory.create()
            }

        val columns = viewModel.columns
        val rows = integerResource(R.integer.quick_settings_paginated_grid_num_rows)
        val largeTiles by viewModel.largeTilesState

        val pages =
            remember(tiles, columns, rows, largeTiles) {
                delegateGridLayout.splitIntoPages(tiles, rows = rows, columns = columns)
            remember(tiles, rows, *delegateGridViewModel.pageKeys) {
                delegateGridViewModel.splitIntoPages(tiles, rows = rows)
            }

        val pagerState = rememberPagerState(0) { pages.size }
@@ -136,7 +138,14 @@ constructor(
            ) {
                val page = pages[it]

                with(delegateGridLayout) { TileGrid(tiles = page, modifier = Modifier, listening) }
                with(delegateGridLayout) {
                    TileGridPage(
                        viewModel = delegateGridViewModel,
                        tiles = page,
                        modifier = Modifier,
                        listening,
                    )
                }
            }
            FooterBar(
                buildNumberViewModelFactory = viewModel.buildNumberViewModelFactory,
Loading