summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddLog.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSetting.kt82
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingList.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddable.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddable.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt70
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddable.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt91
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt68
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt94
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddable.kt57
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractor.kt123
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddSignal.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddTracking.kt49
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddable.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt44
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt120
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt28
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt99
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt117
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt139
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt133
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt85
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt115
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt83
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddableTest.kt126
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt108
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt163
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt81
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddableTest.kt123
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt201
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt42
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/autoaddable/FakeAutoAddable.kt52
43 files changed, 2945 insertions, 127 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index c99e5c5c4ab0..b318edd643a6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -161,7 +161,9 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P
// finishes before creating any tiles.
tunerService.addTunable(this, TILES_SETTING);
// AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
- mAutoTiles = autoTiles.get();
+ if (!mFeatureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)) {
+ mAutoTiles = autoTiles.get();
+ }
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt
new file mode 100644
index 000000000000..adea26e5aa26
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.dagger
+
+import android.content.res.Resources
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSetting
+import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSettingList
+import com.android.systemui.qs.pipeline.domain.autoaddable.CastAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.DataSaverAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.DeviceControlsAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.HotspotAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.NightDisplayAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.ReduceBrightColorsAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.WalletAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.WorkTileAutoAddable
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
+import dagger.multibindings.IntoSet
+
+@Module
+interface BaseAutoAddableModule {
+
+ companion object {
+ @Provides
+ @ElementsIntoSet
+ fun providesAutoAddableSetting(
+ @Main resources: Resources,
+ autoAddableSettingFactory: AutoAddableSetting.Factory
+ ): Set<AutoAddable> {
+ return AutoAddableSettingList.parseSettingsResource(
+ resources,
+ autoAddableSettingFactory
+ )
+ .toSet()
+ }
+ }
+
+ @Binds @IntoSet fun bindCastAutoAddable(impl: CastAutoAddable): AutoAddable
+
+ @Binds @IntoSet fun bindDataSaverAutoAddable(impl: DataSaverAutoAddable): AutoAddable
+
+ @Binds @IntoSet fun bindDeviceControlsAutoAddable(impl: DeviceControlsAutoAddable): AutoAddable
+
+ @Binds @IntoSet fun bindHotspotAutoAddable(impl: HotspotAutoAddable): AutoAddable
+
+ @Binds @IntoSet fun bindNightDisplayAutoAddable(impl: NightDisplayAutoAddable): AutoAddable
+
+ @Binds
+ @IntoSet
+ fun bindReduceBrightColorsAutoAddable(impl: ReduceBrightColorsAutoAddable): AutoAddable
+
+ @Binds @IntoSet fun bindWalletAutoAddable(impl: WalletAutoAddable): AutoAddable
+
+ @Binds @IntoSet fun bindWorkModeAutoAddable(impl: WorkTileAutoAddable): AutoAddable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddLog.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddLog.kt
new file mode 100644
index 000000000000..91cb5bb60e13
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddLog.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.dagger
+
+import javax.inject.Qualifier
+
+/** A [LogBuffer] for the QS pipeline to track auto-added tiles */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class QSAutoAddLog
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt
index 99792286d962..a010ac46a553 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt
@@ -16,13 +16,40 @@
package com.android.systemui.qs.pipeline.dagger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
import com.android.systemui.qs.pipeline.data.repository.AutoAddRepository
import com.android.systemui.qs.pipeline.data.repository.AutoAddSettingRepository
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import dagger.Binds
import dagger.Module
+import dagger.Provides
+import dagger.multibindings.Multibinds
-@Module
+@Module(
+ includes =
+ [
+ BaseAutoAddableModule::class,
+ ]
+)
abstract class QSAutoAddModule {
@Binds abstract fun bindAutoAddRepository(impl: AutoAddSettingRepository): AutoAddRepository
+
+ @Multibinds abstract fun providesAutoAddableSet(): Set<AutoAddable>
+
+ companion object {
+ /**
+ * Provides a logging buffer for all logs related to the new Quick Settings pipeline to log
+ * auto added tiles.
+ */
+ @Provides
+ @SysUISingleton
+ @QSAutoAddLog
+ fun provideQSAutoAddLogBuffer(factory: LogBufferFactory): LogBuffer {
+ return factory.create(QSPipelineLogger.AUTO_ADD_TAG, maxSize = 100, systrace = false)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
index d7ae575724dd..a4600fbbf4bd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
@@ -26,7 +26,7 @@ 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.domain.startable.QSPipelineCoreStartable
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import dagger.Binds
import dagger.Module
@@ -53,8 +53,8 @@ abstract class QSPipelineModule {
@Binds
@IntoMap
- @ClassKey(PrototypeCoreStartable::class)
- abstract fun providePrototypeCoreStartable(startable: PrototypeCoreStartable): CoreStartable
+ @ClassKey(QSPipelineCoreStartable::class)
+ abstract fun provideCoreStartable(startable: QSPipelineCoreStartable): CoreStartable
companion object {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt
index ad8bfeabc676..c56ca8c27a1f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt
@@ -19,5 +19,5 @@ import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import javax.inject.Qualifier
-/** A {@link LogBuffer} for the new QS Pipeline for logging changes to the set of current tiles. */
+/** A [LogBuffer] for the new QS Pipeline for logging changes to the set of current tiles. */
@Qualifier @MustBeDocumented @Retention(RetentionPolicy.RUNTIME) annotation class QSTileListLog
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSetting.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSetting.kt
new file mode 100644
index 000000000000..45129b937b48
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSetting.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.autoaddable
+
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.Objects
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+/**
+ * It tracks a specific `Secure` int [setting] and when its value changes to non-zero, it will emit
+ * a [AutoAddSignal.Add] for [spec].
+ */
+class AutoAddableSetting
+@AssistedInject
+constructor(
+ private val secureSettings: SecureSettings,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Assisted private val setting: String,
+ @Assisted private val spec: TileSpec,
+) : AutoAddable {
+
+ override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return secureSettings
+ .observerFlow(userId, setting)
+ .onStart { emit(Unit) }
+ .map { secureSettings.getIntForUser(setting, 0, userId) != 0 }
+ .distinctUntilChanged()
+ .filter { it }
+ .map { AutoAddSignal.Add(spec) }
+ .flowOn(bgDispatcher)
+ }
+
+ override val autoAddTracking = AutoAddTracking.IfNotAdded(spec)
+
+ override val description = "AutoAddableSetting: $setting:$spec ($autoAddTracking)"
+
+ override fun equals(other: Any?): Boolean {
+ return other is AutoAddableSetting && spec == other.spec && setting == other.setting
+ }
+
+ override fun hashCode(): Int {
+ return Objects.hash(spec, setting)
+ }
+
+ override fun toString(): String {
+ return description
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(setting: String, spec: TileSpec): AutoAddableSetting
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingList.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingList.kt
new file mode 100644
index 000000000000..b1c7433770de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingList.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.autoaddable
+
+import android.content.res.Resources
+import android.util.Log
+import com.android.systemui.R
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+object AutoAddableSettingList {
+
+ /** Parses [R.array.config_quickSettingsAutoAdd] into a collection of [AutoAddableSetting]. */
+ fun parseSettingsResource(
+ resources: Resources,
+ autoAddableSettingFactory: AutoAddableSetting.Factory,
+ ): Iterable<AutoAddable> {
+ val autoAddList = resources.getStringArray(R.array.config_quickSettingsAutoAdd)
+ return autoAddList.mapNotNull {
+ val elements = it.split(SETTING_SEPARATOR, limit = 2)
+ if (elements.size == 2) {
+ val setting = elements[0]
+ val spec = elements[1]
+ val tileSpec = TileSpec.create(spec)
+ if (tileSpec == TileSpec.Invalid) {
+ Log.w(TAG, "Malformed item in array: $it")
+ null
+ } else {
+ autoAddableSettingFactory.create(setting, TileSpec.create(spec))
+ }
+ } else {
+ Log.w(TAG, "Malformed item in array: $it")
+ null
+ }
+ }
+ }
+
+ private const val SETTING_SEPARATOR = ":"
+ private const val TAG = "AutoAddableSettingList"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt
new file mode 100644
index 000000000000..88a49ee109aa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.autoaddable
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.statusbar.policy.CallbackController
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Generic [AutoAddable] for tiles that are added based on a signal from a [CallbackController]. */
+abstract class CallbackControllerAutoAddable<
+ Callback : Any, Controller : CallbackController<Callback>>(
+ private val controller: Controller,
+) : AutoAddable {
+
+ /** [TileSpec] for the tile to add. */
+ protected abstract val spec: TileSpec
+
+ /**
+ * Callback to be used to determine when to add the tile. When the callback determines that the
+ * feature has been enabled, it should call [sendAdd].
+ */
+ protected abstract fun ProducerScope<AutoAddSignal>.getCallback(): Callback
+
+ /** Sends an [AutoAddSignal.Add] for [spec]. */
+ protected fun ProducerScope<AutoAddSignal>.sendAdd() {
+ trySend(AutoAddSignal.Add(spec))
+ }
+
+ final override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return conflatedCallbackFlow {
+ val callback = getCallback()
+ controller.addCallback(callback)
+
+ awaitClose { controller.removeCallback(callback) }
+ }
+ }
+
+ override val autoAddTracking: AutoAddTracking
+ get() = AutoAddTracking.IfNotAdded(spec)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddable.kt
new file mode 100644
index 000000000000..b5bef9f6ebe8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddable.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.CastTile
+import com.android.systemui.statusbar.policy.CastController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.ProducerScope
+
+/**
+ * [AutoAddable] for [CastTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when there's a casting device connected or connecting.
+ */
+@SysUISingleton
+class CastAutoAddable
+@Inject
+constructor(
+ private val controller: CastController,
+) : CallbackControllerAutoAddable<CastController.Callback, CastController>(controller) {
+
+ override val spec: TileSpec
+ get() = TileSpec.create(CastTile.TILE_SPEC)
+
+ override fun ProducerScope<AutoAddSignal>.getCallback(): CastController.Callback {
+ return CastController.Callback {
+ val isCasting =
+ controller.castDevices.any {
+ it.state == CastController.CastDevice.STATE_CONNECTED ||
+ it.state == CastController.CastDevice.STATE_CONNECTING
+ }
+ if (isCasting) {
+ sendAdd()
+ }
+ }
+ }
+
+ override val description = "CastAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddable.kt
new file mode 100644
index 000000000000..a877aee335f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddable.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.DataSaverTile
+import com.android.systemui.statusbar.policy.DataSaverController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.ProducerScope
+
+/**
+ * [AutoAddable] for [DataSaverTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when data saver is enabled.
+ */
+@SysUISingleton
+class DataSaverAutoAddable
+@Inject
+constructor(
+ dataSaverController: DataSaverController,
+) :
+ CallbackControllerAutoAddable<DataSaverController.Listener, DataSaverController>(
+ dataSaverController
+ ) {
+
+ override val spec
+ get() = TileSpec.create(DataSaverTile.TILE_SPEC)
+
+ override fun ProducerScope<AutoAddSignal>.getCallback(): DataSaverController.Listener {
+ return DataSaverController.Listener { enabled ->
+ if (enabled) {
+ sendAdd()
+ }
+ }
+ }
+
+ override val description = "DataSaverAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt
new file mode 100644
index 000000000000..76bfad936116
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.autoaddable
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.DeviceControlsTile
+import com.android.systemui.statusbar.policy.DeviceControlsController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * [AutoAddable] for [DeviceControlsTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when updating to a device that supports device controls. It
+ * will send a signal to remove the tile when the device does not support controls.
+ */
+@SysUISingleton
+class DeviceControlsAutoAddable
+@Inject
+constructor(
+ private val deviceControlsController: DeviceControlsController,
+) : AutoAddable {
+
+ private val spec = TileSpec.create(DeviceControlsTile.TILE_SPEC)
+
+ override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return conflatedCallbackFlow {
+ val callback =
+ object : DeviceControlsController.Callback {
+ override fun onControlsUpdate(position: Int?) {
+ position?.let { trySend(AutoAddSignal.Add(spec, position)) }
+ deviceControlsController.removeCallback()
+ }
+
+ override fun removeControlsAutoTracker() {
+ trySend(AutoAddSignal.Remove(spec))
+ }
+ }
+
+ deviceControlsController.setCallback(callback)
+
+ awaitClose { deviceControlsController.removeCallback() }
+ }
+ }
+
+ override val autoAddTracking: AutoAddTracking
+ get() = AutoAddTracking.Always
+
+ override val description = "DeviceControlsAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddable.kt
new file mode 100644
index 000000000000..9c59e1268695
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddable.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.HotspotTile
+import com.android.systemui.statusbar.policy.HotspotController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.ProducerScope
+
+/**
+ * [AutoAddable] for [HotspotTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when hotspot is enabled.
+ */
+@SysUISingleton
+class HotspotAutoAddable
+@Inject
+constructor(
+ hotspotController: HotspotController,
+) :
+ CallbackControllerAutoAddable<HotspotController.Callback, HotspotController>(
+ hotspotController
+ ) {
+
+ override val spec
+ get() = TileSpec.create(HotspotTile.TILE_SPEC)
+
+ override fun ProducerScope<AutoAddSignal>.getCallback(): HotspotController.Callback {
+ return HotspotController.Callback { enabled, _ ->
+ if (enabled) {
+ sendAdd()
+ }
+ }
+ }
+
+ override val description = "HotspotAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt
new file mode 100644
index 000000000000..31ea734fb842
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.autoaddable
+
+import android.content.Context
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.NightDisplayTile
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * [AutoAddable] for [NightDisplayTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when night display is enabled or when the auto mode changes
+ * to one that supports night display.
+ */
+@SysUISingleton
+class NightDisplayAutoAddable
+@Inject
+constructor(
+ private val nightDisplayListenerBuilder: NightDisplayListenerModule.Builder,
+ context: Context,
+) : AutoAddable {
+
+ private val enabled = ColorDisplayManager.isNightDisplayAvailable(context)
+ private val spec = TileSpec.create(NightDisplayTile.TILE_SPEC)
+
+ override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return conflatedCallbackFlow {
+ val nightDisplayListener = nightDisplayListenerBuilder.setUser(userId).build()
+
+ val callback =
+ object : NightDisplayListener.Callback {
+ override fun onActivated(activated: Boolean) {
+ if (activated) {
+ sendAdd()
+ }
+ }
+
+ override fun onAutoModeChanged(autoMode: Int) {
+ if (
+ autoMode == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME ||
+ autoMode == ColorDisplayManager.AUTO_MODE_TWILIGHT
+ ) {
+ sendAdd()
+ }
+ }
+
+ private fun sendAdd() {
+ trySend(AutoAddSignal.Add(spec))
+ }
+ }
+
+ nightDisplayListener.setCallback(callback)
+
+ awaitClose { nightDisplayListener.setCallback(null) }
+ }
+ }
+
+ override val autoAddTracking =
+ if (enabled) {
+ AutoAddTracking.IfNotAdded(spec)
+ } else {
+ AutoAddTracking.Disabled
+ }
+
+ override val description = "NightDisplayAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
new file mode 100644
index 000000000000..267e2b7d0609
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.ReduceBrightColorsController
+import com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+import javax.inject.Inject
+import javax.inject.Named
+import kotlinx.coroutines.channels.ProducerScope
+
+/**
+ * [AutoAddable] for [ReduceBrightColorsTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when reduce bright colors is enabled.
+ */
+@SysUISingleton
+class ReduceBrightColorsAutoAddable
+@Inject
+constructor(
+ controller: ReduceBrightColorsController,
+ @Named(RBC_AVAILABLE) private val available: Boolean,
+) :
+ CallbackControllerAutoAddable<
+ ReduceBrightColorsController.Listener, ReduceBrightColorsController
+ >(controller) {
+
+ override val spec: TileSpec
+ get() = TileSpec.create(ReduceBrightColorsTile.TILE_SPEC)
+
+ override fun ProducerScope<AutoAddSignal>.getCallback(): ReduceBrightColorsController.Listener {
+ return object : ReduceBrightColorsController.Listener {
+ override fun onActivated(activated: Boolean) {
+ if (activated) {
+ sendAdd()
+ }
+ }
+ }
+ }
+
+ override val autoAddTracking
+ get() =
+ if (available) {
+ super.autoAddTracking
+ } else {
+ AutoAddTracking.Disabled
+ }
+
+ override val description = "ReduceBrightColorsAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt
new file mode 100644
index 000000000000..58a31bc22a57
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.autoaddable
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.text.TextUtils
+import com.android.systemui.R
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.statusbar.policy.SafetyController
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+
+/**
+ * [AutoAddable] for the safety tile.
+ *
+ * It will send a signal to add the tile when the feature is enabled, indicating the component
+ * corresponding to the tile. If the feature is disabled, it will send a signal to remove the tile.
+ */
+@SysUISingleton
+class SafetyCenterAutoAddable
+@Inject
+constructor(
+ private val safetyController: SafetyController,
+ private val packageManager: PackageManager,
+ @Main private val resources: Resources,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+) : AutoAddable {
+
+ private suspend fun getSpec(): TileSpec? {
+ val specClass = resources.getString(R.string.safety_quick_settings_tile_class)
+ return if (TextUtils.isEmpty(specClass)) {
+ null
+ } else {
+ val packageName =
+ withContext(bgDispatcher) { packageManager.permissionControllerPackageName }
+ TileSpec.create(ComponentName(packageName, specClass))
+ }
+ }
+
+ override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return conflatedCallbackFlow {
+ val spec = getSpec()
+ if (spec != null) {
+ // If not added, we always try to add it
+ trySend(AutoAddSignal.Add(spec))
+ val listener =
+ SafetyController.Listener { isSafetyCenterEnabled ->
+ if (isSafetyCenterEnabled) {
+ trySend(AutoAddSignal.Add(spec))
+ } else {
+ trySend(AutoAddSignal.Remove(spec))
+ }
+ }
+
+ safetyController.addCallback(listener)
+
+ awaitClose { safetyController.removeCallback(listener) }
+ } else {
+ awaitClose {}
+ }
+ }
+ }
+
+ override val autoAddTracking: AutoAddTracking
+ get() = AutoAddTracking.Always
+
+ override val description = "SafetyCenterAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddable.kt
new file mode 100644
index 000000000000..b3bc25fcfd69
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddable.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.autoaddable
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.QuickAccessWalletTile
+import com.android.systemui.statusbar.policy.WalletController
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+/**
+ * [AutoAddable] for [QuickAccessWalletTile.TILE_SPEC].
+ *
+ * It will always try to add the tile if [WalletController.getWalletPosition] is non-null.
+ */
+@SysUISingleton
+class WalletAutoAddable
+@Inject
+constructor(
+ private val walletController: WalletController,
+) : AutoAddable {
+
+ private val spec = TileSpec.create(QuickAccessWalletTile.TILE_SPEC)
+
+ override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return flow {
+ val position = walletController.getWalletPosition()
+ if (position != null) {
+ emit(AutoAddSignal.Add(spec, position))
+ }
+ }
+ }
+
+ override val autoAddTracking: AutoAddTracking
+ get() = AutoAddTracking.IfNotAdded(spec)
+
+ override val description = "WalletAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt
new file mode 100644
index 000000000000..5e3c34841c50
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.autoaddable
+
+import android.content.pm.UserInfo
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.WorkModeTile
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * [AutoAddable] for [WorkModeTile.TILE_SPEC].
+ *
+ * It will send a signal to add the tile when there is a managed profile for the current user, and a
+ * signal to remove it if there is not.
+ */
+@SysUISingleton
+class WorkTileAutoAddable @Inject constructor(private val userTracker: UserTracker) : AutoAddable {
+
+ private val spec = TileSpec.create(WorkModeTile.TILE_SPEC)
+
+ override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return conflatedCallbackFlow {
+ fun maybeSend(profiles: List<UserInfo>) {
+ if (profiles.any { it.id == userId }) {
+ // We are looking at the profiles of the correct user.
+ if (profiles.any { it.isManagedProfile }) {
+ trySend(AutoAddSignal.Add(spec))
+ } else {
+ trySend(AutoAddSignal.Remove(spec))
+ }
+ }
+ }
+
+ val callback =
+ object : UserTracker.Callback {
+ override fun onProfilesChanged(profiles: List<UserInfo>) {
+ maybeSend(profiles)
+ }
+ }
+
+ userTracker.addCallback(callback) { it.run() }
+ maybeSend(userTracker.userProfiles)
+
+ awaitClose { userTracker.removeCallback(callback) }
+ }
+ }
+
+ override val autoAddTracking = AutoAddTracking.Always
+
+ override val description = "WorkTileAutoAddable ($autoAddTracking)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractor.kt
new file mode 100644
index 000000000000..b74739322fcd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractor.kt
@@ -0,0 +1,123 @@
+/*
+ * 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 com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.qs.pipeline.data.repository.AutoAddRepository
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.indentIfPossible
+import java.io.PrintWriter
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+
+/**
+ * Collects the signals coming from all registered [AutoAddable] and adds/removes tiles accordingly.
+ */
+@SysUISingleton
+class AutoAddInteractor
+@Inject
+constructor(
+ private val autoAddables: Set<@JvmSuppressWildcards AutoAddable>,
+ private val repository: AutoAddRepository,
+ private val dumpManager: DumpManager,
+ private val qsPipelineLogger: QSPipelineLogger,
+ @Application private val scope: CoroutineScope,
+) : Dumpable {
+
+ private val initialized = AtomicBoolean(false)
+
+ /** Start collection of signals following the user from [currentTilesInteractor]. */
+ fun init(currentTilesInteractor: CurrentTilesInteractor) {
+ if (!initialized.compareAndSet(false, true)) {
+ return
+ }
+
+ dumpManager.registerNormalDumpable(TAG, this)
+
+ scope.launch {
+ currentTilesInteractor.userId.collectLatest { userId ->
+ coroutineScope {
+ val previouslyAdded = repository.autoAddedTiles(userId).stateIn(this)
+
+ autoAddables
+ .map { addable ->
+ val autoAddSignal = addable.autoAddSignal(userId)
+ when (val lifecycle = addable.autoAddTracking) {
+ is AutoAddTracking.Always -> autoAddSignal
+ is AutoAddTracking.Disabled -> emptyFlow()
+ is AutoAddTracking.IfNotAdded -> {
+ if (lifecycle.spec !in previouslyAdded.value) {
+ autoAddSignal.filterIsInstance<AutoAddSignal.Add>().take(1)
+ } else {
+ emptyFlow()
+ }
+ }
+ }
+ }
+ .merge()
+ .collect { signal ->
+ when (signal) {
+ is AutoAddSignal.Add -> {
+ if (signal.spec !in previouslyAdded.value) {
+ currentTilesInteractor.addTile(signal.spec, signal.position)
+ qsPipelineLogger.logTileAutoAdded(
+ userId,
+ signal.spec,
+ signal.position
+ )
+ repository.markTileAdded(userId, signal.spec)
+ }
+ }
+ is AutoAddSignal.Remove -> {
+ currentTilesInteractor.removeTiles(setOf(signal.spec))
+ qsPipelineLogger.logTileAutoRemoved(userId, signal.spec)
+ repository.unmarkTileAdded(userId, signal.spec)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ with(pw.asIndenting()) {
+ println("AutoAddables:")
+ indentIfPossible { autoAddables.forEach { println(it.description) } }
+ }
+ }
+
+ companion object {
+ private const val TAG = "AutoAddInteractor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddSignal.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddSignal.kt
new file mode 100644
index 000000000000..ed7b8bd4c2f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddSignal.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/** Signal indicating when a tile needs to be auto-added or removed */
+sealed interface AutoAddSignal {
+ /** Tile for this object */
+ val spec: TileSpec
+
+ /** Signal for auto-adding a tile at [position]. */
+ data class Add(
+ override val spec: TileSpec,
+ val position: Int = POSITION_AT_END,
+ ) : AutoAddSignal
+
+ /** Signal for removing a tile. */
+ data class Remove(
+ override val spec: TileSpec,
+ ) : AutoAddSignal
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddTracking.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddTracking.kt
new file mode 100644
index 000000000000..154d0455713b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddTracking.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.qs.pipeline.shared.TileSpec
+
+/** Strategy for when to track a particular [AutoAddable]. */
+sealed interface AutoAddTracking {
+
+ /**
+ * Indicates that the signals from the associated [AutoAddable] should all be collected and
+ * reacted accordingly. It may have [AutoAddSignal.Add] and [AutoAddSignal.Remove].
+ */
+ object Always : AutoAddTracking {
+ override fun toString(): String {
+ return "Always"
+ }
+ }
+
+ /**
+ * Indicates that the associated [AutoAddable] is [Disabled] and doesn't need to be collected.
+ */
+ object Disabled : AutoAddTracking {
+ override fun toString(): String {
+ return "Disabled"
+ }
+ }
+
+ /**
+ * Only the first [AutoAddSignal.Add] for each flow of signals needs to be collected, and only
+ * if the tile hasn't been auto-added yet. The associated [AutoAddable] will only emit
+ * [AutoAddSignal.Add].
+ */
+ data class IfNotAdded(val spec: TileSpec) : AutoAddTracking
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddable.kt
new file mode 100644
index 000000000000..61fe5b4c9079
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/model/AutoAddable.kt
@@ -0,0 +1,47 @@
+/*
+ * 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 kotlinx.coroutines.flow.Flow
+
+/**
+ * Tracks conditions for auto-adding or removing specific tiles.
+ *
+ * When creating a new [AutoAddable], it needs to be registered in a [Module] like
+ * [BaseAutoAddableModule], for example:
+ * ```
+ * @Binds
+ * @IntoSet
+ * fun providesMyAutoAddable(autoAddable: MyAutoAddable): AutoAddable
+ * ```
+ */
+interface AutoAddable {
+
+ /**
+ * Signals associated with a particular user indicating whether a particular tile needs to be
+ * auto-added or auto-removed.
+ */
+ fun autoAddSignal(userId: Int): Flow<AutoAddSignal>
+
+ /**
+ * Lifecycle for this object. It indicates in which cases [autoAddSignal] should be collected
+ */
+ val autoAddTracking: AutoAddTracking
+
+ /** Human readable description */
+ val description: String
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
new file mode 100644
index 000000000000..224fc1ae864f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.startable
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.qs.pipeline.domain.interactor.AutoAddInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import javax.inject.Inject
+
+@SysUISingleton
+class QSPipelineCoreStartable
+@Inject
+constructor(
+ private val currentTilesInteractor: CurrentTilesInteractor,
+ private val autoAddInteractor: AutoAddInteractor,
+ private val featureFlags: FeatureFlags,
+) : CoreStartable {
+
+ override fun start() {
+ if (
+ featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST) &&
+ featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)
+ ) {
+ autoAddInteractor.init(currentTilesInteractor)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt
deleted file mode 100644
index bbd72341acc6..000000000000
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.prototyping
-
-import android.util.Log
-import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.qs.pipeline.data.repository.AutoAddRepository
-import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.statusbar.commandline.Command
-import com.android.systemui.statusbar.commandline.CommandRegistry
-import com.android.systemui.user.data.repository.UserRepository
-import java.io.PrintWriter
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.launch
-
-/**
- * Class for observing results while prototyping.
- *
- * The flows do their own logging, so we just need to make sure that they collect.
- *
- * This will be torn down together with the last of the new pipeline flags remaining here.
- */
-// TODO(b/270385608)
-@SysUISingleton
-class PrototypeCoreStartable
-@Inject
-constructor(
- private val tileSpecRepository: TileSpecRepository,
- private val autoAddRepository: AutoAddRepository,
- private val userRepository: UserRepository,
- private val featureFlags: FeatureFlags,
- @Application private val scope: CoroutineScope,
- private val commandRegistry: CommandRegistry,
-) : CoreStartable {
-
- @OptIn(ExperimentalCoroutinesApi::class)
- override fun start() {
- if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
- scope.launch {
- userRepository.selectedUserInfo
- .flatMapLatest { user -> tileSpecRepository.tilesSpecs(user.id) }
- .collect {}
- }
- if (featureFlags.isEnabled(Flags.QS_PIPELINE_AUTO_ADD)) {
- scope.launch {
- userRepository.selectedUserInfo
- .flatMapLatest { user -> autoAddRepository.autoAddedTiles(user.id) }
- .collect { tiles -> Log.d(TAG, "Auto-added tiles: $tiles") }
- }
- }
- commandRegistry.registerCommand(COMMAND, ::CommandExecutor)
- }
- }
-
- private inner class CommandExecutor : Command {
- override fun execute(pw: PrintWriter, args: List<String>) {
- if (args.size < 2) {
- pw.println("Error: needs at least two arguments")
- return
- }
- val spec = TileSpec.create(args[1])
- if (spec == TileSpec.Invalid) {
- pw.println("Error: Invalid tile spec ${args[1]}")
- }
- if (args[0] == "add") {
- performAdd(args, spec)
- pw.println("Requested tile added")
- } else if (args[0] == "remove") {
- performRemove(args, spec)
- pw.println("Requested tile removed")
- } else {
- pw.println("Error: unknown command")
- }
- }
-
- private fun performAdd(args: List<String>, spec: TileSpec) {
- val position = args.getOrNull(2)?.toInt() ?: TileSpecRepository.POSITION_AT_END
- val user = args.getOrNull(3)?.toInt() ?: userRepository.getSelectedUserInfo().id
- scope.launch { tileSpecRepository.addTile(user, spec, position) }
- }
-
- private fun performRemove(args: List<String>, spec: TileSpec) {
- val user = args.getOrNull(2)?.toInt() ?: userRepository.getSelectedUserInfo().id
- scope.launch { tileSpecRepository.removeTiles(user, listOf(spec)) }
- }
-
- override fun help(pw: PrintWriter) {
- pw.println("Usage: adb shell cmd statusbar $COMMAND:")
- pw.println(" add <spec> [position] [user]")
- pw.println(" remove <spec> [user]")
- }
- }
-
- companion object {
- private const val COMMAND = "qs-pipeline"
- private const val TAG = "PrototypeCoreStartable"
- }
-}
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 af1cd0995a21..11b5dd7cb036 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
@@ -52,7 +52,11 @@ sealed class TileSpec private constructor(open val spec: String) {
internal constructor(
override val spec: String,
val componentName: ComponentName,
- ) : TileSpec(spec)
+ ) : TileSpec(spec) {
+ override fun toString(): String {
+ return "CustomTileSpec(${componentName.toShortString()})"
+ }
+ }
companion object {
/** Create a [TileSpec] from the string [spec]. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
index 8318ec99e530..d400faa3091e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
@@ -19,6 +19,7 @@ package com.android.systemui.qs.pipeline.shared.logging
import android.annotation.UserIdInt
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogLevel
+import com.android.systemui.qs.pipeline.dagger.QSAutoAddLog
import com.android.systemui.qs.pipeline.dagger.QSTileListLog
import com.android.systemui.qs.pipeline.shared.TileSpec
import javax.inject.Inject
@@ -32,10 +33,12 @@ class QSPipelineLogger
@Inject
constructor(
@QSTileListLog private val tileListLogBuffer: LogBuffer,
+ @QSAutoAddLog private val tileAutoAddLogBuffer: LogBuffer,
) {
companion object {
const val TILE_LIST_TAG = "QSTileListLog"
+ const val AUTO_ADD_TAG = "QSAutoAddableLog"
}
/**
@@ -136,6 +139,31 @@ constructor(
)
}
+ fun logTileAutoAdded(userId: Int, spec: TileSpec, position: Int) {
+ tileAutoAddLogBuffer.log(
+ AUTO_ADD_TAG,
+ LogLevel.DEBUG,
+ {
+ int1 = userId
+ int2 = position
+ str1 = spec.toString()
+ },
+ { "Tile $str1 auto added for user $int1 at position $int2" }
+ )
+ }
+
+ fun logTileAutoRemoved(userId: Int, spec: TileSpec) {
+ tileAutoAddLogBuffer.log(
+ AUTO_ADD_TAG,
+ LogLevel.DEBUG,
+ {
+ int1 = userId
+ str1 = spec.toString()
+ },
+ { "Tile $str1 auto removed for user $int1" }
+ )
+ }
+
/** Reasons for destroying an existing tile. */
enum class TileDestroyedReason(val readable: String) {
TILE_REMOVED("Tile removed from current set"),
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 41742b667d99..61dd6360dc63 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -140,6 +140,7 @@ public class QSTileHostTest extends SysuiTestCase {
mFeatureFlags = new FakeFeatureFlags();
mFeatureFlags.set(Flags.QS_PIPELINE_NEW_HOST, false);
+ mFeatureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, false);
mMainExecutor = new FakeExecutor(new FakeSystemClock());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt
new file mode 100644
index 000000000000..817ac61e5303
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingListTest.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.autoaddable
+
+import android.content.ComponentName
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AutoAddableSettingListTest : SysuiTestCase() {
+
+ private val factory =
+ object : AutoAddableSetting.Factory {
+ override fun create(setting: String, spec: TileSpec): AutoAddableSetting {
+ return AutoAddableSetting(
+ mock(),
+ mock(),
+ setting,
+ spec,
+ )
+ }
+ }
+
+ @Test
+ fun correctLines_correctAutoAddables() {
+ val setting1 = "setting1"
+ val setting2 = "setting2"
+ val spec1 = TileSpec.create("spec1")
+ val spec2 = TileSpec.create(ComponentName("pkg", "cls"))
+
+ context.orCreateTestableResources.addOverride(
+ R.array.config_quickSettingsAutoAdd,
+ arrayOf(toStringLine(setting1, spec1), toStringLine(setting2, spec2))
+ )
+
+ val autoAddables = AutoAddableSettingList.parseSettingsResource(context.resources, factory)
+
+ assertThat(autoAddables)
+ .containsExactly(factory.create(setting1, spec1), factory.create(setting2, spec2))
+ }
+
+ @Test
+ fun malformedLine_ignored() {
+ val setting = "setting"
+ val spec = TileSpec.create("spec")
+
+ context.orCreateTestableResources.addOverride(
+ R.array.config_quickSettingsAutoAdd,
+ arrayOf(toStringLine(setting, spec), "bad_line")
+ )
+
+ val autoAddables = AutoAddableSettingList.parseSettingsResource(context.resources, factory)
+
+ assertThat(autoAddables).containsExactly(factory.create(setting, spec))
+ }
+
+ @Test
+ fun invalidSpec_ignored() {
+ val setting = "setting"
+ val spec = TileSpec.create("spec")
+
+ context.orCreateTestableResources.addOverride(
+ R.array.config_quickSettingsAutoAdd,
+ arrayOf(toStringLine(setting, spec), "invalid:")
+ )
+
+ val autoAddables = AutoAddableSettingList.parseSettingsResource(context.resources, factory)
+
+ assertThat(autoAddables).containsExactly(factory.create(setting, spec))
+ }
+
+ companion object {
+ private fun toStringLine(setting: String, spec: TileSpec) =
+ "$setting$SETTINGS_SEPARATOR${spec.spec}"
+ private const val SETTINGS_SEPARATOR = ":"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt
new file mode 100644
index 000000000000..36c3c9d79e86
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AutoAddableSettingTest : SysuiTestCase() {
+
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val secureSettings = FakeSettings()
+ private val underTest =
+ AutoAddableSetting(
+ secureSettings,
+ testDispatcher,
+ SETTING,
+ SPEC,
+ )
+
+ @Test
+ fun settingNotSet_noSignal() =
+ testScope.runTest {
+ val userId = 0
+ val signal by collectLastValue(underTest.autoAddSignal(userId))
+
+ assertThat(signal).isNull() // null means no emitted value
+ }
+
+ @Test
+ fun settingSetTo0_noSignal() =
+ testScope.runTest {
+ val userId = 0
+ val signal by collectLastValue(underTest.autoAddSignal(userId))
+
+ secureSettings.putIntForUser(SETTING, 0, userId)
+
+ assertThat(signal).isNull() // null means no emitted value
+ }
+
+ @Test
+ fun settingSetToNon0_signal() =
+ testScope.runTest {
+ val userId = 0
+ val signal by collectLastValue(underTest.autoAddSignal(userId))
+
+ secureSettings.putIntForUser(SETTING, 42, userId)
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun settingSetForUser_onlySignalInThatUser() =
+ testScope.runTest {
+ val signal0 by collectLastValue(underTest.autoAddSignal(0))
+ val signal1 by collectLastValue(underTest.autoAddSignal(1))
+
+ secureSettings.putIntForUser(SETTING, /* value */ 42, /* userHandle */ 1)
+
+ assertThat(signal0).isNull()
+ assertThat(signal1).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun multipleNonZeroChanges_onlyOneSignal() =
+ testScope.runTest {
+ val userId = 0
+ val signals by collectValues(underTest.autoAddSignal(userId))
+
+ secureSettings.putIntForUser(SETTING, 1, userId)
+ secureSettings.putIntForUser(SETTING, 2, userId)
+
+ assertThat(signals.size).isEqualTo(1)
+ }
+
+ @Test
+ fun strategyIfNotAdded() {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+ }
+
+ companion object {
+ private const val SETTING = "setting"
+ private val SPEC = TileSpec.create("spec")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt
new file mode 100644
index 000000000000..afb43c7e9c16
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddableTest.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.statusbar.policy.CallbackController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class CallbackControllerAutoAddableTest : SysuiTestCase() {
+
+ @Test
+ fun callbackAddedAndRemoved() = runTest {
+ val controller = TestableController()
+ val callback = object : TestableController.Callback {}
+ val underTest =
+ object :
+ CallbackControllerAutoAddable<TestableController.Callback, TestableController>(
+ controller
+ ) {
+ override val description: String = ""
+ override val spec: TileSpec
+ get() = SPEC
+
+ override fun ProducerScope<AutoAddSignal>.getCallback():
+ TestableController.Callback {
+ return callback
+ }
+ }
+
+ val job = launch { underTest.autoAddSignal(0).collect {} }
+ runCurrent()
+ assertThat(controller.callbacks).containsExactly(callback)
+ job.cancel()
+ runCurrent()
+ assertThat(controller.callbacks).isEmpty()
+ }
+
+ @Test
+ fun sendAddFromCallback() = runTest {
+ val controller = TestableController()
+ val underTest =
+ object :
+ CallbackControllerAutoAddable<TestableController.Callback, TestableController>(
+ controller
+ ) {
+ override val description: String = ""
+
+ override val spec: TileSpec
+ get() = SPEC
+
+ override fun ProducerScope<AutoAddSignal>.getCallback():
+ TestableController.Callback {
+ return object : TestableController.Callback {
+ override fun change() {
+ sendAdd()
+ }
+ }
+ }
+ }
+
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ assertThat(signal).isNull()
+
+ controller.callbacks.first().change()
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun strategyIfNotAdded() {
+ val underTest =
+ object :
+ CallbackControllerAutoAddable<TestableController.Callback, TestableController>(
+ TestableController()
+ ) {
+ override val description: String = ""
+ override val spec: TileSpec
+ get() = SPEC
+
+ override fun ProducerScope<AutoAddSignal>.getCallback():
+ TestableController.Callback {
+ return object : TestableController.Callback {}
+ }
+ }
+
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+ }
+
+ private class TestableController : CallbackController<TestableController.Callback> {
+
+ val callbacks = mutableSetOf<Callback>()
+
+ override fun addCallback(listener: Callback) {
+ callbacks.add(listener)
+ }
+
+ override fun removeCallback(listener: Callback) {
+ callbacks.remove(listener)
+ }
+
+ interface Callback {
+ fun change() {}
+ }
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create("test")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt
new file mode 100644
index 000000000000..a357dad65b2a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/CastAutoAddableTest.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.CastTile
+import com.android.systemui.statusbar.policy.CastController
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class CastAutoAddableTest : SysuiTestCase() {
+
+ @Mock private lateinit var castController: CastController
+ @Captor private lateinit var callbackCaptor: ArgumentCaptor<CastController.Callback>
+
+ private lateinit var underTest: CastAutoAddable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest = CastAutoAddable(castController)
+ }
+
+ @Test
+ fun onCastDevicesChanged_noDevices_noSignal() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(castController).addCallback(capture(callbackCaptor))
+
+ callbackCaptor.value.onCastDevicesChanged()
+
+ assertThat(signal).isNull()
+ }
+
+ @Test
+ fun onCastDevicesChanged_deviceNotConnectedOrConnecting_noSignal() = runTest {
+ val device =
+ CastController.CastDevice().apply {
+ state = CastController.CastDevice.STATE_DISCONNECTED
+ }
+ whenever(castController.castDevices).thenReturn(listOf(device))
+
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(castController).addCallback(capture(callbackCaptor))
+
+ callbackCaptor.value.onCastDevicesChanged()
+
+ assertThat(signal).isNull()
+ }
+
+ @Test
+ fun onCastDevicesChanged_someDeviceConnecting_addSignal() = runTest {
+ val disconnectedDevice =
+ CastController.CastDevice().apply {
+ state = CastController.CastDevice.STATE_DISCONNECTED
+ }
+ val connectingDevice =
+ CastController.CastDevice().apply { state = CastController.CastDevice.STATE_CONNECTING }
+ whenever(castController.castDevices)
+ .thenReturn(listOf(disconnectedDevice, connectingDevice))
+
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(castController).addCallback(capture(callbackCaptor))
+
+ callbackCaptor.value.onCastDevicesChanged()
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun onCastDevicesChanged_someDeviceConnected_addSignal() = runTest {
+ val disconnectedDevice =
+ CastController.CastDevice().apply {
+ state = CastController.CastDevice.STATE_DISCONNECTED
+ }
+ val connectedDevice =
+ CastController.CastDevice().apply { state = CastController.CastDevice.STATE_CONNECTED }
+ whenever(castController.castDevices).thenReturn(listOf(disconnectedDevice, connectedDevice))
+
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(castController).addCallback(capture(callbackCaptor))
+
+ callbackCaptor.value.onCastDevicesChanged()
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create(CastTile.TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt
new file mode 100644
index 000000000000..098ffc304a7a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DataSaverAutoAddableTest.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.DataSaverTile
+import com.android.systemui.statusbar.policy.DataSaverController
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DataSaverAutoAddableTest : SysuiTestCase() {
+
+ @Mock private lateinit var dataSaverController: DataSaverController
+ @Captor private lateinit var callbackCaptor: ArgumentCaptor<DataSaverController.Listener>
+
+ private lateinit var underTest: DataSaverAutoAddable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest = DataSaverAutoAddable(dataSaverController)
+ }
+
+ @Test
+ fun dataSaverNotEnabled_NoSignal() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(dataSaverController).addCallback(capture(callbackCaptor))
+
+ callbackCaptor.value.onDataSaverChanged(false)
+
+ assertThat(signal).isNull()
+ }
+
+ @Test
+ fun dataSaverEnabled_addSignal() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(dataSaverController).addCallback(capture(callbackCaptor))
+
+ callbackCaptor.value.onDataSaverChanged(true)
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create(DataSaverTile.TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt
new file mode 100644
index 000000000000..a2e353877e67
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddableTest.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.DeviceControlsTile
+import com.android.systemui.statusbar.policy.DeviceControlsController
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DeviceControlsAutoAddableTest : SysuiTestCase() {
+
+ @Mock private lateinit var deviceControlsController: DeviceControlsController
+ @Captor private lateinit var callbackCaptor: ArgumentCaptor<DeviceControlsController.Callback>
+
+ private lateinit var underTest: DeviceControlsAutoAddable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest = DeviceControlsAutoAddable(deviceControlsController)
+ }
+
+ @Test
+ fun strategyAlways() {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always)
+ }
+
+ @Test
+ fun onControlsUpdate_position_addSignal() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ val position = 5
+ runCurrent()
+
+ verify(deviceControlsController).setCallback(capture(callbackCaptor))
+ callbackCaptor.value.onControlsUpdate(position)
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC, position))
+ verify(deviceControlsController).removeCallback()
+ }
+
+ @Test
+ fun onControlsUpdate_nullPosition_noAddSignal() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(deviceControlsController).setCallback(capture(callbackCaptor))
+ callbackCaptor.value.onControlsUpdate(null)
+
+ assertThat(signal).isNull()
+ verify(deviceControlsController).removeCallback()
+ }
+
+ @Test
+ fun onRemoveControlsAutoTracker_removeSignal() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(deviceControlsController).setCallback(capture(callbackCaptor))
+ callbackCaptor.value.removeControlsAutoTracker()
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun flowCancelled_removeCallback() = runTest {
+ val job = launch { underTest.autoAddSignal(0).collect() }
+ runCurrent()
+
+ job.cancel()
+ runCurrent()
+ verify(deviceControlsController).removeCallback()
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create(DeviceControlsTile.TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt
new file mode 100644
index 000000000000..ee96b471bc18
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/HotspotAutoAddableTest.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.HotspotTile
+import com.android.systemui.statusbar.policy.HotspotController
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class HotspotAutoAddableTest : SysuiTestCase() {
+
+ @Mock private lateinit var hotspotController: HotspotController
+ @Captor private lateinit var callbackCaptor: ArgumentCaptor<HotspotController.Callback>
+
+ private lateinit var underTest: HotspotAutoAddable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest = HotspotAutoAddable(hotspotController)
+ }
+
+ @Test
+ fun enabled_addSignal() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(hotspotController).addCallback(capture(callbackCaptor))
+ callbackCaptor.value.onHotspotChanged(/* enabled = */ true, /* numDevices = */ 5)
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun notEnabled_noSignal() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(hotspotController).addCallback(capture(callbackCaptor))
+ callbackCaptor.value.onHotspotChanged(/* enabled = */ false, /* numDevices = */ 0)
+
+ assertThat(signal).isNull()
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create(HotspotTile.TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddableTest.kt
new file mode 100644
index 000000000000..e03072abce27
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddableTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.autoaddable
+
+import android.hardware.display.NightDisplayListener
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.NightDisplayTile
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestResult
+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.Answers
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NightDisplayAutoAddableTest : SysuiTestCase() {
+
+ @Mock(answer = Answers.RETURNS_SELF)
+ private lateinit var nightDisplayListenerBuilder: NightDisplayListenerModule.Builder
+ @Mock private lateinit var nightDisplayListener: NightDisplayListener
+ @Captor private lateinit var callbackCaptor: ArgumentCaptor<NightDisplayListener.Callback>
+
+ private lateinit var underTest: NightDisplayAutoAddable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(nightDisplayListenerBuilder.build()).thenReturn(nightDisplayListener)
+ }
+
+ @Test
+ fun disabled_strategyDisabled() =
+ testWithFeatureAvailability(enabled = false) {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Disabled)
+ }
+
+ @Test
+ fun enabled_strategyIfNotAdded() = testWithFeatureAvailability {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+ }
+
+ @Test
+ fun listenerCreatedForCorrectUser() = testWithFeatureAvailability {
+ val user = 42
+ backgroundScope.launch { underTest.autoAddSignal(user).collect() }
+ runCurrent()
+
+ val inOrder = inOrder(nightDisplayListenerBuilder)
+ inOrder.verify(nightDisplayListenerBuilder).setUser(user)
+ inOrder.verify(nightDisplayListenerBuilder).build()
+ }
+
+ @Test
+ fun onCancelFlow_removeCallback() = testWithFeatureAvailability {
+ val job = launch { underTest.autoAddSignal(0).collect() }
+ runCurrent()
+ job.cancel()
+ runCurrent()
+ verify(nightDisplayListener).setCallback(null)
+ }
+
+ @Test
+ fun onActivatedTrue_addSignal() = testWithFeatureAvailability {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(nightDisplayListener).setCallback(capture(callbackCaptor))
+ callbackCaptor.value.onActivated(true)
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ private fun testWithFeatureAvailability(
+ enabled: Boolean = true,
+ body: suspend TestScope.() -> TestResult
+ ) = runTest {
+ context.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_nightDisplayAvailable,
+ enabled
+ )
+ underTest = NightDisplayAutoAddable(nightDisplayListenerBuilder, context)
+ body()
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create(NightDisplayTile.TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt
new file mode 100644
index 000000000000..7b4a55ed1750
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddableTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.ReduceBrightColorsController
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestResult
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ReduceBrightColorsAutoAddableTest : SysuiTestCase() {
+
+ @Mock private lateinit var reduceBrightColorsController: ReduceBrightColorsController
+ @Captor
+ private lateinit var reduceBrightColorsListenerCaptor:
+ ArgumentCaptor<ReduceBrightColorsController.Listener>
+
+ private lateinit var underTest: ReduceBrightColorsAutoAddable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun notAvailable_strategyDisabled() =
+ testWithFeatureAvailability(available = false) {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Disabled)
+ }
+
+ @Test
+ fun available_strategyIfNotAdded() =
+ testWithFeatureAvailability(available = true) {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+ }
+
+ @Test
+ fun activated_addSignal() = testWithFeatureAvailability {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(reduceBrightColorsController).addCallback(capture(reduceBrightColorsListenerCaptor))
+
+ reduceBrightColorsListenerCaptor.value.onActivated(true)
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun notActivated_noSignal() = testWithFeatureAvailability {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(reduceBrightColorsController).addCallback(capture(reduceBrightColorsListenerCaptor))
+
+ reduceBrightColorsListenerCaptor.value.onActivated(false)
+
+ assertThat(signal).isNull()
+ }
+
+ private fun testWithFeatureAvailability(
+ available: Boolean = true,
+ body: suspend TestScope.() -> TestResult
+ ) = runTest {
+ underTest = ReduceBrightColorsAutoAddable(reduceBrightColorsController, available)
+ body()
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create(ReduceBrightColorsTile.TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt
new file mode 100644
index 000000000000..fb35a3a70e92
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddableTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.autoaddable
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.statusbar.policy.SafetyController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SafetyCenterAutoAddableTest : SysuiTestCase() {
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Mock private lateinit var safetyController: SafetyController
+ @Mock private lateinit var packageManager: PackageManager
+ @Captor
+ private lateinit var safetyControllerListenerCaptor: ArgumentCaptor<SafetyController.Listener>
+
+ private lateinit var underTest: SafetyCenterAutoAddable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ context.ensureTestableResources()
+
+ // Set these by default, will also test special cases
+ context.orCreateTestableResources.addOverride(
+ R.string.safety_quick_settings_tile_class,
+ SAFETY_TILE_CLASS_NAME
+ )
+ whenever(packageManager.permissionControllerPackageName)
+ .thenReturn(PERMISSION_CONTROLLER_PACKAGE_NAME)
+
+ underTest =
+ SafetyCenterAutoAddable(
+ safetyController,
+ packageManager,
+ context.resources,
+ testDispatcher,
+ )
+ }
+
+ @Test
+ fun strategyAlwaysTrack() =
+ testScope.runTest {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always)
+ }
+
+ @Test
+ fun tileAlwaysAdded() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun safetyCenterDisabled_removeSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(safetyController).addCallback(capture(safetyControllerListenerCaptor))
+ safetyControllerListenerCaptor.value.onSafetyCenterEnableChanged(false)
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun safetyCenterEnabled_newAddSignal() =
+ testScope.runTest {
+ val signals by collectValues(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(safetyController).addCallback(capture(safetyControllerListenerCaptor))
+ safetyControllerListenerCaptor.value.onSafetyCenterEnableChanged(true)
+
+ assertThat(signals.size).isEqualTo(2)
+ assertThat(signals.last()).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun flowCancelled_removeListener() =
+ testScope.runTest {
+ val job = launch { underTest.autoAddSignal(0).collect() }
+ runCurrent()
+
+ verify(safetyController).addCallback(capture(safetyControllerListenerCaptor))
+
+ job.cancel()
+ runCurrent()
+ verify(safetyController).removeCallback(safetyControllerListenerCaptor.value)
+ }
+
+ @Test
+ fun emptyClassName_noSignals() =
+ testScope.runTest {
+ context.orCreateTestableResources.addOverride(
+ R.string.safety_quick_settings_tile_class,
+ ""
+ )
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+ runCurrent()
+
+ verify(safetyController, never()).addCallback(any())
+
+ assertThat(signal).isNull()
+ }
+
+ companion object {
+ private const val SAFETY_TILE_CLASS_NAME = "cls"
+ private const val PERMISSION_CONTROLLER_PACKAGE_NAME = "pkg"
+ private val SPEC =
+ TileSpec.create(
+ ComponentName(PERMISSION_CONTROLLER_PACKAGE_NAME, SAFETY_TILE_CLASS_NAME)
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt
new file mode 100644
index 000000000000..6b250f4d9af9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WalletAutoAddableTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.autoaddable
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.QuickAccessWalletTile
+import com.android.systemui.statusbar.policy.WalletController
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class WalletAutoAddableTest : SysuiTestCase() {
+
+ @Mock private lateinit var walletController: WalletController
+
+ private lateinit var underTest: WalletAutoAddable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest = WalletAutoAddable(walletController)
+ }
+
+ @Test
+ fun strategyIfNotAdded() {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.IfNotAdded(SPEC))
+ }
+
+ @Test
+ fun walletPositionNull_noSignal() = runTest {
+ whenever(walletController.getWalletPosition()).thenReturn(null)
+
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+
+ assertThat(signal).isNull()
+ }
+
+ @Test
+ fun walletPositionNumber_addedInThatPosition() = runTest {
+ val position = 4
+ whenever(walletController.getWalletPosition()).thenReturn(4)
+
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC, position))
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create(QuickAccessWalletTile.TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddableTest.kt
new file mode 100644
index 000000000000..e9f7c8ab20cf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddableTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.autoaddable
+
+import android.content.pm.UserInfo
+import android.content.pm.UserInfo.FLAG_FULL
+import android.content.pm.UserInfo.FLAG_MANAGED_PROFILE
+import android.content.pm.UserInfo.FLAG_PRIMARY
+import android.content.pm.UserInfo.FLAG_PROFILE
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.WorkModeTile
+import com.android.systemui.settings.FakeUserTracker
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class WorkTileAutoAddableTest : SysuiTestCase() {
+
+ private lateinit var userTracker: FakeUserTracker
+
+ private lateinit var underTest: WorkTileAutoAddable
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ userTracker =
+ FakeUserTracker(
+ _userId = USER_INFO_0.id,
+ _userInfo = USER_INFO_0,
+ _userProfiles = listOf(USER_INFO_0)
+ )
+
+ underTest = WorkTileAutoAddable(userTracker)
+ }
+
+ @Test
+ fun changeInProfiles_hasManagedProfile_sendsAddSignal() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+
+ userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun changeInProfiles_noManagedProfile_sendsRemoveSignal() = runTest {
+ userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
+
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+
+ userTracker.set(listOf(USER_INFO_0), selectedUserIndex = 0)
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun startingWithManagedProfile_sendsAddSignal() = runTest {
+ userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
+
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun userChangeToUserWithProfile_noSignalForOriginalUser() = runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+
+ userTracker.set(listOf(USER_INFO_1, USER_INFO_WORK), selectedUserIndex = 0)
+
+ assertThat(signal).isNotEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun userChangeToUserWithoutProfile_noSignalForOriginalUser() = runTest {
+ userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
+ val signal by collectLastValue(underTest.autoAddSignal(0))
+
+ userTracker.set(listOf(USER_INFO_1), selectedUserIndex = 0)
+
+ assertThat(signal).isNotEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun strategyAlways() {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always)
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create(WorkModeTile.TILE_SPEC)
+ private val USER_INFO_0 = UserInfo(0, "", FLAG_PRIMARY or FLAG_FULL)
+ private val USER_INFO_1 = UserInfo(1, "", FLAG_FULL)
+ private val USER_INFO_WORK = UserInfo(10, "", FLAG_PROFILE or FLAG_MANAGED_PROFILE)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
new file mode 100644
index 000000000000..f924b35d9c9c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
@@ -0,0 +1,201 @@
+/*
+ * 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.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository
+import com.android.systemui.qs.pipeline.domain.autoaddable.FakeAutoAddable
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+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.anyInt
+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 AutoAddInteractorTest : SysuiTestCase() {
+ private val testScope = TestScope()
+
+ private val autoAddRepository = FakeAutoAddRepository()
+
+ @Mock private lateinit var dumpManager: DumpManager
+ @Mock private lateinit var currentTilesInteractor: CurrentTilesInteractor
+ @Mock private lateinit var logger: QSPipelineLogger
+ private lateinit var underTest: AutoAddInteractor
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(currentTilesInteractor.userId).thenReturn(MutableStateFlow(USER))
+ }
+
+ @Test
+ fun autoAddable_alwaysTrack_addSignal_tileAddedAndMarked() =
+ testScope.runTest {
+ val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)
+ val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+
+ underTest = createInteractor(setOf(fakeAutoAddable))
+
+ val position = 3
+ fakeAutoAddable.sendAddSignal(USER, position)
+ runCurrent()
+
+ verify(currentTilesInteractor).addTile(SPEC, position)
+ assertThat(autoAddedTiles).contains(SPEC)
+ }
+
+ @Test
+ fun autoAddable_alwaysTrack_addThenRemoveSignal_tileAddedAndRemoved() =
+ testScope.runTest {
+ val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)
+ val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+
+ underTest = createInteractor(setOf(fakeAutoAddable))
+
+ val position = 3
+ fakeAutoAddable.sendAddSignal(USER, position)
+ runCurrent()
+ fakeAutoAddable.sendRemoveSignal(USER)
+ runCurrent()
+
+ val inOrder = inOrder(currentTilesInteractor)
+ inOrder.verify(currentTilesInteractor).addTile(SPEC, position)
+ inOrder.verify(currentTilesInteractor).removeTiles(setOf(SPEC))
+ assertThat(autoAddedTiles).doesNotContain(SPEC)
+ }
+
+ @Test
+ fun autoAddable_alwaysTrack_addSignalWhenAddedPreviously_noop() =
+ testScope.runTest {
+ val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)
+ autoAddRepository.markTileAdded(USER, SPEC)
+ runCurrent()
+
+ underTest = createInteractor(setOf(fakeAutoAddable))
+
+ val position = 3
+ fakeAutoAddable.sendAddSignal(USER, position)
+ runCurrent()
+
+ verify(currentTilesInteractor, never()).addTile(SPEC, position)
+ }
+
+ @Test
+ fun autoAddable_disabled_noInteractionsWithCurrentTilesInteractor() =
+ testScope.runTest {
+ val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Disabled)
+ val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+
+ underTest = createInteractor(setOf(fakeAutoAddable))
+
+ val position = 3
+ fakeAutoAddable.sendAddSignal(USER, position)
+ runCurrent()
+ fakeAutoAddable.sendRemoveSignal(USER)
+ runCurrent()
+
+ verify(currentTilesInteractor, never()).addTile(any(), anyInt())
+ verify(currentTilesInteractor, never()).removeTiles(any())
+ assertThat(autoAddedTiles).doesNotContain(SPEC)
+ }
+
+ @Test
+ fun autoAddable_trackIfNotAdded_removeSignal_noop() =
+ testScope.runTest {
+ val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
+ runCurrent()
+
+ underTest = createInteractor(setOf(fakeAutoAddable))
+
+ fakeAutoAddable.sendRemoveSignal(USER)
+ runCurrent()
+
+ verify(currentTilesInteractor, never()).addTile(any(), anyInt())
+ verify(currentTilesInteractor, never()).removeTiles(any())
+ }
+
+ @Test
+ fun autoAddable_trackIfNotAdded_addSignalWhenPreviouslyAdded_noop() =
+ testScope.runTest {
+ val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
+ autoAddRepository.markTileAdded(USER, SPEC)
+ runCurrent()
+
+ underTest = createInteractor(setOf(fakeAutoAddable))
+
+ fakeAutoAddable.sendAddSignal(USER)
+ runCurrent()
+
+ verify(currentTilesInteractor, never()).addTile(any(), anyInt())
+ verify(currentTilesInteractor, never()).removeTiles(any())
+ }
+
+ @Test
+ fun autoAddable_trackIfNotAdded_addSignal_addedAndMarked() =
+ testScope.runTest {
+ val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.IfNotAdded(SPEC))
+ val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
+
+ underTest = createInteractor(setOf(fakeAutoAddable))
+
+ val position = 3
+ fakeAutoAddable.sendAddSignal(USER, position)
+ runCurrent()
+
+ verify(currentTilesInteractor).addTile(SPEC, position)
+ assertThat(autoAddedTiles).contains(SPEC)
+ }
+
+ private fun createInteractor(autoAddables: Set<AutoAddable>): AutoAddInteractor {
+ return AutoAddInteractor(
+ autoAddables,
+ autoAddRepository,
+ dumpManager,
+ logger,
+ testScope.backgroundScope
+ )
+ .apply { init(currentTilesInteractor) }
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create("spec")
+ private val USER = 10
+ }
+}
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 e7ad4896810b..30cea2d3a487 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
@@ -100,6 +100,7 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
featureFlags.set(Flags.QS_PIPELINE_NEW_HOST, true)
+ featureFlags.set(Flags.QS_PIPELINE_AUTO_ADD, true)
userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt
new file mode 100644
index 000000000000..9ea079fc9c4b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt
@@ -0,0 +1,42 @@
+/*
+ * 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 com.android.systemui.qs.pipeline.shared.TileSpec
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeAutoAddRepository : AutoAddRepository {
+
+ private val autoAddedTilesPerUser = mutableMapOf<Int, MutableStateFlow<Set<TileSpec>>>()
+
+ override fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> {
+ return getFlow(userId)
+ }
+
+ override suspend fun markTileAdded(userId: Int, spec: TileSpec) {
+ if (spec == TileSpec.Invalid) return
+ with(getFlow(userId)) { value = value.toMutableSet().apply { add(spec) } }
+ }
+
+ override suspend fun unmarkTileAdded(userId: Int, spec: TileSpec) {
+ with(getFlow(userId)) { value = value.toMutableSet().apply { remove(spec) } }
+ }
+
+ private fun getFlow(userId: Int): MutableStateFlow<Set<TileSpec>> =
+ autoAddedTilesPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/autoaddable/FakeAutoAddable.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/autoaddable/FakeAutoAddable.kt
new file mode 100644
index 000000000000..ebdd6fd7aac0
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/autoaddable/FakeAutoAddable.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.autoaddable
+
+import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+
+class FakeAutoAddable(
+ private val spec: TileSpec,
+ override val autoAddTracking: AutoAddTracking,
+) : AutoAddable {
+
+ private val signalsPerUser = mutableMapOf<Int, MutableStateFlow<AutoAddSignal?>>()
+ private fun getFlow(userId: Int): MutableStateFlow<AutoAddSignal?> =
+ signalsPerUser.getOrPut(userId) { MutableStateFlow(null) }
+
+ override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return getFlow(userId).asStateFlow().filterNotNull()
+ }
+
+ suspend fun sendRemoveSignal(userId: Int) {
+ getFlow(userId).value = AutoAddSignal.Remove(spec)
+ }
+
+ suspend fun sendAddSignal(userId: Int, position: Int = POSITION_AT_END) {
+ getFlow(userId).value = AutoAddSignal.Add(spec, position)
+ }
+
+ override val description: String
+ get() = "FakeAutoAddable($spec)"
+}