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

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

Apply restore to repositories

This CL also changes the source of truth of the list of tiles:
* Tiles are read once from Settings and kept in memory.
* After tiles change, the new value is applied to memory.
* Changes to the list are posted to a shared flow and applied to the
  current list sequentially.
* If a change occurs in Settings that makes the list different from what
  we have, we overwrite it with our value.

The same thing is done to AutoAddRepository.

The restore is used to trigger a change in the repositories.

It's important to note that the restores are tracked starting at the
point where we check the setting for the value there (on start of the
user). This means that if the restore happens before we get the tiles
for the first time, the setting would have the restored values. This is
probably the best scenario: restore happening before SystemUI starts
interfering.

Given that now we don't listen to Settings, a new shell command has been
added to StatusBarShellCommands to modify the set of tiles.

Fixes: 289502851
Test: atest com.android.systemui.qs.pipeline
Test: atest CtsTileServiceTestCases android.host.systemui
Test: quicksettings PlatformScenarioTests
Test: manual restore using LocalTransport
Change-Id: I287bb3c47c3a4181f29f942bcb18bd81c129273c
parent 4533782e
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -154,6 +154,7 @@ oneway interface IStatusBar

    void addQsTile(in ComponentName tile);
    void remQsTile(in ComponentName tile);
    void setQsTiles(in String[] tiles);
    void clickQsTile(in ComponentName tile);
    void handleSystemKey(in KeyEvent key);

