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

Commit e65fce6d authored by Anton Potapov's avatar Anton Potapov
Browse files

Migrate from using userId to UserHandle for better type safety

Also renamed:
 - BaseQSTileViewModel to QSTileViewModelImpl
 - QSViewModelFactory to QSTileViewModelFactory

Test: atest DisabledByPolicyInteractorTest
Test: atest QSTileViewModelInterfaceComplianceTest
Bug: 299908705
Change-Id: Ie66ddc8d8788906840196e77d7851c4e1b9e69d9
parent 57ae6eea
Loading
Loading
Loading
Loading
+6 −5
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.qs.tiles.base.interactor

import android.content.Context
import android.os.UserHandle
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.android.settingslib.RestrictedLockUtils
@@ -32,7 +33,7 @@ import kotlinx.coroutines.withContext

/**
 * Provides restrictions data for the tiles. This is used in
 * [com.android.systemui.qs.tiles.base.viewmodel.BaseQSTileViewModel] to determine if the tile is
 * [com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl] to determine if the tile is
 * disabled based on the [com.android.systemui.qs.tiles.viewmodel.QSTileConfig.policy].
 */
interface DisabledByPolicyInteractor {
@@ -41,7 +42,7 @@ interface DisabledByPolicyInteractor {
     * Checks if the tile is restricted by the policy for a specific user. Pass the result to the
     * [handlePolicyResult] to let the user know that the tile is disable by the admin.
     */
    suspend fun isDisabled(userId: Int, userRestriction: String?): PolicyResult
    suspend fun isDisabled(user: UserHandle, userRestriction: String?): PolicyResult

    /**
     * Returns true when [policyResult] is [PolicyResult.TileDisabled] and has been handled by this
@@ -75,14 +76,14 @@ constructor(
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) : DisabledByPolicyInteractor {

    override suspend fun isDisabled(userId: Int, userRestriction: String?): PolicyResult =
    override suspend fun isDisabled(user: UserHandle, userRestriction: String?): PolicyResult =
        withContext(backgroundDispatcher) {
            val admin: EnforcedAdmin =
                restrictedLockProxy.getEnforcedAdmin(userId, userRestriction)
                restrictedLockProxy.getEnforcedAdmin(user.identifier, userRestriction)
                    ?: return@withContext PolicyResult.TileEnabled

            return@withContext if (
                !restrictedLockProxy.hasBaseUserRestriction(userId, userRestriction)
                !restrictedLockProxy.hasBaseUserRestriction(user.identifier, userRestriction)
            ) {
                PolicyResult.TileDisabled(admin)
            } else {
+4 −4
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.qs.tiles.base.interactor

import android.os.UserHandle
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

@@ -29,13 +30,12 @@ interface QSTileDataInteractor<DATA_TYPE> {

    /**
     * Returns a data flow scoped to the user. This means the subscription will live when the tile
     * is listened for the [userId]. It's cancelled when the tile is not listened or the user
     * changes.
     * is listened for the [user]. It's cancelled when the tile is not listened or the user changes.
     *
     * You can use [Flow.onStart] on the returned to update the tile with the current state as soon
     * as possible.
     */
    fun tileData(userId: Int, triggers: Flow<DataUpdateTrigger>): Flow<DATA_TYPE>
    fun tileData(user: UserHandle, triggers: Flow<DataUpdateTrigger>): Flow<DATA_TYPE>

    /**
     * Returns tile availability - whether this device currently supports this tile.
@@ -43,5 +43,5 @@ interface QSTileDataInteractor<DATA_TYPE> {
     * You can use [Flow.onStart] on the returned to update the tile with the current state as soon
     * as possible.
     */
    fun availability(userId: Int): Flow<Boolean>
    fun availability(user: UserHandle): Flow<Boolean>
}
+2 −1
Original line number Diff line number Diff line
@@ -16,11 +16,12 @@

package com.android.systemui.qs.tiles.base.interactor

import android.os.UserHandle
import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction

/** @see QSTileUserActionInteractor.handleInput */
data class QSTileInput<T>(
    val userId: Int,
    val user: UserHandle,
    val action: QSTileUserAction,
    val data: T,
)
+13 −13
Original line number Diff line number Diff line
@@ -33,17 +33,17 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher

/**
 * Factory to create an appropriate [BaseQSTileViewModel] instance depending on your circumstances.
 * Factory to create an appropriate [QSTileViewModelImpl] instance depending on your circumstances.
 *
 * @see [QSViewModelFactory.Component]
 * @see [QSViewModelFactory.Static]
 * @see [QSTileViewModelFactory.Component]
 * @see [QSTileViewModelFactory.Static]
 */
sealed interface QSViewModelFactory<T> {
sealed interface QSTileViewModelFactory<T> {

