Merge "Add a CurrentTilesInteractor" into udc-dev

This commit is contained in:
Fabian Kozynski
2023-04-07 18:57:54 +00:00
committed by Android (Google) Code Review
18 changed files with 1656 additions and 55 deletions

View File

@@ -26,8 +26,6 @@ import com.android.systemui.R;
import com.android.systemui.plugins.qs.QSFactory;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTileView;
import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository;
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
import com.android.systemui.util.leak.GarbageMonitor;
import java.util.ArrayList;
@@ -35,7 +33,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public interface QSHost extends PanelInteractor, CustomTileAddedRepository {
public interface QSHost {
String TILES_SETTING = Settings.Secure.QS_TILES;
int POSITION_AT_END = -1;
@@ -75,7 +73,11 @@ public interface QSHost extends PanelInteractor, CustomTileAddedRepository {
* @see QSFactory#createTileView
*/
QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView);
/** Create a {@link QSTile} of a {@code tileSpec} type. */
/** Create a {@link QSTile} of a {@code tileSpec} type.
*
* This should only be called by classes that need to create one-off instances of tiles.
* Do not use to create {@code custom} tiles without explicitly taking care of its lifecycle.
*/
QSTile createTile(String tileSpec);
/**

View File

@@ -0,0 +1,226 @@
/*
* 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
import android.content.ComponentName
import android.content.Context
import androidx.annotation.GuardedBy
import com.android.internal.logging.InstanceId
import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTileView
import com.android.systemui.qs.external.TileServiceRequestController
import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
/**
* Adapter to determine what real class to use for classes that depend on [QSHost].
*
* * When [Flags.QS_PIPELINE_NEW_HOST] is off, all calls will be routed to [QSTileHost].
* * When [Flags.QS_PIPELINE_NEW_HOST] is on, calls regarding the current set of tiles will be
* routed to [CurrentTilesInteractor]. Other calls (like [warn]) will still be routed to
* [QSTileHost].
*
* This routing also includes dumps.
*/
@SysUISingleton
class QSHostAdapter
@Inject
constructor(
private val qsTileHost: QSTileHost,
private val interactor: CurrentTilesInteractor,
private val context: Context,
private val tileServiceRequestControllerBuilder: TileServiceRequestController.Builder,
@Application private val scope: CoroutineScope,
private val featureFlags: FeatureFlags,
dumpManager: DumpManager,
) : QSHost {
companion object {
private const val TAG = "QSTileHost"
}
private val useNewHost = featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)
@GuardedBy("callbacksMap") private val callbacksMap = mutableMapOf<QSHost.Callback, Job>()
init {
scope.launch { tileServiceRequestControllerBuilder.create(this@QSHostAdapter).init() }
// Redirect dump to the correct host (needed for CTS tests)
dumpManager.registerCriticalDumpable(
TAG,
if (useNewHost) interactor else qsTileHost
)
}
override fun getTiles(): Collection<QSTile> {
return if (useNewHost) {
interactor.currentQSTiles
} else {
qsTileHost.getTiles()
}
}
override fun getSpecs(): List<String> {
return if (useNewHost) {
interactor.currentTilesSpecs.map { it.spec }
} else {
qsTileHost.getSpecs()
}
}
override fun removeTile(spec: String) {
if (useNewHost) {
interactor.removeTiles(listOf(TileSpec.create(spec)))
} else {
qsTileHost.removeTile(spec)
}
}
override fun addCallback(callback: QSHost.Callback) {
if (useNewHost) {
val job =
scope.launch {
interactor.currentTiles.collect { callback.onTilesChanged() }
}
synchronized(callbacksMap) { callbacksMap.put(callback, job) }
} else {
qsTileHost.addCallback(callback)
}
}
override fun removeCallback(callback: QSHost.Callback) {
if (useNewHost) {
synchronized(callbacksMap) { callbacksMap.get(callback)?.cancel() }
} else {
qsTileHost.removeCallback(callback)
}
}
override fun removeTiles(specs: Collection<String>) {
if (useNewHost) {
interactor.removeTiles(specs.map(TileSpec::create))
} else {
qsTileHost.removeTiles(specs)
}
}
override fun removeTileByUser(component: ComponentName) {
if (useNewHost) {
interactor.removeTiles(listOf(TileSpec.create(component)))
} else {
qsTileHost.removeTileByUser(component)
}
}
override fun addTile(spec: String, position: Int) {
if (useNewHost) {
interactor.addTile(TileSpec.create(spec), position)
} else {
qsTileHost.addTile(spec, position)
}
}
override fun addTile(component: ComponentName, end: Boolean) {
if (useNewHost) {
interactor.addTile(
TileSpec.create(component),
if (end) POSITION_AT_END else 0
)
} else {
qsTileHost.addTile(component, end)
}
}
override fun changeTilesByUser(previousTiles: List<String>, newTiles: List<String>) {
if (useNewHost) {
interactor.setTiles(newTiles.map(TileSpec::create))
} else {
qsTileHost.changeTilesByUser(previousTiles, newTiles)
}
}
override fun warn(message: String?, t: Throwable?) {
qsTileHost.warn(message, t)
}
override fun getContext(): Context {
return if (useNewHost) {
context
} else {
qsTileHost.context
}
}
override fun getUserContext(): Context {
return if (useNewHost) {
interactor.userContext.value
} else {
qsTileHost.userContext
}
}
override fun getUserId(): Int {
return if (useNewHost) {
interactor.userId.value
} else {
qsTileHost.userId
}
}
override fun getUiEventLogger(): UiEventLogger {
return qsTileHost.uiEventLogger
}
override fun createTileView(
themedContext: Context?,
tile: QSTile?,
collapsedView: Boolean
): QSTileView {
return qsTileHost.createTileView(themedContext, tile, collapsedView)
}
override fun createTile(tileSpec: String): QSTile? {
return qsTileHost.createTile(tileSpec)
}
override fun addTile(spec: String) {
return addTile(spec, QSHost.POSITION_AT_END)
}
override fun addTile(tile: ComponentName) {
return addTile(tile, false)
}
override fun indexOf(tileSpec: String): Int {
return specs.indexOf(tileSpec)
}
override fun getNewInstanceId(): InstanceId {
return qsTileHost.newInstanceId
}
}

View File

@@ -37,8 +37,9 @@ import com.android.systemui.ProtoDumpable;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.nano.SystemUIProtoDump;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.qs.QSFactory;
@@ -48,9 +49,10 @@ import com.android.systemui.qs.external.CustomTile;
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.external.TileServiceRequestController;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.nano.QsTileState;
import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository;
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
import com.android.systemui.settings.UserFileManager;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.AutoTileManager;
@@ -85,7 +87,8 @@ import javax.inject.Provider;
* This class also provides the interface for adding/removing/changing tiles.
*/
@SysUISingleton
public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable {
public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable,
PanelInteractor, CustomTileAddedRepository {
private static final String TAG = "QSTileHost";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final int MAX_QS_INSTANCE_ID = 1 << 20;
@@ -99,7 +102,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P
private final ArrayList<String> mTileSpecs = new ArrayList<>();
private final TunerService mTunerService;
private final PluginManager mPluginManager;
private final DumpManager mDumpManager;
private final QSLogger mQSLogger;
private final UiEventLogger mUiEventLogger;
private final InstanceIdSequence mInstanceIdSequence;
@@ -122,9 +124,10 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P
// This is enforced by only cleaning the flag at the end of a successful run of #onTuningChanged
private boolean mTilesListDirty = true;
private final TileServiceRequestController mTileServiceRequestController;
private TileLifecycleManager.Factory mTileLifeCycleManagerFactory;
private final FeatureFlags mFeatureFlags;
@Inject
public QSTileHost(Context context,
QSFactory defaultFactory,
@@ -132,35 +135,32 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P
PluginManager pluginManager,
TunerService tunerService,
Provider<AutoTileManager> autoTiles,
DumpManager dumpManager,
Optional<CentralSurfaces> centralSurfacesOptional,
QSLogger qsLogger,
UiEventLogger uiEventLogger,
UserTracker userTracker,
SecureSettings secureSettings,
CustomTileStatePersister customTileStatePersister,
TileServiceRequestController.Builder tileServiceRequestControllerBuilder,
TileLifecycleManager.Factory tileLifecycleManagerFactory,
UserFileManager userFileManager
UserFileManager userFileManager,
FeatureFlags featureFlags
) {
mContext = context;
mUserContext = context;
mTunerService = tunerService;
mPluginManager = pluginManager;
mDumpManager = dumpManager;
mQSLogger = qsLogger;
mUiEventLogger = uiEventLogger;
mMainExecutor = mainExecutor;
mTileServiceRequestController = tileServiceRequestControllerBuilder.create(this);
mTileLifeCycleManagerFactory = tileLifecycleManagerFactory;
mUserFileManager = userFileManager;
mFeatureFlags = featureFlags;
mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID);
mCentralSurfacesOptional = centralSurfacesOptional;
mQsFactories.add(defaultFactory);
pluginManager.addPluginListener(this, QSFactory.class, true);
mDumpManager.registerDumpable(TAG, this);
mUserTracker = userTracker;
mSecureSettings = secureSettings;
mCustomTileStatePersister = customTileStatePersister;
@@ -172,7 +172,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P
tunerService.addTunable(this, TILES_SETTING);
// AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
mAutoTiles = autoTiles.get();
mTileServiceRequestController.init();
});
}
@@ -186,8 +185,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P
mAutoTiles.destroy();
mTunerService.removeTunable(this);
mPluginManager.removePluginListener(this);
mDumpManager.unregisterDumpable(TAG);
mTileServiceRequestController.destroy();
}
@Override
@@ -300,6 +297,10 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P
if (!TILES_SETTING.equals(key)) {
return;
}
// Do not process tiles if the flag is enabled.
if (mFeatureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
return;
}
if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
}

