summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSHost.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt226
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt344
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/TileModel.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt55
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java35
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt44
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt674
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt95
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt36
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt36
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt61
18 files changed, 1656 insertions, 55 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
index ce690e239da0..57b479e6899f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
@@ -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);
/**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt
new file mode 100644
index 000000000000..14acb4b3ccf4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt
@@ -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
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 8bbdeeda356c..0ca897373d13 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -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);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt
index 964fe7104324..3ddd9f1cfe5f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSHostModule.kt
@@ -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)) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
index 00f0a67dbe22..e212bc4e7f5d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
@@ -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
@@ -38,6 +40,11 @@ abstract class QSPipelineModule {
abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository
@Binds
+ abstract fun bindCurrentTilesInteractor(
+ impl: CurrentTilesInteractorImpl
+ ): CurrentTilesInteractor
+
+ @Binds
@IntoMap
@ClassKey(PrototypeCoreStartable::class)
abstract fun providePrototypeCoreStartable(startable: PrototypeCoreStartable): CoreStartable
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
index d254e1b3d0d7..595b29a9dcb8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
@@ -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())
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
new file mode 100644
index 000000000000..91c6e8b5fcb6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -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
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/TileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/TileModel.kt
new file mode 100644
index 000000000000..e2381ecd8b97
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/TileModel.kt
@@ -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)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt
index 69d8248a11f5..89408006c300 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt
@@ -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) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
index c691c2f668ad..af1cd0995a21 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
@@ -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)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
index 200f7431e906..767ce919d027 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
@@ -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"),
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 34d2b14d46a9..aa92177e863e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
index c03849b35f54..50a8d2630d79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
@@ -170,6 +170,21 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
}
@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 {
val tilesUser0 by collectLastValue(underTest.tilesSpecs(0))
@@ -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())
@@ -246,6 +261,19 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
}
@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 {
val tiles by collectLastValue(underTest.tilesSpecs(0))
@@ -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")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
new file mode 100644
index 000000000000..7ecb4dc9376a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -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)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
new file mode 100644
index 000000000000..e50969641a71
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
@@ -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
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt
new file mode 100644
index 000000000000..9383a0a68844
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt
@@ -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")
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt
new file mode 100644
index 000000000000..777130409aad
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeCustomTileAddedRepository.kt
@@ -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)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
new file mode 100644
index 000000000000..2865710c2eae
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
@@ -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()) }
+}