Add a CurrentTilesInteractor
This will be the layer that will interact with UI (like QSPanel, QSCustomizer) as well as API (CommandQueue) to retrieve and modify the list of current tiles. For now it exposes state methods for interop with Java. When the flag is off, it won't collect changes in the repository (to prevent tiles being created twice). QSTileHost will be used. When the flag is on, QSTileHost won't react to changes, and CurrentTilesInteractor will be used to track the tiles. Also, provide a QSHostAdapter that switches between QSTileHost and CurrentTilesInteractor so the new behavior can be observed without having to change consumers. Flag: QS_PIPELINE_NEW_HOST Test: atest com.android.systemui.qs Test: atest android.host.systemui CtsTileServiceTestCases Test: manual, enable flag and observe no difference Fixes: 275597752 Change-Id: I3bafe78c7f191faca1a9dd6e7e9d22c88c70926f
This commit is contained in:
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
226
packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt
Normal file
226
packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()) }
|
||||
}
|
||||
Reference in New Issue
Block a user