Add adapter to support new ViewModel
This adapter would proxy old QSTile interface to the new QSTileViewModel
one. It also misses some of the behaviour of QSTileImpl like logging,
analytics and policy restrictions. I plan to add it in future CLs
because those are improvements to the ViewModel and/or base stack and
are out of scope of this CL to keep its size manageable.
More tests autotests covering this adapter TBD
Test: atest QSTileHostTest CurrentTilesInteractorImplTest TileQueryHelperTest QSTileIntentUserActionHandlerTest QSTileViewModelInterfaceComplianceTest
Bug: 299908705
Change-Id: Ib38714b0fbfff456dd14316152ae1c7a8662fc39
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 4c292e7..292c4f8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -48,6 +48,7 @@
import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository;
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository;
+import com.android.systemui.qs.tiles.di.NewQSTileFactory;
import com.android.systemui.settings.UserFileManager;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
@@ -56,6 +57,8 @@
import com.android.systemui.tuner.TunerService.Tunable;
import com.android.systemui.util.settings.SecureSettings;
+import dagger.Lazy;
+
import org.jetbrains.annotations.NotNull;
import java.io.PrintWriter;
@@ -121,6 +124,7 @@
@Inject
public QSTileHost(Context context,
+ Lazy<NewQSTileFactory> newQsTileFactoryProvider,
QSFactory defaultFactory,
@Main Executor mainExecutor,
PluginManager pluginManager,
@@ -147,6 +151,9 @@
mShadeController = shadeController;
+ if (featureFlags.getPipelineTilesEnabled()) {
+ mQsFactories.add(newQsTileFactoryProvider.get());
+ }
mQsFactories.add(defaultFactory);
pluginManager.addPluginListener(this, QSFactory.class, true);
mUserTracker = userTracker;
@@ -326,7 +333,6 @@
try {
tile = createTile(tileSpec);
if (tile != null) {
- tile.setTileSpec(tileSpec);
if (tile.isAvailable()) {
newTiles.put(tileSpec, tile);
mQSLogger.logTileAdded(tileSpec);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index d9f4484..6d92e2d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -130,11 +130,9 @@
if (tile == null) {
continue;
} else if (!tile.isAvailable()) {
- tile.setTileSpec(spec);
tile.destroy();
continue;
}
- tile.setTileSpec(spec);
tilesToAdd.add(tile);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
index 03de3a0..c6ffd78 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -31,6 +31,7 @@
import com.android.systemui.qs.external.QSExternalModule;
import com.android.systemui.qs.pipeline.dagger.QSPipelineModule;
import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel;
import com.android.systemui.statusbar.phone.AutoTileManager;
import com.android.systemui.statusbar.phone.ManagedProfileController;
import com.android.systemui.statusbar.policy.CastController;
@@ -41,14 +42,14 @@
import com.android.systemui.statusbar.policy.WalletController;
import com.android.systemui.util.settings.SecureSettings;
-import java.util.Map;
-
-import javax.inject.Named;
-
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.Multibinds;
+import java.util.Map;
+
+import javax.inject.Named;
+
/**
* Module for QS dependencies
*/
@@ -68,6 +69,11 @@
@Multibinds
Map<String, QSTileImpl<?>> tileMap();
+ /** A map of internal QS tile ViewModels. Ensures that this can be injected even if
+ * it is empty */
+ @Multibinds
+ Map<String, QSTileViewModel> tileViewModelMap();
+
@Provides
@SysUISingleton
static AutoTileManager provideAutoTileManager(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 5a5e47a..9f921fd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -40,10 +40,12 @@
import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.qs.tiles.di.NewQSTileFactory
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 dagger.Lazy
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -130,6 +132,7 @@
private val installedTilesComponentRepository: InstalledTilesComponentRepository,
private val userRepository: UserRepository,
private val customTileStatePersister: CustomTileStatePersister,
+ private val newQSTileFactory: Lazy<NewQSTileFactory>,
private val tileFactory: QSFactory,
private val customTileAddedRepository: CustomTileAddedRepository,
private val tileLifecycleManagerFactory: TileLifecycleManager.Factory,
@@ -138,7 +141,7 @@
@Background private val backgroundDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
private val logger: QSPipelineLogger,
- featureFlags: QSPipelineFlagsRepository,
+ private val featureFlags: QSPipelineFlagsRepository,
) : CurrentTilesInteractor {
private val _currentSpecsAndTiles: MutableStateFlow<List<TileModel>> =
@@ -331,12 +334,19 @@
}
private suspend fun createTile(spec: TileSpec): QSTile? {
- val tile = withContext(mainDispatcher) { tileFactory.createTile(spec.spec) }
+ val tile =
+ withContext(mainDispatcher) {
+ if (featureFlags.pipelineTilesEnabled) {
+ newQSTileFactory.get().createTile(spec.spec)
+ } else {
+ null
+ }
+ ?: tileFactory.createTile(spec.spec)
+ }
if (tile == null) {
logger.logTileNotFoundInFactory(spec)
return null
} else {
- tile.tileSpec = spec.spec
return if (!tile.isAvailable) {
logger.logTileDestroyed(
spec,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt
index 551b0f4..1a71b71 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepository.kt
@@ -1,7 +1,7 @@
package com.android.systemui.qs.pipeline.shared
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import javax.inject.Inject
@@ -10,7 +10,7 @@
class QSPipelineFlagsRepository
@Inject
constructor(
- private val featureFlags: FeatureFlags,
+ private val featureFlags: FeatureFlagsClassic,
) {
/** @see Flags.QS_PIPELINE_NEW_HOST */
@@ -20,4 +20,8 @@
/** @see Flags.QS_PIPELINE_AUTO_ADD */
val pipelineAutoAddEnabled: Boolean
get() = pipelineHostEnabled && featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)
+
+ /** @see Flags.QS_PIPELINE_NEW_TILES */
+ val pipelineTilesEnabled: Boolean
+ get() = featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_TILES)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
index 11b5dd7..aed08f8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
@@ -31,11 +31,7 @@
sealed class TileSpec private constructor(open val spec: String) {
/** Represents a spec that couldn't be parsed into a valid type of tile. */
- object Invalid : TileSpec("") {
- override fun toString(): String {
- return "TileSpec.INVALID"
- }
- }
+ data object Invalid : TileSpec("")
/** Container for the spec of a tile provided by SystemUI. */
data class PlatformTileSpec
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index 9c7a734..632aa63 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -70,6 +70,7 @@
if (tile != null) {
tile.initialize();
tile.postStale(); // Tile was just created, must be stale.
+ tile.setTileSpec(tileSpec);
}
return tile;
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt
index e9f907c..dc9e115 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt
@@ -1,11 +1,11 @@
package com.android.systemui.qs.tiles.base.actions
import android.content.Intent
+import android.view.View
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
import javax.inject.Inject
/**
@@ -17,9 +17,9 @@
@Inject
constructor(private val activityStarter: ActivityStarter) {
- fun handle(userAction: QSTileUserAction, intent: Intent) {
+ fun handle(view: View?, intent: Intent) {
val animationController: ActivityLaunchAnimator.Controller? =
- userAction.view?.let {
+ view?.let {
ActivityLaunchAnimator.Controller.fromView(
it,
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt
index c2a75fa..bb4de80 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt
@@ -92,7 +92,7 @@
.stateIn(
tileScope,
SharingStarted.WhileSubscribed(),
- false,
+ true,
)
private var currentLifeState: QSTileLifecycle = QSTileLifecycle.DEAD
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
new file mode 100644
index 0000000..3fedbfc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
@@ -0,0 +1,28 @@
+package com.android.systemui.qs.tiles.di
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.qs.QSFactory
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.tiles.viewmodel.QSTileLifecycle
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModelAdapter
+import javax.inject.Inject
+import javax.inject.Provider
+
+// TODO(b/http://b/299909989): Rename the factory after rollout
+@SysUISingleton
+class NewQSTileFactory
+@Inject
+constructor(
+ private val adapterFactory: QSTileViewModelAdapter.Factory,
+ private val tileMap:
+ Map<String, @JvmSuppressWildcards Provider<@JvmSuppressWildcards QSTileViewModel>>,
+) : QSFactory {
+
+ override fun createTile(tileSpec: String): QSTile? =
+ tileMap[tileSpec]?.let {
+ val tile = it.get()
+ tile.onLifecycle(QSTileLifecycle.ALIVE)
+ adapterFactory.create(tile)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
index a5eaac1..019d3c0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
@@ -1,14 +1,13 @@
package com.android.systemui.qs.tiles.viewmodel
-import android.graphics.drawable.Icon
+import androidx.annotation.StringRes
+import com.android.internal.logging.InstanceId
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.qs.pipeline.shared.TileSpec
data class QSTileConfig(
val tileSpec: TileSpec,
val tileIcon: Icon,
- val tileLabel: CharSequence,
-// TODO(b/299908705): Fill necessary params
-/*
-val instanceId: InstanceId,
- */
+ @StringRes val tileLabelRes: Int,
+ val instanceId: InstanceId,
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
index 53f9edf..dc5c690 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
@@ -1,18 +1,96 @@
package com.android.systemui.qs.tiles.viewmodel
-import android.graphics.drawable.Icon
+import android.service.quicksettings.Tile
+import com.android.systemui.common.shared.model.Icon
+/**
+ * Represents current a state of the tile to be displayed in on the view. Consider using
+ * [QSTileState.build] for better state creation experience and preset default values for certain
+ * fields.
+ *
+ * // TODO(b/http://b/299909989): Clean up legacy mappings after the transition
+ */
data class QSTileState(
- val icon: Icon,
+ val icon: () -> Icon,
val label: CharSequence,
-// TODO(b/299908705): Fill necessary params
-/*
- val subtitle: CharSequence = "",
- val activeState: ActivationState = Active,
- val enabledState: Enabled = Enabled,
- val loopIconAnimation: Boolean = false,
- val secondaryIcon: Icon? = null,
- val slashState: SlashState? = null,
- val supportedActions: Collection<UserAction> = listOf(Click), clicks should be a default action
-*/
-)
+ val activationState: ActivationState,
+ val secondaryLabel: CharSequence?,
+ val supportedActions: Set<UserAction>,
+ val contentDescription: CharSequence?,
+ val stateDescription: CharSequence?,
+ val sideViewIcon: SideViewIcon,
+ val enabledState: EnabledState,
+ val expandedAccessibilityClassName: String?,
+) {
+
+ companion object {
+
+ fun build(icon: () -> Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState =
+ Builder(icon, label).apply(build).build()
+
+ fun build(icon: Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState =
+ build({ icon }, label, build)
+ }
+
+ enum class ActivationState(val legacyState: Int) {
+ // An unavailable state indicates that for some reason this tile is not currently available
+ // to the user, and will have no click action. The tile's icon will be tinted differently to
+ // reflect this state.
+ UNAVAILABLE(Tile.STATE_UNAVAILABLE),
+ // This represents a tile that is currently active. (e.g. wifi is connected, bluetooth is
+ // on, cast is casting). This is the default state.
+ ACTIVE(Tile.STATE_ACTIVE),
+ // This represents a tile that is currently in a disabled state but is still interactable. A
+ // disabled state indicates that the tile is not currently active (e.g. wifi disconnected or
+ // bluetooth disabled), but is still interactable by the user to modify this state.
+ INACTIVE(Tile.STATE_INACTIVE),
+ }
+
+ /**
+ * Enabled tile behaves as usual where is disabled one is frozen and inactive in its current
+ * [ActivationState].
+ */
+ enum class EnabledState {
+ ENABLED,
+ DISABLED,
+ }
+
+ enum class UserAction {
+ CLICK,
+ LONG_CLICK,
+ }
+
+ sealed interface SideViewIcon {
+ data class Custom(val icon: Icon) : SideViewIcon
+ data object Chevron : SideViewIcon
+ data object None : SideViewIcon
+ }
+
+ class Builder(
+ var icon: () -> Icon,
+ var label: CharSequence,
+ ) {
+ var activationState: ActivationState = ActivationState.INACTIVE
+ var secondaryLabel: CharSequence? = null
+ var supportedActions: Set<UserAction> = setOf(UserAction.CLICK)
+ var contentDescription: CharSequence? = null
+ var stateDescription: CharSequence? = null
+ var sideViewIcon: SideViewIcon = SideViewIcon.None
+ var enabledState: EnabledState = EnabledState.ENABLED
+ var expandedAccessibilityClassName: String? = null
+
+ fun build(): QSTileState =
+ QSTileState(
+ icon,
+ label,
+ activationState,
+ secondaryLabel,
+ supportedActions,
+ contentDescription,
+ stateDescription,
+ sideViewIcon,
+ enabledState,
+ expandedAccessibilityClassName,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
index f1f8f01..0b232c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
@@ -1,13 +1,11 @@
package com.android.systemui.qs.tiles.viewmodel
-import android.content.Context
import android.view.View
sealed interface QSTileUserAction {
- val context: Context
val view: View?
- class Click(override val context: Context, override val view: View?) : QSTileUserAction
- class LongClick(override val context: Context, override val view: View?) : QSTileUserAction
+ class Click(override val view: View?) : QSTileUserAction
+ class LongClick(override val view: View?) : QSTileUserAction
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
new file mode 100644
index 0000000..d4bdb77
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -0,0 +1,232 @@
+package com.android.systemui.qs.tiles.viewmodel
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import androidx.annotation.GuardedBy
+import com.android.internal.logging.InstanceId
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon
+import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.function.Supplier
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.flow.collectIndexed
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+// TODO(b/http://b/299909989): Use QSTileViewModel directly after the rollout
+class QSTileViewModelAdapter
+@AssistedInject
+constructor(
+ private val qsHost: QSHost,
+ @Assisted private val qsTileViewModel: QSTileViewModel,
+) : QSTile {
+
+ private val context
+ get() = qsHost.context
+
+ @GuardedBy("callbacks")
+ private val callbacks: MutableCollection<QSTile.Callback> = mutableSetOf()
+ @GuardedBy("listeningClients")
+ private val listeningClients: MutableCollection<Any> = mutableSetOf()
+
+ // Cancels the jobs when the adapter is no longer alive
+ private val adapterScope = CoroutineScope(SupervisorJob())
+ // Cancels the jobs when clients stop listening
+ private val listeningScope = CoroutineScope(SupervisorJob())
+
+ init {
+ adapterScope.launch {
+ qsTileViewModel.isAvailable.collectIndexed { index, isAvailable ->
+ if (!isAvailable) {
+ qsHost.removeTile(tileSpec)
+ }
+ // qsTileViewModel.isAvailable flow often starts with isAvailable == true. That's
+ // why we only allow isAvailable == true once and throw an exception afterwards.
+ if (index > 0 && isAvailable) {
+ // See com.android.systemui.qs.pipeline.domain.model.AutoAddable for additional
+ // guidance on how to auto add your tile
+ throw UnsupportedOperationException("Turning on tile is not supported now")
+ }
+ }
+ }
+
+ // QSTileHost doesn't call this when userId is initialized
+ userSwitch(qsHost.userId)
+
+ if (DEBUG) {
+ Log.d(TAG, "Using new tiles for: $tileSpec")
+ }
+ }
+
+ override fun isAvailable(): Boolean = qsTileViewModel.isAvailable.value
+
+ override fun setTileSpec(tileSpec: String?) {
+ throw UnsupportedOperationException("Tile spec is immutable in new tiles")
+ }
+
+ override fun refreshState() {
+ qsTileViewModel.forceUpdate()
+ }
+
+ override fun addCallback(callback: QSTile.Callback?) {
+ callback ?: return
+ synchronized(callbacks) { callbacks.add(callback) }
+ }
+
+ override fun removeCallback(callback: QSTile.Callback?) {
+ callback ?: return
+ synchronized(callbacks) { callbacks.remove(callback) }
+ }
+
+ override fun removeCallbacks() {
+ synchronized(callbacks) { callbacks.clear() }
+ }
+
+ override fun click(view: View?) {
+ if (isActionSupported(QSTileState.UserAction.CLICK)) {
+ qsTileViewModel.onActionPerformed(QSTileUserAction.Click(view))
+ }
+ }
+
+ override fun secondaryClick(view: View?) {
+ if (isActionSupported(QSTileState.UserAction.CLICK)) {
+ qsTileViewModel.onActionPerformed(QSTileUserAction.Click(view))
+ }
+ }
+
+ override fun longClick(view: View?) {
+ if (isActionSupported(QSTileState.UserAction.LONG_CLICK)) {
+ qsTileViewModel.onActionPerformed(QSTileUserAction.LongClick(view))
+ }
+ }
+
+ private fun isActionSupported(action: QSTileState.UserAction): Boolean =
+ qsTileViewModel.currentState?.supportedActions?.contains(action) == true
+
+ override fun userSwitch(currentUser: Int) {
+ qsTileViewModel.onUserIdChanged(currentUser)
+ }
+
+ @Deprecated(
+ "Not needed as {@link com.android.internal.logging.UiEvent} will use #getMetricsSpec",
+ replaceWith = ReplaceWith("getMetricsSpec"),
+ )
+ override fun getMetricsCategory(): Int = 0
+
+ override fun setListening(client: Any?, listening: Boolean) {
+ client ?: return
+ synchronized(listeningClients) {
+ if (listening) {
+ listeningClients.add(client)
+ if (listeningClients.size == 1) {
+ qsTileViewModel.state
+ .map { mapState(context, it, qsTileViewModel.config) }
+ .onEach { legacyState ->
+ synchronized(callbacks) {
+ callbacks.forEach { it.onStateChanged(legacyState) }
+ }
+ }
+ .launchIn(listeningScope)
+ }
+ } else {
+ listeningClients.remove(client)
+ if (listeningClients.isEmpty()) {
+ listeningScope.coroutineContext.cancelChildren()
+ }
+ }
+ }
+ }
+
+ override fun isListening(): Boolean =
+ synchronized(listeningClients) { listeningClients.isNotEmpty() }
+
+ override fun setDetailListening(show: Boolean) {
+ // do nothing like QSTileImpl
+ }
+
+ override fun destroy() {
+ adapterScope.cancel()
+ listeningScope.cancel()
+ qsTileViewModel.onLifecycle(QSTileLifecycle.DEAD)
+ }
+
+ override fun getState(): QSTile.State? =
+ qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) }
+
+ override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId
+ override fun getTileLabel(): CharSequence =
+ context.getString(qsTileViewModel.config.tileLabelRes)
+ override fun getTileSpec(): String = qsTileViewModel.config.tileSpec.spec
+
+ private companion object {
+
+ const val DEBUG = false
+ const val TAG = "QSTileVMAdapter"
+
+ fun mapState(
+ context: Context,
+ viewModelState: QSTileState,
+ config: QSTileConfig
+ ): QSTile.State =
+ // we have to use QSTile.BooleanState to support different side icons
+ // which are bound to instanceof QSTile.BooleanState in QSTileView.
+ QSTile.BooleanState().apply {
+ spec = config.tileSpec.spec
+ label = viewModelState.label
+ // This value is synthetic and doesn't have any meaning
+ value = false
+
+ secondaryLabel = viewModelState.secondaryLabel
+ handlesLongClick =
+ viewModelState.supportedActions.contains(QSTileState.UserAction.LONG_CLICK)
+
+ iconSupplier = Supplier {
+ when (val stateIcon = viewModelState.icon()) {
+ is Icon.Loaded -> DrawableIcon(stateIcon.drawable)
+ is Icon.Resource -> ResourceIcon.get(stateIcon.res)
+ }
+ }
+ state = viewModelState.activationState.legacyState
+
+ contentDescription = viewModelState.contentDescription
+ stateDescription = viewModelState.stateDescription
+
+ disabledByPolicy = viewModelState.enabledState == QSTileState.EnabledState.DISABLED
+ expandedAccessibilityClassName = viewModelState.expandedAccessibilityClassName
+
+ when (viewModelState.sideViewIcon) {
+ is QSTileState.SideViewIcon.Custom -> {
+ sideViewCustomDrawable =
+ when (viewModelState.sideViewIcon.icon) {
+ is Icon.Loaded -> viewModelState.sideViewIcon.icon.drawable
+ is Icon.Resource ->
+ context.getDrawable(viewModelState.sideViewIcon.icon.res)
+ }
+ }
+ is QSTileState.SideViewIcon.Chevron -> {
+ forceExpandIcon = true
+ }
+ is QSTileState.SideViewIcon.None -> {
+ forceExpandIcon = false
+ }
+ }
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+
+ fun create(qsTileViewModel: QSTileViewModel): QSTileViewModelAdapter
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 64d3b82..1c70d20 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -66,6 +66,7 @@
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository;
import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.qs.tiles.di.NewQSTileFactory;
import com.android.systemui.settings.UserFileManager;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
@@ -77,6 +78,8 @@
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.time.FakeSystemClock;
+import dagger.Lazy;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -102,8 +105,6 @@
private static final String SETTING = QSHost.TILES_SETTING;
@Mock
- private QSFactory mDefaultFactory;
- @Mock
private PluginManager mPluginManager;
@Mock
private TunerService mTunerService;
@@ -117,7 +118,6 @@
private CustomTile mCustomTile;
@Mock
private UserTracker mUserTracker;
- private SecureSettings mSecureSettings;
@Mock
private CustomTileStatePersister mCustomTileStatePersister;
@Mock
@@ -127,6 +127,10 @@
@Mock
private UserFileManager mUserFileManager;
+ private SecureSettings mSecureSettings;
+
+ private QSFactory mDefaultFactory;
+
private SparseArray<SharedPreferences> mSharedPreferencesByUser;
private FakeFeatureFlags mFeatureFlags;
@@ -144,6 +148,8 @@
mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false);
mFeatureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, false);
+ // TODO(b/299909337): Add test checking the new factory is used when the flag is on
+ mFeatureFlags.set(Flags.QS_PIPELINE_NEW_TILES, false);
mQSPipelineFlagsRepository = new QSPipelineFlagsRepository(mFeatureFlags);
mMainExecutor = new FakeExecutor(new FakeSystemClock());
@@ -164,7 +170,8 @@
mSecureSettings = new FakeSettings();
saveSetting("");
- mQSTileHost = new TestQSTileHost(mContext, mDefaultFactory, mMainExecutor,
+ setUpTileFactory();
+ mQSTileHost = new TestQSTileHost(mContext, () -> null, mDefaultFactory, mMainExecutor,
mPluginManager, mTunerService, () -> mAutoTiles, mShadeController,
mQSLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister,
mTileLifecycleManagerFactory, mUserFileManager, mQSPipelineFlagsRepository);
@@ -178,7 +185,6 @@
mMainExecutor.runAllReady();
}
}, mUserTracker.getUserId());
- setUpTileFactory();
}
private void saveSetting(String value) {
@@ -191,32 +197,29 @@
}
private void setUpTileFactory() {
- // Only create this kind of tiles
- when(mDefaultFactory.createTile(anyString())).thenAnswer(
- invocation -> {
- String spec = invocation.getArgument(0);
- if ("spec1".equals(spec)) {
- return new TestTile1(mQSTileHost);
- } else if ("spec2".equals(spec)) {
- return new TestTile2(mQSTileHost);
- } else if ("spec3".equals(spec)) {
- return new TestTile3(mQSTileHost);
- } else if ("na".equals(spec)) {
- return new NotAvailableTile(mQSTileHost);
- } else if (CUSTOM_TILE_SPEC.equals(spec)) {
- QSTile tile = mCustomTile;
- QSTile.State s = mock(QSTile.State.class);
- s.spec = spec;
- when(mCustomTile.getState()).thenReturn(s);
- return tile;
- } else if ("internet".equals(spec)
- || "wifi".equals(spec)
- || "cell".equals(spec)) {
- return new TestTile1(mQSTileHost);
- } else {
- return null;
- }
- });
+ mDefaultFactory = new FakeQSFactory(spec -> {
+ if ("spec1".equals(spec)) {
+ return new TestTile1(mQSTileHost);
+ } else if ("spec2".equals(spec)) {
+ return new TestTile2(mQSTileHost);
+ } else if ("spec3".equals(spec)) {
+ return new TestTile3(mQSTileHost);
+ } else if ("na".equals(spec)) {
+ return new NotAvailableTile(mQSTileHost);
+ } else if (CUSTOM_TILE_SPEC.equals(spec)) {
+ QSTile tile = mCustomTile;
+ QSTile.State s = mock(QSTile.State.class);
+ s.spec = spec;
+ when(mCustomTile.getState()).thenReturn(s);
+ return tile;
+ } else if ("internet".equals(spec)
+ || "wifi".equals(spec)
+ || "cell".equals(spec)) {
+ return new TestTile1(mQSTileHost);
+ } else {
+ return null;
+ }
+ });
when(mCustomTile.isAvailable()).thenReturn(true);
}
@@ -703,7 +706,7 @@
}
private class TestQSTileHost extends QSTileHost {
- TestQSTileHost(Context context,
+ TestQSTileHost(Context context, Lazy<NewQSTileFactory> newQSTileFactoryProvider,
QSFactory defaultFactory, Executor mainExecutor,
PluginManager pluginManager, TunerService tunerService,
Provider<AutoTileManager> autoTiles,
@@ -712,7 +715,7 @@
CustomTileStatePersister customTileStatePersister,
TileLifecycleManager.Factory tileLifecycleManagerFactory,
UserFileManager userFileManager, QSPipelineFlagsRepository featureFlags) {
- super(context, defaultFactory, mainExecutor, pluginManager,
+ super(context, newQSTileFactoryProvider, defaultFactory, mainExecutor, pluginManager,
tunerService, autoTiles, shadeController, qsLogger,
userTracker, secureSettings, customTileStatePersister,
tileLifecycleManagerFactory, userFileManager, featureFlags);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
index 2eed38f..1af194a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
@@ -124,6 +124,7 @@
if (FACTORY_TILES.contains(spec)) {
FakeQSTile tile = new FakeQSTile(mBgExecutor, mMainExecutor);
tile.setState(mState);
+ tile.setTileSpec(spec);
return tile;
} else {
return null;
@@ -284,7 +285,10 @@
Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, null);
QSTile t = mock(QSTile.class);
- when(mQSHost.createTile("hotspot")).thenReturn(t);
+ when(mQSHost.createTile("hotspot")).thenAnswer(invocation -> {
+ t.setTileSpec("hotspot");
+ return t;
+ });
mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock,
"hotspot");
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index dc1b9c4..a750524 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -46,6 +46,7 @@
import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.qs.tiles.di.NewQSTileFactory
import com.android.systemui.qs.toProto
import com.android.systemui.settings.UserTracker
import com.android.systemui.user.data.repository.FakeUserRepository
@@ -91,6 +92,8 @@
@Mock private lateinit var logger: QSPipelineLogger
+ @Mock private lateinit var newQSTileFactory: NewQSTileFactory
+
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
@@ -105,6 +108,8 @@
featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true)
featureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, true)
+ // TODO(b/299909337): Add test checking the new factory is used when the flag is on
+ featureFlags.set(Flags.QS_PIPELINE_NEW_TILES, true)
userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
@@ -117,6 +122,7 @@
userRepository = userRepository,
customTileStatePersister = customTileStatePersister,
tileFactory = tileFactory,
+ newQSTileFactory = { newQSTileFactory },
customTileAddedRepository = customTileAddedRepository,
tileLifecycleManagerFactory = tileLifecycleManagerFactory,
userTracker = userTracker,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/QSTileIntentUserActionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/QSTileIntentUserActionHandlerTest.kt
index 47b4244..077c813 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/QSTileIntentUserActionHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/QSTileIntentUserActionHandlerTest.kt
@@ -6,7 +6,6 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserActionHandler
-import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,7 +33,7 @@
fun testPassesIntentToStarter() {
val intent = Intent("test.ACTION")
- underTest.handle(QSTileUserAction.Click(context, null), intent)
+ underTest.handle(null, intent)
verify(activityStarted).postStartActivityDismissingKeyguard(eq(intent), eq(0), any())
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
index 643866e..eacb080 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
@@ -1,11 +1,14 @@
package com.android.systemui.qs.tiles.viewmodel
-import android.graphics.drawable.Icon
+import android.graphics.drawable.ShapeDrawable
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.MediumTest
+import com.android.internal.logging.InstanceId
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor
@@ -71,20 +74,21 @@
fakeQSTileUserActionInteractor,
fakeQSTileDataInteractor,
object : QSTileDataToStateMapper<Any> {
- override fun map(config: QSTileConfig, data: Any): QSTileState {
- return QSTileState(config.tileIcon, config.tileLabel)
- }
+ override fun map(config: QSTileConfig, data: Any): QSTileState =
+ QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}
},
testCoroutineDispatcher,
tileScope = scope.backgroundScope,
) {}
private companion object {
+
val TEST_QS_TILE_CONFIG =
QSTileConfig(
TileSpec.create("default"),
- Icon.createWithContentUri(""),
- "",
+ Icon.Loaded(ShapeDrawable(), null),
+ 0,
+ InstanceId.fakeInstanceId(0),
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt
index bf26e71..cbf4ae5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSFactory.kt
@@ -21,6 +21,6 @@
class FakeQSFactory(private val tileCreator: (String) -> QSTile?) : QSFactory {
override fun createTile(tileSpec: String): QSTile? {
- return tileCreator(tileSpec)
+ return tileCreator(tileSpec)?.also { it.tileSpec = tileSpec }
}
}