Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f428b8bd authored by Fabián Kozynski's avatar Fabián Kozynski
Browse files

Add a TileSpecRepository

This tracks and modifies Settings.Secure.QS_TILES.

Also add a PrototypeCoreStartable to hook up parts of the pipeline in progress for testing.

Flag: QS_PIPELINE_NEW_HOST
Test: atest TileSpecSettingsRepositoryTest
Test: manual see logs
Test: adb shell cmd statusbar qs-pipeline
Fixes: 274108007
Change-Id: Ia6b5cd0627040d9833a1ec56c51ad445c192f8b5
parent a56d656b
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -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(",")));
+1 −1
Original line number Diff line number Diff line
@@ -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);
+1 −1
Original line number Diff line number Diff line
@@ -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() {
+17 −0
Original line number Diff line number Diff line
@@ -16,15 +16,32 @@

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
+191 −0
Original line number Diff line number Diff line
/*
 * 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 = ","
    }
}
Loading