diff options
14 files changed, 265 insertions, 205 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt index 9d100728e643..905d8effdadf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.base.actions +import android.app.PendingIntent import android.content.Intent import android.view.View import com.android.internal.jank.InteractionJankMonitor @@ -29,7 +30,7 @@ import javax.inject.Inject * dismissing and tile from-view animations. */ @SysUISingleton -class QSTileIntentUserActionHandler +class QSTileIntentUserInputHandler @Inject constructor(private val activityStarter: ActivityStarter) { @@ -43,4 +44,19 @@ constructor(private val activityStarter: ActivityStarter) { } activityStarter.postStartActivityDismissingKeyguard(intent, 0, animationController) } + + // TODO(b/249804373): make sure to allow showing activities over the lockscreen. See b/292112939 + fun handle(view: View?, pendingIntent: PendingIntent) { + if (!pendingIntent.isActivity) { + return + } + val animationController: ActivityLaunchAnimator.Controller? = + view?.let { + ActivityLaunchAnimator.Controller.fromView( + it, + InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE, + ) + } + activityStarter.postStartActivityDismissingKeyguard(pendingIntent, animationController) + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/StateUpdateTrigger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DataUpdateTrigger.kt index ffe38ddacfda..4f25d3cde6c3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/StateUpdateTrigger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DataUpdateTrigger.kt @@ -16,12 +16,18 @@ package com.android.systemui.qs.tiles.base.interactor -import com.android.systemui.qs.tiles.viewmodel.QSTileState -import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +/** Event that triggers data update */ +sealed interface DataUpdateTrigger { + /** + * State update is requested in a response to a user action. + * - [action] is the action that happened + * - [tileData] is the data state of the tile when that action took place + */ + class UserInput<T>(val input: QSTileInput<T>) : DataUpdateTrigger -sealed interface StateUpdateTrigger { - class UserAction<T>(val action: QSTileUserAction, val tileState: QSTileState, val tileData: T) : - StateUpdateTrigger - data object ForceUpdate : StateUpdateTrigger - data object InitialRequest : StateUpdateTrigger + /** Force update current state. This is passed when the view needs a new state to show */ + data object ForceUpdate : DataUpdateTrigger + + /** The data is requested loaded for the first time */ + data object InitialRequest : DataUpdateTrigger } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt index 7a22e3cf8bc8..a3e38500123e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.tiles.base.interactor -import com.android.systemui.qs.tiles.viewmodel.QSTileState import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -29,14 +28,20 @@ import kotlinx.coroutines.flow.Flow interface QSTileDataInteractor<DATA_TYPE> { /** - * Returns the data to be mapped to [QSTileState]. Make sure to start the flow [Flow.onStart] - * with the current state to update the tile as soon as possible. + * 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. + * + * You can use [Flow.onStart] on the returned to update the tile with the current state as soon + * as possible. */ - fun tileData(qsTileDataRequest: QSTileDataRequest): Flow<DATA_TYPE> + fun tileData(userId: Int, triggers: Flow<DataUpdateTrigger>): Flow<DATA_TYPE> /** - * Returns tile availability - whether this device currently supports this tile. Make sure to - * start the flow [Flow.onStart] with the current state to update the tile as soon as possible. + * Returns tile availability - whether this device currently supports this tile. + * + * You can use [Flow.onStart] on the returned to update the tile with the current state as soon + * as possible. */ - fun availability(): Flow<Boolean> + fun availability(userId: Int): Flow<Boolean> } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataRequest.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileInput.kt index 0aa6b0be5485..102fa3641ff4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataRequest.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileInput.kt @@ -16,7 +16,11 @@ package com.android.systemui.qs.tiles.base.interactor -data class QSTileDataRequest( +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction + +/** @see QSTileUserActionInteractor.handleInput */ +data class QSTileInput<T>( val userId: Int, - val trigger: StateUpdateTrigger, + val action: QSTileUserAction, + val data: T, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt index 14fc639c8aa8..09d7a1f7142d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt @@ -17,13 +17,14 @@ package com.android.systemui.qs.tiles.base.interactor import android.annotation.WorkerThread -import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction interface QSTileUserActionInteractor<DATA_TYPE> { - /** - * Processes user input based on [userAction] and [currentData]. It's safe to run long running - * computations inside this function in this. + * Processes user input based on [QSTileInput.userId], [QSTileInput.action], and + * [QSTileInput.data]. It's guaranteed that [QSTileInput.userId] is the same as the id passed to + * [QSTileDataInteractor] to get [QSTileInput.data]. + * + * It's safe to run long running computations inside this function in this. */ - @WorkerThread suspend fun handleInput(userAction: QSTileUserAction, currentData: DATA_TYPE) + @WorkerThread suspend fun handleInput(input: QSTileInput<DATA_TYPE>) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt index f8de36563dd2..4dc1c82c5282 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt @@ -24,7 +24,7 @@ import com.android.systemui.log.core.LogLevel import com.android.systemui.log.dagger.QSTilesLogBuffers import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.pipeline.shared.TileSpec -import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.statusbar.StatusBarState @@ -128,10 +128,21 @@ constructor( ) } + fun logForceUpdate(tileSpec: TileSpec) { + tileSpec + .getLogBuffer() + .log(tileSpec.getLogTag(), LogLevel.DEBUG, {}, { "tile data force update" }) + } + + fun logInitialRequest(tileSpec: TileSpec) { + tileSpec + .getLogBuffer() + .log(tileSpec.getLogTag(), LogLevel.DEBUG, {}, { "tile data initial update" }) + } + /** Tracks state changes based on the data and trigger event. */ fun <T> logStateUpdate( tileSpec: TileSpec, - trigger: StateUpdateTrigger, tileState: QSTileState, data: T, ) { @@ -141,11 +152,10 @@ constructor( tileSpec.getLogTag(), LogLevel.DEBUG, { - str1 = trigger.toLogString() - str2 = tileState.toLogString() - str3 = data.toString().take(DATA_MAX_LENGTH) + str1 = tileState.toLogString() + str2 = data.toString().take(DATA_MAX_LENGTH) }, - { "tile state update: trigger=$str1, state=$str2, data=$str3" } + { "tile state update: state=$str1, data=$str2" } ) } @@ -162,11 +172,11 @@ constructor( } } - private fun StateUpdateTrigger.toLogString(): String = + private fun DataUpdateTrigger.toLogString(): String = when (this) { - is StateUpdateTrigger.ForceUpdate -> "force" - is StateUpdateTrigger.InitialRequest -> "init" - is StateUpdateTrigger.UserAction<*> -> action.toLogString() + is DataUpdateTrigger.ForceUpdate -> "force" + is DataUpdateTrigger.InitialRequest -> "init" + is DataUpdateTrigger.UserInput<*> -> input.action.toLogString() } private fun QSTileUserAction.toLogString(): String = diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt index 2114751ef57b..14de5eb8be7f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt @@ -22,12 +22,12 @@ 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 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.QSTileDataRequest import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +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.interactor.StateUpdateTrigger 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 @@ -35,26 +35,31 @@ import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel -import com.android.systemui.util.kotlin.sample +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.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn @@ -66,6 +71,7 @@ import kotlinx.coroutines.flow.stateIn * * Inject [BaseQSTileViewModel.Factory] to create a new instance of this class. */ +@OptIn(ExperimentalCoroutinesApi::class) class BaseQSTileViewModel<DATA_TYPE> @VisibleForTesting constructor( @@ -74,9 +80,11 @@ constructor( private val tileDataInteractor: QSTileDataInteractor<DATA_TYPE>, private val mapper: QSTileDataToStateMapper<DATA_TYPE>, private val disabledByPolicyInteractor: DisabledByPolicyInteractor, + userRepository: UserRepository, private val falsingManager: FalsingManager, private val qsTileAnalytics: QSTileAnalytics, private val qsTileLogger: QSTileLogger, + private val systemClock: SystemClock, private val backgroundDispatcher: CoroutineDispatcher, private val tileScope: CoroutineScope, ) : QSTileViewModel { @@ -88,9 +96,11 @@ constructor( @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, @@ -98,28 +108,30 @@ constructor( 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> = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - private val userIds: MutableSharedFlow<Int> = - MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val forceUpdates: MutableSharedFlow<Unit> = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val spec get() = config.tileSpec - private lateinit var tileData: SharedFlow<DataWithTrigger<DATA_TYPE>> + private lateinit var tileData: SharedFlow<DATA_TYPE> override lateinit var state: SharedFlow<QSTileState> override val isAvailable: StateFlow<Boolean> = - tileDataInteractor - .availability() + userIds + .flatMapLatest { tileDataInteractor.availability(it) } .flowOn(backgroundDispatcher) .stateIn( tileScope, @@ -162,15 +174,9 @@ constructor( tileData = createTileDataFlow() state = tileData - // TODO(b/299908705): log data and corresponding tile state - .map { dataWithTrigger -> - mapper.map(config, dataWithTrigger.data).also { state -> - qsTileLogger.logStateUpdate( - spec, - dataWithTrigger.trigger, - state, - dataWithTrigger.data - ) + .map { data -> + mapper.map(config, data).also { state -> + qsTileLogger.logStateUpdate(spec, state, data) } } .flowOn(backgroundDispatcher) @@ -188,88 +194,99 @@ constructor( currentLifeState = lifecycle } - private fun createTileDataFlow(): SharedFlow<DataWithTrigger<DATA_TYPE>> = + private fun createTileDataFlow(): SharedFlow<DATA_TYPE> = userIds .flatMapLatest { userId -> - merge( - userInputFlow(userId), - forceUpdates.map { StateUpdateTrigger.ForceUpdate }, - ) - .onStart { emit(StateUpdateTrigger.InitialRequest) } - .map { trigger -> QSTileDataRequest(userId, trigger) } - } - .flatMapLatest { request -> - // 1) get an updated data source - // 2) process user input, possibly triggering new data to be emitted - // This handles the case when the data isn't buffered in the interactor - // TODO(b/299908705): Log events that trigger data flow to update - val dataFlow = tileDataInteractor.tileData(request) - if (request.trigger is StateUpdateTrigger.UserAction<*>) { - userActionInteractor.handleInput( - request.trigger.action, - request.trigger.tileData as DATA_TYPE, - ) - } - dataFlow.map { DataWithTrigger(it, request.trigger) } + val updateTriggers = + merge( + userInputFlow(userId), + forceUpdates + .map { DataUpdateTrigger.ForceUpdate } + .onEach { qsTileLogger.logForceUpdate(spec) }, + ) + .onStart { + emit(DataUpdateTrigger.InitialRequest) + qsTileLogger.logInitialRequest(spec) + } + tileDataInteractor + .tileData(userId, updateTriggers) + .cancellable() + .flowOn(backgroundDispatcher) } - .flowOn(backgroundDispatcher) .shareIn( tileScope, SharingStarted.WhileSubscribed(), replay = 1, // we only care about the most recent value ) - private fun userInputFlow(userId: Int): Flow<StateUpdateTrigger> { - data class StateWithData<T>(val state: QSTileState, val data: T) + /** + * Creates a user input flow which: + * - filters false inputs with [falsingManager] + * - takes care of a tile being disable by policy using [disabledByPolicyInteractor] + * - notifies [userActionInteractor] about the action + * - logs it accordingly using [qsTileLogger] and [qsTileAnalytics] + * + * Subscribing to the result flow twice will result in doubling all actions, logs and analytics. + */ + private fun userInputFlow(userId: Int): Flow<DataUpdateTrigger> { + return userInputs + .filterFalseActions() + .filterByPolicy(userId) + .throttle(CLICK_THROTTLE_DURATION, systemClock) + // Skip the input until there is some data + .mapNotNull { action -> + val state: QSTileState = state.replayCache.lastOrNull() ?: return@mapNotNull null + val data: DATA_TYPE = tileData.replayCache.lastOrNull() ?: return@mapNotNull null + qsTileLogger.logUserActionPipeline(spec, action, state, data) + qsTileAnalytics.trackUserAction(config, action) - return when (config.policy) { - is QSTilePolicy.NoRestrictions -> userInputs - is QSTilePolicy.Restricted -> - userInputs.filter { action -> - val result = - disabledByPolicyInteractor.isDisabled( - userId, - config.policy.userRestriction - ) - !disabledByPolicyInteractor.handlePolicyResult(result).also { isDisabled -> - if (isDisabled) { - qsTileLogger.logUserActionRejectedByPolicy(action, spec) - } - } - } + DataUpdateTrigger.UserInput(QSTileInput(userId, action, data)) } - .filter { action -> - val isFalseAction = - when (action) { - is QSTileUserAction.Click -> - falsingManager.isFalseTap(FalsingManager.LOW_PENALTY) - is QSTileUserAction.LongClick -> - falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY) + .onEach { userActionInteractor.handleInput(it.input) } + .flowOn(backgroundDispatcher) + } + + private fun Flow<QSTileUserAction>.filterByPolicy(userId: Int): Flow<QSTileUserAction> = + when (config.policy) { + is QSTilePolicy.NoRestrictions -> this + is QSTilePolicy.Restricted -> + filter { action -> + val result = + disabledByPolicyInteractor.isDisabled(userId, config.policy.userRestriction) + !disabledByPolicyInteractor.handlePolicyResult(result).also { isDisabled -> + if (isDisabled) { + qsTileLogger.logUserActionRejectedByPolicy(action, spec) + } } - if (isFalseAction) { - qsTileLogger.logUserActionRejectedByFalsing(action, spec) } - !isFalseAction - } - .throttle(500) - // Skip the input until there is some data - .sample(state.combine(tileData) { state, data -> StateWithData(state, data) }) { - input, - stateWithData -> - StateUpdateTrigger.UserAction(input, stateWithData.state, stateWithData.data).also { - qsTileLogger.logUserActionPipeline( - spec, - it.action, - stateWithData.state, - stateWithData.data - ) - qsTileAnalytics.trackUserAction(config, it.action) + } + + private fun Flow<QSTileUserAction>.filterFalseActions(): Flow<QSTileUserAction> = + filter { action -> + val isFalseAction = + when (action) { + is QSTileUserAction.Click -> + falsingManager.isFalseTap(FalsingManager.LOW_PENALTY) + is QSTileUserAction.LongClick -> + falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY) } + if (isFalseAction) { + qsTileLogger.logUserActionRejectedByFalsing(action, spec) } - } + !isFalseAction + } - private data class DataWithTrigger<T>(val data: T, val trigger: StateUpdateTrigger) + 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> { /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt index 0ccde741e2cc..dc5ccccd6f7f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt @@ -59,7 +59,16 @@ data class QSTileState( // This represents a tile that is currently in a disabled state but is still interactable. A // disabled state indicates that the tile is not currently active (e.g. wifi disconnected or // bluetooth disabled), but is still interactable by the user to modify this state. - INACTIVE(Tile.STATE_INACTIVE), + INACTIVE(Tile.STATE_INACTIVE); + + companion object { + fun valueOf(legacyState: Int): ActivationState = + when (legacyState) { + Tile.STATE_ACTIVE -> ACTIVE + Tile.STATE_INACTIVE -> INACTIVE + else -> UNAVAILABLE + } + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index f6299e38ae18..33f55ab53233 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -22,6 +22,7 @@ import android.view.View import androidx.annotation.GuardedBy import com.android.internal.logging.InstanceId import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.QSHost import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon @@ -31,9 +32,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.util.function.Supplier import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectIndexed import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -44,6 +43,7 @@ import kotlinx.coroutines.launch class QSTileViewModelAdapter @AssistedInject constructor( + @Application private val applicationScope: CoroutineScope, private val qsHost: QSHost, @Assisted private val qsTileViewModel: QSTileViewModel, ) : QSTile { @@ -57,25 +57,28 @@ constructor( private val listeningClients: MutableCollection<Any> = mutableSetOf() // Cancels the jobs when the adapter is no longer alive - private val adapterScope = CoroutineScope(SupervisorJob()) + private var availabilityJob: Job? = null // Cancels the jobs when clients stop listening - private val listeningScope = CoroutineScope(SupervisorJob()) + private var stateJob: Job? = null init { - adapterScope.launch { - qsTileViewModel.isAvailable.collectIndexed { index, isAvailable -> - if (!isAvailable) { - qsHost.removeTile(tileSpec) - } - // qsTileViewModel.isAvailable flow often starts with isAvailable == true. That's - // why we only allow isAvailable == true once and throw an exception afterwards. - if (index > 0 && isAvailable) { - // See com.android.systemui.qs.pipeline.domain.model.AutoAddable for additional - // guidance on how to auto add your tile - throw UnsupportedOperationException("Turning on tile is not supported now") + availabilityJob = + applicationScope.launch { + qsTileViewModel.isAvailable.collectIndexed { index, isAvailable -> + if (!isAvailable) { + qsHost.removeTile(tileSpec) + } + // qsTileViewModel.isAvailable flow often starts with isAvailable == true. + // That's + // why we only allow isAvailable == true once and throw an exception afterwards. + if (index > 0 && isAvailable) { + // See com.android.systemui.qs.pipeline.domain.model.AutoAddable for + // additional + // guidance on how to auto add your tile + throw UnsupportedOperationException("Turning on tile is not supported now") + } } } - } // QSTileHost doesn't call this when userId is initialized userSwitch(qsHost.userId) @@ -140,25 +143,28 @@ constructor( ) override fun getMetricsCategory(): Int = 0 + override fun isTileReady(): Boolean = qsTileViewModel.currentState != null + override fun setListening(client: Any?, listening: Boolean) { client ?: return synchronized(listeningClients) { if (listening) { listeningClients.add(client) if (listeningClients.size == 1) { - qsTileViewModel.state - .map { mapState(context, it, qsTileViewModel.config) } - .onEach { legacyState -> - synchronized(callbacks) { - callbacks.forEach { it.onStateChanged(legacyState) } + stateJob = + qsTileViewModel.state + .map { mapState(context, it, qsTileViewModel.config) } + .onEach { legacyState -> + synchronized(callbacks) { + callbacks.forEach { it.onStateChanged(legacyState) } + } } - } - .launchIn(listeningScope) + .launchIn(applicationScope) } } else { listeningClients.remove(client) if (listeningClients.isEmpty()) { - listeningScope.coroutineContext.cancelChildren() + stateJob?.cancel() } } } @@ -172,8 +178,8 @@ constructor( } override fun destroy() { - adapterScope.cancel() - listeningScope.cancel() + stateJob?.cancel() + availabilityJob?.cancel() qsTileViewModel.onLifecycle(QSTileLifecycle.DEAD) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt index 5659f0173860..95ee3b7a8495 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt @@ -32,16 +32,16 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) -class QSTileIntentUserActionHandlerTest : SysuiTestCase() { +class QSTileIntentUserInputHandlerTest : SysuiTestCase() { @Mock private lateinit var activityStarted: ActivityStarter - lateinit var underTest: QSTileIntentUserActionHandler + lateinit var underTest: QSTileIntentUserInputHandler @Before fun setup() { MockitoAnnotations.initMocks(this) - underTest = QSTileIntentUserActionHandler(activityStarted) + underTest = QSTileIntentUserInputHandler(activityStarted) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt index 9907278402b0..31d02ed78404 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt @@ -26,7 +26,6 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.pipeline.shared.TileSpec -import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.util.mockito.any @@ -144,7 +143,6 @@ class QSTileLoggerTest : SysuiTestCase() { fun testLogStateUpdate() { underTest.logStateUpdate( TileSpec.create("test_spec"), - StateUpdateTrigger.ForceUpdate, QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}, "test_data", ) @@ -152,21 +150,36 @@ class QSTileLoggerTest : SysuiTestCase() { assertThat(logBuffer.getStringBuffer()) .contains( "tile state update: " + - "trigger=force, " + - "state=[" + - "label=, " + + "state=[label=, " + "state=INACTIVE, " + "s_label=null, " + "cd=null, " + "sd=null, " + "svi=None, " + "enabled=ENABLED, " + - "a11y=null" + - "], " + + "a11y=null], " + "data=test_data" ) } + @Test + fun testLogForceUpdate() { + underTest.logForceUpdate( + TileSpec.create("test_spec"), + ) + + assertThat(logBuffer.getStringBuffer()).contains("tile data force update") + } + + @Test + fun testLogInitialUpdate() { + underTest.logInitialRequest( + TileSpec.create("test_spec"), + ) + + assertThat(logBuffer.getStringBuffer()).contains("tile data initial update") + } + private fun LogBuffer.getStringBuffer(): String { val stringWriter = StringWriter() dump(PrintWriter(stringWriter), 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt index 2084aeb7fe83..9b85012b29a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt @@ -29,11 +29,11 @@ import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor -import com.android.systemui.qs.tiles.base.interactor.QSTileDataRequest import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper -import com.android.systemui.qs.tiles.base.interactor.StateUpdateTrigger import com.android.systemui.qs.tiles.base.logging.QSTileLogger import com.android.systemui.qs.tiles.base.viewmodel.BaseQSTileViewModel +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.StandardTestDispatcher @@ -55,6 +55,7 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { @Mock private lateinit var qsTileLogger: QSTileLogger @Mock private lateinit var qsTileAnalytics: QSTileAnalytics + private val fakeUserRepository = FakeUserRepository() private val fakeQSTileDataInteractor = FakeQSTileDataInteractor<Any>() private val fakeQSTileUserActionInteractor = FakeQSTileUserActionInteractor<Any>() private val fakeDisabledByPolicyInteractor = FakeDisabledByPolicyInteractor() @@ -86,7 +87,7 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { assertThat(fakeQSTileDataInteractor.dataRequests).isNotEmpty() assertThat(fakeQSTileDataInteractor.dataRequests.first()) - .isEqualTo(QSTileDataRequest(1, StateUpdateTrigger.InitialRequest)) + .isEqualTo(FakeQSTileDataInteractor.DataRequest(1)) } private fun createViewModel( @@ -102,9 +103,11 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {} }, fakeDisabledByPolicyInteractor, + fakeUserRepository, fakeFalsingManager, qsTileAnalytics, qsTileLogger, + FakeSystemClock(), testCoroutineDispatcher, scope.backgroundScope, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt index 1cb4ab76c9d5..55935961466d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt @@ -19,62 +19,35 @@ package com.android.systemui.qs.tiles.base.interactor import javax.annotation.CheckReturnValue import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flatMapLatest class FakeQSTileDataInteractor<T>( - private val dataFlow: MutableSharedFlow<FakeData<T>> = - MutableSharedFlow(replay = Int.MAX_VALUE), + private val dataFlow: MutableSharedFlow<T> = MutableSharedFlow(replay = Int.MAX_VALUE), private val availabilityFlow: MutableSharedFlow<Boolean> = MutableSharedFlow(replay = Int.MAX_VALUE), ) : QSTileDataInteractor<T> { - private val mutableDataRequests = mutableListOf<QSTileDataRequest>() - val dataRequests: List<QSTileDataRequest> = mutableDataRequests + private val mutableDataRequests = mutableListOf<DataRequest>() + val dataRequests: List<DataRequest> = mutableDataRequests - private val mutableAvailabilityRequests = mutableListOf<Unit>() - val availabilityRequests: List<Unit> = mutableAvailabilityRequests + private val mutableAvailabilityRequests = mutableListOf<AvailabilityRequest>() + val availabilityRequests: List<AvailabilityRequest> = mutableAvailabilityRequests - @CheckReturnValue - fun emitData(data: T): FilterEmit = - object : FilterEmit { - override fun forRequest(request: QSTileDataRequest): Boolean = - dataFlow.tryEmit(FakeData(data, DataFilter.ForRequest(request))) - override fun forAnyRequest(): Boolean = dataFlow.tryEmit(FakeData(data, DataFilter.Any)) - } + @CheckReturnValue fun emitData(data: T): Boolean = dataFlow.tryEmit(data) fun tryEmitAvailability(isAvailable: Boolean): Boolean = availabilityFlow.tryEmit(isAvailable) suspend fun emitAvailability(isAvailable: Boolean) = availabilityFlow.emit(isAvailable) - override fun tileData(qsTileDataRequest: QSTileDataRequest): Flow<T> { - mutableDataRequests.add(qsTileDataRequest) - return dataFlow - .filter { - when (it.filter) { - is DataFilter.Any -> true - is DataFilter.ForRequest -> it.filter.request == qsTileDataRequest - } - } - .map { it.data } + override fun tileData(userId: Int, triggers: Flow<DataUpdateTrigger>): Flow<T> { + mutableDataRequests.add(DataRequest(userId)) + return triggers.flatMapLatest { dataFlow } } - override fun availability(): Flow<Boolean> { - mutableAvailabilityRequests.add(Unit) + override fun availability(userId: Int): Flow<Boolean> { + mutableAvailabilityRequests.add(AvailabilityRequest(userId)) return availabilityFlow } - interface FilterEmit { - fun forRequest(request: QSTileDataRequest): Boolean - fun forAnyRequest(): Boolean - } - - class FakeData<T>( - val data: T, - val filter: DataFilter, - ) - - sealed class DataFilter { - object Any : DataFilter() - class ForRequest(val request: QSTileDataRequest) : DataFilter() - } + data class DataRequest(val userId: Int) + data class AvailabilityRequest(val userId: Int) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt index 9c99cb52d5ee..597d52dcb299 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt @@ -16,22 +16,19 @@ package com.android.systemui.qs.tiles.base.interactor -import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock class FakeQSTileUserActionInteractor<T> : QSTileUserActionInteractor<T> { private val mutex: Mutex = Mutex() - private val mutableInputs: MutableList<FakeInput<T>> = mutableListOf() + private val mutableInputs: MutableList<QSTileInput<T>> = mutableListOf() - val inputs: List<FakeInput<T>> = mutableInputs + val inputs: List<QSTileInput<T>> = mutableInputs - fun lastInput(): FakeInput<T>? = inputs.lastOrNull() + fun lastInput(): QSTileInput<T>? = inputs.lastOrNull() - override suspend fun handleInput(userAction: QSTileUserAction, currentData: T) { - mutex.withLock { mutableInputs.add(FakeInput(userAction, currentData)) } + override suspend fun handleInput(input: QSTileInput<T>) { + mutex.withLock { mutableInputs.add(input) } } - - data class FakeInput<T>(val userAction: QSTileUserAction, val data: T) } |