Loading packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +1 −3 Original line number Diff line number Diff line Loading @@ -65,14 +65,12 @@ import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.settings.DisplayTracker; import dagger.Lazy; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Inject; import dagger.Lazy; public class CustomTile extends QSTileImpl<State> implements TileChangeListener { public static final String PREFIX = "custom("; Loading packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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.InstalledTilesComponentRepository import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepositoryImpl import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor Loading @@ -44,6 +46,11 @@ abstract class QSPipelineModule { impl: CurrentTilesInteractorImpl ): CurrentTilesInteractor @Binds abstract fun provideInstalledTilesPackageRepository( impl: InstalledTilesComponentRepositoryImpl ): InstalledTilesComponentRepository @Binds @IntoMap @ClassKey(PrototypeCoreStartable::class) Loading packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt 0 → 100644 +109 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.qs.pipeline.data.repository import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE import android.annotation.WorkerThread import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags import android.os.UserHandle import android.service.quicksettings.TileService import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.kotlin.isComponentActuallyEnabled 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 interface InstalledTilesComponentRepository { fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> } @SysUISingleton class InstalledTilesComponentRepositoryImpl @Inject constructor( @Application private val applicationContext: Context, private val packageManager: PackageManager, @Background private val backgroundDispatcher: CoroutineDispatcher, ) : InstalledTilesComponentRepository { override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> = conflatedCallbackFlow { val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { trySend(Unit) } } applicationContext.registerReceiverAsUser( receiver, UserHandle.of(userId), INTENT_FILTER, /* broadcastPermission = */ null, /* scheduler = */ null ) awaitClose { applicationContext.unregisterReceiver(receiver) } } .onStart { emit(Unit) } .map { reloadComponents(userId) } .distinctUntilChanged() .flowOn(backgroundDispatcher) @WorkerThread private fun reloadComponents(userId: Int): Set<ComponentName> { return packageManager .queryIntentServicesAsUser(INTENT, FLAGS, userId) .mapNotNull { it.serviceInfo } .filter { it.permission == BIND_QUICK_SETTINGS_TILE } .filter { packageManager.isComponentActuallyEnabled(it) } .mapTo(mutableSetOf()) { it.componentName } } companion object { private val INTENT_FILTER = IntentFilter().apply { addAction(Intent.ACTION_PACKAGE_ADDED) addAction(Intent.ACTION_PACKAGE_CHANGED) addAction(Intent.ACTION_PACKAGE_REMOVED) addAction(Intent.ACTION_PACKAGE_REPLACED) addDataScheme("package") } private val INTENT = Intent(TileService.ACTION_QS_TILE) private val FLAGS = ResolveInfoFlags.of( (PackageManager.GET_SERVICES or PackageManager.MATCH_DIRECT_BOOT_AWARE or PackageManager.MATCH_DIRECT_BOOT_UNAWARE) .toLong() ) } } packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt +32 −25 Original line number Diff line number Diff line Loading @@ -42,6 +42,8 @@ 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. */ Loading Loading @@ -104,6 +106,8 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, ) : TileSpecRepository { private val mutex = Mutex() private val retailModeTiles by lazy { resources .getString(R.string.quick_settings_tiles_retail_mode) Loading Loading @@ -145,7 +149,8 @@ constructor( .flowOn(backgroundDispatcher) } override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) { override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) = mutex.withLock { if (tile == TileSpec.Invalid) { return } Loading @@ -160,7 +165,8 @@ constructor( } } override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) { override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) = mutex.withLock { if (tiles.all { it == TileSpec.Invalid }) { return } Loading @@ -170,7 +176,8 @@ constructor( } } override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) { override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) = mutex.withLock { val filtered = tiles.filter { it != TileSpec.Invalid } if (filtered.isNotEmpty()) { storeTiles(userId, filtered) Loading packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +158 −85 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.android.systemui.qs.external.CustomTileStatePersister import com.android.systemui.qs.external.TileLifecycleManager import com.android.systemui.qs.external.TileServiceKey import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import com.android.systemui.qs.pipeline.domain.model.TileModel import com.android.systemui.qs.pipeline.shared.TileSpec Loading @@ -52,6 +53,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn Loading Loading @@ -117,11 +120,13 @@ interface CurrentTilesInteractor : ProtoDumpable { * * Platform tiles will be kept between users, with a call to [QSTile.userSwitch] * * [CustomTile]s will only be destroyed if the user changes. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class CurrentTilesInteractorImpl @Inject constructor( private val tileSpecRepository: TileSpecRepository, private val installedTilesComponentRepository: InstalledTilesComponentRepository, private val userRepository: UserRepository, private val customTileStatePersister: CustomTileStatePersister, private val tileFactory: QSFactory, Loading @@ -141,7 +146,7 @@ constructor( override val currentTiles: StateFlow<List<TileModel>> = _currentSpecsAndTiles.asStateFlow() // This variable should only be accessed inside the collect of `startTileCollection`. private val specsToTiles = mutableMapOf<TileSpec, QSTile>() private val specsToTiles = mutableMapOf<TileSpec, TileOrNotInstalled>() private val currentUser = MutableStateFlow(userTracker.userId) override val userId = currentUser.asStateFlow() Loading @@ -149,6 +154,20 @@ constructor( private val _userContext = MutableStateFlow(userTracker.userContext) override val userContext = _userContext.asStateFlow() private val userAndTiles = currentUser .flatMapLatest { userId -> tileSpecRepository.tilesSpecs(userId).map { UserAndTiles(userId, it) } } .distinctUntilChanged() .pairwise(UserAndTiles(-1, emptyList())) .flowOn(backgroundDispatcher) private val installedPackagesWithTiles = currentUser.flatMapLatest { installedTilesComponentRepository.getInstalledTilesComponents(it) } init { if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { startTileCollection() Loading @@ -158,23 +177,33 @@ constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun startTileCollection() { scope.launch { userRepository.selectedUserInfo .flatMapLatest { user -> launch { userRepository.selectedUserInfo.collect { user -> currentUser.value = user.id _userContext.value = userTracker.userContext tileSpecRepository.tilesSpecs(user.id).map { user.id to it } } .distinctUntilChanged() .pairwise(-1 to emptyList()) .flowOn(backgroundDispatcher) .collect { (old, new) -> val newTileList = new.second val userChanged = old.first != new.first val newUser = new.first } launch(backgroundDispatcher) { userAndTiles .combine(installedPackagesWithTiles) { usersAndTiles, packages -> Data( usersAndTiles.previousValue, usersAndTiles.newValue, packages, ) } .collectLatest { val newTileList = it.newData.tiles val userChanged = it.oldData.userId != it.newData.userId val newUser = it.newData.userId val components = it.installedComponents // Destroy all tiles that are not in the new set specsToTiles .filter { it.key !in newTileList } .filter { it.key !in newTileList && it.value is TileOrNotInstalled.Tile } .forEach { entry -> logger.logTileDestroyed( entry.key, Loading @@ -185,13 +214,21 @@ constructor( QSPipelineLogger.TileDestroyedReason.TILE_REMOVED } ) entry.value.destroy() (entry.value as TileOrNotInstalled.Tile).tile.destroy() } // MutableMap will keep the insertion order val newTileMap = mutableMapOf<TileSpec, QSTile>() val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>() newTileList.forEach { tileSpec -> if (tileSpec !in newTileMap) { if ( tileSpec is TileSpec.CustomTileSpec && tileSpec.componentName !in components ) { newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled } else { // Create tile here will never try to create a CustomTile that // is not installed val newTile = if (tileSpec in specsToTiles) { processExistingTile( Loading @@ -205,7 +242,8 @@ constructor( createTile(tileSpec) } if (newTile != null) { newTileMap[tileSpec] = newTile newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile) } } } } Loading @@ -213,15 +251,26 @@ constructor( val resolvedSpecs = newTileMap.keys.toList() specsToTiles.clear() specsToTiles.putAll(newTileMap) _currentSpecsAndTiles.value = newTileMap.map { TileModel(it.key, it.value) } _currentSpecsAndTiles.value = newTileMap .filter { it.value is TileOrNotInstalled.Tile } .map { TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile) } logger.logTilesNotInstalled( newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys, newUser ) if (resolvedSpecs != newTileList) { // There were some tiles that couldn't be created. Change the value in the // There were some tiles that couldn't be created. Change the value in // the // repository launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) } } } } } } override fun addTile(spec: TileSpec, position: Int) { scope.launch { Loading Loading @@ -301,11 +350,15 @@ constructor( private fun processExistingTile( tileSpec: TileSpec, qsTile: QSTile, tileOrNotInstalled: TileOrNotInstalled, userChanged: Boolean, user: Int, ): QSTile? { return when { return when (tileOrNotInstalled) { is TileOrNotInstalled.NotInstalled -> null is TileOrNotInstalled.Tile -> { val qsTile = tileOrNotInstalled.tile when { !qsTile.isAvailable -> { logger.logTileDestroyed( tileSpec, Loading @@ -317,7 +370,8 @@ constructor( // Tile is in the current list of tiles and available. // We have a handful of different cases qsTile !is CustomTile -> { // The tile is not a custom tile. Make sure they are reset to the correct user // The tile is not a custom tile. Make sure they are reset to the correct // user if (userChanged) { qsTile.userSwitch(user) logger.logTileUserChanged(tileSpec, user) Loading @@ -340,3 +394,22 @@ constructor( } } } } private sealed interface TileOrNotInstalled { object NotInstalled : TileOrNotInstalled @JvmInline value class Tile(val tile: QSTile) : TileOrNotInstalled } private data class UserAndTiles( val userId: Int, val tiles: List<TileSpec>, ) private data class Data( val oldData: UserAndTiles, val newData: UserAndTiles, val installedComponents: Set<ComponentName>, ) } Loading
packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +1 −3 Original line number Diff line number Diff line Loading @@ -65,14 +65,12 @@ import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.settings.DisplayTracker; import dagger.Lazy; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Inject; import dagger.Lazy; public class CustomTile extends QSTileImpl<State> implements TileChangeListener { public static final String PREFIX = "custom("; Loading
packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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.InstalledTilesComponentRepository import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepositoryImpl import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor Loading @@ -44,6 +46,11 @@ abstract class QSPipelineModule { impl: CurrentTilesInteractorImpl ): CurrentTilesInteractor @Binds abstract fun provideInstalledTilesPackageRepository( impl: InstalledTilesComponentRepositoryImpl ): InstalledTilesComponentRepository @Binds @IntoMap @ClassKey(PrototypeCoreStartable::class) Loading
packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt 0 → 100644 +109 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.qs.pipeline.data.repository import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE import android.annotation.WorkerThread import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags import android.os.UserHandle import android.service.quicksettings.TileService import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.kotlin.isComponentActuallyEnabled 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 interface InstalledTilesComponentRepository { fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> } @SysUISingleton class InstalledTilesComponentRepositoryImpl @Inject constructor( @Application private val applicationContext: Context, private val packageManager: PackageManager, @Background private val backgroundDispatcher: CoroutineDispatcher, ) : InstalledTilesComponentRepository { override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> = conflatedCallbackFlow { val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { trySend(Unit) } } applicationContext.registerReceiverAsUser( receiver, UserHandle.of(userId), INTENT_FILTER, /* broadcastPermission = */ null, /* scheduler = */ null ) awaitClose { applicationContext.unregisterReceiver(receiver) } } .onStart { emit(Unit) } .map { reloadComponents(userId) } .distinctUntilChanged() .flowOn(backgroundDispatcher) @WorkerThread private fun reloadComponents(userId: Int): Set<ComponentName> { return packageManager .queryIntentServicesAsUser(INTENT, FLAGS, userId) .mapNotNull { it.serviceInfo } .filter { it.permission == BIND_QUICK_SETTINGS_TILE } .filter { packageManager.isComponentActuallyEnabled(it) } .mapTo(mutableSetOf()) { it.componentName } } companion object { private val INTENT_FILTER = IntentFilter().apply { addAction(Intent.ACTION_PACKAGE_ADDED) addAction(Intent.ACTION_PACKAGE_CHANGED) addAction(Intent.ACTION_PACKAGE_REMOVED) addAction(Intent.ACTION_PACKAGE_REPLACED) addDataScheme("package") } private val INTENT = Intent(TileService.ACTION_QS_TILE) private val FLAGS = ResolveInfoFlags.of( (PackageManager.GET_SERVICES or PackageManager.MATCH_DIRECT_BOOT_AWARE or PackageManager.MATCH_DIRECT_BOOT_UNAWARE) .toLong() ) } }
packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt +32 −25 Original line number Diff line number Diff line Loading @@ -42,6 +42,8 @@ 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. */ Loading Loading @@ -104,6 +106,8 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, ) : TileSpecRepository { private val mutex = Mutex() private val retailModeTiles by lazy { resources .getString(R.string.quick_settings_tiles_retail_mode) Loading Loading @@ -145,7 +149,8 @@ constructor( .flowOn(backgroundDispatcher) } override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) { override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) = mutex.withLock { if (tile == TileSpec.Invalid) { return } Loading @@ -160,7 +165,8 @@ constructor( } } override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) { override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) = mutex.withLock { if (tiles.all { it == TileSpec.Invalid }) { return } Loading @@ -170,7 +176,8 @@ constructor( } } override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) { override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) = mutex.withLock { val filtered = tiles.filter { it != TileSpec.Invalid } if (filtered.isNotEmpty()) { storeTiles(userId, filtered) Loading
packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +158 −85 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.android.systemui.qs.external.CustomTileStatePersister import com.android.systemui.qs.external.TileLifecycleManager import com.android.systemui.qs.external.TileServiceKey import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository import com.android.systemui.qs.pipeline.domain.model.TileModel import com.android.systemui.qs.pipeline.shared.TileSpec Loading @@ -52,6 +53,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn Loading Loading @@ -117,11 +120,13 @@ interface CurrentTilesInteractor : ProtoDumpable { * * Platform tiles will be kept between users, with a call to [QSTile.userSwitch] * * [CustomTile]s will only be destroyed if the user changes. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class CurrentTilesInteractorImpl @Inject constructor( private val tileSpecRepository: TileSpecRepository, private val installedTilesComponentRepository: InstalledTilesComponentRepository, private val userRepository: UserRepository, private val customTileStatePersister: CustomTileStatePersister, private val tileFactory: QSFactory, Loading @@ -141,7 +146,7 @@ constructor( override val currentTiles: StateFlow<List<TileModel>> = _currentSpecsAndTiles.asStateFlow() // This variable should only be accessed inside the collect of `startTileCollection`. private val specsToTiles = mutableMapOf<TileSpec, QSTile>() private val specsToTiles = mutableMapOf<TileSpec, TileOrNotInstalled>() private val currentUser = MutableStateFlow(userTracker.userId) override val userId = currentUser.asStateFlow() Loading @@ -149,6 +154,20 @@ constructor( private val _userContext = MutableStateFlow(userTracker.userContext) override val userContext = _userContext.asStateFlow() private val userAndTiles = currentUser .flatMapLatest { userId -> tileSpecRepository.tilesSpecs(userId).map { UserAndTiles(userId, it) } } .distinctUntilChanged() .pairwise(UserAndTiles(-1, emptyList())) .flowOn(backgroundDispatcher) private val installedPackagesWithTiles = currentUser.flatMapLatest { installedTilesComponentRepository.getInstalledTilesComponents(it) } init { if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { startTileCollection() Loading @@ -158,23 +177,33 @@ constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun startTileCollection() { scope.launch { userRepository.selectedUserInfo .flatMapLatest { user -> launch { userRepository.selectedUserInfo.collect { user -> currentUser.value = user.id _userContext.value = userTracker.userContext tileSpecRepository.tilesSpecs(user.id).map { user.id to it } } .distinctUntilChanged() .pairwise(-1 to emptyList()) .flowOn(backgroundDispatcher) .collect { (old, new) -> val newTileList = new.second val userChanged = old.first != new.first val newUser = new.first } launch(backgroundDispatcher) { userAndTiles .combine(installedPackagesWithTiles) { usersAndTiles, packages -> Data( usersAndTiles.previousValue, usersAndTiles.newValue, packages, ) } .collectLatest { val newTileList = it.newData.tiles val userChanged = it.oldData.userId != it.newData.userId val newUser = it.newData.userId val components = it.installedComponents // Destroy all tiles that are not in the new set specsToTiles .filter { it.key !in newTileList } .filter { it.key !in newTileList && it.value is TileOrNotInstalled.Tile } .forEach { entry -> logger.logTileDestroyed( entry.key, Loading @@ -185,13 +214,21 @@ constructor( QSPipelineLogger.TileDestroyedReason.TILE_REMOVED } ) entry.value.destroy() (entry.value as TileOrNotInstalled.Tile).tile.destroy() } // MutableMap will keep the insertion order val newTileMap = mutableMapOf<TileSpec, QSTile>() val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>() newTileList.forEach { tileSpec -> if (tileSpec !in newTileMap) { if ( tileSpec is TileSpec.CustomTileSpec && tileSpec.componentName !in components ) { newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled } else { // Create tile here will never try to create a CustomTile that // is not installed val newTile = if (tileSpec in specsToTiles) { processExistingTile( Loading @@ -205,7 +242,8 @@ constructor( createTile(tileSpec) } if (newTile != null) { newTileMap[tileSpec] = newTile newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile) } } } } Loading @@ -213,15 +251,26 @@ constructor( val resolvedSpecs = newTileMap.keys.toList() specsToTiles.clear() specsToTiles.putAll(newTileMap) _currentSpecsAndTiles.value = newTileMap.map { TileModel(it.key, it.value) } _currentSpecsAndTiles.value = newTileMap .filter { it.value is TileOrNotInstalled.Tile } .map { TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile) } logger.logTilesNotInstalled( newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys, newUser ) if (resolvedSpecs != newTileList) { // There were some tiles that couldn't be created. Change the value in the // There were some tiles that couldn't be created. Change the value in // the // repository launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) } } } } } } override fun addTile(spec: TileSpec, position: Int) { scope.launch { Loading Loading @@ -301,11 +350,15 @@ constructor( private fun processExistingTile( tileSpec: TileSpec, qsTile: QSTile, tileOrNotInstalled: TileOrNotInstalled, userChanged: Boolean, user: Int, ): QSTile? { return when { return when (tileOrNotInstalled) { is TileOrNotInstalled.NotInstalled -> null is TileOrNotInstalled.Tile -> { val qsTile = tileOrNotInstalled.tile when { !qsTile.isAvailable -> { logger.logTileDestroyed( tileSpec, Loading @@ -317,7 +370,8 @@ constructor( // Tile is in the current list of tiles and available. // We have a handful of different cases qsTile !is CustomTile -> { // The tile is not a custom tile. Make sure they are reset to the correct user // The tile is not a custom tile. Make sure they are reset to the correct // user if (userChanged) { qsTile.userSwitch(user) logger.logTileUserChanged(tileSpec, user) Loading @@ -340,3 +394,22 @@ constructor( } } } } private sealed interface TileOrNotInstalled { object NotInstalled : TileOrNotInstalled @JvmInline value class Tile(val tile: QSTile) : TileOrNotInstalled } private data class UserAndTiles( val userId: Int, val tiles: List<TileSpec>, ) private data class Data( val oldData: UserAndTiles, val newData: UserAndTiles, val installedComponents: Set<ComponentName>, ) }