diff options
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)" +} |