Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 378447f6 authored by Fabian Kozynski's avatar Fabian Kozynski Committed by Fabián Kozynski
Browse files

Support restoring TileService before app install

Before, CustomTile were destroyed if the app was not installed (before
that, they were kept as UNAVAILABLE, but that caused other issues). This
was an issue when restoring from a backup, as the setting would include
custom tiles that were restored before the app was installed and
therefore immediately removed (and never brought back).

With this change:
* Do not create CustomTile that don't have a corresponding valid
  component. Instead, keep them in the setting and mark them as
  NotInstalled internally.
* If the component later appears, actually create the tile, and send
  `onTileAdded`.

Note that if a component for a TileService disappears (it's
uninstalled), the tile will be automatically removed and would need to
be manually added. This is existing behavior.

Finally, in order to prevent race conditions when a package with
multiple tiles is uninstalled, use a mutex whenever making changes to
the setting.

Fixes: 282003318
Test: atest com.android.systemui.qs
Test: atest CtsTileServiceTestCases
Test: manual, add custom tiles through adb to settings and later install
app
Test: atest android.host.systemui
Flag: QS_PIPELINE_NEW_HOST
Change-Id: I0a774969ad20611103b09ce4ace6175ad53350ce

Change-Id: I8b48f45124928702fcdcf88484ccf3b501ea86ed
parent 755737cf
Loading
Loading
Loading
Loading
+1 −3
Original line number Diff line number Diff line
@@ -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(";
+7 −0
Original line number Diff line number Diff line
@@ -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)
+109 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.qs.pipeline.data.repository

import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE
import android.annotation.WorkerThread
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
import android.os.UserHandle
import android.service.quicksettings.TileService
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.kotlin.isComponentActuallyEnabled
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart

interface InstalledTilesComponentRepository {

    fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>>
}

