Merge "Add a CurrentTilesInteractor" into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
91cc19947d
@@ -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