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:
committed by
Android (Google) Code Review
commit
042239289a
@@ -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(",")));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 = ","
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user