+8 −1
Original line number Diff line number Diff line
@@ -20,6 +20,8 @@ import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.qs.pipeline.data.repository.DefaultTilesQSHostRepository
import com.android.systemui.qs.pipeline.data.repository.DefaultTilesRepository
import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository
import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepositoryImpl
import com.android.systemui.qs.pipeline.data.repository.QSSettingsRestoredBroadcastRepository
@@ -43,6 +45,11 @@ abstract class QSPipelineModule {
    @Binds
    abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository

    @Binds
    abstract fun provideDefaultTilesRepository(
        impl: DefaultTilesQSHostRepository
    ): DefaultTilesRepository

    @Binds
    abstract fun bindCurrentTilesInteractor(
        impl: CurrentTilesInteractorImpl
+22 −79
Original line number Diff line number Diff line
@@ -16,28 +16,19 @@

package com.android.systemui.qs.pipeline.data.repository

import android.database.ContentObserver
import android.provider.Settings
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import android.util.SparseArray
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.util.settings.SecureSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow

/** Repository to track what QS tiles have been auto-added */
interface AutoAddRepository {

    /** Flow of tiles that have been auto-added */
    fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>>
    suspend fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>>

    /** Mark a tile as having been auto-added */
    suspend fun markTileAdded(userId: Int, spec: TileSpec)
@@ -47,87 +38,39 @@ interface AutoAddRepository {
     * multiple times.
     */
    suspend fun unmarkTileAdded(userId: Int, spec: TileSpec)

    suspend fun reconcileRestore(restoreData: RestoreData)
}

/**
 * Implementation that tracks the auto-added tiles stored in [Settings.Secure.QS_AUTO_ADDED_TILES].
 * Implementation of [AutoAddRepository] that delegates to an instance of [UserAutoAddRepository]
 * for each user.
 */
@SysUISingleton
class AutoAddSettingRepository
@Inject
constructor(
    private val secureSettings: SecureSettings,
    @Background private val bgDispatcher: CoroutineDispatcher,
) : AutoAddRepository {
    override fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> {
        return conflatedCallbackFlow {
                val observer =
                    object : ContentObserver(null) {
                        override fun onChange(selfChange: Boolean) {
                            trySend(Unit)
                        }
                    }
constructor(private val userAutoAddRepositoryFactory: UserAutoAddRepository.Factory) :
    AutoAddRepository {

                secureSettings.registerContentObserverForUser(SETTING, observer, userId)
    private val userAutoAddRepositories = SparseArray<UserAutoAddRepository>()

                awaitClose { secureSettings.unregisterContentObserver(observer) }
    override suspend fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> {
        if (userId !in userAutoAddRepositories) {
            val repository = userAutoAddRepositoryFactory.create(userId)
            userAutoAddRepositories.put(userId, repository)
        }
            .onStart { emit(Unit) }
            .map { secureSettings.getStringForUser(SETTING, userId) ?: "" }
            .distinctUntilChanged()
            .map {
                it.split(DELIMITER).map(TileSpec::create).filter { it !is TileSpec.Invalid }.toSet()
            }
            .flowOn(bgDispatcher)
        return userAutoAddRepositories.get(userId).autoAdded()
    }

    override suspend fun markTileAdded(userId: Int, spec: TileSpec) {
        if (spec is TileSpec.Invalid) {
            return
        }
        val added = load(userId).toMutableSet()
        if (added.add(spec)) {
            store(userId, added)
        }
        userAutoAddRepositories.get(userId)?.markTileAdded(spec)
    }

    override suspend fun unmarkTileAdded(userId: Int, spec: TileSpec) {
        if (spec is TileSpec.Invalid) {
            return
        }
        val added = load(userId).toMutableSet()
        if (added.remove(spec)) {
            store(userId, added)
        userAutoAddRepositories.get(userId)?.unmarkTileAdded(spec)
    }
    }

    private suspend fun store(userId: Int, tiles: Set<TileSpec>) {
        val toStore =
            tiles
                .filter { it !is TileSpec.Invalid }
                .joinToString(DELIMITER, transform = TileSpec::spec)
        withContext(bgDispatcher) {
            secureSettings.putStringForUser(
                SETTING,
                toStore,
                null,
                false,
                userId,
                true,
            )
        }
    }

    private suspend fun load(userId: Int): Set<TileSpec> {
        return withContext(bgDispatcher) {
            (secureSettings.getStringForUser(SETTING, userId) ?: "").toTilesSet()
        }
    }

    companion object {
        private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES
        private const val DELIMITER = ","

        private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this)
    override suspend fun reconcileRestore(restoreData: RestoreData) {
        userAutoAddRepositories.get(restoreData.userId)?.reconcileRestore(restoreData)
    }
}
+25 −0
Original line number Diff line number Diff line
package com.android.systemui.qs.pipeline.data.repository

import android.content.res.Resources
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.pipeline.shared.TileSpec
import javax.inject.Inject

interface DefaultTilesRepository {
    val defaultTiles: List<TileSpec>
}

@SysUISingleton
class DefaultTilesQSHostRepository
@Inject
constructor(
    @Main private val resources: Resources,
) : DefaultTilesRepository {
    override val defaultTiles: List<TileSpec>
        get() =
            QSHost.getDefaultSpecs(resources).map(TileSpec::create).filter {
                it != TileSpec.Invalid
            }
}
+38 −120
Original line number Diff line number Diff line
@@ -18,34 +18,19 @@ package com.android.systemui.qs.pipeline.data.repository

import android.annotation.UserIdInt
import android.content.res.Resources
import android.database.ContentObserver
import android.provider.Settings
import android.util.SparseArray
import com.android.systemui.res.R
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.retail.data.repository.RetailModeRepository
import com.android.systemui.util.settings.SecureSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext

/** Repository that tracks the current tiles. */
interface TileSpecRepository {
@@ -55,7 +40,7 @@ interface TileSpecRepository {
     *
     * Tiles will never be [TileSpec.Invalid] in the list and it will never be empty.
     */
    fun tilesSpecs(@UserIdInt userId: Int): Flow<List<TileSpec>>
    suspend fun tilesSpecs(@UserIdInt userId: Int): Flow<List<TileSpec>>

    /**
     * Adds a [tile] for a given [userId] at [position]. Using [POSITION_AT_END] will add the tile
@@ -81,6 +66,8 @@ interface TileSpecRepository {
     */
    suspend fun setTiles(@UserIdInt userId: Int, tiles: List<TileSpec>)

    suspend fun reconcileRestore(restoreData: RestoreData, currentAutoAdded: Set<TileSpec>)

    companion object {
        /** Position to indicate the end of the list */
        const val POSITION_AT_END = -1
@@ -88,28 +75,23 @@ interface TileSpecRepository {
}

/**
 * Implementation of [TileSpecRepository] that persist the values of tiles in
 * [Settings.Secure.QS_TILES].
 *
 * All operations against [Settings] will be performed in a background thread.
 * Implementation of [TileSpecRepository] that delegates to an instance of [UserTileSpecRepository]
 * for each user.
 *
 * If the device is in retail mode, the tiles are fixed to the value of
 * [R.string.quick_settings_tiles_retail_mode].
 */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class TileSpecSettingsRepository
@Inject
constructor(
    private val secureSettings: SecureSettings,
    @Main private val resources: Resources,
    private val logger: QSPipelineLogger,
    private val retailModeRepository: RetailModeRepository,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val userTileSpecRepositoryFactory: UserTileSpecRepository.Factory,
) : TileSpecRepository {

    private val mutex = Mutex()
    private val tileSpecsPerUser = SparseArray<List<TileSpec>>()

    private val retailModeTiles by lazy {
        resources
            .getString(R.string.quick_settings_tiles_retail_mode)
@@ -118,123 +100,59 @@ constructor(
            .filter { it !is TileSpec.Invalid }
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> {
    private val userTileRepositories = SparseArray<UserTileSpecRepository>()

    override suspend fun tilesSpecs(userId: Int): Flow<List<TileSpec>> {
        if (userId !in userTileRepositories) {
            val userTileRepository = userTileSpecRepositoryFactory.create(userId)
            userTileRepositories.put(userId, userTileRepository)
        }
        val realTiles = userTileRepositories.get(userId).tiles()

        return retailModeRepository.retailMode.flatMapLatest { inRetailMode ->
            if (inRetailMode) {
                logger.logUsingRetailTiles()
                flowOf(retailModeTiles)
            } else {
                settingsTiles(userId)
                realTiles
            }
        }
    }

    private fun settingsTiles(userId: Int): Flow<List<TileSpec>> {
        return conflatedCallbackFlow {
                val observer =
                    object : ContentObserver(null) {
                        override fun onChange(selfChange: Boolean) {
                            trySend(Unit)
                        }
                    }

                secureSettings.registerContentObserverForUser(SETTING, observer, userId)

                awaitClose { secureSettings.unregisterContentObserver(observer) }
            }
            .onStart { emit(Unit) }
            .map { loadTiles(userId) }
            .onEach { logger.logTilesChangedInSettings(it, userId) }
            .distinctUntilChanged()
            .map { parseTileSpecs(it, userId).also { storeTiles(userId, it) } }
            .distinctUntilChanged()
            .onEach { mutex.withLock { tileSpecsPerUser.put(userId, it) } }
            .flowOn(backgroundDispatcher)
    }

    override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) =
        mutex.withLock {
            if (tile == TileSpec.Invalid) {
    override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) {
        if (retailModeRepository.inRetailMode) {
            return
        }
            val tilesList = tileSpecsPerUser.get(userId, emptyList()).toMutableList()
            if (tile !in tilesList) {
                if (position < 0 || position >= tilesList.size) {
                    tilesList.add(tile)
                } else {
                    tilesList.add(position, tile)
                }
                storeTiles(userId, tilesList)
                tileSpecsPerUser.put(userId, tilesList)
            }
        }

    override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) =
        mutex.withLock {
            if (tiles.all { it == TileSpec.Invalid }) {
        if (tile is TileSpec.Invalid) {
            return
        }
            val tilesList = tileSpecsPerUser.get(userId, emptyList()).toMutableList()
            if (tilesList.removeAll(tiles)) {
                storeTiles(userId, tilesList.toList())
                tileSpecsPerUser.put(userId, tilesList)
            }
        }

    override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) =
        mutex.withLock {
            val filtered = tiles.filter { it != TileSpec.Invalid }
            if (filtered.isNotEmpty()) {
                storeTiles(userId, filtered)
                tileSpecsPerUser.put(userId, tiles)
            }
        userTileRepositories.get(userId)?.addTile(tile, position)
    }

    private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) {
    override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) {
        if (retailModeRepository.inRetailMode) {
            // No storing tiles when in retail mode
            return
        }
        val toStore =
            tiles
                .filter { it !is TileSpec.Invalid }
                .joinToString(DELIMITER, transform = TileSpec::spec)
        withContext(backgroundDispatcher) {
            secureSettings.putStringForUser(
                SETTING,
                toStore,
                null,
                false,
                forUser,
                true,
            )
        }
        userTileRepositories.get(userId)?.removeTiles(tiles)
    }

    private suspend fun loadTiles(userId: Int): String {
        return withContext(backgroundDispatcher) {
            secureSettings.getStringForUser(SETTING, userId) ?: ""
    override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) {
        if (retailModeRepository.inRetailMode) {
            return
        }
        userTileRepositories.get(userId)?.setTiles(tiles)
    }

    private fun parseTileSpecs(tilesFromSettings: String, user: Int): List<TileSpec> {
        val fromSettings =
            tilesFromSettings.toTilesList()
        return if (fromSettings.isNotEmpty()) {
            fromSettings.also { logger.logParsedTiles(it, false, user) }
        } else {
            QSHost.getDefaultSpecs(resources)
                .map(TileSpec::create)
                .filter { it != TileSpec.Invalid }
                .also { logger.logParsedTiles(it, true, user) }
        }
    override suspend fun reconcileRestore(
        restoreData: RestoreData,
        currentAutoAdded: Set<TileSpec>
    ) {
        userTileRepositories
            .get(restoreData.userId)
            ?.reconcileRestore(restoreData, currentAutoAdded)
    }

    companion object {
        private const val SETTING = Settings.Secure.QS_TILES
        private const val DELIMITER = TilesSettingConverter.DELIMITER

        private fun String.toTilesList() = TilesSettingConverter.toTilesList(this)
    }
}
Loading