@SysUISingleton
class InstalledTilesComponentRepositoryImpl
@Inject
constructor(
    @Application private val applicationContext: Context,
    private val packageManager: PackageManager,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) : InstalledTilesComponentRepository {

    override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> =
        conflatedCallbackFlow {
                val receiver =
                    object : BroadcastReceiver() {
                        override fun onReceive(context: Context?, intent: Intent?) {
                            trySend(Unit)
                        }
                    }
                applicationContext.registerReceiverAsUser(
                    receiver,
                    UserHandle.of(userId),
                    INTENT_FILTER,
                    /* broadcastPermission = */ null,
                    /* scheduler = */ null
                )

                awaitClose { applicationContext.unregisterReceiver(receiver) }
            }
            .onStart { emit(Unit) }
            .map { reloadComponents(userId) }
            .distinctUntilChanged()
            .flowOn(backgroundDispatcher)

    @WorkerThread
    private fun reloadComponents(userId: Int): Set<ComponentName> {
        return packageManager
            .queryIntentServicesAsUser(INTENT, FLAGS, userId)
            .mapNotNull { it.serviceInfo }
            .filter { it.permission == BIND_QUICK_SETTINGS_TILE }
            .filter { packageManager.isComponentActuallyEnabled(it) }
            .mapTo(mutableSetOf()) { it.componentName }
    }

    companion object {
        private val INTENT_FILTER =
            IntentFilter().apply {
                addAction(Intent.ACTION_PACKAGE_ADDED)
                addAction(Intent.ACTION_PACKAGE_CHANGED)
                addAction(Intent.ACTION_PACKAGE_REMOVED)
                addAction(Intent.ACTION_PACKAGE_REPLACED)
                addDataScheme("package")
            }
        private val INTENT = Intent(TileService.ACTION_QS_TILE)
        private val FLAGS =
            ResolveInfoFlags.of(
                (PackageManager.GET_SERVICES or
                        PackageManager.MATCH_DIRECT_BOOT_AWARE or
                        PackageManager.MATCH_DIRECT_BOOT_UNAWARE)
                    .toLong()
            )
    }
}
+32 −25
Original line number Diff line number Diff line
@@ -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,7 +149,8 @@ constructor(
            .flowOn(backgroundDispatcher)
    }

    override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) {
    override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) =
        mutex.withLock {
            if (tile == TileSpec.Invalid) {
                return
            }
@@ -160,7 +165,8 @@ constructor(
            }
        }

    override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) {
    override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) =
        mutex.withLock {
            if (tiles.all { it == TileSpec.Invalid }) {
                return
            }
@@ -170,7 +176,8 @@ constructor(
            }
        }

    override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) {
    override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) =
        mutex.withLock {
            val filtered = tiles.filter { it != TileSpec.Invalid }
            if (filtered.isNotEmpty()) {
                storeTiles(userId, filtered)
+158 −85
Original line number Diff line number Diff line
@@ -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,23 +177,33 @@ constructor(
    @OptIn(ExperimentalCoroutinesApi::class)
    private fun startTileCollection() {
        scope.launch {
            userRepository.selectedUserInfo
                .flatMapLatest { user ->
            launch {
                userRepository.selectedUserInfo.collect { user ->
                    currentUser.value = user.id
                    _userContext.value = userTracker.userContext
                    tileSpecRepository.tilesSpecs(user.id).map { user.id to it }
                }
                .distinctUntilChanged()
                .pairwise(-1 to emptyList())
                .flowOn(backgroundDispatcher)
                .collect { (old, new) ->
                    val newTileList = new.second
                    val userChanged = old.first != new.first
                    val newUser = new.first
            }

            launch(backgroundDispatcher) {
                userAndTiles
                    .combine(installedPackagesWithTiles) { usersAndTiles, packages ->
                        Data(
                            usersAndTiles.previousValue,
                            usersAndTiles.newValue,
                            packages,
                        )
                    }
                    .collectLatest {
                        val newTileList = it.newData.tiles
                        val userChanged = it.oldData.userId != it.newData.userId
                        val newUser = it.newData.userId
                        val components = it.installedComponents

                        // Destroy all tiles that are not in the new set
                        specsToTiles
                        .filter { it.key !in newTileList }
                            .filter {
                                it.key !in newTileList && it.value is TileOrNotInstalled.Tile
                            }
                            .forEach { entry ->
                                logger.logTileDestroyed(
                                    entry.key,
@@ -185,13 +214,21 @@ constructor(
                                        QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
                                    }
                                )
                            entry.value.destroy()
                                (entry.value as TileOrNotInstalled.Tile).tile.destroy()
                            }
                        // MutableMap will keep the insertion order
                    val newTileMap = mutableMapOf<TileSpec, QSTile>()
                        val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>()

                        newTileList.forEach { tileSpec ->
                            if (tileSpec !in newTileMap) {
                                if (
                                    tileSpec is TileSpec.CustomTileSpec &&
                                        tileSpec.componentName !in components
                                ) {
                                    newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled
                                } else {
                                    // Create tile here will never try to create a CustomTile that
                                    // is not installed
                                    val newTile =
                                        if (tileSpec in specsToTiles) {
                                            processExistingTile(
@@ -205,7 +242,8 @@ constructor(
                                            createTile(tileSpec)
                                        }
                                    if (newTile != null) {
                                newTileMap[tileSpec] = newTile
                                        newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile)
                                    }
                                }
                            }
                        }
@@ -213,15 +251,26 @@ constructor(
                        val resolvedSpecs = newTileMap.keys.toList()
                        specsToTiles.clear()
                        specsToTiles.putAll(newTileMap)
                    _currentSpecsAndTiles.value = newTileMap.map { TileModel(it.key, it.value) }
                        _currentSpecsAndTiles.value =
                            newTileMap
                                .filter { it.value is TileOrNotInstalled.Tile }
                                .map {
                                    TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile)
                                }
                        logger.logTilesNotInstalled(
                            newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys,
                            newUser
                        )
                        if (resolvedSpecs != newTileList) {
                        // There were some tiles that couldn't be created. Change the value in the
                            // There were some tiles that couldn't be created. Change the value in
                            // the
                            // repository
                            launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
                        }
                    }
            }
        }
    }

    override fun addTile(spec: TileSpec, position: Int) {
        scope.launch {
@@ -301,11 +350,15 @@ constructor(

    private fun processExistingTile(
        tileSpec: TileSpec,
        qsTile: QSTile,
        tileOrNotInstalled: TileOrNotInstalled,
        userChanged: Boolean,
        user: Int,
    ): QSTile? {
        return when {
        return when (tileOrNotInstalled) {
            is TileOrNotInstalled.NotInstalled -> null
            is TileOrNotInstalled.Tile -> {
                val qsTile = tileOrNotInstalled.tile
                when {
                    !qsTile.isAvailable -> {
                        logger.logTileDestroyed(
                            tileSpec,
@@ -317,7 +370,8 @@ constructor(
                    // Tile is in the current list of tiles and available.
                    // We have a handful of different cases
                    qsTile !is CustomTile -> {
                // The tile is not a custom tile. Make sure they are reset to the correct user
                        // The tile is not a custom tile. Make sure they are reset to the correct
                        // user
                        if (userChanged) {
                            qsTile.userSwitch(user)
                            logger.logTileUserChanged(tileSpec, user)
@@ -340,3 +394,22 @@ constructor(
                }
            }
        }
    }

    private sealed interface TileOrNotInstalled {
        object NotInstalled : TileOrNotInstalled

        @JvmInline value class Tile(val tile: QSTile) : TileOrNotInstalled
    }

    private data class UserAndTiles(
        val userId: Int,
        val tiles: List<TileSpec>,
    )

    private data class Data(
        val oldData: UserAndTiles,
        val newData: UserAndTiles,
        val installedComponents: Set<ComponentName>,
    )
}
Loading