Support restoring TileService before app install
Before, CustomTile were destroyed if the app was not installed (before that, they were kept as UNAVAILABLE, but that caused other issues). This was an issue when restoring from a backup, as the setting would include custom tiles that were restored before the app was installed and therefore immediately removed (and never brought back). With this change: * Do not create CustomTile that don't have a corresponding valid component. Instead, keep them in the setting and mark them as NotInstalled internally. * If the component later appears, actually create the tile, and send `onTileAdded`. Note that if a component for a TileService disappears (it's uninstalled), the tile will be automatically removed and would need to be manually added. This is existing behavior. Finally, in order to prevent race conditions when a package with multiple tiles is uninstalled, use a mutex whenever making changes to the setting. Fixes: 282003318 Test: atest com.android.systemui.qs Test: atest CtsTileServiceTestCases Test: manual, add custom tiles through adb to settings and later install app Test: atest android.host.systemui Flag: QS_PIPELINE_NEW_HOST Change-Id: I0a774969ad20611103b09ce4ace6175ad53350ce Change-Id: I8b48f45124928702fcdcf88484ccf3b501ea86ed
This commit is contained in:
committed by
Fabián Kozynski
parent
755737cf6e
commit
378447f694
@@ -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(";
|
||||
|
||||
@@ -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
|
||||
@@ -44,6 +46,11 @@ abstract class QSPipelineModule {
|
||||
impl: CurrentTilesInteractorImpl
|
||||
): CurrentTilesInteractor
|
||||
|
||||
@Binds
|
||||
abstract fun provideInstalledTilesPackageRepository(
|
||||
impl: InstalledTilesComponentRepositoryImpl
|
||||
): InstalledTilesComponentRepository
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(PrototypeCoreStartable::class)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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. */
|
||||
@@ -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)
|
||||
@@ -145,37 +149,40 @@ constructor(
|
||||
.flowOn(backgroundDispatcher)
|
||||
}
|
||||
|
||||
override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) {
|
||||
if (tile == TileSpec.Invalid) {
|
||||
return
|
||||
}
|
||||
val tilesList = loadTiles(userId).toMutableList()
|
||||
if (tile !in tilesList) {
|
||||
if (position < 0 || position >= tilesList.size) {
|
||||
tilesList.add(tile)
|
||||
} else {
|
||||
tilesList.add(position, tile)
|
||||
override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) =
|
||||
mutex.withLock {
|
||||
if (tile == TileSpec.Invalid) {
|
||||
return
|
||||
}
|
||||
val tilesList = loadTiles(userId).toMutableList()
|
||||
if (tile !in tilesList) {
|
||||
if (position < 0 || position >= tilesList.size) {
|
||||
tilesList.add(tile)
|
||||
} else {
|
||||
tilesList.add(position, tile)
|
||||
}
|
||||
storeTiles(userId, tilesList)
|
||||
}
|
||||
storeTiles(userId, tilesList)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) {
|
||||
if (tiles.all { it == TileSpec.Invalid }) {
|
||||
return
|
||||
override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) =
|
||||
mutex.withLock {
|
||||
if (tiles.all { it == TileSpec.Invalid }) {
|
||||
return
|
||||
}
|
||||
val tilesList = loadTiles(userId).toMutableList()
|
||||
if (tilesList.removeAll(tiles)) {
|
||||
storeTiles(userId, tilesList.toList())
|
||||
}
|
||||
}
|
||||
val tilesList = loadTiles(userId).toMutableList()
|
||||
if (tilesList.removeAll(tiles)) {
|
||||
storeTiles(userId, tilesList.toList())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) {
|
||||
val filtered = tiles.filter { it != TileSpec.Invalid }
|
||||
if (filtered.isNotEmpty()) {
|
||||
storeTiles(userId, filtered)
|
||||
override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) =
|
||||
mutex.withLock {
|
||||
val filtered = tiles.filter { it != TileSpec.Invalid }
|
||||
if (filtered.isNotEmpty()) {
|
||||
storeTiles(userId, filtered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadTiles(@UserIdInt forUser: Int): List<TileSpec> {
|
||||
return withContext(backgroundDispatcher) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -158,68 +177,98 @@ 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
|
||||
}
|
||||
|
||||
// Destroy all tiles that are not in the new set
|
||||
specsToTiles
|
||||
.filter { it.key !in newTileList }
|
||||
.forEach { entry ->
|
||||
logger.logTileDestroyed(
|
||||
entry.key,
|
||||
if (userChanged) {
|
||||
QSPipelineLogger.TileDestroyedReason
|
||||
.TILE_NOT_PRESENT_IN_NEW_USER
|
||||
} else {
|
||||
QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
|
||||
}
|
||||
)
|
||||
entry.value.destroy()
|
||||
}
|
||||
// MutableMap will keep the insertion order
|
||||
val newTileMap = mutableMapOf<TileSpec, QSTile>()
|
||||
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
|
||||
|
||||
newTileList.forEach { tileSpec ->
|
||||
if (tileSpec !in newTileMap) {
|
||||
val newTile =
|
||||
if (tileSpec in specsToTiles) {
|
||||
processExistingTile(
|
||||
tileSpec,
|
||||
specsToTiles.getValue(tileSpec),
|
||||
userChanged,
|
||||
newUser
|
||||
)
|
||||
?: createTile(tileSpec)
|
||||
// Destroy all tiles that are not in the new set
|
||||
specsToTiles
|
||||
.filter {
|
||||
it.key !in newTileList && it.value is TileOrNotInstalled.Tile
|
||||
}
|
||||
.forEach { entry ->
|
||||
logger.logTileDestroyed(
|
||||
entry.key,
|
||||
if (userChanged) {
|
||||
QSPipelineLogger.TileDestroyedReason
|
||||
.TILE_NOT_PRESENT_IN_NEW_USER
|
||||
} else {
|
||||
QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
|
||||
}
|
||||
)
|
||||
(entry.value as TileOrNotInstalled.Tile).tile.destroy()
|
||||
}
|
||||
// MutableMap will keep the insertion order
|
||||
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 {
|
||||
createTile(tileSpec)
|
||||
// Create tile here will never try to create a CustomTile that
|
||||
// is not installed
|
||||
val newTile =
|
||||
if (tileSpec in specsToTiles) {
|
||||
processExistingTile(
|
||||
tileSpec,
|
||||
specsToTiles.getValue(tileSpec),
|
||||
userChanged,
|
||||
newUser
|
||||
)
|
||||
?: createTile(tileSpec)
|
||||
} else {
|
||||
createTile(tileSpec)
|
||||
}
|
||||
if (newTile != null) {
|
||||
newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile)
|
||||
}
|
||||
}
|
||||
if (newTile != null) {
|
||||
newTileMap[tileSpec] = newTile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val resolvedSpecs = newTileMap.keys.toList()
|
||||
specsToTiles.clear()
|
||||
specsToTiles.putAll(newTileMap)
|
||||
_currentSpecsAndTiles.value = newTileMap.map { TileModel(it.key, it.value) }
|
||||
if (resolvedSpecs != newTileList) {
|
||||
// There were some tiles that couldn't be created. Change the value in the
|
||||
// repository
|
||||
launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
|
||||
val resolvedSpecs = newTileMap.keys.toList()
|
||||
specsToTiles.clear()
|
||||
specsToTiles.putAll(newTileMap)
|
||||
_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
|
||||
// repository
|
||||
launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,42 +350,66 @@ constructor(
|
||||
|
||||
private fun processExistingTile(
|
||||
tileSpec: TileSpec,
|
||||
qsTile: QSTile,
|
||||
tileOrNotInstalled: TileOrNotInstalled,
|
||||
userChanged: Boolean,
|
||||
user: Int,
|
||||
): QSTile? {
|
||||
return when {
|
||||
!qsTile.isAvailable -> {
|
||||
logger.logTileDestroyed(
|
||||
tileSpec,
|
||||
QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE
|
||||
)
|
||||
qsTile.destroy()
|
||||
null
|
||||
}
|
||||
// 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
|
||||
if (userChanged) {
|
||||
qsTile.userSwitch(user)
|
||||
logger.logTileUserChanged(tileSpec, user)
|
||||
return when (tileOrNotInstalled) {
|
||||
is TileOrNotInstalled.NotInstalled -> null
|
||||
is TileOrNotInstalled.Tile -> {
|
||||
val qsTile = tileOrNotInstalled.tile
|
||||
when {
|
||||
!qsTile.isAvailable -> {
|
||||
logger.logTileDestroyed(
|
||||
tileSpec,
|
||||
QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE
|
||||
)
|
||||
qsTile.destroy()
|
||||
null
|
||||
}
|
||||
// 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
|
||||
if (userChanged) {
|
||||
qsTile.userSwitch(user)
|
||||
logger.logTileUserChanged(tileSpec, user)
|
||||
}
|
||||
qsTile
|
||||
}
|
||||
qsTile.user == user -> {
|
||||
// The tile is a custom tile for the same user, just return it
|
||||
qsTile
|
||||
}
|
||||
else -> {
|
||||
// The tile is a custom tile and the user has changed. Destroy it
|
||||
qsTile.destroy()
|
||||
logger.logTileDestroyed(
|
||||
tileSpec,
|
||||
QSPipelineLogger.TileDestroyedReason.CUSTOM_TILE_USER_CHANGED
|
||||
)
|
||||
null
|
||||
}
|
||||
}
|
||||
qsTile
|
||||
}
|
||||
qsTile.user == user -> {
|
||||
// The tile is a custom tile for the same user, just return it
|
||||
qsTile
|
||||
}
|
||||
else -> {
|
||||
// The tile is a custom tile and the user has changed. Destroy it
|
||||
qsTile.destroy()
|
||||
logger.logTileDestroyed(
|
||||
tileSpec,
|
||||
QSPipelineLogger.TileDestroyedReason.CUSTOM_TILE_USER_CHANGED
|
||||
)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -124,6 +124,18 @@ constructor(
|
||||
tileListLogBuffer.log(TILE_LIST_TAG, LogLevel.DEBUG, {}, { "Using retail tiles" })
|
||||
}
|
||||
|
||||
fun logTilesNotInstalled(tiles: Collection<TileSpec>, user: Int) {
|
||||
tileListLogBuffer.log(
|
||||
TILE_LIST_TAG,
|
||||
LogLevel.DEBUG,
|
||||
{
|
||||
str1 = tiles.toString()
|
||||
int1 = user
|
||||
},
|
||||
{ "Tiles kept for not installed packages for user $int1: $str1" }
|
||||
)
|
||||
}
|
||||
|
||||
/** Reasons for destroying an existing tile. */
|
||||
enum class TileDestroyedReason(val readable: String) {
|
||||
TILE_REMOVED("Tile removed from current set"),
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.util.kotlin
|
||||
|
||||
import android.annotation.WorkerThread
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.android.systemui.util.Assert
|
||||
|
||||
@WorkerThread
|
||||
fun PackageManager.isComponentActuallyEnabled(componentInfo: ComponentInfo): Boolean {
|
||||
Assert.isNotMainThread()
|
||||
return when (getComponentEnabledSetting(componentInfo.componentName)) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> false
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> componentInfo.isEnabled
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* 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.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.ResolveInfoFlags
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.UserHandle
|
||||
import android.service.quicksettings.TileService
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.argThat
|
||||
import com.android.systemui.util.mockito.argumentCaptor
|
||||
import com.android.systemui.util.mockito.capture
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.mockito.nullable
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
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.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Captor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val testScope = TestScope(testDispatcher)
|
||||
|
||||
@Mock private lateinit var context: Context
|
||||
@Mock private lateinit var packageManager: PackageManager
|
||||
@Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
|
||||
|
||||
private lateinit var underTest: InstalledTilesComponentRepositoryImpl
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
// Use the default value set in the ServiceInfo
|
||||
whenever(packageManager.getComponentEnabledSetting(any()))
|
||||
.thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
|
||||
|
||||
// Return empty by default
|
||||
whenever(packageManager.queryIntentServicesAsUser(any(), any<ResolveInfoFlags>(), anyInt()))
|
||||
.thenReturn(emptyList())
|
||||
|
||||
underTest =
|
||||
InstalledTilesComponentRepositoryImpl(
|
||||
context,
|
||||
packageManager,
|
||||
testDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun registersAndUnregistersBroadcastReceiver() =
|
||||
testScope.runTest {
|
||||
val user = 10
|
||||
val job = launch { underTest.getInstalledTilesComponents(user).collect {} }
|
||||
runCurrent()
|
||||
|
||||
verify(context)
|
||||
.registerReceiverAsUser(
|
||||
capture(receiverCaptor),
|
||||
eq(UserHandle.of(user)),
|
||||
any(),
|
||||
nullable(),
|
||||
nullable(),
|
||||
)
|
||||
|
||||
verify(context, never()).unregisterReceiver(receiverCaptor.value)
|
||||
|
||||
job.cancel()
|
||||
runCurrent()
|
||||
verify(context).unregisterReceiver(receiverCaptor.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun intentFilterForCorrectActionsAndScheme() =
|
||||
testScope.runTest {
|
||||
val filterCaptor = argumentCaptor<IntentFilter>()
|
||||
|
||||
backgroundScope.launch { underTest.getInstalledTilesComponents(0).collect {} }
|
||||
runCurrent()
|
||||
|
||||
verify(context)
|
||||
.registerReceiverAsUser(
|
||||
any(),
|
||||
any(),
|
||||
capture(filterCaptor),
|
||||
nullable(),
|
||||
nullable(),
|
||||
)
|
||||
|
||||
with(filterCaptor.value) {
|
||||
assertThat(matchAction(Intent.ACTION_PACKAGE_CHANGED)).isTrue()
|
||||
assertThat(matchAction(Intent.ACTION_PACKAGE_ADDED)).isTrue()
|
||||
assertThat(matchAction(Intent.ACTION_PACKAGE_REMOVED)).isTrue()
|
||||
assertThat(matchAction(Intent.ACTION_PACKAGE_REPLACED)).isTrue()
|
||||
assertThat(countActions()).isEqualTo(4)
|
||||
|
||||
assertThat(hasDataScheme("package")).isTrue()
|
||||
assertThat(countDataSchemes()).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun componentsLoadedOnStart() =
|
||||
testScope.runTest {
|
||||
val userId = 0
|
||||
val resolveInfo =
|
||||
ResolveInfo(TEST_COMPONENT, hasPermission = true, defaultEnabled = true)
|
||||
whenever(
|
||||
packageManager.queryIntentServicesAsUser(
|
||||
matchIntent(),
|
||||
matchFlags(),
|
||||
eq(userId)
|
||||
)
|
||||
)
|
||||
.thenReturn(listOf(resolveInfo))
|
||||
|
||||
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
|
||||
|
||||
assertThat(componentNames).containsExactly(TEST_COMPONENT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun componentAdded_foundAfterBroadcast() =
|
||||
testScope.runTest {
|
||||
val userId = 0
|
||||
val resolveInfo =
|
||||
ResolveInfo(TEST_COMPONENT, hasPermission = true, defaultEnabled = true)
|
||||
|
||||
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
|
||||
assertThat(componentNames).isEmpty()
|
||||
|
||||
whenever(
|
||||
packageManager.queryIntentServicesAsUser(
|
||||
matchIntent(),
|
||||
matchFlags(),
|
||||
eq(userId)
|
||||
)
|
||||
)
|
||||
.thenReturn(listOf(resolveInfo))
|
||||
getRegisteredReceiver().onReceive(context, Intent(Intent.ACTION_PACKAGE_ADDED))
|
||||
|
||||
assertThat(componentNames).containsExactly(TEST_COMPONENT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun componentWithoutPermission_notValid() =
|
||||
testScope.runTest {
|
||||
val userId = 0
|
||||
val resolveInfo =
|
||||
ResolveInfo(TEST_COMPONENT, hasPermission = false, defaultEnabled = true)
|
||||
whenever(
|
||||
packageManager.queryIntentServicesAsUser(
|
||||
matchIntent(),
|
||||
matchFlags(),
|
||||
eq(userId)
|
||||
)
|
||||
)
|
||||
.thenReturn(listOf(resolveInfo))
|
||||
|
||||
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
|
||||
assertThat(componentNames).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun componentNotEnabled_notValid() =
|
||||
testScope.runTest {
|
||||
val userId = 0
|
||||
val resolveInfo =
|
||||
ResolveInfo(TEST_COMPONENT, hasPermission = true, defaultEnabled = false)
|
||||
whenever(
|
||||
packageManager.queryIntentServicesAsUser(
|
||||
matchIntent(),
|
||||
matchFlags(),
|
||||
eq(userId)
|
||||
)
|
||||
)
|
||||
.thenReturn(listOf(resolveInfo))
|
||||
|
||||
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
|
||||
assertThat(componentNames).isEmpty()
|
||||
}
|
||||
|
||||
private fun getRegisteredReceiver(): BroadcastReceiver {
|
||||
verify(context)
|
||||
.registerReceiverAsUser(
|
||||
capture(receiverCaptor),
|
||||
any(),
|
||||
any(),
|
||||
nullable(),
|
||||
nullable(),
|
||||
)
|
||||
|
||||
return receiverCaptor.value
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val INTENT = Intent(TileService.ACTION_QS_TILE)
|
||||
private val FLAGS =
|
||||
ResolveInfoFlags.of(
|
||||
(PackageManager.MATCH_DIRECT_BOOT_AWARE or
|
||||
PackageManager.MATCH_DIRECT_BOOT_UNAWARE or
|
||||
PackageManager.GET_SERVICES)
|
||||
.toLong()
|
||||
)
|
||||
private val PERMISSION = BIND_QUICK_SETTINGS_TILE
|
||||
|
||||
private val TEST_COMPONENT = ComponentName("pkg", "cls")
|
||||
|
||||
private fun matchFlags() =
|
||||
argThat<ResolveInfoFlags> { flags -> flags?.value == FLAGS.value }
|
||||
private fun matchIntent() = argThat<Intent> { intent -> intent.action == INTENT.action }
|
||||
|
||||
private fun ResolveInfo(
|
||||
componentName: ComponentName,
|
||||
hasPermission: Boolean,
|
||||
defaultEnabled: Boolean
|
||||
): ResolveInfo {
|
||||
val applicationInfo = ApplicationInfo().apply { enabled = true }
|
||||
val serviceInfo =
|
||||
ServiceInfo().apply {
|
||||
packageName = componentName.packageName
|
||||
name = componentName.className
|
||||
if (hasPermission) {
|
||||
permission = PERMISSION
|
||||
}
|
||||
enabled = defaultEnabled
|
||||
this.applicationInfo = applicationInfo
|
||||
}
|
||||
val resolveInfo = ResolveInfo()
|
||||
resolveInfo.serviceInfo = serviceInfo
|
||||
return resolveInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ 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.FakeCustomTileAddedRepository
|
||||
import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
|
||||
import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository
|
||||
import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
|
||||
import com.android.systemui.qs.pipeline.domain.model.TileModel
|
||||
@@ -73,6 +74,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
|
||||
|
||||
private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository()
|
||||
private val userRepository = FakeUserRepository()
|
||||
private val installedTilesPackageRepository = FakeInstalledTilesComponentRepository()
|
||||
private val tileFactory = FakeQSFactory(::tileCreator)
|
||||
private val customTileAddedRepository: CustomTileAddedRepository =
|
||||
FakeCustomTileAddedRepository()
|
||||
@@ -100,11 +102,13 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
|
||||
featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true)
|
||||
|
||||
userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
|
||||
|
||||
setUserTracker(0)
|
||||
|
||||
underTest =
|
||||
CurrentTilesInteractorImpl(
|
||||
tileSpecRepository = tileSpecRepository,
|
||||
installedTilesComponentRepository = installedTilesPackageRepository,
|
||||
userRepository = userRepository,
|
||||
customTileStatePersister = customTileStatePersister,
|
||||
tileFactory = tileFactory,
|
||||
@@ -609,6 +613,40 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
|
||||
assertThat((tileA as FakeQSTile).callbacks).containsExactly(callback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun packageNotInstalled_customTileNotVisible() =
|
||||
testScope.runTest(USER_INFO_0) {
|
||||
installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
|
||||
|
||||
val tiles by collectLastValue(underTest.currentTiles)
|
||||
|
||||
val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
|
||||
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
|
||||
|
||||
assertThat(tiles!!.size).isEqualTo(1)
|
||||
assertThat(tiles!![0].spec).isEqualTo(specs[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun packageInstalledLater_customTileAdded() =
|
||||
testScope.runTest(USER_INFO_0) {
|
||||
installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
|
||||
|
||||
val tiles by collectLastValue(underTest.currentTiles)
|
||||
val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC, TileSpec.create("b"))
|
||||
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
|
||||
|
||||
assertThat(tiles!!.size).isEqualTo(2)
|
||||
|
||||
installedTilesPackageRepository.setInstalledPackagesForUser(
|
||||
USER_INFO_0.id,
|
||||
setOf(TEST_COMPONENT)
|
||||
)
|
||||
|
||||
assertThat(tiles!!.size).isEqualTo(3)
|
||||
assertThat(tiles!![1].spec).isEqualTo(CUSTOM_TILE_SPEC)
|
||||
}
|
||||
|
||||
private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) {
|
||||
this.state = state
|
||||
this.label = label
|
||||
@@ -654,6 +692,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
|
||||
|
||||
private suspend fun switchUser(user: UserInfo) {
|
||||
setUserTracker(user.id)
|
||||
installedTilesPackageRepository.setInstalledPackagesForUser(user.id, setOf(TEST_COMPONENT))
|
||||
userRepository.setSelectedUserInfo(user)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.util.kotlin
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.junit.runners.Parameterized.Parameters
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(Parameterized::class)
|
||||
internal class PackageManagerExtComponentEnabledTest(private val testCase: TestCase) :
|
||||
SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var packageManager: PackageManager
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testComponentActuallyEnabled() {
|
||||
whenever(packageManager.getComponentEnabledSetting(TEST_COMPONENT))
|
||||
.thenReturn(testCase.componentEnabledSetting)
|
||||
val componentInfo =
|
||||
mock<ComponentInfo>() {
|
||||
whenever(isEnabled).thenReturn(testCase.componentIsEnabled)
|
||||
whenever(componentName).thenReturn(TEST_COMPONENT)
|
||||
}
|
||||
|
||||
assertThat(packageManager.isComponentActuallyEnabled(componentInfo))
|
||||
.isEqualTo(testCase.expected)
|
||||
}
|
||||
|
||||
internal data class TestCase(
|
||||
@PackageManager.EnabledState val componentEnabledSetting: Int,
|
||||
val componentIsEnabled: Boolean,
|
||||
val expected: Boolean,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "WHEN(" +
|
||||
"componentIsEnabled = $componentIsEnabled, " +
|
||||
"componentEnabledSetting = ${enabledStateToString()}) then " +
|
||||
"EXPECTED = $expected"
|
||||
}
|
||||
|
||||
private fun enabledStateToString() =
|
||||
when (componentEnabledSetting) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> "STATE_DEFAULT"
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> "STATE_DISABLED"
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED -> {
|
||||
"STATE_DISABLED_UNTIL_USED"
|
||||
}
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> "STATE_DISABLED_USER"
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> "STATE_ENABLED"
|
||||
else -> "INVALID STATE"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Parameters(name = "{0}") @JvmStatic fun data(): Collection<TestCase> = testData
|
||||
|
||||
private val testDataComponentIsEnabled =
|
||||
listOf(
|
||||
TestCase(
|
||||
componentEnabledSetting = PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
componentIsEnabled = true,
|
||||
expected = true,
|
||||
),
|
||||
TestCase(
|
||||
componentEnabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER,
|
||||
componentIsEnabled = true,
|
||||
expected = false,
|
||||
),
|
||||
TestCase(
|
||||
componentEnabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
componentIsEnabled = true,
|
||||
expected = false,
|
||||
),
|
||||
TestCase(
|
||||
componentEnabledSetting =
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
|
||||
componentIsEnabled = true,
|
||||
expected = false,
|
||||
),
|
||||
TestCase(
|
||||
componentEnabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
|
||||
componentIsEnabled = true,
|
||||
expected = true,
|
||||
),
|
||||
)
|
||||
|
||||
private val testDataComponentIsDisabled =
|
||||
listOf(
|
||||
TestCase(
|
||||
componentEnabledSetting = PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
componentIsEnabled = false,
|
||||
expected = true,
|
||||
),
|
||||
TestCase(
|
||||
componentEnabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER,
|
||||
componentIsEnabled = false,
|
||||
expected = false,
|
||||
),
|
||||
TestCase(
|
||||
componentEnabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
componentIsEnabled = false,
|
||||
expected = false,
|
||||
),
|
||||
TestCase(
|
||||
componentEnabledSetting =
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
|
||||
componentIsEnabled = false,
|
||||
expected = false,
|
||||
),
|
||||
TestCase(
|
||||
componentEnabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
|
||||
componentIsEnabled = false,
|
||||
expected = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val testData = testDataComponentIsDisabled + testDataComponentIsEnabled
|
||||
|
||||
private val TEST_COMPONENT = ComponentName("pkg", "cls")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.content.ComponentName
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class FakeInstalledTilesComponentRepository : InstalledTilesComponentRepository {
|
||||
|
||||
private val installedComponentsPerUser =
|
||||
mutableMapOf<Int, MutableStateFlow<Set<ComponentName>>>()
|
||||
|
||||
override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> {
|
||||
return getFlow(userId).asStateFlow()
|
||||
}
|
||||
|
||||
fun setInstalledPackagesForUser(userId: Int, components: Set<ComponentName>) {
|
||||
getFlow(userId).value = components
|
||||
}
|
||||
|
||||
private fun getFlow(userId: Int): MutableStateFlow<Set<ComponentName>> =
|
||||
installedComponentsPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) }
|
||||
}
|
||||
Reference in New Issue
Block a user