View File

@@ -19,6 +19,7 @@ package com.android.systemui.qs.dagger
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QSHostAdapter
import com.android.systemui.qs.QSTileHost
import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository
import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedSharedPrefsRepository
@@ -31,7 +32,7 @@ import dagger.Provides
@Module
interface QSHostModule {
@Binds fun provideQsHost(controllerImpl: QSTileHost): QSHost
@Binds fun provideQsHost(controllerImpl: QSHostAdapter): QSHost
@Module
companion object {
@@ -39,7 +40,7 @@ interface QSHostModule {
@JvmStatic
fun providePanelInteractor(
featureFlags: FeatureFlags,
qsHost: QSHost,
qsHost: QSTileHost,
panelInteractorImpl: PanelInteractorImpl
): PanelInteractor {
return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
@@ -53,7 +54,7 @@ interface QSHostModule {
@JvmStatic
fun provideCustomTileAddedRepository(
featureFlags: FeatureFlags,
qsHost: QSHost,
qsHost: QSTileHost,
customTileAddedRepository: CustomTileAddedSharedPrefsRepository
): CustomTileAddedRepository {
return if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {

View File

@@ -22,6 +22,8 @@ import com.android.systemui.log.LogBufferFactory
import com.android.systemui.plugins.log.LogBuffer
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
import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractorImpl
import com.android.systemui.qs.pipeline.prototyping.PrototypeCoreStartable
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import dagger.Binds
@@ -37,6 +39,11 @@ abstract class QSPipelineModule {
@Binds
abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository
@Binds
abstract fun bindCurrentTilesInteractor(
impl: CurrentTilesInteractorImpl
): CurrentTilesInteractor
@Binds
@IntoMap
@ClassKey(PrototypeCoreStartable::class)

View File

@@ -32,6 +32,7 @@ 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.onEach
@@ -53,6 +54,8 @@ interface TileSpecRepository {
* at the end of the list.
*
* Passing [TileSpec.Invalid] is a noop.
*
* Trying to add a tile beyond the end of the list will add it at the end.
*/
suspend fun addTile(@UserIdInt userId: Int, tile: TileSpec, position: Int = POSITION_AT_END)
@@ -61,7 +64,7 @@ interface TileSpecRepository {
*
* Passing [TileSpec.Invalid] or a non present tile is a noop.
*/
suspend fun removeTile(@UserIdInt userId: Int, tile: TileSpec)
suspend fun removeTiles(@UserIdInt userId: Int, tiles: Collection<TileSpec>)
/**
* Sets the list of current [tiles] for a given [userId].
@@ -106,6 +109,7 @@ constructor(
}
.onStart { emit(Unit) }
.map { secureSettings.getStringForUser(SETTING, userId) ?: "" }
.distinctUntilChanged()
.onEach { logger.logTilesChangedInSettings(it, userId) }
.map { parseTileSpecs(it, userId) }
.flowOn(backgroundDispatcher)
@@ -117,7 +121,7 @@ constructor(
}
val tilesList = loadTiles(userId).toMutableList()
if (tile !in tilesList) {
if (position < 0) {
if (position < 0 || position >= tilesList.size) {
tilesList.add(tile)
} else {
tilesList.add(position, tile)
@@ -126,12 +130,12 @@ constructor(
}
}
override suspend fun removeTile(userId: Int, tile: TileSpec) {
if (tile == TileSpec.Invalid) {
override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) {
if (tiles.all { it == TileSpec.Invalid }) {
return
}
val tilesList = loadTiles(userId).toMutableList()
if (tilesList.remove(tile)) {
if (tilesList.removeAll(tiles)) {
storeTiles(userId, tilesList.toList())
}
}

View File

@@ -0,0 +1,344 @@
/*
* 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.domain.interactor
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.UserHandle
import com.android.systemui.Dumpable
import com.android.systemui.ProtoDumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.nano.SystemUIProtoDump
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.plugins.qs.QSFactory
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.external.CustomTile
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.TileSpecRepository
import com.android.systemui.qs.pipeline.domain.model.TileModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.qs.toProto
import com.android.systemui.settings.UserTracker
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.kotlin.pairwise
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Interactor for retrieving the list of current QS tiles, as well as making changes to this list
*
* It is [ProtoDumpable] as it needs to be able to dump state for CTS tests.
*/
interface CurrentTilesInteractor : ProtoDumpable {
/** Current list of tiles with their corresponding spec. */
val currentTiles: StateFlow<List<TileModel>>
/** User for the [currentTiles]. */
val userId: StateFlow<Int>
/** [Context] corresponding to [userId] */
val userContext: StateFlow<Context>
/** List of specs corresponding to the last value of [currentTiles] */
val currentTilesSpecs: List<TileSpec>
get() = currentTiles.value.map(TileModel::spec)
/** List of tiles corresponding to the last value of [currentTiles] */
val currentQSTiles: List<QSTile>
get() = currentTiles.value.map(TileModel::tile)
/**
* Requests that a tile be added in the list of tiles for the current user.
*
* @see TileSpecRepository.addTile
*/
fun addTile(spec: TileSpec, position: Int = TileSpecRepository.POSITION_AT_END)
/**
* Requests that tiles be removed from the list of tiles for the current user
*
* If tiles with [TileSpec.CustomTileSpec] are removed, their lifecycle will be terminated and
* marked as removed.
*
* @see TileSpecRepository.removeTiles
*/
fun removeTiles(specs: Collection<TileSpec>)
/**
* Requests that the list of tiles for the current user is changed to [specs].
*
* If tiles with [TileSpec.CustomTileSpec] are removed, their lifecycle will be terminated and
* marked as removed.
*
* @see TileSpecRepository.setTiles
*/
fun setTiles(specs: List<TileSpec>)
}
/**
* This implementation of [CurrentTilesInteractor] will try to re-use existing [QSTile] objects when
* possible, in particular:
* * It will only destroy tiles when they are not part of the list of tiles anymore
* * Platform tiles will be kept between users, with a call to [QSTile.userSwitch]
* * [CustomTile]s will only be destroyed if the user changes.
*/
@SysUISingleton
class CurrentTilesInteractorImpl
@Inject
constructor(
private val tileSpecRepository: TileSpecRepository,
private val userRepository: UserRepository,
private val customTileStatePersister: CustomTileStatePersister,
private val tileFactory: QSFactory,
private val customTileAddedRepository: CustomTileAddedRepository,
private val tileLifecycleManagerFactory: TileLifecycleManager.Factory,
private val userTracker: UserTracker,
@Main private val mainDispatcher: CoroutineDispatcher,
@Background private val backgroundDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
private val logger: QSPipelineLogger,
featureFlags: FeatureFlags,
) : CurrentTilesInteractor {
private val _currentSpecsAndTiles: MutableStateFlow<List<TileModel>> =
MutableStateFlow(emptyList())
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 currentUser = MutableStateFlow(userTracker.userId)
override val userId = currentUser.asStateFlow()
private val _userContext = MutableStateFlow(userTracker.userContext)
override val userContext = _userContext.asStateFlow()
init {
if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
startTileCollection()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun startTileCollection() {
scope.launch {
userRepository.selectedUserInfo
.flatMapLatest { 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>()
newTileList.forEach { tileSpec ->
if (tileSpec !in newTileMap) {
val newTile =
if (tileSpec in specsToTiles) {
processExistingTile(
tileSpec,
specsToTiles.getValue(tileSpec),
userChanged,
newUser
)
?: createTile(tileSpec)
} else {
createTile(tileSpec)
}
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) }
}
}
}
}
override fun addTile(spec: TileSpec, position: Int) {
scope.launch {
tileSpecRepository.addTile(userRepository.getSelectedUserInfo().id, spec, position)
}
}
override fun removeTiles(specs: Collection<TileSpec>) {
val currentSpecsCopy = currentTilesSpecs.toSet()
val user = currentUser.value
// intersect: tiles that are there and are being removed
val toFree = currentSpecsCopy.intersect(specs).filterIsInstance<TileSpec.CustomTileSpec>()
toFree.forEach { onCustomTileRemoved(it.componentName, user) }
if (currentSpecsCopy.intersect(specs).isNotEmpty()) {
// We don't want to do the call to set in case getCurrentTileSpecs is not the most
// up to date for this user.
scope.launch { tileSpecRepository.removeTiles(user, specs) }
}
}
override fun setTiles(specs: List<TileSpec>) {
val currentSpecsCopy = currentTilesSpecs
val user = currentUser.value
if (currentSpecsCopy != specs) {
// minus: tiles that were there but are not there anymore
val toFree = currentSpecsCopy.minus(specs).filterIsInstance<TileSpec.CustomTileSpec>()
toFree.forEach { onCustomTileRemoved(it.componentName, user) }
scope.launch { tileSpecRepository.setTiles(user, specs) }
}
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.println("CurrentTileInteractorImpl:")
pw.println("User: ${userId.value}")
currentTiles.value
.map { it.tile }
.filterIsInstance<Dumpable>()
.forEach { it.dump(pw, args) }
}
override fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>) {
val data =
currentTiles.value.map { it.tile.state }.mapNotNull { it.toProto() }.toTypedArray()
systemUIProtoDump.tiles = data
}
private fun onCustomTileRemoved(componentName: ComponentName, userId: Int) {
val intent = Intent().setComponent(componentName)
val lifecycleManager = tileLifecycleManagerFactory.create(intent, UserHandle.of(userId))
lifecycleManager.onStopListening()
lifecycleManager.onTileRemoved()
customTileStatePersister.removeState(TileServiceKey(componentName, userId))
customTileAddedRepository.setTileAdded(componentName, userId, false)
lifecycleManager.flushMessagesAndUnbind()
}
private suspend fun createTile(spec: TileSpec): QSTile? {
val tile = withContext(mainDispatcher) { tileFactory.createTile(spec.spec) }
if (tile == null) {
logger.logTileNotFoundInFactory(spec)
return null
} else {
tile.tileSpec = spec.spec
return if (!tile.isAvailable) {
logger.logTileDestroyed(
spec,
QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE,
)
tile.destroy()
null
} else {
logger.logTileCreated(spec)
tile
}
}
}
private fun processExistingTile(
tileSpec: TileSpec,
qsTile: QSTile,
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
qsTile.removeCallbacks()
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.removeCallbacks()
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
}
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.domain.model
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.pipeline.shared.TileSpec
/**
* Container for a [tile] and its [spec]. The following must be true:
* ```
* spec.spec == tile.tileSpec
* ```
*/
data class TileModel(val spec: TileSpec, val tile: QSTile) {
init {
check(spec.spec == tile.tileSpec)
}
}

View File

@@ -93,7 +93,7 @@ constructor(
private fun performRemove(args: List<String>, spec: TileSpec) {
val user = args.getOrNull(2)?.toInt() ?: userRepository.getSelectedUserInfo().id
scope.launch { tileSpecRepository.removeTile(user, spec) }
scope.launch { tileSpecRepository.removeTiles(user, listOf(spec)) }
}
override fun help(pw: PrintWriter) {

View File

@@ -66,6 +66,10 @@ sealed class TileSpec private constructor(open val spec: String) {
}
}
fun create(component: ComponentName): CustomTileSpec {
return CustomTileSpec(CustomTile.toSpec(component), component)
}
private val String.isCustomTileSpec: Boolean
get() = startsWith(CustomTile.PREFIX)

View File

@@ -73,4 +73,59 @@ constructor(
{ "Tiles changed in settings for user $int1: $str1" }
)
}
/** Log when a tile is destroyed and its reason for destroying. */
fun logTileDestroyed(spec: TileSpec, reason: TileDestroyedReason) {
tileListLogBuffer.log(
TILE_LIST_TAG,
LogLevel.DEBUG,
{
str1 = spec.toString()
str2 = reason.readable
},
{ "Tile $str1 destroyed. Reason: $str2" }
)
}
/** Log when a tile is created. */
fun logTileCreated(spec: TileSpec) {
tileListLogBuffer.log(
TILE_LIST_TAG,
LogLevel.DEBUG,
{ str1 = spec.toString() },
{ "Tile $str1 created" }
)
}
/** Ĺog when trying to create a tile, but it's not found in the factory. */
fun logTileNotFoundInFactory(spec: TileSpec) {
tileListLogBuffer.log(
TILE_LIST_TAG,
LogLevel.VERBOSE,
{ str1 = spec.toString() },
{ "Tile $str1 not found in factory" }
)
}
/** Log when the user is changed for a platform tile. */
fun logTileUserChanged(spec: TileSpec, user: Int) {
tileListLogBuffer.log(
TILE_LIST_TAG,
LogLevel.VERBOSE,
{
str1 = spec.toString()
int1 = user
},
{ "User changed to $int1 for tile $str1" }
)
}
/** Reasons for destroying an existing tile. */
enum class TileDestroyedReason(val readable: String) {
TILE_REMOVED("Tile removed from current set"),
CUSTOM_TILE_USER_CHANGED("User changed for custom tile"),
NEW_TILE_NOT_AVAILABLE("New tile not available"),
EXISTING_TILE_NOT_AVAILABLE("Existing tile not available"),
TILE_NOT_PRESENT_IN_NEW_USER("Tile not present in new user"),
}
}

View File

@@ -51,8 +51,10 @@ import com.android.internal.util.CollectionUtils;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.nano.SystemUIProtoDump;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.qs.QSFactory;
@@ -62,7 +64,6 @@ import com.android.systemui.qs.external.CustomTile;
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.external.TileServiceRequestController;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.settings.UserFileManager;
@@ -110,8 +111,6 @@ public class QSTileHostTest extends SysuiTestCase {
@Mock
private Provider<AutoTileManager> mAutoTiles;
@Mock
private DumpManager mDumpManager;
@Mock
private CentralSurfaces mCentralSurfaces;
@Mock
private QSLogger mQSLogger;
@@ -125,10 +124,6 @@ public class QSTileHostTest extends SysuiTestCase {
@Mock
private CustomTileStatePersister mCustomTileStatePersister;
@Mock
private TileServiceRequestController.Builder mTileServiceRequestControllerBuilder;
@Mock
private TileServiceRequestController mTileServiceRequestController;
@Mock
private TileLifecycleManager.Factory mTileLifecycleManagerFactory;
@Mock
private TileLifecycleManager mTileLifecycleManager;
@@ -137,6 +132,8 @@ public class QSTileHostTest extends SysuiTestCase {
private SparseArray<SharedPreferences> mSharedPreferencesByUser;
private FakeFeatureFlags mFeatureFlags;
private FakeExecutor mMainExecutor;
private QSTileHost mQSTileHost;
@@ -144,12 +141,13 @@ public class QSTileHostTest extends SysuiTestCase {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mFeatureFlags = new FakeFeatureFlags();
mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false);
mMainExecutor = new FakeExecutor(new FakeSystemClock());
mSharedPreferencesByUser = new SparseArray<>();
when(mTileServiceRequestControllerBuilder.create(any()))
.thenReturn(mTileServiceRequestController);
when(mTileLifecycleManagerFactory.create(any(Intent.class), any(UserHandle.class)))
.thenReturn(mTileLifecycleManager);
when(mUserFileManager.getSharedPreferences(anyString(), anyInt(), anyInt()))
@@ -165,10 +163,9 @@ public class QSTileHostTest extends SysuiTestCase {
mSecureSettings = new FakeSettings();
saveSetting("");
mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor,
mPluginManager, mTunerService, mAutoTiles, mDumpManager, mCentralSurfaces,
mPluginManager, mTunerService, mAutoTiles, mCentralSurfaces,
mQSLogger, mUiEventLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister,
mTileServiceRequestControllerBuilder, mTileLifecycleManagerFactory,
mUserFileManager);
mTileLifecycleManagerFactory, mUserFileManager, mFeatureFlags);
mSecureSettings.registerContentObserverForUser(SETTING, new ContentObserver(null) {
@Override
@@ -686,18 +683,16 @@ public class QSTileHostTest extends SysuiTestCase {
TestQSTileHost(Context context,
QSFactory defaultFactory, Executor mainExecutor,
PluginManager pluginManager, TunerService tunerService,
Provider<AutoTileManager> autoTiles, DumpManager dumpManager,
Provider<AutoTileManager> autoTiles,
CentralSurfaces centralSurfaces, QSLogger qsLogger, UiEventLogger uiEventLogger,
UserTracker userTracker, SecureSettings secureSettings,
CustomTileStatePersister customTileStatePersister,
TileServiceRequestController.Builder tileServiceRequestControllerBuilder,
TileLifecycleManager.Factory tileLifecycleManagerFactory,
UserFileManager userFileManager) {
UserFileManager userFileManager, FeatureFlags featureFlags) {
super(context, defaultFactory, mainExecutor, pluginManager,
tunerService, autoTiles, dumpManager, Optional.of(centralSurfaces), qsLogger,
tunerService, autoTiles, Optional.of(centralSurfaces), qsLogger,
uiEventLogger, userTracker, secureSettings, customTileStatePersister,
tileServiceRequestControllerBuilder, tileLifecycleManagerFactory,
userFileManager);
tileLifecycleManagerFactory, userFileManager, featureFlags);
}
@Override

View File

@@ -169,6 +169,21 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
assertThat(tiles).isEqualTo(specs.toTileSpecs())
}
@Test
fun addTileAtPosition_tooLarge_addedAtEnd() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
val specs = "a,custom(b/c)"
storeTilesForUser(specs, 0)
underTest.addTile(userId = 0, TileSpec.create("d"), position = 100)
val expected = "a,custom(b/c),d"
assertThat(loadTilesForUser(0)).isEqualTo(expected)
assertThat(tiles).isEqualTo(expected.toTileSpecs())
}
@Test
fun addTileForOtherUser_addedInThatUser() =
testScope.runTest {
@@ -187,27 +202,27 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
}
@Test
fun removeTile() =
fun removeTiles() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
storeTilesForUser("a,b", 0)
underTest.removeTile(userId = 0, TileSpec.create("a"))
underTest.removeTiles(userId = 0, listOf(TileSpec.create("a")))
assertThat(loadTilesForUser(0)).isEqualTo("b")
assertThat(tiles).isEqualTo("b".toTileSpecs())
}
@Test
fun removeTileNotThere_noop() =
fun removeTilesNotThere_noop() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
val specs = "a,b"
storeTilesForUser(specs, 0)
underTest.removeTile(userId = 0, TileSpec.create("c"))
underTest.removeTiles(userId = 0, listOf(TileSpec.create("c")))
assertThat(loadTilesForUser(0)).isEqualTo(specs)
assertThat(tiles).isEqualTo(specs.toTileSpecs())
@@ -221,7 +236,7 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
val specs = "a,b"
storeTilesForUser(specs, 0)
underTest.removeTile(userId = 0, TileSpec.Invalid)
underTest.removeTiles(userId = 0, listOf(TileSpec.Invalid))
assertThat(loadTilesForUser(0)).isEqualTo(specs)
assertThat(tiles).isEqualTo(specs.toTileSpecs())
@@ -237,7 +252,7 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
storeTilesForUser(specs, 0)
storeTilesForUser(specs, 1)
underTest.removeTile(userId = 1, TileSpec.create("a"))
underTest.removeTiles(userId = 1, listOf(TileSpec.create("a")))
assertThat(loadTilesForUser(0)).isEqualTo(specs)
assertThat(user0Tiles).isEqualTo(specs.toTileSpecs())
@@ -245,6 +260,19 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
assertThat(user1Tiles).isEqualTo("b".toTileSpecs())
}
@Test
fun removeMultipleTiles() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
storeTilesForUser("a,b,c,d", 0)
underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"), TileSpec.create("c")))
assertThat(loadTilesForUser(0)).isEqualTo("b,d")
assertThat(tiles).isEqualTo("b,d".toTileSpecs())
}
@Test
fun changeTiles() =
testScope.runTest {
@@ -310,8 +338,8 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
storeTilesForUser(specs, 0)
coroutineScope {
underTest.removeTile(userId = 0, TileSpec.create("c"))
underTest.removeTile(userId = 0, TileSpec.create("a"))
underTest.removeTiles(userId = 0, listOf(TileSpec.create("c")))
underTest.removeTiles(userId = 0, listOf(TileSpec.create("a")))
}
assertThat(loadTilesForUser(0)).isEqualTo("b")

View File

@@ -0,0 +1,674 @@
/*
* 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.domain.interactor
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.UserInfo
import android.os.UserHandle
import android.service.quicksettings.Tile
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.nano.SystemUIProtoDump
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTile.BooleanState
import com.android.systemui.qs.FakeQSFactory
import com.android.systemui.qs.external.CustomTile
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.FakeCustomTileAddedRepository
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
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.qs.toProto
import com.android.systemui.settings.UserTracker
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import com.google.protobuf.nano.MessageNano
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
class CurrentTilesInteractorImplTest : SysuiTestCase() {
private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository()
private val userRepository = FakeUserRepository()
private val tileFactory = FakeQSFactory(::tileCreator)
private val customTileAddedRepository: CustomTileAddedRepository =
FakeCustomTileAddedRepository()
private val featureFlags = FakeFeatureFlags()
private val tileLifecycleManagerFactory = TLMFactory()
@Mock private lateinit var customTileStatePersister: CustomTileStatePersister
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var logger: QSPipelineLogger
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val unavailableTiles = mutableSetOf("e")
private lateinit var underTest: CurrentTilesInteractorImpl
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true)
userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
setUserTracker(0)
underTest =
CurrentTilesInteractorImpl(
tileSpecRepository = tileSpecRepository,
userRepository = userRepository,
customTileStatePersister = customTileStatePersister,
tileFactory = tileFactory,
customTileAddedRepository = customTileAddedRepository,
tileLifecycleManagerFactory = tileLifecycleManagerFactory,
userTracker = userTracker,
mainDispatcher = testDispatcher,
backgroundDispatcher = testDispatcher,
scope = testScope.backgroundScope,
logger = logger,
featureFlags = featureFlags,
)
}
@Test
fun initialState() =
testScope.runTest(USER_INFO_0) {
assertThat(underTest.currentTiles.value).isEmpty()
assertThat(underTest.currentQSTiles).isEmpty()
assertThat(underTest.currentTilesSpecs).isEmpty()
assertThat(underTest.userId.value).isEqualTo(0)
assertThat(underTest.userContext.value.userId).isEqualTo(0)
}
@Test
fun correctTiles() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val specs =
listOf(
TileSpec.create("a"),
TileSpec.create("e"),
CUSTOM_TILE_SPEC,
TileSpec.create("d"),
TileSpec.create("non_existent")
)
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
// check each tile
// Tile a
val tile0 = tiles!![0]
assertThat(tile0.spec).isEqualTo(specs[0])
assertThat(tile0.tile.tileSpec).isEqualTo(specs[0].spec)
assertThat(tile0.tile).isInstanceOf(FakeQSTile::class.java)
assertThat(tile0.tile.isAvailable).isTrue()
// Tile e is not available and is not in the list
// Custom Tile
val tile1 = tiles!![1]
assertThat(tile1.spec).isEqualTo(specs[2])
assertThat(tile1.tile.tileSpec).isEqualTo(specs[2].spec)
assertThat(tile1.tile).isInstanceOf(CustomTile::class.java)
assertThat(tile1.tile.isAvailable).isTrue()
// Tile d
val tile2 = tiles!![2]
assertThat(tile2.spec).isEqualTo(specs[3])
assertThat(tile2.tile.tileSpec).isEqualTo(specs[3].spec)
assertThat(tile2.tile).isInstanceOf(FakeQSTile::class.java)
assertThat(tile2.tile.isAvailable).isTrue()
// Tile non-existent shouldn't be created. Therefore, only 3 tiles total
assertThat(tiles?.size).isEqualTo(3)
}
@Test
fun logTileCreated() =
testScope.runTest(USER_INFO_0) {
val specs =
listOf(
TileSpec.create("a"),
CUSTOM_TILE_SPEC,
)
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
runCurrent()
specs.forEach { verify(logger).logTileCreated(it) }
}
@Test
fun logTileNotFoundInFactory() =
testScope.runTest(USER_INFO_0) {
val specs =
listOf(
TileSpec.create("non_existing"),
)
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
runCurrent()
verify(logger, never()).logTileCreated(any())
verify(logger).logTileNotFoundInFactory(specs[0])
}
@Test
fun tileNotAvailableDestroyed_logged() =
testScope.runTest(USER_INFO_0) {
val specs =
listOf(
TileSpec.create("e"),
)
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
runCurrent()
verify(logger, never()).logTileCreated(any())
verify(logger)
.logTileDestroyed(
specs[0],
QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE
)
}
@Test
fun someTilesNotValid_repositorySetToDefinitiveList() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
val specs =
listOf(
TileSpec.create("a"),
TileSpec.create("e"),
)
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
assertThat(tiles).isEqualTo(listOf(TileSpec.create("a")))
}
@Test
fun deduplicatedTiles() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val specs = listOf(TileSpec.create("a"), TileSpec.create("a"))
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
assertThat(tiles?.size).isEqualTo(1)
assertThat(tiles!![0].spec).isEqualTo(specs[0])
}
@Test
fun tilesChange_platformTileNotRecreated() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val specs =
listOf(
TileSpec.create("a"),
)
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
val originalTileA = tiles!![0].tile
tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
assertThat(tiles?.size).isEqualTo(2)
assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
}
@Test
fun tileRemovedIsDestroyed() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val specs = listOf(TileSpec.create("a"), TileSpec.create("c"))
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
val originalTileC = tiles!![1].tile
tileSpecRepository.removeTiles(USER_INFO_0.id, listOf(TileSpec.create("c")))
assertThat(tiles?.size).isEqualTo(1)
assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("a"))
assertThat((originalTileC as FakeQSTile).destroyed).isTrue()
verify(logger)
.logTileDestroyed(
TileSpec.create("c"),
QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
)
}
@Test
fun tileBecomesNotAvailable_destroyed() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val repoTiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
val specs = listOf(TileSpec.create("a"))
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
val originalTileA = tiles!![0].tile
// Tile becomes unavailable
(originalTileA as FakeQSTile).available = false
unavailableTiles.add("a")
// and there is some change in the specs
tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
runCurrent()
assertThat(originalTileA.destroyed).isTrue()
verify(logger)
.logTileDestroyed(
TileSpec.create("a"),
QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE
)
assertThat(tiles?.size).isEqualTo(1)
assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("b"))
assertThat(tiles!![0].tile).isNotSameInstanceAs(originalTileA)
assertThat(repoTiles).isEqualTo(tiles!!.map(TileModel::spec))
}
@Test
fun userChange_tilesChange() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val specs0 = listOf(TileSpec.create("a"))
val specs1 = listOf(TileSpec.create("b"))
tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
switchUser(USER_INFO_1)
assertThat(tiles!![0].spec).isEqualTo(specs1[0])
assertThat(tiles!![0].tile.tileSpec).isEqualTo(specs1[0].spec)
}
@Test
fun tileNotPresentInSecondaryUser_destroyedInUserChange() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val specs0 = listOf(TileSpec.create("a"))
val specs1 = listOf(TileSpec.create("b"))
tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
val originalTileA = tiles!![0].tile
switchUser(USER_INFO_1)
runCurrent()
assertThat((originalTileA as FakeQSTile).destroyed).isTrue()
verify(logger)
.logTileDestroyed(
specs0[0],
QSPipelineLogger.TileDestroyedReason.TILE_NOT_PRESENT_IN_NEW_USER
)
}
@Test
fun userChange_customTileDestroyed_lifecycleNotTerminated() {
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val specs = listOf(CUSTOM_TILE_SPEC)
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
tileSpecRepository.setTiles(USER_INFO_1.id, specs)
val originalCustomTile = tiles!![0].tile
switchUser(USER_INFO_1)
runCurrent()
verify(originalCustomTile).destroy()
assertThat(tileLifecycleManagerFactory.created).isEmpty()
}
}
@Test
fun userChange_sameTileUserChanged() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val specs = listOf(TileSpec.create("a"))
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
tileSpecRepository.setTiles(USER_INFO_1.id, specs)
val originalTileA = tiles!![0].tile as FakeQSTile
assertThat(originalTileA.user).isEqualTo(USER_INFO_0.id)
switchUser(USER_INFO_1)
runCurrent()
assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
assertThat(originalTileA.user).isEqualTo(USER_INFO_1.id)
verify(logger).logTileUserChanged(specs[0], USER_INFO_1.id)
}
@Test
fun addTile() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
val spec = TileSpec.create("a")
val currentSpecs =
listOf(
TileSpec.create("b"),
TileSpec.create("c"),
)
tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
underTest.addTile(spec, position = 1)
val expectedSpecs =
listOf(
TileSpec.create("b"),
spec,
TileSpec.create("c"),
)
assertThat(tiles).isEqualTo(expectedSpecs)
}
@Test
fun addTile_currentUser() =
testScope.runTest(USER_INFO_1) {
val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
val spec = TileSpec.create("a")
val currentSpecs =
listOf(
TileSpec.create("b"),
TileSpec.create("c"),
)
tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
switchUser(USER_INFO_1)
underTest.addTile(spec, position = 1)
assertThat(tiles0).isEqualTo(currentSpecs)
val expectedSpecs =
listOf(
TileSpec.create("b"),
spec,
TileSpec.create("c"),
)
assertThat(tiles1).isEqualTo(expectedSpecs)
}
@Test
fun removeTile_platform() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
runCurrent()
underTest.removeTiles(specs.subList(0, 1))
assertThat(tiles).isEqualTo(specs.subList(1, 2))
}
@Test
fun removeTile_customTile_lifecycleEnded() {
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
runCurrent()
assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
.isTrue()
underTest.removeTiles(listOf(CUSTOM_TILE_SPEC))
assertThat(tiles).isEqualTo(specs.subList(0, 1))
val tileLifecycleManager =
tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT]
assertThat(tileLifecycleManager).isNotNull()
with(inOrder(tileLifecycleManager!!)) {
verify(tileLifecycleManager).onStopListening()
verify(tileLifecycleManager).onTileRemoved()
verify(tileLifecycleManager).flushMessagesAndUnbind()
}
assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
.isFalse()
verify(customTileStatePersister)
.removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id))
}
}
@Test
fun removeTiles_currentUser() =
testScope.runTest {
val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
val currentSpecs =
listOf(
TileSpec.create("a"),
TileSpec.create("b"),
TileSpec.create("c"),
)
tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
switchUser(USER_INFO_1)
runCurrent()
underTest.removeTiles(currentSpecs.subList(0, 2))
assertThat(tiles0).isEqualTo(currentSpecs)
assertThat(tiles1).isEqualTo(currentSpecs.subList(2, 3))
}
@Test
fun setTiles() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
val currentSpecs = listOf(TileSpec.create("a"), TileSpec.create("b"))
tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
runCurrent()
val newSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"), TileSpec.create("a"))
underTest.setTiles(newSpecs)
runCurrent()
assertThat(tiles).isEqualTo(newSpecs)
}
@Test
fun setTiles_customTiles_lifecycleEndedIfGone() =
testScope.runTest(USER_INFO_0) {
val otherCustomTileSpec = TileSpec.create("custom(b/c)")
val currentSpecs = listOf(CUSTOM_TILE_SPEC, TileSpec.create("a"), otherCustomTileSpec)
tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
runCurrent()
val newSpecs =
listOf(
otherCustomTileSpec,
TileSpec.create("a"),
)
underTest.setTiles(newSpecs)
runCurrent()
val tileLifecycleManager =
tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT]!!
with(inOrder(tileLifecycleManager)) {
verify(tileLifecycleManager).onStopListening()
verify(tileLifecycleManager).onTileRemoved()
verify(tileLifecycleManager).flushMessagesAndUnbind()
}
assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
.isFalse()
verify(customTileStatePersister)
.removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id))
}
@Test
fun protoDump() =
testScope.runTest(USER_INFO_0) {
val tiles by collectLastValue(underTest.currentTiles)
val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
tileSpecRepository.setTiles(USER_INFO_0.id, specs)
val stateA = tiles!![0].tile.state
stateA.fillIn(Tile.STATE_INACTIVE, "A", "AA")
val stateCustom = QSTile.BooleanState()
stateCustom.fillIn(Tile.STATE_ACTIVE, "B", "BB")
stateCustom.spec = CUSTOM_TILE_SPEC.spec
whenever(tiles!![1].tile.state).thenReturn(stateCustom)
val proto = SystemUIProtoDump()
underTest.dumpProto(proto, emptyArray())
assertThat(MessageNano.messageNanoEquals(proto.tiles[0], stateA.toProto())).isTrue()
assertThat(MessageNano.messageNanoEquals(proto.tiles[1], stateCustom.toProto()))
.isTrue()
}
private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) {
this.state = state
this.label = label
this.secondaryLabel = secondaryLabel
if (this is BooleanState) {
value = state == Tile.STATE_ACTIVE
}
}
private fun tileCreator(spec: String): QSTile? {
val currentUser = userTracker.userId
return when (spec) {
CUSTOM_TILE_SPEC.spec ->
mock<CustomTile> {
var tileSpecReference: String? = null
whenever(user).thenReturn(currentUser)
whenever(component).thenReturn(CUSTOM_TILE_SPEC.componentName)
whenever(isAvailable).thenReturn(true)
whenever(setTileSpec(anyString())).thenAnswer {
tileSpecReference = it.arguments[0] as? String
Unit
}
whenever(tileSpec).thenAnswer { tileSpecReference }
// Also, add it to the set of added tiles (as this happens as part of the tile
// creation).
customTileAddedRepository.setTileAdded(
CUSTOM_TILE_SPEC.componentName,
currentUser,
true
)
}
in VALID_TILES -> FakeQSTile(currentUser, available = spec !in unavailableTiles)
else -> null
}
}
private fun TestScope.runTest(user: UserInfo, body: suspend TestScope.() -> Unit) {
return runTest {
switchUser(user)
body()
}
}
private suspend fun switchUser(user: UserInfo) {
setUserTracker(user.id)
userRepository.setSelectedUserInfo(user)
}
private fun setUserTracker(user: Int) {
val mockContext = mockUserContext(user)
whenever(userTracker.userContext).thenReturn(mockContext)
whenever(userTracker.userId).thenReturn(user)
}
private class TLMFactory : TileLifecycleManager.Factory {
val created = mutableMapOf<Pair<Int, ComponentName>, TileLifecycleManager>()
override fun create(intent: Intent, userHandle: UserHandle): TileLifecycleManager {
val componentName = intent.component!!
val user = userHandle.identifier
val manager: TileLifecycleManager = mock()
created[user to componentName] = manager
return manager
}
}
private fun mockUserContext(user: Int): Context {
return mock {
whenever(this.userId).thenReturn(user)
whenever(this.user).thenReturn(UserHandle.of(user))
}
}
companion object {
private val USER_INFO_0 = UserInfo().apply { id = 0 }
private val USER_INFO_1 = UserInfo().apply { id = 1 }
private val VALID_TILES = setOf("a", "b", "c", "d", "e")
private val TEST_COMPONENT = ComponentName("pkg", "cls")
private val CUSTOM_TILE_SPEC = TileSpec.Companion.create(TEST_COMPONENT)
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.domain.interactor
import android.content.Context
import android.view.View
import com.android.internal.logging.InstanceId
import com.android.systemui.plugins.qs.QSIconView
import com.android.systemui.plugins.qs.QSTile
class FakeQSTile(
var user: Int,
var available: Boolean = true,
) : QSTile {
private var tileSpec: String? = null
var destroyed = false
private val state = QSTile.State()
override fun getTileSpec(): String? {
return tileSpec
}
override fun isAvailable(): Boolean {
return available
}
override fun setTileSpec(tileSpec: String?) {
this.tileSpec = tileSpec
state.spec = tileSpec
}
override fun refreshState() {}
override fun addCallback(callback: QSTile.Callback?) {}
override fun removeCallback(callback: QSTile.Callback?) {}
override fun removeCallbacks() {}
override fun createTileView(context: Context?): QSIconView? {
return null
}
override fun click(view: View?) {}
override fun secondaryClick(view: View?) {}
override fun longClick(view: View?) {}
override fun userSwitch(currentUser: Int) {
user = currentUser
}
override fun getMetricsCategory(): Int {
return 0
}
override fun setListening(client: Any?, listening: Boolean) {}
override fun setDetailListening(show: Boolean) {}
override fun destroy() {
destroyed = true
}
override fun getTileLabel(): CharSequence {
return ""
}
override fun getState(): QSTile.State {
return state
}
override fun getInstanceId(): InstanceId {
return InstanceId.fakeInstanceId(0)
}
override fun isListening(): Boolean {
return false
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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
import android.content.Context
import com.android.systemui.plugins.qs.QSFactory
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTileView
class FakeQSFactory(private val tileCreator: (String) -> QSTile?) : QSFactory {
override fun createTile(tileSpec: String): QSTile? {
return tileCreator(tileSpec)
}
override fun createTileView(
context: Context?,
tile: QSTile?,
collapsedView: Boolean
): QSTileView {
throw NotImplementedError("Not implemented")
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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
class FakeCustomTileAddedRepository : CustomTileAddedRepository {
private val tileAddedRegistry = mutableSetOf<Pair<Int, ComponentName>>()
override fun isTileAdded(componentName: ComponentName, userId: Int): Boolean {
return (userId to componentName) in tileAddedRegistry
}
override fun setTileAdded(componentName: ComponentName, userId: Int, added: Boolean) {
if (added) {
tileAddedRegistry.add(userId to componentName)
} else {
tileAddedRegistry.remove(userId to componentName)
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.util.Log
import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
import com.android.systemui.qs.pipeline.shared.TileSpec
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeTileSpecRepository : TileSpecRepository {
private val tilesPerUser = mutableMapOf<Int, MutableStateFlow<List<TileSpec>>>()
override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> {
return getFlow(userId).asStateFlow().also { Log.d("Fabian", "Retrieving flow for $userId") }
}
override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) {
if (tile == TileSpec.Invalid) return
with(getFlow(userId)) {
value =
value.toMutableList().apply {
if (position == POSITION_AT_END) {
add(tile)
} else {
add(position, tile)
}
}
}
}
override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) {
with(getFlow(userId)) {
value =
value.toMutableList().apply { removeAll(tiles.filter { it != TileSpec.Invalid }) }
}
}
override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) {
getFlow(userId).value = tiles.filter { it != TileSpec.Invalid }
}
private fun getFlow(userId: Int): MutableStateFlow<List<TileSpec>> =
tilesPerUser.getOrPut(userId) { MutableStateFlow(emptyList()) }
}