summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBar.aidl1
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt101
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/DefaultTilesRepository.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt158
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverter.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepository.kt186
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt252
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractor.kt53
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt141
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt290
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt74
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt160
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt351
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt94
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt9
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeQSSettingsRestoredRepository.kt16
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt12
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerService.java11
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarShellCommand.java10
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("");