Add an AutoAddRepository

Flag: QS_PIPELINE_AUTO_ADD
Test: atest AutoAddSettingsRepositoryTest
Fixes: 277945691
Change-Id: I39e092356a429d351222a14df391d10b622ce633
This commit is contained in:
Fabián Kozynski
2023-04-13 10:22:36 -04:00
parent 4f180ef36d
commit d8af4ab7b4
5 changed files with 364 additions and 1 deletions

View File

@@ -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
}

View File

@@ -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] */

View File

@@ -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<Set<TileSpec>>
/** 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<Set<TileSpec>> {
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<TileSpec>) {
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<TileSpec> {
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 = ","
}
}

View File

@@ -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"
}
}

View File

@@ -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<TileSpec>.toSeparatedString() = joinToString(DELIMITER, transform = TileSpec::spec)
fun String.toSet(): Set<TileSpec> {
return if (isNullOrBlank()) {
emptySet()
} else {
split(DELIMITER).map(TileSpec::create).toSet()
}
}
}
}