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

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

Add an AutoAddRepository

Flag: QS_PIPELINE_AUTO_ADD
Test: atest AutoAddSettingsRepositoryTest
Fixes: 277945691
Change-Id: I39e092356a429d351222a14df391d10b622ce633
parent 4f180ef3
Loading
Loading
Loading
Loading
+28 −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.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
}
+1 −1
Original line number Diff line number Diff line
@@ -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] */
+135 −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.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 = ","
    }
}
+11 −0
Original line number Diff line number Diff line
@@ -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"
    }
}
+189 −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.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()
            }
        }
    }
}