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.qs.tileimpl.QSTileImpl;
|
||||||
import com.android.systemui.settings.DisplayTracker;
|
import com.android.systemui.settings.DisplayTracker;
|
||||||
|
|
||||||
import dagger.Lazy;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
|
||||||
public class CustomTile extends QSTileImpl<State> implements TileChangeListener {
|
public class CustomTile extends QSTileImpl<State> implements TileChangeListener {
|
||||||
public static final String PREFIX = "custom(";
|
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.dagger.SysUISingleton
|
||||||
import com.android.systemui.log.LogBuffer
|
import com.android.systemui.log.LogBuffer
|
||||||
import com.android.systemui.log.LogBufferFactory
|
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.TileSpecRepository
|
||||||
import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository
|
import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository
|
||||||
import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
|
import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
|
||||||
@@ -44,6 +46,11 @@ abstract class QSPipelineModule {
|
|||||||
impl: CurrentTilesInteractorImpl
|
impl: CurrentTilesInteractorImpl
|
||||||
): CurrentTilesInteractor
|
): CurrentTilesInteractor
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun provideInstalledTilesPackageRepository(
|
||||||
|
impl: InstalledTilesComponentRepositoryImpl
|
||||||
|
): InstalledTilesComponentRepository
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ClassKey(PrototypeCoreStartable::class)
|
@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.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/** Repository that tracks the current tiles. */
|
/** Repository that tracks the current tiles. */
|
||||||
@@ -104,6 +106,8 @@ constructor(
|
|||||||
@Background private val backgroundDispatcher: CoroutineDispatcher,
|
@Background private val backgroundDispatcher: CoroutineDispatcher,
|
||||||
) : TileSpecRepository {
|
) : TileSpecRepository {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private val retailModeTiles by lazy {
|
private val retailModeTiles by lazy {
|
||||||
resources
|
resources
|
||||||
.getString(R.string.quick_settings_tiles_retail_mode)
|
.getString(R.string.quick_settings_tiles_retail_mode)
|
||||||
@@ -145,7 +149,8 @@ constructor(
|
|||||||
.flowOn(backgroundDispatcher)
|
.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) {
|
if (tile == TileSpec.Invalid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -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 }) {
|
if (tiles.all { it == TileSpec.Invalid }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -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 }
|
val filtered = tiles.filter { it != TileSpec.Invalid }
|
||||||
if (filtered.isNotEmpty()) {
|
if (filtered.isNotEmpty()) {
|
||||||
storeTiles(userId, filtered)
|
storeTiles(userId, filtered)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.android.systemui.qs.external.CustomTileStatePersister
|
|||||||
import com.android.systemui.qs.external.TileLifecycleManager
|
import com.android.systemui.qs.external.TileLifecycleManager
|
||||||
import com.android.systemui.qs.external.TileServiceKey
|
import com.android.systemui.qs.external.TileServiceKey
|
||||||
import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository
|
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.data.repository.TileSpecRepository
|
||||||
import com.android.systemui.qs.pipeline.domain.model.TileModel
|
import com.android.systemui.qs.pipeline.domain.model.TileModel
|
||||||
import com.android.systemui.qs.pipeline.shared.TileSpec
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOn
|
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]
|
* * Platform tiles will be kept between users, with a call to [QSTile.userSwitch]
|
||||||
* * [CustomTile]s will only be destroyed if the user changes.
|
* * [CustomTile]s will only be destroyed if the user changes.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
class CurrentTilesInteractorImpl
|
class CurrentTilesInteractorImpl
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val tileSpecRepository: TileSpecRepository,
|
private val tileSpecRepository: TileSpecRepository,
|
||||||
|
private val installedTilesComponentRepository: InstalledTilesComponentRepository,
|
||||||
private val userRepository: UserRepository,
|
private val userRepository: UserRepository,
|
||||||
private val customTileStatePersister: CustomTileStatePersister,
|
private val customTileStatePersister: CustomTileStatePersister,
|
||||||
private val tileFactory: QSFactory,
|
private val tileFactory: QSFactory,
|
||||||
@@ -141,7 +146,7 @@ constructor(
|
|||||||
override val currentTiles: StateFlow<List<TileModel>> = _currentSpecsAndTiles.asStateFlow()
|
override val currentTiles: StateFlow<List<TileModel>> = _currentSpecsAndTiles.asStateFlow()
|
||||||
|
|
||||||
// This variable should only be accessed inside the collect of `startTileCollection`.
|
// 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)
|
private val currentUser = MutableStateFlow(userTracker.userId)
|
||||||
override val userId = currentUser.asStateFlow()
|
override val userId = currentUser.asStateFlow()
|
||||||
@@ -149,6 +154,20 @@ constructor(
|
|||||||
private val _userContext = MutableStateFlow(userTracker.userContext)
|
private val _userContext = MutableStateFlow(userTracker.userContext)
|
||||||
override val userContext = _userContext.asStateFlow()
|
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 {
|
init {
|
||||||
if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
|
if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
|
||||||
startTileCollection()
|
startTileCollection()
|
||||||
@@ -158,23 +177,33 @@ constructor(
|
|||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun startTileCollection() {
|
private fun startTileCollection() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
userRepository.selectedUserInfo
|
launch {
|
||||||
.flatMapLatest { user ->
|
userRepository.selectedUserInfo.collect { user ->
|
||||||
currentUser.value = user.id
|
currentUser.value = user.id
|
||||||
_userContext.value = userTracker.userContext
|
_userContext.value = userTracker.userContext
|
||||||
tileSpecRepository.tilesSpecs(user.id).map { user.id to it }
|
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
}
|
||||||
.pairwise(-1 to emptyList())
|
|
||||||
.flowOn(backgroundDispatcher)
|
launch(backgroundDispatcher) {
|
||||||
.collect { (old, new) ->
|
userAndTiles
|
||||||
val newTileList = new.second
|
.combine(installedPackagesWithTiles) { usersAndTiles, packages ->
|
||||||
val userChanged = old.first != new.first
|
Data(
|
||||||
val newUser = new.first
|
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
|
// Destroy all tiles that are not in the new set
|
||||||
specsToTiles
|
specsToTiles
|
||||||
.filter { it.key !in newTileList }
|
.filter {
|
||||||
|
it.key !in newTileList && it.value is TileOrNotInstalled.Tile
|
||||||
|
}
|
||||||
.forEach { entry ->
|
.forEach { entry ->
|
||||||
logger.logTileDestroyed(
|
logger.logTileDestroyed(
|
||||||
entry.key,
|
entry.key,
|
||||||
@@ -185,13 +214,21 @@ constructor(
|
|||||||
QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
|
QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
entry.value.destroy()
|
(entry.value as TileOrNotInstalled.Tile).tile.destroy()
|
||||||
}
|
}
|
||||||
// MutableMap will keep the insertion order
|
// MutableMap will keep the insertion order
|
||||||
val newTileMap = mutableMapOf<TileSpec, QSTile>()
|
val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>()
|
||||||
|
|
||||||
newTileList.forEach { tileSpec ->
|
newTileList.forEach { tileSpec ->
|
||||||
if (tileSpec !in newTileMap) {
|
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 =
|
val newTile =
|
||||||
if (tileSpec in specsToTiles) {
|
if (tileSpec in specsToTiles) {
|
||||||
processExistingTile(
|
processExistingTile(
|
||||||
@@ -205,7 +242,8 @@ constructor(
|
|||||||
createTile(tileSpec)
|
createTile(tileSpec)
|
||||||
}
|
}
|
||||||
if (newTile != null) {
|
if (newTile != null) {
|
||||||
newTileMap[tileSpec] = newTile
|
newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,15 +251,26 @@ constructor(
|
|||||||
val resolvedSpecs = newTileMap.keys.toList()
|
val resolvedSpecs = newTileMap.keys.toList()
|
||||||
specsToTiles.clear()
|
specsToTiles.clear()
|
||||||
specsToTiles.putAll(newTileMap)
|
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) {
|
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
|
// repository
|
||||||
launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
|
launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun addTile(spec: TileSpec, position: Int) {
|
override fun addTile(spec: TileSpec, position: Int) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -301,11 +350,15 @@ constructor(
|
|||||||
|
|
||||||
private fun processExistingTile(
|
private fun processExistingTile(
|
||||||
tileSpec: TileSpec,
|
tileSpec: TileSpec,
|
||||||
qsTile: QSTile,
|
tileOrNotInstalled: TileOrNotInstalled,
|
||||||
userChanged: Boolean,
|
userChanged: Boolean,
|
||||||
user: Int,
|
user: Int,
|
||||||
): QSTile? {
|
): QSTile? {
|
||||||
return when {
|
return when (tileOrNotInstalled) {
|
||||||
|
is TileOrNotInstalled.NotInstalled -> null
|
||||||
|
is TileOrNotInstalled.Tile -> {
|
||||||
|
val qsTile = tileOrNotInstalled.tile
|
||||||
|
when {
|
||||||
!qsTile.isAvailable -> {
|
!qsTile.isAvailable -> {
|
||||||
logger.logTileDestroyed(
|
logger.logTileDestroyed(
|
||||||
tileSpec,
|
tileSpec,
|
||||||
@@ -317,7 +370,8 @@ constructor(
|
|||||||
// Tile is in the current list of tiles and available.
|
// Tile is in the current list of tiles and available.
|
||||||
// We have a handful of different cases
|
// We have a handful of different cases
|
||||||
qsTile !is CustomTile -> {
|
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) {
|
if (userChanged) {
|
||||||
qsTile.userSwitch(user)
|
qsTile.userSwitch(user)
|
||||||
logger.logTileUserChanged(tileSpec, user)
|
logger.logTileUserChanged(tileSpec, user)
|
||||||
@@ -339,4 +393,23 @@ 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>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,18 @@ constructor(
|
|||||||
tileListLogBuffer.log(TILE_LIST_TAG, LogLevel.DEBUG, {}, { "Using retail tiles" })
|
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. */
|
/** Reasons for destroying an existing tile. */
|
||||||
enum class TileDestroyedReason(val readable: String) {
|
enum class TileDestroyedReason(val readable: String) {
|
||||||
TILE_REMOVED("Tile removed from current set"),
|
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.external.TileServiceKey
|
||||||
import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository
|
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.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.FakeTileSpecRepository
|
||||||
import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
|
import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
|
||||||
import com.android.systemui.qs.pipeline.domain.model.TileModel
|
import com.android.systemui.qs.pipeline.domain.model.TileModel
|
||||||
@@ -73,6 +74,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
|
|||||||
|
|
||||||
private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository()
|
private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository()
|
||||||
private val userRepository = FakeUserRepository()
|
private val userRepository = FakeUserRepository()
|
||||||
|
private val installedTilesPackageRepository = FakeInstalledTilesComponentRepository()
|
||||||
private val tileFactory = FakeQSFactory(::tileCreator)
|
private val tileFactory = FakeQSFactory(::tileCreator)
|
||||||
private val customTileAddedRepository: CustomTileAddedRepository =
|
private val customTileAddedRepository: CustomTileAddedRepository =
|
||||||
FakeCustomTileAddedRepository()
|
FakeCustomTileAddedRepository()
|
||||||
@@ -100,11 +102,13 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
|
|||||||
featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true)
|
featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true)
|
||||||
|
|
||||||
userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
|
userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
|
||||||
|
|
||||||
setUserTracker(0)
|
setUserTracker(0)
|
||||||
|
|
||||||
underTest =
|
underTest =
|
||||||
CurrentTilesInteractorImpl(
|
CurrentTilesInteractorImpl(
|
||||||
tileSpecRepository = tileSpecRepository,
|
tileSpecRepository = tileSpecRepository,
|
||||||
|
installedTilesComponentRepository = installedTilesPackageRepository,
|
||||||
userRepository = userRepository,
|
userRepository = userRepository,
|
||||||
customTileStatePersister = customTileStatePersister,
|
customTileStatePersister = customTileStatePersister,
|
||||||
tileFactory = tileFactory,
|
tileFactory = tileFactory,
|
||||||
@@ -609,6 +613,40 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
|
|||||||
assertThat((tileA as FakeQSTile).callbacks).containsExactly(callback)
|
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) {
|
private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) {
|
||||||
this.state = state
|
this.state = state
|
||||||
this.label = label
|
this.label = label
|
||||||
@@ -654,6 +692,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
|
|||||||
|
|
||||||
private suspend fun switchUser(user: UserInfo) {
|
private suspend fun switchUser(user: UserInfo) {
|
||||||
setUserTracker(user.id)
|
setUserTracker(user.id)
|
||||||
|
installedTilesPackageRepository.setInstalledPackagesForUser(user.id, setOf(TEST_COMPONENT))
|
||||||
userRepository.setSelectedUserInfo(user)
|
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