summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/commons/TileExt.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt196
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt112
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt259
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt187
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeCustomTileStatePersister.kt34
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt71
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileDefaultsRepository.kt11
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt58
13 files changed, 1016 insertions, 24 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt
index a321eef75a14..6f5dea32bd83 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTileStatePersister.kt
@@ -18,17 +18,19 @@ package com.android.systemui.qs.external
import android.content.ComponentName
import android.content.Context
+import android.content.SharedPreferences
import android.service.quicksettings.Tile
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
+import javax.inject.Inject
import org.json.JSONException
import org.json.JSONObject
-import javax.inject.Inject
data class TileServiceKey(val componentName: ComponentName, val user: Int) {
private val string = "${componentName.flattenToString()}:$user"
override fun toString() = string
}
+
private const val STATE = "state"
private const val LABEL = "label"
private const val SUBTITLE = "subtitle"
@@ -44,12 +46,7 @@ private const val STATE_DESCRIPTION = "state_description"
* It persists the state from a [Tile] necessary to present the view in the same state when
* retrieved, with the exception of the icon.
*/
-class CustomTileStatePersister @Inject constructor(context: Context) {
- companion object {
- private const val FILE_NAME = "custom_tiles_state"
- }
-
- private val sharedPreferences = context.getSharedPreferences(FILE_NAME, 0)
+interface CustomTileStatePersister {
/**
* Read the state from [SharedPreferences].
@@ -58,7 +55,31 @@ class CustomTileStatePersister @Inject constructor(context: Context) {
*
* Any fields that have not been saved will be set to `null`
*/
- fun readState(key: TileServiceKey): Tile? {
+ fun readState(key: TileServiceKey): Tile?
+ /**
+ * Persists the state into [SharedPreferences].
+ *
+ * The implementation does not store fields that are `null` or icons.
+ */
+ fun persistState(key: TileServiceKey, tile: Tile)
+ /**
+ * Removes the state for a given tile, user pair.
+ *
+ * Used when the tile is removed by the user.
+ */
+ fun removeState(key: TileServiceKey)
+}
+
+// TODO(b/299909989) Merge this class into into CustomTileRepository
+class CustomTileStatePersisterImpl @Inject constructor(context: Context) :
+ CustomTileStatePersister {
+ companion object {
+ private const val FILE_NAME = "custom_tiles_state"
+ }
+
+ private val sharedPreferences: SharedPreferences = context.getSharedPreferences(FILE_NAME, 0)
+
+ override fun readState(key: TileServiceKey): Tile? {
val state = sharedPreferences.getString(key.toString(), null) ?: return null
return try {
readTileFromString(state)
@@ -68,23 +89,13 @@ class CustomTileStatePersister @Inject constructor(context: Context) {
}
}
- /**
- * Persists the state into [SharedPreferences].
- *
- * The implementation does not store fields that are `null` or icons.
- */
- fun persistState(key: TileServiceKey, tile: Tile) {
+ override fun persistState(key: TileServiceKey, tile: Tile) {
val state = writeToString(tile)
sharedPreferences.edit().putString(key.toString(), state).apply()
}
- /**
- * Removes the state for a given tile, user pair.
- *
- * Used when the tile is removed by the user.
- */
- fun removeState(key: TileServiceKey) {
+ override fun removeState(key: TileServiceKey) {
sharedPreferences.edit().remove(key.toString()).apply()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
index 94137c88098e..4a34276671c1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
@@ -16,6 +16,8 @@
package com.android.systemui.qs.tiles.di
+import com.android.systemui.qs.external.CustomTileStatePersister
+import com.android.systemui.qs.external.CustomTileStatePersisterImpl
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerImpl
import com.android.systemui.qs.tiles.impl.custom.di.CustomTileComponent
@@ -52,4 +54,7 @@ interface QSTilesModule {
fun bindQSTileIntentUserInputHandler(
impl: QSTileIntentUserInputHandlerImpl
): QSTileIntentUserInputHandler
+
+ @Binds
+ fun bindCustomTileStatePersister(impl: CustomTileStatePersisterImpl): CustomTileStatePersister
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/commons/TileExt.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/commons/TileExt.kt
new file mode 100644
index 000000000000..869f6f321d21
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/commons/TileExt.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.tiles.impl.custom.commons
+
+import android.service.quicksettings.Tile
+
+fun Tile.copy(): Tile =
+ Tile().also {
+ it.icon = icon
+ it.label = label
+ it.subtitle = subtitle
+ it.contentDescription = contentDescription
+ it.stateDescription = stateDescription
+ it.activityLaunchForClick = activityLaunchForClick
+ it.state = state
+ }
+
+fun Tile.setFrom(otherTile: Tile) {
+ if (otherTile.icon != null) {
+ icon = otherTile.icon
+ }
+ if (otherTile.customLabel != null) {
+ label = otherTile.customLabel
+ }
+ if (otherTile.subtitle != null) {
+ subtitle = otherTile.subtitle
+ }
+ if (otherTile.contentDescription != null) {
+ contentDescription = otherTile.contentDescription
+ }
+ if (otherTile.stateDescription != null) {
+ stateDescription = otherTile.stateDescription
+ }
+ activityLaunchForClick = otherTile.activityLaunchForClick
+ state = otherTile.state
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
new file mode 100644
index 000000000000..ca5302e13545
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepository.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.tiles.impl.custom.data.repository
+
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import android.service.quicksettings.Tile
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.external.CustomTileStatePersister
+import com.android.systemui.qs.external.TileServiceKey
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.custom.commons.copy
+import com.android.systemui.qs.tiles.impl.custom.commons.setFrom
+import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+
+/**
+ * Repository store the [Tile] associated with the custom tile. It lives on [QSTileScope] which
+ * allows it to survive service rebinding. Given that, it provides the last received state when
+ * connected again.
+ */
+interface CustomTileRepository {
+
+ /**
+ * Restores the [Tile] if it's [isPersistable]. Restored [Tile] will be available via [getTile]
+ * (but there is no guarantee that restoration is synchronous) and emitted in [getTiles] for a
+ * corresponding [user].
+ */
+ suspend fun restoreForTheUserIfNeeded(user: UserHandle, isPersistable: Boolean)
+
+ /** Returns [Tile] updates for a [user]. */
+ fun getTiles(user: UserHandle): Flow<Tile>
+
+ /**
+ * Return current [Tile] for a [user] or null if the [user] doesn't match currently cached one.
+ * Suspending until [getTiles] returns something is a way to wait for this to become available.
+ *
+ * @throws IllegalStateException when there is no current tile.
+ */
+ fun getTile(user: UserHandle): Tile?
+
+ /**
+ * Updates tile with the non-null values from [newTile]. Overwrites the current cache when
+ * [user] differs from the cached one. [isPersistable] tile will be persisted to be possibly
+ * loaded when the [restoreForTheUserIfNeeded].
+ */
+ suspend fun updateWithTile(
+ user: UserHandle,
+ newTile: Tile,
+ isPersistable: Boolean,
+ )
+
+ /**
+ * Updates tile with the values from [defaults]. Overwrites the current cache when [user]
+ * differs from the cached one. [isPersistable] tile will be persisted to be possibly loaded
+ * when the [restoreForTheUserIfNeeded].
+ */
+ suspend fun updateWithDefaults(
+ user: UserHandle,
+ defaults: CustomTileDefaults,
+ isPersistable: Boolean,
+ )
+}
+
+@QSTileScope
+class CustomTileRepositoryImpl
+@Inject
+constructor(
+ private val tileSpec: TileSpec.CustomTileSpec,
+ private val customTileStatePersister: CustomTileStatePersister,
+ @Background private val backgroundContext: CoroutineContext,
+) : CustomTileRepository {
+
+ private val tileUpdateMutex = Mutex()
+ private val tileWithUserState =
+ MutableSharedFlow<TileWithUser>(onBufferOverflow = BufferOverflow.DROP_OLDEST, replay = 1)
+
+ override suspend fun restoreForTheUserIfNeeded(user: UserHandle, isPersistable: Boolean) {
+ if (isPersistable && getCurrentTileWithUser()?.user != user) {
+ withContext(backgroundContext) {
+ customTileStatePersister.readState(user.getKey())?.let {
+ updateWithTile(
+ user,
+ it,
+ true,
+ )
+ }
+ }
+ }
+ }
+
+ override fun getTiles(user: UserHandle): Flow<Tile> =
+ tileWithUserState.filter { it.user == user }.map { it.tile }
+
+ override fun getTile(user: UserHandle): Tile? {
+ val tileWithUser =
+ getCurrentTileWithUser() ?: throw IllegalStateException("Tile is not set")
+ return if (tileWithUser.user == user) {
+ tileWithUser.tile
+ } else {
+ null
+ }
+ }
+
+ override suspend fun updateWithTile(
+ user: UserHandle,
+ newTile: Tile,
+ isPersistable: Boolean,
+ ) = updateTile(user, isPersistable) { setFrom(newTile) }
+
+ override suspend fun updateWithDefaults(
+ user: UserHandle,
+ defaults: CustomTileDefaults,
+ isPersistable: Boolean,
+ ) {
+ if (defaults is CustomTileDefaults.Result) {
+ updateTile(user, isPersistable) {
+ // Update the icon if it's not set or is the default icon.
+ val updateIcon = (icon == null || icon.isResourceEqual(defaults.icon))
+ if (updateIcon) {
+ icon = defaults.icon
+ }
+ setDefaultLabel(defaults.label)
+ }
+ }
+ }
+
+ private suspend fun updateTile(
+ user: UserHandle,
+ isPersistable: Boolean,
+ update: Tile.() -> Unit
+ ): Unit =
+ tileUpdateMutex.withLock {
+ val currentTileWithUser = getCurrentTileWithUser()
+ val tileToUpdate =
+ if (currentTileWithUser?.user == user) {
+ currentTileWithUser.tile.copy()
+ } else {
+ Tile()
+ }
+ tileToUpdate.update()
+ if (isPersistable) {
+ withContext(backgroundContext) {
+ customTileStatePersister.persistState(user.getKey(), tileToUpdate)
+ }
+ }
+ tileWithUserState.tryEmit(TileWithUser(user, tileToUpdate))
+ }
+
+ private fun getCurrentTileWithUser(): TileWithUser? = tileWithUserState.replayCache.lastOrNull()
+
+ /** Compare two icons, only works for resources. */
+ private fun Icon.isResourceEqual(icon2: Icon?): Boolean {
+ if (icon2 == null) {
+ return false
+ }
+ if (this === icon2) {
+ return true
+ }
+ if (type != Icon.TYPE_RESOURCE || icon2.type != Icon.TYPE_RESOURCE) {
+ return false
+ }
+ if (resId != icon2.resId) {
+ return false
+ }
+ return resPackage == icon2.resPackage
+ }
+
+ private fun UserHandle.getKey() = TileServiceKey(tileSpec.componentName, this.identifier)
+
+ private data class TileWithUser(val user: UserHandle, val tile: Tile)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
index 83767aa9d444..d956fdebcd32 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
@@ -24,6 +24,8 @@ import com.android.systemui.qs.tiles.impl.custom.CustomTileMapper
import com.android.systemui.qs.tiles.impl.custom.CustomTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepositoryImpl
+import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepositoryImpl
import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundComponent
import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
import dagger.Binds
@@ -50,4 +52,6 @@ interface CustomTileModule {
fun bindCustomTileDefaultsRepository(
impl: CustomTileDefaultsRepositoryImpl
): CustomTileDefaultsRepository
+
+ @Binds fun bindCustomTileRepository(impl: CustomTileRepositoryImpl): CustomTileRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
new file mode 100644
index 000000000000..351bba538463
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.tiles.impl.custom.domain.interactor
+
+import android.os.UserHandle
+import android.service.quicksettings.Tile
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.external.TileServiceManager
+import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepository
+import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+/** Manages updates of the [Tile] assigned for the current custom tile. */
+@CustomTileBoundScope
+class CustomTileInteractor
+@Inject
+constructor(
+ private val user: UserHandle,
+ private val defaultsRepository: CustomTileDefaultsRepository,
+ private val customTileRepository: CustomTileRepository,
+ private val tileServiceManager: TileServiceManager,
+ @CustomTileBoundScope private val boundScope: CoroutineScope,
+ @Background private val backgroundContext: CoroutineContext,
+) {
+
+ private val tileUpdates =
+ MutableSharedFlow<Tile>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+
+ /** [Tile] updates. [updateTile] to emit a new one. */
+ val tiles: Flow<Tile>
+ get() = customTileRepository.getTiles(user)
+
+ /**
+ * Current [Tile]
+ *
+ * @throws IllegalStateException when the repository stores a tile for another user. This means
+ * the tile hasn't been updated for the current user. Can happen when this is accessed before
+ * [init] returns.
+ */
+ val tile: Tile
+ get() =
+ customTileRepository.getTile(user)
+ ?: throw IllegalStateException("Attempt to get a tile for a wrong user")
+
+ /**
+ * Initializes the repository for the current user. Suspends until it's safe to call [tile]
+ * which needs at least one of the following:
+ * - defaults are loaded;
+ * - receive tile update in [updateTile];
+ * - restoration happened for a persisted tile.
+ */
+ suspend fun init() {
+ launchUpdates()
+ customTileRepository.restoreForTheUserIfNeeded(user, tileServiceManager.isActiveTile)
+ // Suspend to make sure it gets the tile from one of the sources: restoration, defaults, or
+ // tile update.
+ customTileRepository.getTiles(user).firstOrNull()
+ }
+
+ private fun launchUpdates() {
+ tileUpdates
+ .onEach {
+ customTileRepository.updateWithTile(
+ user,
+ it,
+ tileServiceManager.isActiveTile,
+ )
+ }
+ .flowOn(backgroundContext)
+ .launchIn(boundScope)
+ defaultsRepository
+ .defaults(user)
+ .onEach {
+ customTileRepository.updateWithDefaults(
+ user,
+ it,
+ tileServiceManager.isActiveTile,
+ )
+ }
+ .flowOn(backgroundContext)
+ .launchIn(boundScope)
+ }
+
+ /** Updates current [Tile]. Emits a new event in [tiles]. */
+ fun updateTile(newTile: Tile) {
+ tileUpdates.tryEmit(newTile)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt
index a9f8ea0194c1..81d02b8043b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileStatePersisterTest.kt
@@ -85,7 +85,7 @@ class CustomTileStatePersisterTest : SysuiTestCase() {
`when`(sharedPreferences.edit()).thenReturn(editor)
tile = Tile()
- customTileStatePersister = CustomTileStatePersister(mockContext)
+ customTileStatePersister = CustomTileStatePersisterImpl(mockContext)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
new file mode 100644
index 000000000000..cf076c557765
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt
@@ -0,0 +1,259 @@
+/*
+ * 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.tiles.impl.custom.data.repository
+
+import android.content.ComponentName
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import android.service.quicksettings.Tile
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.qs.external.TileServiceKey
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat
+import com.android.systemui.qs.tiles.impl.custom.commons.copy
+import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class CustomTileRepositoryTest : SysuiTestCase() {
+
+ private val testScope = TestScope()
+
+ private val persister = FakeCustomTileStatePersister()
+
+ private val underTest: CustomTileRepository =
+ CustomTileRepositoryImpl(
+ TileSpec.create(TEST_COMPONENT),
+ persister,
+ testScope.testScheduler,
+ )
+
+ @Test
+ fun persistableTileIsRestoredForUser() =
+ testScope.runTest {
+ persister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
+ persister.persistState(TEST_TILE_KEY_2, TEST_TILE_2)
+
+ underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
+ runCurrent()
+
+ assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+ assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+ }
+
+ @Test
+ fun notPersistableTileIsNotRestored() =
+ testScope.runTest {
+ persister.persistState(TEST_TILE_KEY_1, TEST_TILE_1)
+ val tiles = collectValues(underTest.getTiles(TEST_USER_1))
+
+ underTest.restoreForTheUserIfNeeded(TEST_USER_1, false)
+ runCurrent()
+
+ assertThat(tiles()).isEmpty()
+ }
+
+ @Test
+ fun emptyPersistedStateIsHandled() =
+ testScope.runTest {
+ val tiles = collectValues(underTest.getTiles(TEST_USER_1))
+
+ underTest.restoreForTheUserIfNeeded(TEST_USER_1, true)
+ runCurrent()
+
+ assertThat(tiles()).isEmpty()
+ }
+
+ @Test
+ fun updatingWithPersistableTilePersists() =
+ testScope.runTest {
+ underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+ runCurrent()
+
+ assertThat(persister.readState(TEST_TILE_KEY_1)).isEqualTo(TEST_TILE_1)
+ }
+
+ @Test
+ fun updatingWithNotPersistableTileDoesntPersist() =
+ testScope.runTest {
+ underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, false)
+ runCurrent()
+
+ assertThat(persister.readState(TEST_TILE_KEY_1)).isNull()
+ }
+
+ @Test
+ fun updateWithTileEmits() =
+ testScope.runTest {
+ underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+ runCurrent()
+
+ assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+ assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+ }
+
+ @Test
+ fun updatingPeristableWithDefaultsPersists() =
+ testScope.runTest {
+ underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+ runCurrent()
+
+ assertThat(persister.readState(TEST_TILE_KEY_1)).isEqualTo(TEST_TILE_1)
+ }
+
+ @Test
+ fun updatingNotPersistableWithDefaultsDoesntPersist() =
+ testScope.runTest {
+ underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, false)
+ runCurrent()
+
+ assertThat(persister.readState(TEST_TILE_KEY_1)).isNull()
+ }
+
+ @Test
+ fun updatingPeristableWithErrorDefaultsDoesntPersist() =
+ testScope.runTest {
+ underTest.updateWithDefaults(TEST_USER_1, CustomTileDefaults.Error, true)
+ runCurrent()
+
+ assertThat(persister.readState(TEST_TILE_KEY_1)).isNull()
+ }
+
+ @Test
+ fun updateWithDefaultsEmits() =
+ testScope.runTest {
+ underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+ runCurrent()
+
+ assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(TEST_TILE_1)
+ assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(TEST_TILE_1)
+ }
+
+ @Test
+ fun getTileForAnotherUserReturnsNull() =
+ testScope.runTest {
+ underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+ runCurrent()
+
+ assertThat(underTest.getTile(TEST_USER_2)).isNull()
+ }
+
+ @Test
+ fun getTilesForAnotherUserEmpty() =
+ testScope.runTest {
+ val tiles = collectValues(underTest.getTiles(TEST_USER_2))
+
+ underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+ runCurrent()
+
+ assertThat(tiles()).isEmpty()
+ }
+
+ @Test
+ fun updatingWithTileForTheSameUserAddsData() =
+ testScope.runTest {
+ underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+ runCurrent()
+
+ underTest.updateWithTile(TEST_USER_1, Tile().apply { subtitle = "test_subtitle" }, true)
+ runCurrent()
+
+ val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
+ assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
+ assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+ }
+
+ @Test
+ fun updatingWithTileForAnotherUserOverridesTile() =
+ testScope.runTest {
+ underTest.updateWithTile(TEST_USER_1, TEST_TILE_1, true)
+ runCurrent()
+
+ val tiles = collectValues(underTest.getTiles(TEST_USER_2))
+ underTest.updateWithTile(TEST_USER_2, TEST_TILE_2, true)
+ runCurrent()
+
+ assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
+ assertThat(tiles()).hasSize(1)
+ assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+ }
+
+ @Test
+ fun updatingWithDefaultsForTheSameUserAddsData() =
+ testScope.runTest {
+ underTest.updateWithTile(TEST_USER_1, Tile().apply { subtitle = "test_subtitle" }, true)
+ runCurrent()
+
+ underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+ runCurrent()
+
+ val expectedTile = TEST_TILE_1.copy().apply { subtitle = "test_subtitle" }
+ assertThat(underTest.getTile(TEST_USER_1)).isEqualTo(expectedTile)
+ assertThat(underTest.getTiles(TEST_USER_1).first()).isEqualTo(expectedTile)
+ }
+
+ @Test
+ fun updatingWithDefaultsForAnotherUserOverridesTile() =
+ testScope.runTest {
+ underTest.updateWithDefaults(TEST_USER_1, TEST_DEFAULTS_1, true)
+ runCurrent()
+
+ val tiles = collectValues(underTest.getTiles(TEST_USER_2))
+ underTest.updateWithDefaults(TEST_USER_2, TEST_DEFAULTS_2, true)
+ runCurrent()
+
+ assertThat(underTest.getTile(TEST_USER_2)).isEqualTo(TEST_TILE_2)
+ assertThat(tiles()).hasSize(1)
+ assertThat(tiles().last()).isEqualTo(TEST_TILE_2)
+ }
+
+ private companion object {
+
+ val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
+
+ val TEST_USER_1 = UserHandle.of(1)!!
+ val TEST_TILE_1 =
+ Tile().apply {
+ label = "test_tile_1"
+ icon = Icon.createWithContentUri("file://test_1")
+ }
+ val TEST_TILE_KEY_1 = TileServiceKey(TEST_COMPONENT, TEST_USER_1.identifier)
+ val TEST_DEFAULTS_1 = CustomTileDefaults.Result(TEST_TILE_1.icon, TEST_TILE_1.label)
+
+ val TEST_USER_2 = UserHandle.of(2)!!
+ val TEST_TILE_2 =
+ Tile().apply {
+ label = "test_tile_2"
+ icon = Icon.createWithContentUri("file://test_2")
+ }
+ val TEST_TILE_KEY_2 = TileServiceKey(TEST_COMPONENT, TEST_USER_2.identifier)
+ val TEST_DEFAULTS_2 = CustomTileDefaults.Result(TEST_TILE_2.icon, TEST_TILE_2.label)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
new file mode 100644
index 000000000000..eebb145ef384
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
@@ -0,0 +1,187 @@
+/*
+ * 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.tiles.impl.custom.domain.interactor
+
+import android.content.ComponentName
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import android.service.quicksettings.Tile
+import android.text.format.DateUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.qs.external.TileServiceKey
+import com.android.systemui.qs.external.TileServiceManager
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat
+import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileRepository
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+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(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class CustomTileInteractorTest : SysuiTestCase() {
+
+ @Mock private lateinit var tileServiceManager: TileServiceManager
+
+ private val testScope = TestScope()
+
+ private val defaultsRepository = FakeCustomTileDefaultsRepository()
+ private val customTileStatePersister = FakeCustomTileStatePersister()
+ private val customTileRepository =
+ FakeCustomTileRepository(
+ TEST_TILE_SPEC,
+ customTileStatePersister,
+ testScope.testScheduler,
+ )
+
+ private lateinit var underTest: CustomTileInteractor
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest =
+ CustomTileInteractor(
+ TEST_USER,
+ defaultsRepository,
+ customTileRepository,
+ tileServiceManager,
+ testScope.backgroundScope,
+ testScope.testScheduler,
+ )
+ }
+
+ @Test
+ fun activeTileIsAvailableAfterRestored() =
+ testScope.runTest {
+ whenever(tileServiceManager.isActiveTile).thenReturn(true)
+ customTileStatePersister.persistState(
+ TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+ TEST_TILE,
+ )
+
+ underTest.init()
+
+ assertThat(underTest.tile).isEqualTo(TEST_TILE)
+ assertThat(underTest.tiles.first()).isEqualTo(TEST_TILE)
+ }
+
+ @Test
+ fun notActiveTileIsAvailableAfterUpdated() =
+ testScope.runTest {
+ whenever(tileServiceManager.isActiveTile).thenReturn(false)
+ customTileStatePersister.persistState(
+ TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+ TEST_TILE,
+ )
+ val tiles = collectValues(underTest.tiles)
+ val initJob = launch { underTest.init() }
+
+ underTest.updateTile(TEST_TILE)
+ runCurrent()
+ initJob.join()
+
+ assertThat(tiles()).hasSize(1)
+ assertThat(tiles().last()).isEqualTo(TEST_TILE)
+ }
+
+ @Test
+ fun notActiveTileIsAvailableAfterDefaultsUpdated() =
+ testScope.runTest {
+ whenever(tileServiceManager.isActiveTile).thenReturn(false)
+ customTileStatePersister.persistState(
+ TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+ TEST_TILE,
+ )
+ val tiles = collectValues(underTest.tiles)
+ val initJob = launch { underTest.init() }
+
+ defaultsRepository.putDefaults(TEST_USER, TEST_COMPONENT, TEST_DEFAULTS)
+ defaultsRepository.requestNewDefaults(TEST_USER, TEST_COMPONENT)
+ runCurrent()
+ initJob.join()
+
+ assertThat(tiles()).hasSize(1)
+ assertThat(tiles().last()).isEqualTo(TEST_TILE)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun getTileBeforeInitThrows() = testScope.runTest { underTest.tile }
+
+ @Test
+ fun initSuspendsForActiveTileNotRestoredAndNotUpdated() =
+ testScope.runTest {
+ whenever(tileServiceManager.isActiveTile).thenReturn(true)
+ val tiles = collectValues(underTest.tiles)
+
+ val initJob = backgroundScope.launch { underTest.init() }
+ advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
+
+ // Is still suspended
+ assertThat(initJob.isActive).isTrue()
+ assertThat(tiles()).isEmpty()
+ }
+
+ @Test
+ fun initSuspendedForNotActiveTileWithoutUpdates() =
+ testScope.runTest {
+ whenever(tileServiceManager.isActiveTile).thenReturn(false)
+ customTileStatePersister.persistState(
+ TileServiceKey(TEST_COMPONENT, TEST_USER.identifier),
+ TEST_TILE,
+ )
+ val tiles = collectValues(underTest.tiles)
+
+ val initJob = backgroundScope.launch { underTest.init() }
+ advanceTimeBy(1 * DateUtils.DAY_IN_MILLIS)
+
+ // Is still suspended
+ assertThat(initJob.isActive).isTrue()
+ assertThat(tiles()).isEmpty()
+ }
+
+ private companion object {
+
+ val TEST_COMPONENT = ComponentName("test.pkg", "test.cls")
+ val TEST_TILE_SPEC = TileSpec.create(TEST_COMPONENT)
+ val TEST_USER = UserHandle.of(1)!!
+ val TEST_TILE =
+ Tile().apply {
+ label = "test_tile_1"
+ icon = Icon.createWithContentUri("file://test_1")
+ }
+ val TEST_DEFAULTS = CustomTileDefaults.Result(TEST_TILE.icon, TEST_TILE.label)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeCustomTileStatePersister.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeCustomTileStatePersister.kt
new file mode 100644
index 000000000000..29702eb2cdc9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/FakeCustomTileStatePersister.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.external
+
+import android.service.quicksettings.Tile
+
+class FakeCustomTileStatePersister : CustomTileStatePersister {
+
+ private val tiles: MutableMap<TileServiceKey, Tile> = mutableMapOf()
+
+ override fun readState(key: TileServiceKey): Tile? = tiles[key]
+
+ override fun persistState(key: TileServiceKey, tile: Tile) {
+ tiles[key] = tile
+ }
+
+ override fun removeState(key: TileServiceKey) {
+ tiles.remove(key)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt
new file mode 100644
index 000000000000..d2351dc8ae18
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.tiles.impl.custom
+
+import android.service.quicksettings.Tile
+import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat
+import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.tiles
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth
+
+/**
+ * [Tile]-specific extension for [Truth]. Use [assertThat] or [tiles] to get an instance of this
+ * subject.
+ */
+class TileSubject private constructor(failureMetadata: FailureMetadata, subject: Tile?) :
+ Subject(failureMetadata, subject) {
+
+ private val actual: Tile? = subject
+
+ /** Asserts if the [Tile] fields are the same. */
+ fun isEqualTo(other: Tile?) {
+ if (actual == null) {
+ check("other").that(other).isNull()
+ return
+ } else {
+ check("other").that(other).isNotNull()
+ other ?: return
+ }
+
+ check("icon").that(actual.icon).isEqualTo(other.icon)
+ check("label").that(actual.label).isEqualTo(other.label)
+ check("subtitle").that(actual.subtitle).isEqualTo(other.subtitle)
+ check("contentDescription")
+ .that(actual.contentDescription)
+ .isEqualTo(other.contentDescription)
+ check("stateDescription").that(actual.stateDescription).isEqualTo(other.stateDescription)
+ check("activityLaunchForClick")
+ .that(actual.activityLaunchForClick)
+ .isEqualTo(other.activityLaunchForClick)
+ check("state").that(actual.state).isEqualTo(other.state)
+ }
+
+ companion object {
+
+ /** Returns a factory to be used with [Truth.assertAbout]. */
+ fun tiles(): Factory<TileSubject, Tile?> {
+ return Factory { failureMetadata: FailureMetadata, subject: Tile? ->
+ TileSubject(failureMetadata, subject)
+ }
+ }
+
+ /** Shortcut for `Truth.assertAbout(tiles()).that(tile)`. */
+ fun assertThat(tile: Tile?): TileSubject = Truth.assertAbout(tiles()).that(tile)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileDefaultsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileDefaultsRepository.kt
index 13910fd5c564..ccba07273f1e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileDefaultsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileDefaultsRepository.kt
@@ -19,15 +19,20 @@ package com.android.systemui.qs.tiles.impl.custom.data.repository
import android.content.ComponentName
import android.os.UserHandle
import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
+import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
class FakeCustomTileDefaultsRepository : CustomTileDefaultsRepository {
private val defaults: MutableMap<DefaultsKey, CustomTileDefaults> = mutableMapOf()
- private val defaultsFlow = MutableSharedFlow<DefaultsRequest>()
+ private val defaultsFlow =
+ MutableSharedFlow<DefaultsRequest>(
+ replay = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
private val mutableDefaultsRequests: MutableList<DefaultsRequest> = mutableListOf()
val defaultsRequests: List<DefaultsRequest> = mutableDefaultsRequests
@@ -41,7 +46,7 @@ class FakeCustomTileDefaultsRepository : CustomTileDefaultsRepository {
old == new
}
}
- .map { defaults[DefaultsKey(it.user, it.componentName)]!! }
+ .mapNotNull { defaults[DefaultsKey(it.user, it.componentName)] }
override fun requestNewDefaults(
user: UserHandle,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
new file mode 100644
index 000000000000..ccf03911495f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.tiles.impl.custom.data.repository
+
+import android.os.UserHandle
+import android.service.quicksettings.Tile
+import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.Flow
+
+class FakeCustomTileRepository(
+ tileSpec: TileSpec.CustomTileSpec,
+ customTileStatePersister: FakeCustomTileStatePersister,
+ testBackgroundContext: CoroutineContext,
+) : CustomTileRepository {
+
+ private val realDelegate: CustomTileRepository =
+ CustomTileRepositoryImpl(
+ tileSpec,
+ customTileStatePersister,
+ testBackgroundContext,
+ )
+
+ override suspend fun restoreForTheUserIfNeeded(user: UserHandle, isPersistable: Boolean) =
+ realDelegate.restoreForTheUserIfNeeded(user, isPersistable)
+
+ override fun getTiles(user: UserHandle): Flow<Tile> = realDelegate.getTiles(user)
+
+ override fun getTile(user: UserHandle): Tile? = realDelegate.getTile(user)
+
+ override suspend fun updateWithTile(
+ user: UserHandle,
+ newTile: Tile,
+ isPersistable: Boolean,
+ ) = realDelegate.updateWithTile(user, newTile, isPersistable)
+
+ override suspend fun updateWithDefaults(
+ user: UserHandle,
+ defaults: CustomTileDefaults,
+ isPersistable: Boolean,
+ ) = realDelegate.updateWithDefaults(user, defaults, isPersistable)
+}