diff options
25 files changed, 1452 insertions, 599 deletions
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index c6f5086b8346..7eeac29e21f7 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -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); diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt index 6f2898504967..21aaa94f9e10 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt @@ -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 @@ -44,6 +46,11 @@ abstract class QSPipelineModule { abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository @Binds + abstract fun provideDefaultTilesRepository( + impl: DefaultTilesQSHostRepository + ): DefaultTilesRepository + + @Binds abstract fun bindCurrentTilesInteractor( impl: CurrentTilesInteractorImpl ): CurrentTilesInteractor @@ -60,7 +67,7 @@ abstract class QSPipelineModule { @Binds abstract fun provideQSSettingsRestoredRepository( - impl: QSSettingsRestoredBroadcastRepository + impl: QSSettingsRestoredBroadcastRepository ): QSSettingsRestoredRepository companion object { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt index 7e388b7de8a1..7998dfbe3f92 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt @@ -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) } - } - .onStart { emit(Unit) } - .map { secureSettings.getStringForUser(SETTING, userId) ?: "" } - .distinctUntilChanged() - .map { - it.split(DELIMITER).map(TileSpec::create).filter { it !is TileSpec.Invalid }.toSet() - } - .flowOn(bgDispatcher) + override suspend fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> { + if (userId !in userAutoAddRepositories) { + val repository = userAutoAddRepositoryFactory.create(userId) + userAutoAddRepositories.put(userId, repository) + } + 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) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/DefaultTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/DefaultTilesRepository.kt new file mode 100644 index 000000000000..fe0a69b03287 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/DefaultTilesRepository.kt @@ -0,0 +1,25 @@ +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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt index 1a0285d17f81..00ea0b5c5ed3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt @@ -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) { - 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 }) { - return - } - val tilesList = tileSpecsPerUser.get(userId, emptyList()).toMutableList() - if (tilesList.removeAll(tiles)) { - storeTiles(userId, tilesList.toList()) - tileSpecsPerUser.put(userId, tilesList) - } + override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) { + if (retailModeRepository.inRetailMode) { + return } - - 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) - } + if (tile is TileSpec.Invalid) { + return } + 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) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverter.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverter.kt index 6ac9b575c16d..bb55fcdac58a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverter.kt @@ -6,12 +6,13 @@ object TilesSettingConverter { const val DELIMITER = "," - fun toTilesList(commaSeparatedTiles: String) = commaSeparatedTiles.split(DELIMITER) - .map(TileSpec::create) - .filter { it != TileSpec.Invalid } + fun toTilesList(commaSeparatedTiles: String) = + commaSeparatedTiles.split(DELIMITER).map(TileSpec::create).filter { it != TileSpec.Invalid } - fun toTilesSet(commaSeparatedTiles: String) = commaSeparatedTiles.split(DELIMITER) + fun toTilesSet(commaSeparatedTiles: String) = + commaSeparatedTiles + .split(DELIMITER) .map(TileSpec::create) .filter { it != TileSpec.Invalid } .toSet() -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepository.kt new file mode 100644 index 000000000000..d452241e86e2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepository.kt @@ -0,0 +1,186 @@ +package com.android.systemui.qs.pipeline.data.repository + +import android.database.ContentObserver +import android.provider.Settings +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Application +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.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.util.settings.SecureSettings +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Single user version of [AutoAddRepository]. It provides a similar interface as + * [AutoAddRepository], but focusing solely on the user it was created for. + * + * This is the source of truth for that user's tiles, after the user has been started. Persisting + * all the changes to [Settings]. Changes in [Settings] that disagree with this repository will be + * reverted + * + * All operations against [Settings] will be performed in a background thread. + */ +class UserAutoAddRepository +@AssistedInject +constructor( + @Assisted private val userId: Int, + private val secureSettings: SecureSettings, + private val logger: QSPipelineLogger, + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, +) { + + private val changeEvents = MutableSharedFlow<ChangeAction>( + extraBufferCapacity = CHANGES_BUFFER_SIZE + ) + + private lateinit var _autoAdded: StateFlow<Set<TileSpec>> + + suspend fun autoAdded(): StateFlow<Set<TileSpec>> { + if (!::_autoAdded.isInitialized) { + _autoAdded = + changeEvents + .scan(load().also { logger.logAutoAddTilesParsed(userId, it) }) { + current, + change -> + change.apply(current).also { + if (change is RestoreTiles) { + logger.logAutoAddTilesRestoredReconciled(userId, it) + } + } + } + .flowOn(bgDispatcher) + .stateIn(applicationScope) + .also { startFlowCollections(it) } + } + return _autoAdded + } + + private fun startFlowCollections(autoAdded: StateFlow<Set<TileSpec>>) { + applicationScope.launch(bgDispatcher) { + launch { autoAdded.collect { store(it) } } + launch { + // As Settings is not the source of truth, once we started tracking tiles for a + // user, we don't want anyone to change the underlying setting. Therefore, if there + // are any changes that don't match with the source of truth (this class), we + // overwrite them with the current value. + ConflatedCallbackFlow.conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + secureSettings.registerContentObserverForUser(SETTING, observer, userId) + awaitClose { secureSettings.unregisterContentObserver(observer) } + } + .map { load() } + .flowOn(bgDispatcher) + .collect { setting -> + val current = autoAdded.value + if (setting != current) { + store(current) + } + } + } + } + } + + suspend fun markTileAdded(spec: TileSpec) { + if (spec is TileSpec.Invalid) { + return + } + changeEvents.emit(MarkTile(spec)) + } + + suspend fun unmarkTileAdded(spec: TileSpec) { + if (spec is TileSpec.Invalid) { + return + } + changeEvents.emit(UnmarkTile(spec)) + } + + private suspend fun store(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(): Set<TileSpec> { + return withContext(bgDispatcher) { + (secureSettings.getStringForUser(SETTING, userId) ?: "").toTilesSet() + } + } + + suspend fun reconcileRestore(restoreData: RestoreData) { + changeEvents.emit(RestoreTiles(restoreData)) + } + + private sealed interface ChangeAction { + fun apply(currentAutoAdded: Set<TileSpec>): Set<TileSpec> + } + + private data class MarkTile( + val tileSpec: TileSpec, + ) : ChangeAction { + override fun apply(currentAutoAdded: Set<TileSpec>): Set<TileSpec> { + return currentAutoAdded.toMutableSet().apply { add(tileSpec) } + } + } + + private data class UnmarkTile( + val tileSpec: TileSpec, + ) : ChangeAction { + override fun apply(currentAutoAdded: Set<TileSpec>): Set<TileSpec> { + return currentAutoAdded.toMutableSet().apply { remove(tileSpec) } + } + } + + private data class RestoreTiles( + val restoredData: RestoreData, + ) : ChangeAction { + override fun apply(currentAutoAdded: Set<TileSpec>): Set<TileSpec> { + return currentAutoAdded + restoredData.restoredAutoAddedTiles + } + } + + companion object { + private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES + private const val DELIMITER = "," + // We want a small buffer in case multiple changes come in at the same time (sometimes + // happens in first start. This should be enough to not lose changes. + private const val CHANGES_BUFFER_SIZE = 10 + + private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this) + } + + @AssistedFactory + interface Factory { + fun create(userId: Int): UserAutoAddRepository + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt new file mode 100644 index 000000000000..152fd0f83811 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt @@ -0,0 +1,252 @@ +package com.android.systemui.qs.pipeline.data.repository + +import android.annotation.UserIdInt +import android.database.ContentObserver +import android.provider.Settings +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Application +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.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.util.settings.SecureSettings +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Single user version of [TileSpecRepository]. It provides a similar interface as + * [TileSpecRepository], but focusing solely on the user it was created for. + * + * This is the source of truth for that user's tiles, after the user has been started. Persisting + * all the changes to [Settings]. Changes in [Settings] that disagree with this repository will be + * reverted + * + * All operations against [Settings] will be performed in a background thread. + */ +class UserTileSpecRepository +@AssistedInject +constructor( + @Assisted private val userId: Int, + private val defaultTilesRepository: DefaultTilesRepository, + private val secureSettings: SecureSettings, + private val logger: QSPipelineLogger, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + + private val defaultTiles: List<TileSpec> + get() = defaultTilesRepository.defaultTiles + + private val changeEvents = MutableSharedFlow<ChangeAction>( + extraBufferCapacity = CHANGES_BUFFER_SIZE + ) + + private lateinit var _tiles: StateFlow<List<TileSpec>> + + suspend fun tiles(): Flow<List<TileSpec>> { + if (!::_tiles.isInitialized) { + _tiles = + changeEvents + .scan(loadTilesFromSettingsAndParse(userId)) { current, change -> + change.apply(current).also { + if (current != it) { + if (change is RestoreTiles) { + logger.logTilesRestoredAndReconciled(current, it, userId) + } else { + logger.logProcessTileChange(change, it, userId) + } + } + } + } + .flowOn(backgroundDispatcher) + .stateIn(applicationScope) + .also { startFlowCollections(it) } + } + return _tiles + } + + private fun startFlowCollections(tiles: StateFlow<List<TileSpec>>) { + applicationScope.launch(backgroundDispatcher) { + launch { tiles.collect { storeTiles(userId, it) } } + launch { + // As Settings is not the source of truth, once we started tracking tiles for a + // user, we don't want anyone to change the underlying setting. Therefore, if there + // are any changes that don't match with the source of truth (this class), we + // overwrite them with the current value. + ConflatedCallbackFlow.conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + secureSettings.registerContentObserverForUser(SETTING, observer, userId) + awaitClose { secureSettings.unregisterContentObserver(observer) } + } + .map { loadTilesFromSettings(userId) } + .flowOn(backgroundDispatcher) + .collect { setting -> + val current = tiles.value + if (setting != current) { + storeTiles(userId, current) + } + } + } + } + } + + private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) { + val toStore = + tiles + .filter { it !is TileSpec.Invalid } + .joinToString(DELIMITER, transform = TileSpec::spec) + withContext(backgroundDispatcher) { + secureSettings.putStringForUser( + SETTING, + toStore, + null, + false, + forUser, + true, + ) + } + } + + suspend fun addTile(tile: TileSpec, position: Int = TileSpecRepository.POSITION_AT_END) { + if (tile is TileSpec.Invalid) { + return + } + changeEvents.emit(AddTile(tile, position)) + } + + suspend fun removeTiles(tiles: Collection<TileSpec>) { + changeEvents.emit(RemoveTiles(tiles)) + } + + suspend fun setTiles(tiles: List<TileSpec>) { + changeEvents.emit(ChangeTiles(tiles)) + } + + private fun parseTileSpecs(fromSettings: List<TileSpec>, user: Int): List<TileSpec> { + return if (fromSettings.isNotEmpty()) { + fromSettings.also { logger.logParsedTiles(it, false, user) } + } else { + defaultTiles.also { logger.logParsedTiles(it, true, user) } + } + } + + private suspend fun loadTilesFromSettingsAndParse(userId: Int): List<TileSpec> { + return parseTileSpecs(loadTilesFromSettings(userId), userId) + } + + private suspend fun loadTilesFromSettings(userId: Int): List<TileSpec> { + return withContext(backgroundDispatcher) { + secureSettings.getStringForUser(SETTING, userId) ?: "" + } + .toTilesList() + } + + suspend fun reconcileRestore(restoreData: RestoreData, currentAutoAdded: Set<TileSpec>) { + changeEvents.emit(RestoreTiles(restoreData, currentAutoAdded)) + } + + sealed interface ChangeAction { + fun apply(currentTiles: List<TileSpec>): List<TileSpec> + } + + private data class AddTile( + val tileSpec: TileSpec, + val position: Int = TileSpecRepository.POSITION_AT_END + ) : ChangeAction { + override fun apply(currentTiles: List<TileSpec>): List<TileSpec> { + val tilesList = currentTiles.toMutableList() + if (tileSpec !in tilesList) { + if (position < 0 || position >= tilesList.size) { + tilesList.add(tileSpec) + } else { + tilesList.add(position, tileSpec) + } + } + return tilesList + } + } + + private data class RemoveTiles(val tileSpecs: Collection<TileSpec>) : ChangeAction { + override fun apply(currentTiles: List<TileSpec>): List<TileSpec> { + return currentTiles.toMutableList().apply { removeAll(tileSpecs) } + } + } + + private data class ChangeTiles( + val newTiles: List<TileSpec>, + ) : ChangeAction { + override fun apply(currentTiles: List<TileSpec>): List<TileSpec> { + val new = newTiles.filter { it !is TileSpec.Invalid } + return if (new.isNotEmpty()) new else currentTiles + } + } + + private data class RestoreTiles( + val restoreData: RestoreData, + val currentAutoAdded: Set<TileSpec>, + ) : ChangeAction { + + override fun apply(currentTiles: List<TileSpec>): List<TileSpec> { + return reconcileTiles(currentTiles, currentAutoAdded, restoreData) + } + } + + companion object { + private const val SETTING = Settings.Secure.QS_TILES + private const val DELIMITER = TilesSettingConverter.DELIMITER + // We want a small buffer in case multiple changes come in at the same time (sometimes + // happens in first start. This should be enough to not lose changes. + private const val CHANGES_BUFFER_SIZE = 10 + + private fun String.toTilesList() = TilesSettingConverter.toTilesList(this) + + fun reconcileTiles( + currentTiles: List<TileSpec>, + currentAutoAdded: Set<TileSpec>, + restoreData: RestoreData + ): List<TileSpec> { + val toRestore = restoreData.restoredTiles.toMutableList() + val freshlyAutoAdded = + currentAutoAdded.filterNot { it in restoreData.restoredAutoAddedTiles } + freshlyAutoAdded + .filter { it in currentTiles && it !in restoreData.restoredTiles } + .map { it to currentTiles.indexOf(it) } + .sortedBy { it.second } + .forEachIndexed { iteration, (tile, position) -> + val insertAt = position + iteration + if (insertAt > toRestore.size) { + toRestore.add(tile) + } else { + toRestore.add(insertAt, tile) + } + } + + return toRestore + } + } + + @AssistedFactory + interface Factory { + fun create( + userId: Int, + ): UserTileSpecRepository + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt index 5a5e47af73c9..00c23582f726 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -20,6 +20,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.UserHandle +import android.util.Log import com.android.systemui.Dumpable import com.android.systemui.ProtoDumpable import com.android.systemui.dagger.SysUISingleton @@ -268,6 +269,7 @@ constructor( // repository launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) } } + Log.d("Fabian", "Finished resolving tiles") } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractor.kt new file mode 100644 index 000000000000..9844903eff26 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractor.kt @@ -0,0 +1,53 @@ +package com.android.systemui.qs.pipeline.domain.interactor + +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.pipeline.data.repository.AutoAddRepository +import com.android.systemui.qs.pipeline.data.repository.QSSettingsRestoredRepository +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch + +/** + * Interactor in charge of triggering reconciliation after QS Secure Settings are restored. For a + * given user, it will trigger the reconciliations in the correct order to prevent race conditions. + * + * Currently, the order is: + * 1. TileSpecRepository, with the restored data and the current (before restore) auto add tiles + * 2. AutoAddRepository + * + * [start] needs to be called to trigger the collection of [QSSettingsRestoredRepository]. + */ +@SysUISingleton +class RestoreReconciliationInteractor +@Inject +constructor( + private val tileSpecRepository: TileSpecRepository, + private val autoAddRepository: AutoAddRepository, + private val qsSettingsRestoredRepository: QSSettingsRestoredRepository, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + fun start() { + applicationScope.launch(backgroundDispatcher) { + qsSettingsRestoredRepository.restoreData.flatMapConcat { data -> + autoAddRepository.autoAddedTiles(data.userId) + .take(1) + .map { tiles -> data to tiles } + }.collect { (restoreData, autoAdded) -> + tileSpecRepository.reconcileRestore(restoreData, autoAdded) + autoAddRepository.reconcileRestore(restoreData) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt index 0743ba0b121a..1539f05508d0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt @@ -20,6 +20,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.pipeline.domain.interactor.AutoAddInteractor import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor +import com.android.systemui.qs.pipeline.domain.interactor.RestoreReconciliationInteractor import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import javax.inject.Inject @@ -30,11 +31,13 @@ constructor( private val currentTilesInteractor: CurrentTilesInteractor, private val autoAddInteractor: AutoAddInteractor, private val featureFlags: QSPipelineFlagsRepository, + private val restoreReconciliationInteractor: RestoreReconciliationInteractor, ) : CoreStartable { override fun start() { if (featureFlags.pipelineAutoAddEnabled) { autoAddInteractor.init(currentTilesInteractor) + restoreReconciliationInteractor.start() } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt index c6302384e4ab..bca86c9ee8af 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt @@ -16,13 +16,13 @@ package com.android.systemui.qs.pipeline.shared.logging -import android.annotation.UserIdInt import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.qs.pipeline.dagger.QSAutoAddLog import com.android.systemui.qs.pipeline.dagger.QSRestoreLog import com.android.systemui.qs.pipeline.dagger.QSTileListLog import com.android.systemui.qs.pipeline.data.model.RestoreData +import com.android.systemui.qs.pipeline.data.repository.UserTileSpecRepository import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject @@ -64,20 +64,37 @@ constructor( ) } - /** - * Logs when the tiles change in Settings. - * - * This could be caused by SystemUI, or restore. - */ - fun logTilesChangedInSettings(newTiles: String, @UserIdInt user: Int) { + fun logTilesRestoredAndReconciled( + currentTiles: List<TileSpec>, + reconciledTiles: List<TileSpec>, + user: Int, + ) { tileListLogBuffer.log( TILE_LIST_TAG, - LogLevel.VERBOSE, + LogLevel.DEBUG, { - str1 = newTiles + str1 = currentTiles.toString() + str2 = reconciledTiles.toString() int1 = user }, - { "Tiles changed in settings for user $int1: $str1" } + { "Tiles restored and reconciled for user: $int1\nWas: $str1\nSet to: $str2" } + ) + } + + fun logProcessTileChange( + action: UserTileSpecRepository.ChangeAction, + newList: List<TileSpec>, + userId: Int, + ) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.DEBUG, + { + str1 = action.toString() + str2 = newList.toString() + int1 = userId + }, + { "Processing $str1 for user $int1\nNew list: $str2" } ) } @@ -143,6 +160,30 @@ constructor( ) } + fun logAutoAddTilesParsed(userId: Int, tiles: Set<TileSpec>) { + tileAutoAddLogBuffer.log( + AUTO_ADD_TAG, + LogLevel.DEBUG, + { + str1 = tiles.toString() + int1 = userId + }, + { "Auto add tiles parsed for user $int1: $str1" } + ) + } + + fun logAutoAddTilesRestoredReconciled(userId: Int, tiles: Set<TileSpec>) { + tileAutoAddLogBuffer.log( + AUTO_ADD_TAG, + LogLevel.DEBUG, + { + str1 = tiles.toString() + int1 = userId + }, + { "Auto-add tiles reconciled for user $int1: $str1" } + ) + } + fun logTileAutoAdded(userId: Int, spec: TileSpec, position: Int) { tileAutoAddLogBuffer.log( AUTO_ADD_TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 670fb1289357..93bb4356a1d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -172,6 +172,7 @@ public class CommandQueue extends IStatusBar.Stub implements private static final int MSG_LOCK_TASK_MODE_CHANGED = 75 << MSG_SHIFT; private static final int MSG_CONFIRM_IMMERSIVE_PROMPT = 77 << MSG_SHIFT; private static final int MSG_IMMERSIVE_CHANGED = 78 << MSG_SHIFT; + private static final int MSG_SET_QS_TILES = 79 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1; @@ -301,6 +302,8 @@ public class CommandQueue extends IStatusBar.Stub implements default void addQsTile(ComponentName tile) { } default void remQsTile(ComponentName tile) { } + + default void setQsTiles(String[] tiles) {} default void clickTile(ComponentName tile) { } default void handleSystemKey(KeyEvent arg1) { } @@ -903,6 +906,13 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override + public void setQsTiles(String[] tiles) { + synchronized (mLock) { + mHandler.obtainMessage(MSG_SET_QS_TILES, tiles).sendToTarget(); + } + } + + @Override public void clickQsTile(ComponentName tile) { synchronized (mLock) { mHandler.obtainMessage(MSG_CLICK_QS_TILE, tile).sendToTarget(); @@ -1533,6 +1543,11 @@ public class CommandQueue extends IStatusBar.Stub implements mCallbacks.get(i).remQsTile((ComponentName) msg.obj); } break; + case MSG_SET_QS_TILES: + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).setQsTiles((String[]) msg.obj); + } + break; case MSG_CLICK_QS_TILE: for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).clickTile((ComponentName) msg.obj); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 485ab3262725..f7ff39c8869b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -75,6 +75,7 @@ import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import dagger.Lazy; +import java.util.Arrays; import java.util.Optional; import javax.inject.Inject; @@ -201,6 +202,11 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba } @Override + public void setQsTiles(String[] tiles) { + mQSHost.changeTilesByUser(mQSHost.getSpecs(), Arrays.stream(tiles).toList()); + } + + @Override public void clickTile(ComponentName tile) { // Can't inject this because it changes with the QS fragment QSPanelController qsPanelController = mCentralSurfaces.getQSPanelController(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt index 9386d711374a..9a55f722c13c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt @@ -23,15 +23,19 @@ import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -39,6 +43,20 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AutoAddSettingsRepositoryTest : SysuiTestCase() { private val secureSettings = FakeSettings() + private val userAutoAddRepositoryFactory = + object : UserAutoAddRepository.Factory { + override fun create(userId: Int): UserAutoAddRepository { + return UserAutoAddRepository( + userId, + secureSettings, + logger, + testScope.backgroundScope, + testDispatcher, + ) + } + } + + @Mock private lateinit var logger: QSPipelineLogger private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -47,110 +65,37 @@ class AutoAddSettingsRepositoryTest : SysuiTestCase() { @Before fun setUp() { - underTest = - AutoAddSettingRepository( - secureSettings, - testDispatcher, - ) - } - - @Test - fun nonExistentSetting_emptySet() = - testScope.runTest { - val specs by collectLastValue(underTest.autoAddedTiles(0)) - - assertThat(specs).isEmpty() - } - - @Test - fun settingsChange_correctValues() = - testScope.runTest { - val userId = 0 - val specs by collectLastValue(underTest.autoAddedTiles(userId)) - - val value = "a,custom(b/c)" - storeForUser(value, userId) - - assertThat(specs).isEqualTo(value.toSet()) + MockitoAnnotations.initMocks(this) - val newValue = "a" - storeForUser(newValue, userId) - - assertThat(specs).isEqualTo(newValue.toSet()) - } + underTest = AutoAddSettingRepository(userAutoAddRepositoryFactory) + } @Test fun tilesForCorrectUsers() = testScope.runTest { - val tilesFromUser0 by collectLastValue(underTest.autoAddedTiles(0)) - val tilesFromUser1 by collectLastValue(underTest.autoAddedTiles(1)) - val user0Tiles = "a" val user1Tiles = "custom(b/c)" storeForUser(user0Tiles, 0) storeForUser(user1Tiles, 1) + val tilesFromUser0 by collectLastValue(underTest.autoAddedTiles(0)) + val tilesFromUser1 by collectLastValue(underTest.autoAddedTiles(1)) + runCurrent() - assertThat(tilesFromUser0).isEqualTo(user0Tiles.toSet()) - assertThat(tilesFromUser1).isEqualTo(user1Tiles.toSet()) - } - - @Test - fun noInvalidTileSpecs() = - testScope.runTest { - val userId = 0 - val tiles by collectLastValue(underTest.autoAddedTiles(userId)) - - val specs = "d,custom(bad)" - storeForUser(specs, userId) - - assertThat(tiles).isEqualTo("d".toSet()) - } - - @Test - fun markAdded() = - testScope.runTest { - val userId = 0 - val specs = mutableSetOf(TileSpec.create("a")) - underTest.markTileAdded(userId, TileSpec.create("a")) - - assertThat(loadForUser(userId).toSet()).containsExactlyElementsIn(specs) - - specs.add(TileSpec.create("b")) - underTest.markTileAdded(userId, TileSpec.create("b")) - - assertThat(loadForUser(userId).toSet()).containsExactlyElementsIn(specs) + assertThat(tilesFromUser0).isEqualTo(user0Tiles.toTilesSet()) + assertThat(tilesFromUser1).isEqualTo(user1Tiles.toTilesSet()) } @Test fun markAdded_multipleUsers() = testScope.runTest { - underTest.markTileAdded(userId = 1, TileSpec.create("a")) - - assertThat(loadForUser(0).toSet()).isEmpty() - assertThat(loadForUser(1).toSet()) - .containsExactlyElementsIn(setOf(TileSpec.create("a"))) - } - - @Test - fun markAdded_Invalid_noop() = - testScope.runTest { - val userId = 0 - underTest.markTileAdded(userId, TileSpec.Invalid) - - assertThat(loadForUser(userId).toSet()).isEmpty() - } - - @Test - fun unmarkAdded() = - testScope.runTest { - val userId = 0 - val specs = "a,custom(b/c)" - storeForUser(specs, userId) + val tilesFromUser0 by collectLastValue(underTest.autoAddedTiles(0)) + val tilesFromUser1 by collectLastValue(underTest.autoAddedTiles(1)) + runCurrent() - underTest.unmarkTileAdded(userId, TileSpec.create("a")) + underTest.markTileAdded(userId = 1, TileSpec.create("a")) - assertThat(loadForUser(userId).toSet()) - .containsExactlyElementsIn(setOf(TileSpec.create("custom(b/c)"))) + assertThat(tilesFromUser0).isEmpty() + assertThat(tilesFromUser1).containsExactlyElementsIn(setOf(TileSpec.create("a"))) } @Test @@ -159,33 +104,23 @@ class AutoAddSettingsRepositoryTest : SysuiTestCase() { val specs = "a,b" storeForUser(specs, 0) storeForUser(specs, 1) + val tilesFromUser0 by collectLastValue(underTest.autoAddedTiles(0)) + val tilesFromUser1 by collectLastValue(underTest.autoAddedTiles(1)) + runCurrent() underTest.unmarkTileAdded(1, TileSpec.create("a")) - assertThat(loadForUser(0).toSet()).isEqualTo(specs.toSet()) - assertThat(loadForUser(1).toSet()).isEqualTo(setOf(TileSpec.create("b"))) + assertThat(tilesFromUser0).isEqualTo(specs.toTilesSet()) + assertThat(tilesFromUser1).isEqualTo(setOf(TileSpec.create("b"))) } private fun storeForUser(specs: String, userId: Int) { secureSettings.putStringForUser(SETTING, specs, userId) } - private fun loadForUser(userId: Int): String { - return secureSettings.getStringForUser(SETTING, userId) ?: "" - } - companion object { private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES - private const val DELIMITER = "," - fun Set<TileSpec>.toSeparatedString() = joinToString(DELIMITER, transform = TileSpec::spec) - - fun String.toSet(): Set<TileSpec> { - return if (isNullOrBlank()) { - emptySet() - } else { - split(DELIMITER).map(TileSpec::create).toSet() - } - } + private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt index 1c28e4c022a6..08adebb87b1b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -23,14 +23,12 @@ import com.android.systemui.res.R import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.qs.QSHost import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger import com.android.systemui.retail.data.repository.FakeRetailModeRepository import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -49,9 +47,28 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { private lateinit var secureSettings: FakeSettings private lateinit var retailModeRepository: FakeRetailModeRepository + private val defaultTilesRepository = + object : DefaultTilesRepository { + override val defaultTiles: List<TileSpec> + get() = DEFAULT_TILES.toTileSpecs() + } @Mock private lateinit var logger: QSPipelineLogger + private val userTileSpecRepositoryFactory = + object : UserTileSpecRepository.Factory { + override fun create(userId: Int): UserTileSpecRepository { + return UserTileSpecRepository( + userId, + defaultTilesRepository, + secureSettings, + logger, + testScope.backgroundScope, + testDispatcher, + ) + } + } + private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -66,293 +83,85 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { retailModeRepository.setRetailMode(false) with(context.orCreateTestableResources) { - addOverride(R.string.quick_settings_tiles_default, DEFAULT_TILES) addOverride(R.string.quick_settings_tiles_retail_mode, RETAIL_TILES) } underTest = TileSpecSettingsRepository( - secureSettings, context.resources, logger, retailModeRepository, - testDispatcher, + userTileSpecRepositoryFactory ) } @Test - fun emptySetting_usesDefaultValue() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - assertThat(tiles).isEqualTo(getDefaultTileSpecs()) - } - - @Test - fun changeInSettings_changesValue() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - storeTilesForUser("a", 0) - assertThat(tiles).isEqualTo(listOf(TileSpec.create("a"))) - - storeTilesForUser("a,custom(b/c)", 0) - assertThat(tiles) - .isEqualTo(listOf(TileSpec.create("a"), TileSpec.create("custom(b/c)"))) - } - - @Test fun tilesForCorrectUsers() = testScope.runTest { - val tilesFromUser0 by collectLastValue(underTest.tilesSpecs(0)) - val tilesFromUser1 by collectLastValue(underTest.tilesSpecs(1)) - val user0Tiles = "a" val user1Tiles = "custom(b/c)" storeTilesForUser(user0Tiles, 0) storeTilesForUser(user1Tiles, 1) + val tilesFromUser0 by collectLastValue(underTest.tilesSpecs(0)) + val tilesFromUser1 by collectLastValue(underTest.tilesSpecs(1)) + assertThat(tilesFromUser0).isEqualTo(user0Tiles.toTileSpecs()) assertThat(tilesFromUser1).isEqualTo(user1Tiles.toTileSpecs()) } @Test - fun invalidTilesAreNotPresent() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - val specs = "d,custom(bad)" - storeTilesForUser(specs, 0) - - assertThat(tiles).isEqualTo(specs.toTileSpecs().filter { it != TileSpec.Invalid }) - } - - @Test - fun noValidTiles_defaultSet() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - storeTilesForUser("custom(bad),custom()", 0) - - assertThat(tiles).isEqualTo(getDefaultTileSpecs()) - } - - @Test - fun addTileAtEnd() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - storeTilesForUser("a", 0) - - underTest.addTile(userId = 0, TileSpec.create("b")) - - val expected = "a,b" - assertThat(loadTilesForUser(0)).isEqualTo(expected) - assertThat(tiles).isEqualTo(expected.toTileSpecs()) - } - - @Test - fun addTileAtPosition() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - storeTilesForUser("a,custom(b/c)", 0) - - underTest.addTile(userId = 0, TileSpec.create("d"), position = 1) - - val expected = "a,d,custom(b/c)" - assertThat(loadTilesForUser(0)).isEqualTo(expected) - assertThat(tiles).isEqualTo(expected.toTileSpecs()) - } - - @Test - fun addInvalidTile_noop() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - val specs = "a,custom(b/c)" - storeTilesForUser(specs, 0) - - underTest.addTile(userId = 0, TileSpec.Invalid) - - assertThat(loadTilesForUser(0)).isEqualTo(specs) - assertThat(tiles).isEqualTo(specs.toTileSpecs()) - } - - @Test - fun addTileAtPosition_tooLarge_addedAtEnd() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - val specs = "a,custom(b/c)" - storeTilesForUser(specs, 0) - - underTest.addTile(userId = 0, TileSpec.create("d"), position = 100) - - val expected = "a,custom(b/c),d" - assertThat(loadTilesForUser(0)).isEqualTo(expected) - assertThat(tiles).isEqualTo(expected.toTileSpecs()) - } - - @Test fun addTileForOtherUser_addedInThatUser() = testScope.runTest { - val tilesUser0 by collectLastValue(underTest.tilesSpecs(0)) - val tilesUser1 by collectLastValue(underTest.tilesSpecs(1)) - storeTilesForUser("a", 0) storeTilesForUser("b", 1) + val tilesUser0 by collectLastValue(underTest.tilesSpecs(0)) + val tilesUser1 by collectLastValue(underTest.tilesSpecs(1)) + runCurrent() underTest.addTile(userId = 1, TileSpec.create("c")) - assertThat(loadTilesForUser(0)).isEqualTo("a") assertThat(tilesUser0).isEqualTo("a".toTileSpecs()) - assertThat(loadTilesForUser(1)).isEqualTo("b,c") + assertThat(loadTilesForUser(0)).isEqualTo("a") assertThat(tilesUser1).isEqualTo("b,c".toTileSpecs()) - } - - @Test - fun removeTiles() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - storeTilesForUser("a,b", 0) - - underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"))) - - assertThat(loadTilesForUser(0)).isEqualTo("b") - assertThat(tiles).isEqualTo("b".toTileSpecs()) - } - - @Test - fun removeTilesNotThere_noop() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - val specs = "a,b" - storeTilesForUser(specs, 0) - - underTest.removeTiles(userId = 0, listOf(TileSpec.create("c"))) - - assertThat(loadTilesForUser(0)).isEqualTo(specs) - assertThat(tiles).isEqualTo(specs.toTileSpecs()) - } - - @Test - fun removeInvalidTile_noop() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - val specs = "a,b" - storeTilesForUser(specs, 0) - - underTest.removeTiles(userId = 0, listOf(TileSpec.Invalid)) - - assertThat(loadTilesForUser(0)).isEqualTo(specs) - assertThat(tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTilesForUser(1)).isEqualTo("b,c") } @Test fun removeTileFromSecondaryUser_removedOnlyInCorrectUser() = testScope.runTest { - val user0Tiles by collectLastValue(underTest.tilesSpecs(0)) - val user1Tiles by collectLastValue(underTest.tilesSpecs(1)) - val specs = "a,b" storeTilesForUser(specs, 0) storeTilesForUser(specs, 1) + val user0Tiles by collectLastValue(underTest.tilesSpecs(0)) + val user1Tiles by collectLastValue(underTest.tilesSpecs(1)) + runCurrent() underTest.removeTiles(userId = 1, listOf(TileSpec.create("a"))) - assertThat(loadTilesForUser(0)).isEqualTo(specs) assertThat(user0Tiles).isEqualTo(specs.toTileSpecs()) - assertThat(loadTilesForUser(1)).isEqualTo("b") - assertThat(user1Tiles).isEqualTo("b".toTileSpecs()) - } - - @Test - fun removeMultipleTiles() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - storeTilesForUser("a,b,c,d", 0) - - underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"), TileSpec.create("c"))) - - assertThat(loadTilesForUser(0)).isEqualTo("b,d") - assertThat(tiles).isEqualTo("b,d".toTileSpecs()) - } - - @Test - fun changeTiles() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - val specs = "a,custom(b/c)" - - underTest.setTiles(userId = 0, specs.toTileSpecs()) - - assertThat(loadTilesForUser(0)).isEqualTo(specs) - assertThat(tiles).isEqualTo(specs.toTileSpecs()) - } - - @Test - fun changeTiles_ignoresInvalid() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - val specs = "a,custom(b/c)" - - underTest.setTiles(userId = 0, listOf(TileSpec.Invalid) + specs.toTileSpecs()) - assertThat(loadTilesForUser(0)).isEqualTo(specs) - assertThat(tiles).isEqualTo(specs.toTileSpecs()) - } - - @Test - fun changeTiles_empty_noChanges() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - underTest.setTiles(userId = 0, emptyList()) - - assertThat(loadTilesForUser(0)).isNull() - assertThat(tiles).isEqualTo(getDefaultTileSpecs()) + assertThat(user1Tiles).isEqualTo("b".toTileSpecs()) + assertThat(loadTilesForUser(1)).isEqualTo("b") } @Test fun changeTiles_forCorrectUser() = testScope.runTest { - val user0Tiles by collectLastValue(underTest.tilesSpecs(0)) - val user1Tiles by collectLastValue(underTest.tilesSpecs(1)) - val specs = "a" storeTilesForUser(specs, 0) storeTilesForUser(specs, 1) + val user0Tiles by collectLastValue(underTest.tilesSpecs(0)) + val user1Tiles by collectLastValue(underTest.tilesSpecs(1)) + runCurrent() underTest.setTiles(userId = 1, "b".toTileSpecs()) - assertThat(loadTilesForUser(0)).isEqualTo("a") assertThat(user0Tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTilesForUser(0)).isEqualTo("a") - assertThat(loadTilesForUser(1)).isEqualTo("b") assertThat(user1Tiles).isEqualTo("b".toTileSpecs()) - } - - @Test - fun multipleConcurrentRemovals_bothRemoved() = - testScope.runTest { - val tiles by collectLastValue(underTest.tilesSpecs(0)) - - val specs = "a,b,c" - storeTilesForUser(specs, 0) - - coroutineScope { - underTest.removeTiles(userId = 0, listOf(TileSpec.create("c"))) - underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"))) - } - - assertThat(loadTilesForUser(0)).isEqualTo("b") - assertThat(tiles).isEqualTo("b".toTileSpecs()) + assertThat(loadTilesForUser(1)).isEqualTo("b") } @Test @@ -361,6 +170,7 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { retailModeRepository.setRetailMode(true) val tiles by collectLastValue(underTest.tilesSpecs(0)) + runCurrent() assertThat(tiles).isEqualTo(RETAIL_TILES.toTileSpecs()) } @@ -369,25 +179,13 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { fun retailMode_cannotModifyTiles() = testScope.runTest { retailModeRepository.setRetailMode(true) - - underTest.setTiles(0, DEFAULT_TILES.toTileSpecs()) - - assertThat(loadTilesForUser(0)).isNull() - } - - @Test - fun emptyTilesReplacedByDefaultInSettings() = - testScope.runTest { val tiles by collectLastValue(underTest.tilesSpecs(0)) runCurrent() - assertThat(loadTilesForUser(0)) - .isEqualTo(getDefaultTileSpecs().map { it.spec }.joinToString(",")) - } + underTest.setTiles(0, listOf(TileSpec.create("a"))) - private fun getDefaultTileSpecs(): List<TileSpec> { - return QSHost.getDefaultSpecs(context.resources).map(TileSpec::create) - } + assertThat(loadTilesForUser(0)).isEqualTo(DEFAULT_TILES) + } private fun TestScope.storeTilesForUser(specs: String, forUser: Int) { secureSettings.putStringForUser(SETTING, specs, forUser) @@ -403,8 +201,6 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() { private const val RETAIL_TILES = "d" private const val SETTING = Settings.Secure.QS_TILES - private fun String.toTileSpecs(): List<TileSpec> { - return split(",").map(TileSpec::create) - } + private fun String.toTileSpecs() = TilesSettingConverter.toTilesList(this) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt index 599ca14ef756..20876237b476 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt @@ -16,73 +16,85 @@ class TilesSettingConverterTest : SysuiTestCase() { @Test fun toTilesList_correctContentAndOrdering() { - val specString = listOf( - "c", - "b", - "custom(x/y)", - "d", - ).joinToString(DELIMITER) + val specString = + listOf( + "c", + "b", + "custom(x/y)", + "d", + ) + .joinToString(DELIMITER) - val expected = listOf( + val expected = + listOf( TileSpec.create("c"), TileSpec.create("b"), TileSpec.create("custom(x/y)"), TileSpec.create("d"), - ) + ) assertThat(TilesSettingConverter.toTilesList(specString)).isEqualTo(expected) } @Test fun toTilesList_removesInvalid() { - val specString = listOf( - "a", - "", - "b", - ).joinToString(DELIMITER) + val specString = + listOf( + "a", + "", + "b", + ) + .joinToString(DELIMITER) assertThat(TileSpec.create("")).isEqualTo(TileSpec.Invalid) - val expected = listOf( + val expected = + listOf( TileSpec.create("a"), TileSpec.create("b"), - ) + ) assertThat(TilesSettingConverter.toTilesList(specString)).isEqualTo(expected) } @Test fun toTilesSet_correctContent() { - val specString = listOf( - "c", - "b", - "custom(x/y)", - "d", - ).joinToString(DELIMITER) + val specString = + listOf( + "c", + "b", + "custom(x/y)", + "d", + ) + .joinToString(DELIMITER) - val expected = setOf( + val expected = + setOf( TileSpec.create("c"), TileSpec.create("b"), TileSpec.create("custom(x/y)"), TileSpec.create("d"), - ) + ) assertThat(TilesSettingConverter.toTilesSet(specString)).isEqualTo(expected) } @Test fun toTilesSet_removesInvalid() { - val specString = listOf( - "a", - "", - "b", - ).joinToString(DELIMITER) + val specString = + listOf( + "a", + "", + "b", + ) + .joinToString(DELIMITER) assertThat(TileSpec.create("")).isEqualTo(TileSpec.Invalid) - val expected = setOf( + val expected = + setOf( TileSpec.create("a"), TileSpec.create("b"), - ) + ) assertThat(TilesSettingConverter.toTilesSet(specString)).isEqualTo(expected) } companion object { private const val DELIMITER = "," } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt new file mode 100644 index 000000000000..81fd72b11227 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt @@ -0,0 +1,160 @@ +package com.android.systemui.qs.pipeline.data.repository + +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +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.util.settings.FakeSettings +import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RoboPilotTest +@SmallTest +@RunWith(AndroidJUnit4::class) +class UserAutoAddRepositoryTest : SysuiTestCase() { + private val secureSettings = FakeSettings() + + @Mock private lateinit var logger: QSPipelineLogger + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var underTest: UserAutoAddRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + UserAutoAddRepository( + USER, + secureSettings, + logger, + testScope.backgroundScope, + testDispatcher, + ) + } + + @Test + fun nonExistentSetting_emptySet() = + testScope.runTest { + val specs by collectLastValue(underTest.autoAdded()) + + assertThat(specs).isEmpty() + } + + @Test + fun settingsChange_noChanges() = + testScope.runTest { + val value = "a,custom(b/c)" + store(value) + val specs by collectLastValue(underTest.autoAdded()) + runCurrent() + + assertThat(specs).isEqualTo(value.toTilesSet()) + + val newValue = "a" + store(newValue) + + assertThat(specs).isEqualTo(value.toTilesSet()) + } + + @Test + fun noInvalidTileSpecs() = + testScope.runTest { + val specs = "d,custom(bad)" + store(specs) + val tiles by collectLastValue(underTest.autoAdded()) + runCurrent() + + assertThat(tiles).isEqualTo("d".toTilesSet()) + } + + @Test + fun markAdded() = + testScope.runTest { + val specs = mutableSetOf(TileSpec.create("a")) + val autoAdded by collectLastValue(underTest.autoAdded()) + runCurrent() + + underTest.markTileAdded(TileSpec.create("a")) + + assertThat(autoAdded).containsExactlyElementsIn(specs) + + specs.add(TileSpec.create("b")) + underTest.markTileAdded(TileSpec.create("b")) + + assertThat(autoAdded).containsExactlyElementsIn(specs) + } + + @Test + fun markAdded_Invalid_noop() = + testScope.runTest { + val autoAdded by collectLastValue(underTest.autoAdded()) + runCurrent() + + underTest.markTileAdded(TileSpec.Invalid) + + Truth.assertThat(autoAdded).isEmpty() + } + + @Test + fun unmarkAdded() = + testScope.runTest { + val specs = "a,custom(b/c)" + store(specs) + val autoAdded by collectLastValue(underTest.autoAdded()) + runCurrent() + + underTest.unmarkTileAdded(TileSpec.create("a")) + + assertThat(autoAdded).containsExactlyElementsIn(setOf(TileSpec.create("custom(b/c)"))) + } + + @Test + fun restore_addsRestoredTiles() = + testScope.runTest { + val specs = "a,b" + val restored = "b,c" + store(specs) + val autoAdded by collectLastValue(underTest.autoAdded()) + runCurrent() + + val restoreData = + RestoreData( + emptyList(), + restored.toTilesSet(), + USER, + ) + underTest.reconcileRestore(restoreData) + + assertThat(autoAdded).containsExactlyElementsIn("a,b,c".toTilesSet()) + } + + private fun store(specs: String) { + secureSettings.putStringForUser(SETTING, specs, USER) + } + + companion object { + private const val USER = 10 + private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES + + private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt new file mode 100644 index 000000000000..389580c1326b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt @@ -0,0 +1,351 @@ +package com.android.systemui.qs.pipeline.data.repository + +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +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.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RoboPilotTest +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class UserTileSpecRepositoryTest : SysuiTestCase() { + private val secureSettings = FakeSettings() + private val defaultTilesRepository = + object : DefaultTilesRepository { + override val defaultTiles: List<TileSpec> + get() = DEFAULT_TILES.toTileSpecs() + } + + @Mock private lateinit var logger: QSPipelineLogger + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var underTest: UserTileSpecRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + underTest = + UserTileSpecRepository( + USER, + defaultTilesRepository, + secureSettings, + logger, + testScope.backgroundScope, + testDispatcher, + ) + } + + @Test + fun emptySetting_usesDefaultValue() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles()) + assertThat(tiles).isEqualTo(getDefaultTileSpecs()) + } + + @Test + fun changeInSettings_valueDoesntChange() = + testScope.runTest { + storeTiles("a") + val tiles by collectLastValue(underTest.tiles()) + + assertThat(tiles).isEqualTo(listOf(TileSpec.create("a"))) + + storeTiles("a,custom(b/c)") + assertThat(tiles).isEqualTo(listOf(TileSpec.create("a"))) + } + + @Test + fun changeInSettings_settingIsRestored() = + testScope.runTest { + storeTiles("a") + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + storeTiles("a,custom(b/c)") + assertThat(loadTiles()).isEqualTo("a") + } + + @Test + fun invalidTilesAreNotPresent() = + testScope.runTest { + val specs = "d,custom(bad)" + storeTiles(specs) + + val tiles by collectLastValue(underTest.tiles()) + + assertThat(tiles).isEqualTo(specs.toTileSpecs().filter { it != TileSpec.Invalid }) + } + + @Test + fun noValidTiles_defaultSet() = + testScope.runTest { + storeTiles("custom(bad),custom()") + + val tiles by collectLastValue(underTest.tiles()) + + assertThat(tiles).isEqualTo(getDefaultTileSpecs()) + } + + /* + * Following tests are for the possible actions that can be performed to the list of tiles. + * In general, the tests follow this scheme: + * + * 1. Set starting tiles in Settings + * 2. Start collection of flows + * 3. Call `runCurrent` so all collectors are started (side effects) + * 4. Perform operation + * 5. Check that the flow contains the right value + * 6. Check that settings contains the right value. + */ + + @Test + fun addTileAtEnd() = + testScope.runTest { + storeTiles("a") + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.addTile(TileSpec.create("b")) + + val expected = "a,b" + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(expected) + } + + @Test + fun addTileAtPosition() = + testScope.runTest { + storeTiles("a,custom(b/c)") + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.addTile(TileSpec.create("d"), position = 1) + + val expected = "a,d,custom(b/c)" + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(expected) + } + + @Test + fun addInvalidTile_noop() = + testScope.runTest { + val specs = "a,custom(b/c)" + storeTiles(specs) + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.addTile(TileSpec.Invalid) + + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(specs) + } + + @Test + fun addTileAtPosition_tooLarge_addedAtEnd() = + testScope.runTest { + val specs = "a,custom(b/c)" + storeTiles(specs) + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.addTile(TileSpec.create("d"), position = 100) + + val expected = "a,custom(b/c),d" + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(expected) + } + + @Test + fun removeTiles() = + testScope.runTest { + storeTiles("a,b") + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.removeTiles(listOf(TileSpec.create("a"))) + + assertThat(tiles).isEqualTo("b".toTileSpecs()) + assertThat(loadTiles()).isEqualTo("b") + } + + @Test + fun removeTilesNotThere_noop() = + testScope.runTest { + val specs = "a,b" + storeTiles(specs) + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.removeTiles(listOf(TileSpec.create("c"))) + + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(specs) + } + + @Test + fun removeInvalidTile_noop() = + testScope.runTest { + val specs = "a,b" + storeTiles(specs) + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.removeTiles(listOf(TileSpec.Invalid)) + + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(specs) + } + + @Test + fun removeMultipleTiles() = + testScope.runTest { + storeTiles("a,b,c,d") + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.removeTiles(listOf(TileSpec.create("a"), TileSpec.create("c"))) + + assertThat(tiles).isEqualTo("b,d".toTileSpecs()) + assertThat(loadTiles()).isEqualTo("b,d") + } + + @Test + fun changeTiles() = + testScope.runTest { + val specs = "a,custom(b/c)" + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.setTiles(specs.toTileSpecs()) + + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(specs) + } + + @Test + fun changeTiles_ignoresInvalid() = + testScope.runTest { + val specs = "a,custom(b/c)" + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.setTiles(listOf(TileSpec.Invalid) + specs.toTileSpecs()) + + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(specs) + } + + @Test + fun changeTiles_empty_noChanges() = + testScope.runTest { + val specs = "a,b,c,d" + storeTiles(specs) + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + underTest.setTiles(emptyList()) + + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(specs) + } + + @Test + fun multipleConcurrentRemovals_bothRemoved() = + testScope.runTest { + val specs = "a,b,c" + storeTiles(specs) + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + coroutineScope { + underTest.removeTiles(listOf(TileSpec.create("c"))) + underTest.removeTiles(listOf(TileSpec.create("a"))) + } + + assertThat(tiles).isEqualTo("b".toTileSpecs()) + assertThat(loadTiles()).isEqualTo("b") + } + + @Test + fun emptyTilesReplacedByDefaultInSettings() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + assertThat(loadTiles()) + .isEqualTo(getDefaultTileSpecs().map { it.spec }.joinToString(",")) + } + + @Test + fun restoreDataIsProperlyReconciled() = + testScope.runTest { + // Tile b was just auto-added, so we should re-add it in position 1 + // Tile e was auto-added before, but the user had removed it (not in the restored set). + // It should not be re-added + val specsBeforeRestore = "a,b,c,d,e" + val restoredSpecs = "a,c,d,f" + val autoAddedBeforeRestore = "b,d" + val restoredAutoAdded = "d,e" + + storeTiles(specsBeforeRestore) + val tiles by collectLastValue(underTest.tiles()) + runCurrent() + + val restoreData = + RestoreData( + restoredSpecs.toTileSpecs(), + restoredAutoAdded.toTilesSet(), + USER, + ) + underTest.reconcileRestore(restoreData, autoAddedBeforeRestore.toTilesSet()) + runCurrent() + + val expected = "a,b,c,d,f" + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + assertThat(loadTiles()).isEqualTo(expected) + } + + private fun getDefaultTileSpecs(): List<TileSpec> { + return defaultTilesRepository.defaultTiles + } + + private fun TestScope.storeTiles(specs: String) { + secureSettings.putStringForUser(SETTING, specs, USER) + runCurrent() + } + + private fun loadTiles(): String? { + return secureSettings.getStringForUser(SETTING, USER) + } + + companion object { + private const val USER = 10 + private const val DEFAULT_TILES = "a,b,c" + private const val SETTING = Settings.Secure.QS_TILES + + private fun String.toTileSpecs() = TilesSettingConverter.toTilesList(this) + private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt new file mode 100644 index 000000000000..5630b9d3b292 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt @@ -0,0 +1,94 @@ +package com.android.systemui.qs.pipeline.domain.interactor + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.RoboPilotTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.qs.pipeline.data.model.RestoreData +import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository +import com.android.systemui.qs.pipeline.data.repository.FakeQSSettingsRestoredRepository +import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository +import com.android.systemui.qs.pipeline.data.repository.TilesSettingConverter +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +@RoboPilotTest +@RunWith(AndroidTestingRunner::class) +@SmallTest +class RestoreReconciliationInteractorTest : SysuiTestCase() { + + private val tileSpecRepository = FakeTileSpecRepository() + private val autoAddRepository = FakeAutoAddRepository() + + private val qsSettingsRestoredRepository = FakeQSSettingsRestoredRepository() + + private lateinit var underTest: RestoreReconciliationInteractor + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + RestoreReconciliationInteractor( + tileSpecRepository, + autoAddRepository, + qsSettingsRestoredRepository, + testScope.backgroundScope, + testDispatcher + ) + underTest.start() + } + + @Test + fun reconciliationInCorrectOrder_hascurrentAutoAdded() = + testScope.runTest { + val user = 10 + val tiles by collectLastValue(tileSpecRepository.tilesSpecs(user)) + val autoAdd by collectLastValue(autoAddRepository.autoAddedTiles(user)) + + // Tile b was just auto-added, so we should re-add it in position 1 + // Tile e was auto-added before, but the user had removed it (not in the restored set). + // It should not be re-added + val specsBeforeRestore = "a,b,c,d,e" + val restoredSpecs = "a,c,d,f" + val autoAddedBeforeRestore = "b,d" + val restoredAutoAdded = "d,e" + + val restoreData = + RestoreData( + restoredSpecs.toTilesList(), + restoredAutoAdded.toTilesSet(), + user, + ) + + autoAddedBeforeRestore.toTilesSet().forEach { + autoAddRepository.markTileAdded(user, it) + } + tileSpecRepository.setTiles(user, specsBeforeRestore.toTilesList()) + + qsSettingsRestoredRepository.onDataRestored(restoreData) + runCurrent() + + val expectedTiles = "a,b,c,d,f" + assertThat(tiles).isEqualTo(expectedTiles.toTilesList()) + + val expectedAutoAdd = "b,d,e" + assertThat(autoAdd).isEqualTo(expectedAutoAdd.toTilesSet()) + } + + companion object { + private fun String.toTilesList() = TilesSettingConverter.toTilesList(this) + private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt index 9ea079fc9c4b..57ad28289ebd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt @@ -16,15 +16,16 @@ package com.android.systemui.qs.pipeline.data.repository +import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.shared.TileSpec -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class FakeAutoAddRepository : AutoAddRepository { private val autoAddedTilesPerUser = mutableMapOf<Int, MutableStateFlow<Set<TileSpec>>>() - override fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> { + override suspend fun autoAddedTiles(userId: Int): StateFlow<Set<TileSpec>> { return getFlow(userId) } @@ -39,4 +40,8 @@ class FakeAutoAddRepository : AutoAddRepository { private fun getFlow(userId: Int): MutableStateFlow<Set<TileSpec>> = autoAddedTilesPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) } + + override suspend fun reconcileRestore(restoreData: RestoreData) { + with(getFlow(restoreData.userId)) { value = value + restoreData.restoredAutoAddedTiles } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeQSSettingsRestoredRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeQSSettingsRestoredRepository.kt new file mode 100644 index 000000000000..e0c2154f2aba --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeQSSettingsRestoredRepository.kt @@ -0,0 +1,16 @@ +package com.android.systemui.qs.pipeline.data.repository + +import com.android.systemui.qs.pipeline.data.model.RestoreData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow + +class FakeQSSettingsRestoredRepository : QSSettingsRestoredRepository { + private val _restoreData = MutableSharedFlow<RestoreData>() + + override val restoreData: Flow<RestoreData> + get() = _restoreData + + suspend fun onDataRestored(restoreData: RestoreData) { + _restoreData.emit(restoreData) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt index aa8dbe120ca4..ae4cf3afe671 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.pipeline.data.repository +import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END import com.android.systemui.qs.pipeline.shared.TileSpec import kotlinx.coroutines.flow.Flow @@ -26,7 +27,7 @@ class FakeTileSpecRepository : TileSpecRepository { private val tilesPerUser = mutableMapOf<Int, MutableStateFlow<List<TileSpec>>>() - override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> { + override suspend fun tilesSpecs(userId: Int): Flow<List<TileSpec>> { return getFlow(userId).asStateFlow() } @@ -57,4 +58,13 @@ class FakeTileSpecRepository : TileSpecRepository { private fun getFlow(userId: Int): MutableStateFlow<List<TileSpec>> = tilesPerUser.getOrPut(userId) { MutableStateFlow(emptyList()) } + + override suspend fun reconcileRestore( + restoreData: RestoreData, + currentAutoAdded: Set<TileSpec> + ) { + with(getFlow(restoreData.userId)) { + value = UserTileSpecRepository.reconcileTiles(value, currentAutoAdded, restoreData) + } + } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 40e9c1305f01..88eaafaee370 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -955,6 +955,17 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } } + public void setTiles(String tiles) { + enforceStatusBarOrShell(); + + if (mBar != null) { + try { + mBar.setQsTiles(tiles.split(",")); + } catch (RemoteException ex) { + } + } + } + public void clickTile(ComponentName component) { enforceStatusBarOrShell(); diff --git a/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java b/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java index 11a4976d945f..d6bf02fcdc47 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java +++ b/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java @@ -61,6 +61,8 @@ public class StatusBarShellCommand extends ShellCommand { return runAddTile(); case "remove-tile": return runRemoveTile(); + case "set-tiles": + return runSetTiles(); case "click-tile": return runClickTile(); case "check-support": @@ -105,6 +107,11 @@ public class StatusBarShellCommand extends ShellCommand { return 0; } + private int runSetTiles() throws RemoteException { + mInterface.setTiles(getNextArgRequired()); + return 0; + } + private int runClickTile() throws RemoteException { mInterface.clickTile(ComponentName.unflattenFromString(getNextArgRequired())); return 0; @@ -242,6 +249,9 @@ public class StatusBarShellCommand extends ShellCommand { pw.println(" remove-tile COMPONENT"); pw.println(" Remove a TileService of the specified component"); pw.println(""); + pw.println(" set-tiles LIST-OF-TILES"); + pw.println(" Sets the list of tiles as the current Quick Settings tiles"); + pw.println(""); pw.println(" click-tile COMPONENT"); pw.println(" Click on a TileService of the specified component"); pw.println(""); |