From 4f180ef36db2f1a31a3876f780940f311bae643f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Kozynski?= Date: Thu, 13 Apr 2023 11:09:35 -0400 Subject: [PATCH 1/2] Add auto add flag Bug: 278068252 Test: build Change-Id: I659d6193260ef7108f8d1b0e6f1e3f406327ce97 --- packages/SystemUI/src/com/android/systemui/flags/Flags.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 2987004347e43..5d2ac456f92aa 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -281,6 +281,10 @@ object Flags { @JvmField val QS_PIPELINE_NEW_HOST = unreleasedFlag(504, "qs_pipeline_new_host", teamfood = false) + // TODO(b/278068252): Tracking Bug + @JvmField + val QS_PIPELINE_AUTO_ADD = unreleasedFlag(505, "qs_pipeline_auto_add", teamfood = false) + // TODO(b/254512383): Tracking Bug @JvmField val FULL_SCREEN_USER_SWITCHER = From d8af4ab7b481ce48eb9f12d6e5f1289b534886f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Kozynski?= Date: Thu, 13 Apr 2023 10:22:36 -0400 Subject: [PATCH 2/2] Add an AutoAddRepository Flag: QS_PIPELINE_AUTO_ADD Test: atest AutoAddSettingsRepositoryTest Fixes: 277945691 Change-Id: I39e092356a429d351222a14df391d10b622ce633 --- .../qs/pipeline/dagger/QSAutoAddModule.kt | 28 +++ .../qs/pipeline/dagger/QSPipelineModule.kt | 2 +- .../data/repository/AutoAddRepository.kt | 135 +++++++++++++ .../prototyping/PrototypeCoreStartable.kt | 11 + .../AutoAddSettingsRepositoryTest.kt | 189 ++++++++++++++++++ 5 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt create mode 100644 packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt 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 new file mode 100644 index 0000000000000..99792286d962e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSAutoAddModule.kt @@ -0,0 +1,28 @@ +/* + * 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 com.android.systemui.qs.pipeline.data.repository.AutoAddRepository +import com.android.systemui.qs.pipeline.data.repository.AutoAddSettingRepository +import dagger.Binds +import dagger.Module + +@Module +abstract class QSAutoAddModule { + + @Binds abstract fun bindAutoAddRepository(impl: AutoAddSettingRepository): AutoAddRepository +} 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 e212bc4e7f5d7..e85440cad6b0c 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 @@ -32,7 +32,7 @@ import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap -@Module +@Module(includes = [QSAutoAddModule::class]) abstract class QSPipelineModule { /** Implementation for [TileSpecRepository] */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt new file mode 100644 index 0000000000000..43a16b69d1a82 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.data.repository + +import android.database.ContentObserver +import android.provider.Settings +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.qs.pipeline.shared.TileSpec +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Repository to track what QS tiles have been auto-added */ +interface AutoAddRepository { + + /** Flow of tiles that have been auto-added */ + fun autoAddedTiles(userId: Int): Flow> + + /** Mark a tile as having been auto-added */ + suspend fun markTileAdded(userId: Int, spec: TileSpec) + + /** + * Unmark a tile as having been auto-added. This is used for tiles that can be auto-added + * multiple times. + */ + suspend fun unmarkTileAdded(userId: Int, spec: TileSpec) +} + +/** + * Implementation that tracks the auto-added tiles stored in [Settings.Secure.QS_AUTO_ADDED_TILES]. + */ +@SysUISingleton +class AutoAddSettingRepository +@Inject +constructor( + private val secureSettings: SecureSettings, + @Background private val bgDispatcher: CoroutineDispatcher, +) : AutoAddRepository { + override fun autoAddedTiles(userId: Int): Flow> { + return conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + secureSettings.registerContentObserverForUser(SETTING, observer, userId) + + awaitClose { secureSettings.unregisterContentObserver(observer) } + } + .onStart { emit(Unit) } + .map { secureSettings.getStringForUser(SETTING, userId) ?: "" } + .distinctUntilChanged() + .map { + it.split(DELIMITER).map(TileSpec::create).filter { it !is TileSpec.Invalid }.toSet() + } + .flowOn(bgDispatcher) + } + + override suspend fun markTileAdded(userId: Int, spec: TileSpec) { + if (spec is TileSpec.Invalid) { + return + } + val added = load(userId).toMutableSet() + if (added.add(spec)) { + store(userId, added) + } + } + + override suspend fun unmarkTileAdded(userId: Int, spec: TileSpec) { + if (spec is TileSpec.Invalid) { + return + } + val added = load(userId).toMutableSet() + if (added.remove(spec)) { + store(userId, added) + } + } + + private suspend fun store(userId: Int, tiles: Set) { + val toStore = + tiles + .filter { it !is TileSpec.Invalid } + .joinToString(DELIMITER, transform = TileSpec::spec) + withContext(bgDispatcher) { + secureSettings.putStringForUser( + SETTING, + toStore, + null, + false, + userId, + true, + ) + } + } + + private suspend fun load(userId: Int): Set { + return withContext(bgDispatcher) { + (secureSettings.getStringForUser(SETTING, userId) ?: "") + .split(",") + .map(TileSpec::create) + .filter { it !is TileSpec.Invalid } + .toSet() + } + } + + companion object { + private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES + private const val DELIMITER = "," + } +} 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 index 89408006c3007..bbd72341acc68 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt @@ -16,11 +16,13 @@ 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 @@ -46,6 +48,7 @@ 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, @@ -60,6 +63,13 @@ constructor( .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) } } @@ -105,5 +115,6 @@ constructor( companion object { private const val COMMAND = "qs-pipeline" + private const val TAG = "PrototypeCoreStartable" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt new file mode 100644 index 0000000000000..77b3e69f43849 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.data.repository + +import android.provider.Settings +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.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.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidTestingRunner::class) +class AutoAddSettingsRepositoryTest : SysuiTestCase() { + private val secureSettings = FakeSettings() + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var underTest: AutoAddSettingRepository + + @Before + fun setUp() { + underTest = + AutoAddSettingRepository( + secureSettings, + testDispatcher, + ) + } + + @Test + fun nonExistentSetting_emptySet() = + testScope.runTest { + val specs by collectLastValue(underTest.autoAddedTiles(0)) + + assertThat(specs).isEmpty() + } + + @Test + fun settingsChange_correctValues() = + testScope.runTest { + val userId = 0 + val specs by collectLastValue(underTest.autoAddedTiles(userId)) + + val value = "a,custom(b/c)" + storeForUser(value, userId) + + assertThat(specs).isEqualTo(value.toSet()) + + val newValue = "a" + storeForUser(newValue, userId) + + assertThat(specs).isEqualTo(newValue.toSet()) + } + + @Test + fun tilesForCorrectUsers() = + testScope.runTest { + val tilesFromUser0 by collectLastValue(underTest.autoAddedTiles(0)) + val tilesFromUser1 by collectLastValue(underTest.autoAddedTiles(1)) + + val user0Tiles = "a" + val user1Tiles = "custom(b/c)" + storeForUser(user0Tiles, 0) + storeForUser(user1Tiles, 1) + + assertThat(tilesFromUser0).isEqualTo(user0Tiles.toSet()) + assertThat(tilesFromUser1).isEqualTo(user1Tiles.toSet()) + } + + @Test + fun noInvalidTileSpecs() = + testScope.runTest { + val userId = 0 + val tiles by collectLastValue(underTest.autoAddedTiles(userId)) + + val specs = "d,custom(bad)" + storeForUser(specs, userId) + + assertThat(tiles).isEqualTo("d".toSet()) + } + + @Test + fun markAdded() = + testScope.runTest { + val userId = 0 + val specs = mutableSetOf(TileSpec.create("a")) + underTest.markTileAdded(userId, TileSpec.create("a")) + + assertThat(loadForUser(userId).toSet()).containsExactlyElementsIn(specs) + + specs.add(TileSpec.create("b")) + underTest.markTileAdded(userId, TileSpec.create("b")) + + assertThat(loadForUser(userId).toSet()).containsExactlyElementsIn(specs) + } + + @Test + fun markAdded_multipleUsers() = + testScope.runTest { + underTest.markTileAdded(userId = 1, TileSpec.create("a")) + + assertThat(loadForUser(0).toSet()).isEmpty() + assertThat(loadForUser(1).toSet()) + .containsExactlyElementsIn(setOf(TileSpec.create("a"))) + } + + @Test + fun markAdded_Invalid_noop() = + testScope.runTest { + val userId = 0 + underTest.markTileAdded(userId, TileSpec.Invalid) + + assertThat(loadForUser(userId).toSet()).isEmpty() + } + + @Test + fun unmarkAdded() = + testScope.runTest { + val userId = 0 + val specs = "a,custom(b/c)" + storeForUser(specs, userId) + + underTest.unmarkTileAdded(userId, TileSpec.create("a")) + + assertThat(loadForUser(userId).toSet()) + .containsExactlyElementsIn(setOf(TileSpec.create("custom(b/c)"))) + } + + @Test + fun unmarkAdded_multipleUsers() = + testScope.runTest { + val specs = "a,b" + storeForUser(specs, 0) + storeForUser(specs, 1) + + underTest.unmarkTileAdded(1, TileSpec.create("a")) + + assertThat(loadForUser(0).toSet()).isEqualTo(specs.toSet()) + assertThat(loadForUser(1).toSet()).isEqualTo(setOf(TileSpec.create("b"))) + } + + private fun storeForUser(specs: String, userId: Int) { + secureSettings.putStringForUser(SETTING, specs, userId) + } + + private fun loadForUser(userId: Int): String { + return secureSettings.getStringForUser(SETTING, userId) ?: "" + } + + companion object { + private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES + private const val DELIMITER = "," + + fun Set.toSeparatedString() = joinToString(DELIMITER, transform = TileSpec::spec) + + fun String.toSet(): Set { + return if (isNullOrBlank()) { + emptySet() + } else { + split(DELIMITER).map(TileSpec::create).toSet() + } + } + } +}