Merge "Support restoring TileService before app install" into udc-dev

This commit is contained in:
Fabian Kozynski
2023-06-05 16:00:52 +00:00
committed by Android (Google) Code Review
11 changed files with 859 additions and 111 deletions

View File

@@ -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(";

View File

@@ -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)

View File

@@ -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()
)
}
}

View File

@@ -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) {

View File

@@ -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>,
)
}

View File

@@ -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"),

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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")
}
}

View File

@@ -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()) }
}