Merge "Support restoring TileService before app install" into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
3937472dd3
@@ -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