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

Commit 57ae6eea authored by Anton Potapov's avatar Anton Potapov
Browse files

Create QS tile component and modules.

This CL creates DI infrastructure for the new QS tiles

Test: passes presubmits
Bug: 301055700
Change-Id: I3a57876136d93bb1a69655a7cdd453649632415a
parent 7f1fb8e2
Loading
Loading
Loading
Loading
+10 −4
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.systemui.qs.ReduceBrightColorsController;
import com.android.systemui.qs.external.QSExternalModule;
import com.android.systemui.qs.pipeline.dagger.QSPipelineModule;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.qs.tiles.di.QSTilesModule;
import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel;
import com.android.systemui.statusbar.phone.AutoTileManager;
import com.android.systemui.statusbar.phone.ManagedProfileController;
@@ -60,17 +61,22 @@ import javax.inject.Named;
                QSFlagsModule.class,
                QSHostModule.class,
                QSPipelineModule.class,
                QSTilesModule.class,
        }
)
public interface QSModule {

    /** A map of internal QS tiles. Ensures that this can be injected even if
     * it is empty */
    /**
     * A map of internal QS tiles. Ensures that this can be injected even if
     * it is empty
     */
    @Multibinds
    Map<String, QSTileImpl<?>> tileMap();

    /** A map of internal QS tile ViewModels. Ensures that this can be injected even if
     * it is empty */
    /**
     * A map of internal QS tile ViewModels. Ensures that this can be injected even if
     * it is empty
     */
    @Multibinds
    Map<String, QSTileViewModel> tileViewModelMap();

+43 −119
Original line number Diff line number Diff line
@@ -17,9 +17,6 @@
package com.android.systemui.qs.tiles.base.viewmodel

import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import com.android.internal.util.Preconditions
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
@@ -30,7 +27,6 @@ import com.android.systemui.qs.tiles.base.interactor.QSTileInput
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
import com.android.systemui.qs.tiles.base.logging.QSTileLogger
import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileLifecycle
import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
@@ -38,13 +34,11 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.kotlin.throttle
import com.android.systemui.util.time.SystemClock
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -66,19 +60,17 @@ 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 it's moved to
 * [QSTileLifecycle.ALIVE] state.
 * standards. This ViewModel is cheap to instantiate and does nothing until its [state] is listened.
 *
 * Inject [BaseQSTileViewModel.Factory] to create a new instance of this class.
 * Don't use this constructor directly. Instead, inject [QSViewModelFactory] to create a new
 * instance of this class.
 */
@OptIn(ExperimentalCoroutinesApi::class)
class BaseQSTileViewModel<DATA_TYPE>
@VisibleForTesting
constructor(
    override val config: QSTileConfig,
    private val userActionInteractor: QSTileUserActionInteractor<DATA_TYPE>,
    private val tileDataInteractor: QSTileDataInteractor<DATA_TYPE>,
    private val mapper: QSTileDataToStateMapper<DATA_TYPE>,
class BaseQSTileViewModel<DATA_TYPE>(
    val tileConfig: () -> QSTileConfig,
    private val userActionInteractor: () -> QSTileUserActionInteractor<DATA_TYPE>,
    private val tileDataInteractor: () -> QSTileDataInteractor<DATA_TYPE>,
    private val mapper: () -> QSTileDataToStateMapper<DATA_TYPE>,
    private val disabledByPolicyInteractor: DisabledByPolicyInteractor,
    userRepository: UserRepository,
    private val falsingManager: FalsingManager,
@@ -86,37 +78,9 @@ constructor(
    private val qsTileLogger: QSTileLogger,
    private val systemClock: SystemClock,
    private val backgroundDispatcher: CoroutineDispatcher,
    private val tileScope: CoroutineScope,
    private val tileScope: CoroutineScope = CoroutineScope(SupervisorJob()),
) : QSTileViewModel {

    @AssistedInject
    constructor(
        @Assisted config: QSTileConfig,
        @Assisted userActionInteractor: QSTileUserActionInteractor<DATA_TYPE>,
        @Assisted tileDataInteractor: QSTileDataInteractor<DATA_TYPE>,
        @Assisted mapper: QSTileDataToStateMapper<DATA_TYPE>,
        disabledByPolicyInteractor: DisabledByPolicyInteractor,
        userRepository: UserRepository,
        falsingManager: FalsingManager,
        qsTileAnalytics: QSTileAnalytics,
        qsTileLogger: QSTileLogger,
        systemClock: SystemClock,
        @Background backgroundDispatcher: CoroutineDispatcher,
    ) : this(
        config,
        userActionInteractor,
        tileDataInteractor,
        mapper,
        disabledByPolicyInteractor,
        userRepository,
        falsingManager,
        qsTileAnalytics,
        qsTileLogger,
        systemClock,
        backgroundDispatcher,
        CoroutineScope(SupervisorJob())
    )

    private val userIds: MutableStateFlow<Int> =
        MutableStateFlow(userRepository.getSelectedUserInfo().id)
    private val userInputs: MutableSharedFlow<QSTileUserAction> =
@@ -126,12 +90,26 @@ constructor(
    private val spec
        get() = config.tileSpec

    private lateinit var tileData: SharedFlow<DATA_TYPE>
    private val tileData: SharedFlow<DATA_TYPE> = createTileDataFlow()

    override lateinit var state: SharedFlow<QSTileState>
    override val config
        get() = tileConfig()
    override val state: SharedFlow<QSTileState> =
        tileData
            .map { data ->
                mapper().map(config, data).also { state ->
                    qsTileLogger.logStateUpdate(spec, state, data)
                }
            }
            .flowOn(backgroundDispatcher)
            .shareIn(
                tileScope,
                SharingStarted.WhileSubscribed(),
                replay = 1,
            )
    override val isAvailable: StateFlow<Boolean> =
        userIds
            .flatMapLatest { tileDataInteractor.availability(it) }
            .flatMapLatest { tileDataInteractor().availability(it) }
            .flowOn(backgroundDispatcher)
            .stateIn(
                tileScope,
@@ -139,24 +117,18 @@ constructor(
                true,
            )

    private var currentLifeState: QSTileLifecycle = QSTileLifecycle.DEAD

    @CallSuper
    override fun forceUpdate() {
        Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE)
        forceUpdates.tryEmit(Unit)
    }

    @CallSuper
    override fun onUserIdChanged(userId: Int) {
        Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE)
        userIds.tryEmit(userId)
    }

    @CallSuper
    override fun onActionPerformed(userAction: QSTileUserAction) {
        Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE)

        qsTileLogger.logUserAction(
            userAction,
            spec,
@@ -166,32 +138,8 @@ constructor(
        userInputs.tryEmit(userAction)
    }

    @CallSuper
    override fun onLifecycle(lifecycle: QSTileLifecycle) {
        when (lifecycle) {
            QSTileLifecycle.ALIVE -> {
                Preconditions.checkState(currentLifeState == QSTileLifecycle.DEAD)
                tileData = createTileDataFlow()
                state =
                    tileData
                        .map { data ->
                            mapper.map(config, data).also { state ->
                                qsTileLogger.logStateUpdate(spec, state, data)
                            }
                        }
                        .flowOn(backgroundDispatcher)
                        .shareIn(
                            tileScope,
                            SharingStarted.WhileSubscribed(),
                            replay = 1,
                        )
            }
            QSTileLifecycle.DEAD -> {
                Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE)
                tileScope.coroutineContext.cancelChildren()
            }
        }
        currentLifeState = lifecycle
    override fun destroy() {
        tileScope.cancel()
    }

    private fun createTileDataFlow(): SharedFlow<DATA_TYPE> =
@@ -208,7 +156,7 @@ constructor(
                            emit(DataUpdateTrigger.InitialRequest)
                            qsTileLogger.logInitialRequest(spec)
                        }
                tileDataInteractor
                tileDataInteractor()
                    .tileData(userId, updateTriggers)
                    .cancellable()
                    .flowOn(backgroundDispatcher)
@@ -242,17 +190,18 @@ constructor(

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

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

    private fun Flow<QSTileUserAction>.filterFalseActions(): Flow<QSTileUserAction> =
        filter { action ->
@@ -279,30 +229,4 @@ constructor(
    private companion object {
        const val CLICK_THROTTLE_DURATION = 200L
    }

    /**
     * Factory interface for assisted inject. Dagger has bad time supporting generics in assisted
     * injection factories now. That's why you need to create an interface implementing this one and
     * annotate it with [dagger.assisted.AssistedFactory].
     *
     * ex: @AssistedFactory interface FooFactory : BaseQSTileViewModel.Factory<FooData>
     */
    interface Factory<T> {

        /**
         * @param config contains all the static information (like TileSpec) about the tile.
         * @param userActionInteractor encapsulates user input processing logic. Use it to start
         *   activities, show dialogs or otherwise update the tile state.
         * @param tileDataInteractor provides [DATA_TYPE] and its availability.
         * @param mapper maps [DATA_TYPE] to the [QSTileState] that is then displayed by the View
         *   layer. It's called in [backgroundDispatcher], so it's safe to perform long running
         *   operations there.
         */
        fun create(
            config: QSTileConfig,
            userActionInteractor: QSTileUserActionInteractor<T>,
            tileDataInteractor: QSTileDataInteractor<T>,
            mapper: QSTileDataToStateMapper<T>,
        ): BaseQSTileViewModel<T>
    }
}
+125 −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.tiles.base.viewmodel

import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
import com.android.systemui.qs.tiles.base.logging.QSTileLogger
import com.android.systemui.qs.tiles.impl.di.QSTileComponent
import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher

/**
 * Factory to create an appropriate [BaseQSTileViewModel] instance depending on your circumstances.
 *
 * @see [QSViewModelFactory.Component]
 * @see [QSViewModelFactory.Static]
 */
sealed interface QSViewModelFactory<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].
     */
    class Component<T>
    @Inject
    constructor(
        private val disabledByPolicyInteractor: DisabledByPolicyInteractor,
        private val userRepository: UserRepository,
        private val falsingManager: FalsingManager,
        private val qsTileAnalytics: QSTileAnalytics,
        private val qsTileLogger: QSTileLogger,
        private val systemClock: SystemClock,
        @Background private val backgroundDispatcher: CoroutineDispatcher,
    ) : QSViewModelFactory<T> {

        /**
         * Creates [BaseQSTileViewModel] 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(
                component::config,
                component::userActionInteractor,
                component::dataInteractor,
                component::dataToStateMapper,
                disabledByPolicyInteractor,
                userRepository,
                falsingManager,
                qsTileAnalytics,
                qsTileLogger,
                systemClock,
                backgroundDispatcher,
            )
    }

    /**
     * This factory passes by necessary implementations to the [BaseQSTileViewModel]. This is a
     * default choice for most of the tiles.
     */
    class Static<T>
    @Inject
    constructor(
        private val disabledByPolicyInteractor: DisabledByPolicyInteractor,
        private val userRepository: UserRepository,
        private val falsingManager: FalsingManager,
        private val qsTileAnalytics: QSTileAnalytics,
        private val qsTileLogger: QSTileLogger,
        private val systemClock: SystemClock,
        @Background private val backgroundDispatcher: CoroutineDispatcher,
    ) : QSViewModelFactory<T> {

        /**
         * @param config contains all the static information (like TileSpec) about the tile.
         * @param userActionInteractor encapsulates user input processing logic. Use it to start
         *   activities, show dialogs or otherwise update the tile state.
         * @param tileDataInteractor provides [DATA_TYPE] and its availability.
         * @param mapper maps [DATA_TYPE] to the [QSTileState] that is then displayed by the View
         *   layer. It's called in [backgroundDispatcher], so it's safe to perform long running
         *   operations there.
         */
        fun create(
            config: QSTileConfig,
            userActionInteractor: QSTileUserActionInteractor<T>,
            tileDataInteractor: QSTileDataInteractor<T>,
            mapper: QSTileDataToStateMapper<T>,
        ): BaseQSTileViewModel<T> =
            BaseQSTileViewModel(
                { config },
                { userActionInteractor },
                { tileDataInteractor },
                { mapper },
                disabledByPolicyInteractor,
                userRepository,
                falsingManager,
                qsTileAnalytics,
                qsTileLogger,
                systemClock,
                backgroundDispatcher,
            )
    }
}
+0 −2
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.systemui.qs.tiles.di
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.plugins.qs.QSFactory
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.tiles.viewmodel.QSTileLifecycle
import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
import com.android.systemui.qs.tiles.viewmodel.QSTileViewModelAdapter
import javax.inject.Inject
@@ -38,7 +37,6 @@ constructor(
    override fun createTile(tileSpec: String): QSTile? =
        tileMap[tileSpec]?.let {
            val tile = it.get()
            tile.onLifecycle(QSTileLifecycle.ALIVE)
            adapterFactory.create(tile)
        }
}
+29 −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.tiles.di

import com.android.systemui.qs.tiles.impl.custom.di.CustomTileComponent
import dagger.Module

/** Module listing subcomponents */
@Module(
    subcomponents =
        [
            CustomTileComponent::class,
        ]
)
interface QSTilesModule
Loading