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

Commit c5bfa1e6 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Add tile interactor and repository to handle Tile update and persisting" into main

parents a29375a5 62937a1f
Loading
Loading
Loading
Loading
+31 −20
Original line number Original line Diff line number Diff line
@@ -18,17 +18,19 @@ package com.android.systemui.qs.external


import android.content.ComponentName
import android.content.ComponentName
import android.content.Context
import android.content.Context
import android.content.SharedPreferences
import android.service.quicksettings.Tile
import android.service.quicksettings.Tile
import android.util.Log
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.annotations.VisibleForTesting
import javax.inject.Inject
import org.json.JSONException
import org.json.JSONException
import org.json.JSONObject
import org.json.JSONObject
import javax.inject.Inject


data class TileServiceKey(val componentName: ComponentName, val user: Int) {
data class TileServiceKey(val componentName: ComponentName, val user: Int) {
    private val string = "${componentName.flattenToString()}:$user"
    private val string = "${componentName.flattenToString()}:$user"
    override fun toString() = string
    override fun toString() = string
}
}

private const val STATE = "state"
private const val STATE = "state"
private const val LABEL = "label"
private const val LABEL = "label"
private const val SUBTITLE = "subtitle"
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
 * It persists the state from a [Tile] necessary to present the view in the same state when
 * retrieved, with the exception of the icon.
 * retrieved, with the exception of the icon.
 */
 */
class CustomTileStatePersister @Inject constructor(context: Context) {
interface CustomTileStatePersister {
    companion object {
        private const val FILE_NAME = "custom_tiles_state"
    }

    private val sharedPreferences = context.getSharedPreferences(FILE_NAME, 0)


    /**
    /**
     * Read the state from [SharedPreferences].
     * 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`
     * 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
        val state = sharedPreferences.getString(key.toString(), null) ?: return null
        return try {
        return try {
            readTileFromString(state)
            readTileFromString(state)
@@ -68,23 +89,13 @@ class CustomTileStatePersister @Inject constructor(context: Context) {
        }
        }
    }
    }


    /**
    override fun persistState(key: TileServiceKey, tile: Tile) {
     * Persists the state into [SharedPreferences].
     *
     * The implementation does not store fields that are `null` or icons.
     */
    fun persistState(key: TileServiceKey, tile: Tile) {
        val state = writeToString(tile)
        val state = writeToString(tile)


        sharedPreferences.edit().putString(key.toString(), state).apply()
        sharedPreferences.edit().putString(key.toString(), state).apply()
    }
    }


    /**
    override fun removeState(key: TileServiceKey) {
     * Removes the state for a given tile, user pair.
     *
     * Used when the tile is removed by the user.
     */
    fun removeState(key: TileServiceKey) {
        sharedPreferences.edit().remove(key.toString()).apply()
        sharedPreferences.edit().remove(key.toString()).apply()
    }
    }
}
}
+5 −0
Original line number Original line Diff line number Diff line
@@ -16,6 +16,8 @@


package com.android.systemui.qs.tiles.di
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.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerImpl
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerImpl
import com.android.systemui.qs.tiles.impl.custom.di.CustomTileComponent
import com.android.systemui.qs.tiles.impl.custom.di.CustomTileComponent
@@ -52,4 +54,7 @@ interface QSTilesModule {
    fun bindQSTileIntentUserInputHandler(
    fun bindQSTileIntentUserInputHandler(
        impl: QSTileIntentUserInputHandlerImpl
        impl: QSTileIntentUserInputHandlerImpl
    ): QSTileIntentUserInputHandler
    ): QSTileIntentUserInputHandler

    @Binds
    fun bindCustomTileStatePersister(impl: CustomTileStatePersisterImpl): CustomTileStatePersister
}
}
+50 −0
Original line number Original line 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.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
}
+196 −0
Original line number Original line 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.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)
}
+4 −0
Original line number Original line Diff line number Diff line
@@ -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.CustomTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository
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.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.di.bound.CustomTileBoundComponent
import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
import dagger.Binds
import dagger.Binds
@@ -50,4 +52,6 @@ interface CustomTileModule {
    fun bindCustomTileDefaultsRepository(
    fun bindCustomTileDefaultsRepository(
        impl: CustomTileDefaultsRepositoryImpl
        impl: CustomTileDefaultsRepositoryImpl
    ): CustomTileDefaultsRepository
    ): CustomTileDefaultsRepository

    @Binds fun bindCustomTileRepository(impl: CustomTileRepositoryImpl): CustomTileRepository
}
}
Loading