summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt (renamed from packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandler.kt)18
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DataUpdateTrigger.kt (renamed from packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/StateUpdateTrigger.kt)20
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileInput.kt (renamed from packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataRequest.kt)8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt183
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt60
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserActionHandlerTest.kt)6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt27
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt9
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt55
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt13
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)
}