Merge changes from topic "274106423" into udc-dev

* changes:
  Add a TileSpecRepository
  Add a TileSpec data type
  Add a log buffer for the new pipeline
This commit is contained in:
Fabian Kozynski
2023-03-29 21:33:08 +00:00
committed by Android (Google) Code Review
12 changed files with 982 additions and 10 deletions

View File

@@ -40,13 +40,12 @@ public interface QSHost extends PanelInteractor {
/**
* Returns the default QS tiles for the context.
* @param context the context to obtain the resources from
* @param res the resources to use to determine the default tiles
* @return a list of specs of the default tiles
*/
static List<String> getDefaultSpecs(Context context) {
static List<String> getDefaultSpecs(Resources res) {
final ArrayList<String> tiles = new ArrayList();
final Resources res = context.getResources();
final String defaultTileList = res.getString(R.string.quick_settings_tiles_default);
tiles.addAll(Arrays.asList(defaultTileList.split(",")));

View File

@@ -600,7 +600,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P
if (tile.isEmpty()) continue;
if (tile.equals("default")) {
if (!addedDefault) {
List<String> defaultSpecs = QSHost.getDefaultSpecs(context);
List<String> defaultSpecs = QSHost.getDefaultSpecs(context.getResources());
for (String spec : defaultSpecs) {
if (!addedSpecs.contains(spec)) {
tiles.add(spec);

View File

@@ -175,7 +175,7 @@ public class QSCustomizerController extends ViewController<QSCustomizer> {
private void reset() {
mTileAdapter.resetTileSpecs(QSHost.getDefaultSpecs(getContext()));
mTileAdapter.resetTileSpecs(QSHost.getDefaultSpecs(getContext().getResources()));
}
public boolean isCustomizing() {

View File

@@ -29,6 +29,7 @@ import com.android.systemui.qs.AutoAddTracker;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.ReduceBrightColorsController;
import com.android.systemui.qs.external.QSExternalModule;
import com.android.systemui.qs.pipeline.dagger.QSPipelineModule;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.statusbar.phone.AutoTileManager;
import com.android.systemui.statusbar.phone.ManagedProfileController;
@@ -40,14 +41,14 @@ import com.android.systemui.statusbar.policy.SafetyController;
import com.android.systemui.statusbar.policy.WalletController;
import com.android.systemui.util.settings.SecureSettings;
import java.util.Map;
import javax.inject.Named;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.Multibinds;
import java.util.Map;
import javax.inject.Named;
/**
* Module for QS dependencies
*/
@@ -56,7 +57,8 @@ import dagger.multibindings.Multibinds;
MediaModule.class,
QSExternalModule.class,
QSFlagsModule.class,
QSHostModule.class
QSHostModule.class,
QSPipelineModule.class,
}
)
public interface QSModule {

View File

@@ -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.dagger
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository
import com.android.systemui.qs.pipeline.prototyping.PrototypeCoreStartable
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
abstract class QSPipelineModule {
/** Implementation for [TileSpecRepository] */
@Binds
abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository
@Binds
@IntoMap
@ClassKey(PrototypeCoreStartable::class)
abstract fun providePrototypeCoreStartable(startable: PrototypeCoreStartable): CoreStartable
companion object {
/**
* Provides a logging buffer for all logs related to the new Quick Settings pipeline to log
* the list of current tiles.
*/
@Provides
@SysUISingleton
@QSTileListLog
fun provideQSTileListLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create(QSPipelineLogger.TILE_LIST_TAG, maxSize = 700, systrace = false)
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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 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. */
@Qualifier @MustBeDocumented @Retention(RetentionPolicy.RUNTIME) annotation class QSTileListLog

View File

@@ -0,0 +1,191 @@
/*
* 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.annotation.UserIdInt
import android.content.res.Resources
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.dagger.qualifiers.Main
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
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.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext
/** Repository that tracks the current tiles. */
interface TileSpecRepository {
/**
* Returns a flow of the current list of [TileSpec] for a given [userId].
*
* Tiles will never be [TileSpec.Invalid] in the list and it will never be empty.
*/
fun tilesSpecs(@UserIdInt userId: Int): Flow<List<TileSpec>>
/**
* Adds a [tile] for a given [userId] at [position]. Using [POSITION_AT_END] will add the tile
* at the end of the list.
*
* Passing [TileSpec.Invalid] is a noop.
*/
suspend fun addTile(@UserIdInt userId: Int, tile: TileSpec, position: Int = POSITION_AT_END)
/**
* Removes a [tile] for a given [userId].
*
* Passing [TileSpec.Invalid] or a non present tile is a noop.
*/
suspend fun removeTile(@UserIdInt userId: Int, tile: TileSpec)
/**
* Sets the list of current [tiles] for a given [userId].
*
* [TileSpec.Invalid] will be ignored, and an effectively empty list will not be stored.
*/
suspend fun setTiles(@UserIdInt userId: Int, tiles: List<TileSpec>)
companion object {
/** Position to indicate the end of the list */
const val POSITION_AT_END = -1
}
}
/**
* Implementation of [TileSpecRepository] that persist the values of tiles in
* [Settings.Secure.QS_TILES].
*
* All operations against [Settings] will be performed in a background thread.
*/
@SysUISingleton
class TileSpecSettingsRepository
@Inject
constructor(
private val secureSettings: SecureSettings,
@Main private val resources: Resources,
private val logger: QSPipelineLogger,
@Background private val backgroundDispatcher: CoroutineDispatcher,
) : TileSpecRepository {
override fun tilesSpecs(userId: Int): Flow<List<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) ?: "" }
.onEach { logger.logTilesChangedInSettings(it, userId) }
.map { parseTileSpecs(it, userId) }
.flowOn(backgroundDispatcher)
}
override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) {
if (tile == TileSpec.Invalid) {
return
}
val tilesList = loadTiles(userId).toMutableList()
if (tile !in tilesList) {
if (position < 0) {
tilesList.add(tile)
} else {
tilesList.add(position, tile)
}
storeTiles(userId, tilesList)
}
}
override suspend fun removeTile(userId: Int, tile: TileSpec) {
if (tile == TileSpec.Invalid) {
return
}
val tilesList = loadTiles(userId).toMutableList()
if (tilesList.remove(tile)) {
storeTiles(userId, tilesList.toList())
}
}
override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) {
val filtered = tiles.filter { it != TileSpec.Invalid }
if (filtered.isNotEmpty()) {
storeTiles(userId, filtered)
}
}
private suspend fun loadTiles(@UserIdInt forUser: Int): List<TileSpec> {
return withContext(backgroundDispatcher) {
(secureSettings.getStringForUser(SETTING, forUser) ?: "")
.split(DELIMITER)
.map(TileSpec::create)
.filter { it !is TileSpec.Invalid }
}
}
private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) {
val toStore =
tiles
.filter { it !is TileSpec.Invalid }
.joinToString(DELIMITER, transform = TileSpec::spec)
withContext(backgroundDispatcher) {
secureSettings.putStringForUser(
SETTING,
toStore,
null,
false,
forUser,
true,
)
}
}
private fun parseTileSpecs(tilesFromSettings: String, user: Int): List<TileSpec> {
val fromSettings =
tilesFromSettings.split(DELIMITER).map(TileSpec::create).filter {
it != TileSpec.Invalid
}
return if (fromSettings.isNotEmpty()) {
fromSettings.also { logger.logParsedTiles(it, false, user) }
} else {
QSHost.getDefaultSpecs(resources)
.map(TileSpec::create)
.filter { it != TileSpec.Invalid }
.also { logger.logParsedTiles(it, true, user) }
}
}
companion object {
private const val SETTING = Settings.Secure.QS_TILES
private const val DELIMITER = ","
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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 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.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 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 {}
}
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.removeTile(user, 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"
}
}

View File

@@ -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.shared
import android.content.ComponentName
import android.text.TextUtils
import com.android.systemui.qs.external.CustomTile
/**
* Container for the spec that identifies a tile.
*
* A tile's [spec] is one of two options:
* * `custom(<componentName>)`: A [ComponentName] surrounded by [CustomTile.PREFIX] and terminated
* by `)`, represents a tile provided by an app, corresponding to a `TileService`.
* * a string not starting with [CustomTile.PREFIX], representing a tile provided by SystemUI.
*/
sealed class TileSpec private constructor(open val spec: String) {
/** Represents a spec that couldn't be parsed into a valid type of tile. */
object Invalid : TileSpec("") {
override fun toString(): String {
return "TileSpec.INVALID"
}
}
/** Container for the spec of a tile provided by SystemUI. */
data class PlatformTileSpec
internal constructor(
override val spec: String,
) : TileSpec(spec)
/**
* Container for the spec of a tile provided by an app.
*
* [componentName] indicates the associated `TileService`.
*/
data class CustomTileSpec
internal constructor(
override val spec: String,
val componentName: ComponentName,
) : TileSpec(spec)
companion object {
/** Create a [TileSpec] from the string [spec]. */
fun create(spec: String): TileSpec {
return if (TextUtils.isEmpty(spec)) {
Invalid
} else if (!spec.isCustomTileSpec) {
PlatformTileSpec(spec)
} else {
spec.componentName?.let { CustomTileSpec(spec, it) } ?: Invalid
}
}
private val String.isCustomTileSpec: Boolean
get() = startsWith(CustomTile.PREFIX)
private val String.componentName: ComponentName?
get() =
if (!isCustomTileSpec) {
null
} else {
if (endsWith(")")) {
val extracted = substring(CustomTile.PREFIX.length, length - 1)
ComponentName.unflattenFromString(extracted)
} else {
null
}
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.shared.logging
import android.annotation.UserIdInt
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.qs.pipeline.dagger.QSTileListLog
import com.android.systemui.qs.pipeline.shared.TileSpec
import javax.inject.Inject
/**
* Logger for the new pipeline.
*
* This may log to different buffers depending of the function of the log.
*/
class QSPipelineLogger
@Inject
constructor(
@QSTileListLog private val tileListLogBuffer: LogBuffer,
) {
companion object {
const val TILE_LIST_TAG = "QSTileListLog"
}
/**
* Log the tiles that are parsed in the repo. This is effectively what is surfaces in the flow.
*
* [usesDefault] indicates if the default tiles were used (due to the setting being empty or
* invalid).
*/
fun logParsedTiles(tiles: List<TileSpec>, usesDefault: Boolean, user: Int) {
tileListLogBuffer.log(
TILE_LIST_TAG,
LogLevel.DEBUG,
{
str1 = tiles.toString()
bool1 = usesDefault
int1 = user
},
{ "Parsed tiles (default=$bool1, user=$int1): $str1" }
)
}
/**
* Logs when the tiles change in Settings.
*
* This could be caused by SystemUI, or restore.
*/
fun logTilesChangedInSettings(newTiles: String, @UserIdInt user: Int) {
tileListLogBuffer.log(
TILE_LIST_TAG,
LogLevel.VERBOSE,
{
str1 = newTiles
int1 = user
},
{ "Tiles changed in settings for user $int1: $str1" }
)
}
}

View File

@@ -0,0 +1,341 @@
/*
* 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.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
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
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
class TileSpecSettingsRepositoryTest : SysuiTestCase() {
private lateinit var secureSettings: FakeSettings
@Mock private lateinit var logger: QSPipelineLogger
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
private lateinit var underTest: TileSpecSettingsRepository
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
secureSettings = FakeSettings()
with(context.orCreateTestableResources) {
addOverride(R.string.quick_settings_tiles_default, DEFAULT_TILES)
}
underTest =
TileSpecSettingsRepository(
secureSettings,
context.resources,
logger,
testDispatcher,
)
}
@Test
fun emptySetting_usesDefaultValue() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
assertThat(tiles).isEqualTo(getDefaultTileSpecs())
}
@Test
fun changeInSettings_changesValue() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
storeTilesForUser("a", 0)
assertThat(tiles).isEqualTo(listOf(TileSpec.create("a")))
storeTilesForUser("a,custom(b/c)", 0)
assertThat(tiles)
.isEqualTo(listOf(TileSpec.create("a"), TileSpec.create("custom(b/c)")))
}
@Test
fun tilesForCorrectUsers() =
testScope.runTest {
val tilesFromUser0 by collectLastValue(underTest.tilesSpecs(0))
val tilesFromUser1 by collectLastValue(underTest.tilesSpecs(1))
val user0Tiles = "a"
val user1Tiles = "custom(b/c)"
storeTilesForUser(user0Tiles, 0)
storeTilesForUser(user1Tiles, 1)
assertThat(tilesFromUser0).isEqualTo(user0Tiles.toTileSpecs())
assertThat(tilesFromUser1).isEqualTo(user1Tiles.toTileSpecs())
}
@Test
fun invalidTilesAreNotPresent() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
val specs = "d,custom(bad)"
storeTilesForUser(specs, 0)
assertThat(tiles).isEqualTo(specs.toTileSpecs().filter { it != TileSpec.Invalid })
}
@Test
fun noValidTiles_defaultSet() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
storeTilesForUser("custom(bad),custom()", 0)
assertThat(tiles).isEqualTo(getDefaultTileSpecs())
}
@Test
fun addTileAtEnd() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
storeTilesForUser("a", 0)
underTest.addTile(userId = 0, TileSpec.create("b"))
val expected = "a,b"
assertThat(loadTilesForUser(0)).isEqualTo(expected)
assertThat(tiles).isEqualTo(expected.toTileSpecs())
}
@Test
fun addTileAtPosition() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
storeTilesForUser("a,custom(b/c)", 0)
underTest.addTile(userId = 0, TileSpec.create("d"), position = 1)
val expected = "a,d,custom(b/c)"
assertThat(loadTilesForUser(0)).isEqualTo(expected)
assertThat(tiles).isEqualTo(expected.toTileSpecs())
}
@Test
fun addInvalidTile_noop() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
val specs = "a,custom(b/c)"
storeTilesForUser(specs, 0)
underTest.addTile(userId = 0, TileSpec.Invalid)
assertThat(loadTilesForUser(0)).isEqualTo(specs)
assertThat(tiles).isEqualTo(specs.toTileSpecs())
}
@Test
fun addTileForOtherUser_addedInThatUser() =
testScope.runTest {
val tilesUser0 by collectLastValue(underTest.tilesSpecs(0))
val tilesUser1 by collectLastValue(underTest.tilesSpecs(1))
storeTilesForUser("a", 0)
storeTilesForUser("b", 1)
underTest.addTile(userId = 1, TileSpec.create("c"))
assertThat(loadTilesForUser(0)).isEqualTo("a")
assertThat(tilesUser0).isEqualTo("a".toTileSpecs())
assertThat(loadTilesForUser(1)).isEqualTo("b,c")
assertThat(tilesUser1).isEqualTo("b,c".toTileSpecs())
}
@Test
fun removeTile() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
storeTilesForUser("a,b", 0)
underTest.removeTile(userId = 0, TileSpec.create("a"))
assertThat(loadTilesForUser(0)).isEqualTo("b")
assertThat(tiles).isEqualTo("b".toTileSpecs())
}
@Test
fun removeTileNotThere_noop() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
val specs = "a,b"
storeTilesForUser(specs, 0)
underTest.removeTile(userId = 0, TileSpec.create("c"))
assertThat(loadTilesForUser(0)).isEqualTo(specs)
assertThat(tiles).isEqualTo(specs.toTileSpecs())
}
@Test
fun removeInvalidTile_noop() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
val specs = "a,b"
storeTilesForUser(specs, 0)
underTest.removeTile(userId = 0, TileSpec.Invalid)
assertThat(loadTilesForUser(0)).isEqualTo(specs)
assertThat(tiles).isEqualTo(specs.toTileSpecs())
}
@Test
fun removeTileFromSecondaryUser_removedOnlyInCorrectUser() =
testScope.runTest {
val user0Tiles by collectLastValue(underTest.tilesSpecs(0))
val user1Tiles by collectLastValue(underTest.tilesSpecs(1))
val specs = "a,b"
storeTilesForUser(specs, 0)
storeTilesForUser(specs, 1)
underTest.removeTile(userId = 1, TileSpec.create("a"))
assertThat(loadTilesForUser(0)).isEqualTo(specs)
assertThat(user0Tiles).isEqualTo(specs.toTileSpecs())
assertThat(loadTilesForUser(1)).isEqualTo("b")
assertThat(user1Tiles).isEqualTo("b".toTileSpecs())
}
@Test
fun changeTiles() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
val specs = "a,custom(b/c)"
underTest.setTiles(userId = 0, specs.toTileSpecs())
assertThat(loadTilesForUser(0)).isEqualTo(specs)
assertThat(tiles).isEqualTo(specs.toTileSpecs())
}
@Test
fun changeTiles_ignoresInvalid() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
val specs = "a,custom(b/c)"
underTest.setTiles(userId = 0, listOf(TileSpec.Invalid) + specs.toTileSpecs())
assertThat(loadTilesForUser(0)).isEqualTo(specs)
assertThat(tiles).isEqualTo(specs.toTileSpecs())
}
@Test
fun changeTiles_empty_noChanges() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
underTest.setTiles(userId = 0, emptyList())
assertThat(loadTilesForUser(0)).isNull()
assertThat(tiles).isEqualTo(getDefaultTileSpecs())
}
@Test
fun changeTiles_forCorrectUser() =
testScope.runTest {
val user0Tiles by collectLastValue(underTest.tilesSpecs(0))
val user1Tiles by collectLastValue(underTest.tilesSpecs(1))
val specs = "a"
storeTilesForUser(specs, 0)
storeTilesForUser(specs, 1)
underTest.setTiles(userId = 1, "b".toTileSpecs())
assertThat(loadTilesForUser(0)).isEqualTo("a")
assertThat(user0Tiles).isEqualTo(specs.toTileSpecs())
assertThat(loadTilesForUser(1)).isEqualTo("b")
assertThat(user1Tiles).isEqualTo("b".toTileSpecs())
}
@Test
fun multipleConcurrentRemovals_bothRemoved() =
testScope.runTest {
val tiles by collectLastValue(underTest.tilesSpecs(0))
val specs = "a,b,c"
storeTilesForUser(specs, 0)
coroutineScope {
underTest.removeTile(userId = 0, TileSpec.create("c"))
underTest.removeTile(userId = 0, TileSpec.create("a"))
}
assertThat(loadTilesForUser(0)).isEqualTo("b")
assertThat(tiles).isEqualTo("b".toTileSpecs())
}
private fun getDefaultTileSpecs(): List<TileSpec> {
return QSHost.getDefaultSpecs(context.resources).map(TileSpec::create)
}
private fun storeTilesForUser(specs: String, forUser: Int) {
secureSettings.putStringForUser(SETTING, specs, forUser)
}
private fun loadTilesForUser(forUser: Int): String? {
return secureSettings.getStringForUser(SETTING, forUser)
}
companion object {
private const val DEFAULT_TILES = "a,b,c"
private const val SETTING = Settings.Secure.QS_TILES
private fun String.toTileSpecs(): List<TileSpec> {
return split(",").map(TileSpec::create)
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.shared
import android.content.ComponentName
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
class TileSpecTest : SysuiTestCase() {
@Test
fun platformTile() {
val spec = "spec"
val tileSpec = TileSpec.create(spec)
assertThat(tileSpec is TileSpec.PlatformTileSpec).isTrue()
assertThat(tileSpec.spec).isEqualTo(spec)
}
@Test
fun customTile() {
val componentName = ComponentName("test_pkg", "test_cls")
val spec = CUSTOM_TILE_PREFIX + componentName.flattenToString() + ")"
val tileSpec = TileSpec.create(spec)
assertThat(tileSpec is TileSpec.CustomTileSpec).isTrue()
assertThat(tileSpec.spec).isEqualTo(spec)
assertThat((tileSpec as TileSpec.CustomTileSpec).componentName).isEqualTo(componentName)
}
@Test
fun emptyCustomTile_invalid() {
val spec = CUSTOM_TILE_PREFIX + ")"
val tileSpec = TileSpec.create(spec)
assertThat(tileSpec).isEqualTo(TileSpec.Invalid)
}
@Test
fun invalidCustomTileSpec_invalid() {
val spec = CUSTOM_TILE_PREFIX + "invalid)"
val tileSpec = TileSpec.create(spec)
assertThat(tileSpec).isEqualTo(TileSpec.Invalid)
}
@Test
fun customTileNotEndsWithParenthesis_invalid() {
val componentName = ComponentName("test_pkg", "test_cls")
val spec = CUSTOM_TILE_PREFIX + componentName.flattenToString()
val tileSpec = TileSpec.create(spec)
assertThat(tileSpec).isEqualTo(TileSpec.Invalid)
}
@Test
fun emptySpec_invalid() {
assertThat(TileSpec.create("")).isEqualTo(TileSpec.Invalid)
}
companion object {
private const val CUSTOM_TILE_PREFIX = "custom("
}
}