    /**
     * This factory allows you to pass an instance of [QSTileComponent] to a view model effectively
     * binding them together. This achieves a DI scope that lives along the instance of
     * [BaseQSTileViewModel].
     * [QSTileViewModelImpl].
     */
    class Component<T>
    @Inject
@@ -55,14 +55,14 @@ sealed interface QSViewModelFactory<T> {
        private val qsTileLogger: QSTileLogger,
        private val systemClock: SystemClock,
        @Background private val backgroundDispatcher: CoroutineDispatcher,
    ) : QSViewModelFactory<T> {
    ) : QSTileViewModelFactory<T> {

        /**
         * Creates [BaseQSTileViewModel] based on the interactors obtained from [component].
         * Creates [QSTileViewModelImpl] based on the interactors obtained from [component].
         * Reference of that [component] is then stored along the view model.
         */
        fun create(component: QSTileComponent<T>): BaseQSTileViewModel<T> =
            BaseQSTileViewModel(
        fun create(component: QSTileComponent<T>): QSTileViewModelImpl<T> =
            QSTileViewModelImpl(
                component::config,
                component::userActionInteractor,
                component::dataInteractor,
@@ -78,7 +78,7 @@ sealed interface QSViewModelFactory<T> {
    }

    /**
     * This factory passes by necessary implementations to the [BaseQSTileViewModel]. This is a
     * This factory passes by necessary implementations to the [QSTileViewModelImpl]. This is a
     * default choice for most of the tiles.
     */
    class Static<T>
@@ -91,7 +91,7 @@ sealed interface QSViewModelFactory<T> {
        private val qsTileLogger: QSTileLogger,
        private val systemClock: SystemClock,
        @Background private val backgroundDispatcher: CoroutineDispatcher,
    ) : QSViewModelFactory<T> {
    ) : QSTileViewModelFactory<T> {

        /**
         * @param config contains all the static information (like TileSpec) about the tile.
@@ -107,8 +107,8 @@ sealed interface QSViewModelFactory<T> {
            userActionInteractor: QSTileUserActionInteractor<T>,
            tileDataInteractor: QSTileDataInteractor<T>,
            mapper: QSTileDataToStateMapper<T>,
        ): BaseQSTileViewModel<T> =
            BaseQSTileViewModel(
        ): QSTileViewModelImpl<T> =
            QSTileViewModelImpl(
                { config },
                { userActionInteractor },
                { tileDataInteractor },
+17 −20
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@

package com.android.systemui.qs.tiles.base.viewmodel

import androidx.annotation.CallSuper
import android.os.UserHandle
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
@@ -62,11 +62,11 @@ import kotlinx.coroutines.flow.stateIn
 * Provides a hassle-free way to implement new tiles according to current System UI architecture
 * standards. This ViewModel is cheap to instantiate and does nothing until its [state] is listened.
 *
 * Don't use this constructor directly. Instead, inject [QSViewModelFactory] to create a new
 * Don't use this constructor directly. Instead, inject [QSTileViewModelFactory] to create a new
 * instance of this class.
 */
@OptIn(ExperimentalCoroutinesApi::class)
class BaseQSTileViewModel<DATA_TYPE>(
class QSTileViewModelImpl<DATA_TYPE>(
    val tileConfig: () -> QSTileConfig,
    private val userActionInteractor: () -> QSTileUserActionInteractor<DATA_TYPE>,
    private val tileDataInteractor: () -> QSTileDataInteractor<DATA_TYPE>,
@@ -81,8 +81,8 @@ class BaseQSTileViewModel<DATA_TYPE>(
    private val tileScope: CoroutineScope = CoroutineScope(SupervisorJob()),
) : QSTileViewModel {

    private val userIds: MutableStateFlow<Int> =
        MutableStateFlow(userRepository.getSelectedUserInfo().id)
    private val users: MutableStateFlow<UserHandle> =
        MutableStateFlow(userRepository.getSelectedUserInfo().userHandle)
    private val userInputs: MutableSharedFlow<QSTileUserAction> =
        MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
    private val forceUpdates: MutableSharedFlow<Unit> =
@@ -108,7 +108,7 @@ class BaseQSTileViewModel<DATA_TYPE>(
                replay = 1,
            )
    override val isAvailable: StateFlow<Boolean> =
        userIds
        users
            .flatMapLatest { tileDataInteractor().availability(it) }
            .flowOn(backgroundDispatcher)
            .stateIn(
@@ -117,17 +117,14 @@ class BaseQSTileViewModel<DATA_TYPE>(
                true,
            )

    @CallSuper
    override fun forceUpdate() {
        forceUpdates.tryEmit(Unit)
    }

    @CallSuper
    override fun onUserIdChanged(userId: Int) {
        userIds.tryEmit(userId)
    override fun onUserChanged(user: UserHandle) {
        users.tryEmit(user)
    }

    @CallSuper
    override fun onActionPerformed(userAction: QSTileUserAction) {
        qsTileLogger.logUserAction(
            userAction,
@@ -143,11 +140,11 @@ class BaseQSTileViewModel<DATA_TYPE>(
    }

    private fun createTileDataFlow(): SharedFlow<DATA_TYPE> =
        userIds
            .flatMapLatest { userId ->
        users
            .flatMapLatest { user ->
                val updateTriggers =
                    merge(
                            userInputFlow(userId),
                            userInputFlow(user),
                            forceUpdates
                                .map { DataUpdateTrigger.ForceUpdate }
                                .onEach { qsTileLogger.logForceUpdate(spec) },
@@ -157,7 +154,7 @@ class BaseQSTileViewModel<DATA_TYPE>(
                            qsTileLogger.logInitialRequest(spec)
                        }
                tileDataInteractor()
                    .tileData(userId, updateTriggers)
                    .tileData(user, updateTriggers)
                    .cancellable()
                    .flowOn(backgroundDispatcher)
            }
@@ -176,10 +173,10 @@ class BaseQSTileViewModel<DATA_TYPE>(
     *
     * Subscribing to the result flow twice will result in doubling all actions, logs and analytics.
     */
    private fun userInputFlow(userId: Int): Flow<DataUpdateTrigger> {
    private fun userInputFlow(user: UserHandle): Flow<DataUpdateTrigger> {
        return userInputs
            .filterFalseActions()
            .filterByPolicy(userId)
            .filterByPolicy(user)
            .throttle(CLICK_THROTTLE_DURATION, systemClock)
            // Skip the input until there is some data
            .mapNotNull { action ->
@@ -188,20 +185,20 @@ class BaseQSTileViewModel<DATA_TYPE>(
                qsTileLogger.logUserActionPipeline(spec, action, state, data)
                qsTileAnalytics.trackUserAction(config, action)

                DataUpdateTrigger.UserInput(QSTileInput(userId, action, data))
                DataUpdateTrigger.UserInput(QSTileInput(user, action, data))
            }
            .onEach { userActionInteractor().handleInput(it.input) }
            .flowOn(backgroundDispatcher)
    }

    private fun Flow<QSTileUserAction>.filterByPolicy(userId: Int): Flow<QSTileUserAction> =
    private fun Flow<QSTileUserAction>.filterByPolicy(user: UserHandle): Flow<QSTileUserAction> =
        config.policy.let { policy ->
            when (policy) {
                is QSTilePolicy.NoRestrictions -> this@filterByPolicy
                is QSTilePolicy.Restricted ->
                    filter { action ->
                        val result =
                            disabledByPolicyInteractor.isDisabled(userId, policy.userRestriction)
                            disabledByPolicyInteractor.isDisabled(user, policy.userRestriction)
                        !disabledByPolicyInteractor.handlePolicyResult(result).also { isDisabled ->
                            if (isDisabled) {
                                qsTileLogger.logUserActionRejectedByPolicy(action, spec)
Loading