1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.qs.tiles.base.viewmodel
18 
19 import android.os.UserHandle
20 import com.android.systemui.Dumpable
21 import com.android.systemui.plugins.FalsingManager
22 import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
23 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
24 import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
25 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
26 import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
27 import com.android.systemui.qs.tiles.base.interactor.QSTileInput
28 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
29 import com.android.systemui.qs.tiles.base.logging.QSTileLogger
30 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
31 import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy
32 import com.android.systemui.qs.tiles.viewmodel.QSTileState
33 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
34 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
35 import com.android.systemui.user.data.repository.UserRepository
36 import com.android.systemui.util.kotlin.throttle
37 import com.android.systemui.util.time.SystemClock
38 import java.io.PrintWriter
39 import kotlinx.coroutines.CoroutineDispatcher
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.ExperimentalCoroutinesApi
42 import kotlinx.coroutines.cancel
43 import kotlinx.coroutines.flow.Flow
44 import kotlinx.coroutines.flow.MutableSharedFlow
45 import kotlinx.coroutines.flow.MutableStateFlow
46 import kotlinx.coroutines.flow.SharedFlow
47 import kotlinx.coroutines.flow.SharingStarted
48 import kotlinx.coroutines.flow.StateFlow
49 import kotlinx.coroutines.flow.cancellable
50 import kotlinx.coroutines.flow.combine
51 import kotlinx.coroutines.flow.distinctUntilChanged
52 import kotlinx.coroutines.flow.filter
53 import kotlinx.coroutines.flow.flatMapLatest
54 import kotlinx.coroutines.flow.flowOn
55 import kotlinx.coroutines.flow.map
56 import kotlinx.coroutines.flow.mapNotNull
57 import kotlinx.coroutines.flow.merge
58 import kotlinx.coroutines.flow.onEach
59 import kotlinx.coroutines.flow.onStart
60 import kotlinx.coroutines.flow.shareIn
61 import kotlinx.coroutines.flow.stateIn
62 import kotlinx.coroutines.launch
63 
64 /**
65  * Provides a hassle-free way to implement new tiles according to current System UI architecture
66  * standards. This ViewModel is cheap to instantiate and does nothing until its [state] is listened.
67  *
68  * Don't use this constructor directly. Instead, inject [QSTileViewModelFactory] to create a new
69  * instance of this class.
70  */
71 @OptIn(ExperimentalCoroutinesApi::class)
72 class QSTileViewModelImpl<DATA_TYPE>(
73     override val config: QSTileConfig,
74     private val userActionInteractor: () -> QSTileUserActionInteractor<DATA_TYPE>,
75     private val tileDataInteractor: () -> QSTileDataInteractor<DATA_TYPE>,
76     private val mapper: () -> QSTileDataToStateMapper<DATA_TYPE>,
77     private val disabledByPolicyInteractor: DisabledByPolicyInteractor,
78     userRepository: UserRepository,
79     private val falsingManager: FalsingManager,
80     private val qsTileAnalytics: QSTileAnalytics,
81     private val qsTileLogger: QSTileLogger,
82     private val systemClock: SystemClock,
83     private val backgroundDispatcher: CoroutineDispatcher,
84     private val tileScope: CoroutineScope,
85 ) : QSTileViewModel, Dumpable {
86 
87     private val users: MutableStateFlow<UserHandle> =
88         MutableStateFlow(userRepository.getSelectedUserInfo().userHandle)
89     private val userInputs: MutableSharedFlow<QSTileUserAction> = MutableSharedFlow()
90     private val forceUpdates: MutableSharedFlow<Unit> = MutableSharedFlow()
91     private val spec
92         get() = config.tileSpec
93 
94     private val tileData: SharedFlow<DATA_TYPE> = createTileDataFlow()
95 
96     override val state: SharedFlow<QSTileState> =
97         tileData
98             .map { data ->
99                 mapper().map(config, data).also { state ->
100                     qsTileLogger.logStateUpdate(spec, state, data)
101                 }
102             }
103             .flowOn(backgroundDispatcher)
104             .shareIn(
105                 tileScope,
106                 SharingStarted.WhileSubscribed(),
107                 replay = 1,
108             )
109     override val isAvailable: StateFlow<Boolean> =
110         users
111             .flatMapLatest { tileDataInteractor().availability(it) }
112             .flowOn(backgroundDispatcher)
113             .stateIn(
114                 tileScope,
115                 SharingStarted.WhileSubscribed(),
116                 true,
117             )
118 
119     override fun forceUpdate() {
120         tileScope.launch { forceUpdates.emit(Unit) }
121     }
122 
123     override fun onUserChanged(user: UserHandle) {
124         users.tryEmit(user)
125     }
126 
127     override fun onActionPerformed(userAction: QSTileUserAction) {
128         qsTileLogger.logUserAction(
129             userAction,
130             spec,
131             tileData.replayCache.isNotEmpty(),
132             state.replayCache.isNotEmpty()
133         )
134         tileScope.launch { userInputs.emit(userAction) }
135     }
136 
137     override fun destroy() {
138         tileScope.cancel()
139     }
140 
141     override fun dump(pw: PrintWriter, args: Array<out String>) =
142         with(pw) {
143             println("${config.tileSpec.spec}:")
144             print("    ")
145             println(state.replayCache.lastOrNull().toString())
146         }
147 
148     private fun createTileDataFlow(): SharedFlow<DATA_TYPE> =
149         users
150             .flatMapLatest { user ->
151                 val updateTriggers =
152                     merge(
153                             userInputFlow(user),
154                             forceUpdates
155                                 .map { DataUpdateTrigger.ForceUpdate }
156                                 .onEach { qsTileLogger.logForceUpdate(spec) },
157                         )
158                         .onStart {
159                             emit(DataUpdateTrigger.InitialRequest)
160                             qsTileLogger.logInitialRequest(spec)
161                         }
162                         .shareIn(tileScope, SharingStarted.WhileSubscribed())
163                 tileDataInteractor()
164                     .tileData(user, updateTriggers)
165                     // combine makes sure updateTriggers is always listened even if
166                     // tileDataInteractor#tileData doesn't flatMapLatest on it
167                     .combine(updateTriggers) { data, _ -> data }
168                     .cancellable()
169                     .flowOn(backgroundDispatcher)
170             }
171             .distinctUntilChanged()
172             .shareIn(
173                 tileScope,
174                 SharingStarted.WhileSubscribed(),
175                 replay = 1, // we only care about the most recent value
176             )
177 
178     /**
179      * Creates a user input flow which:
180      * - filters false inputs with [falsingManager]
181      * - takes care of a tile being disable by policy using [disabledByPolicyInteractor]. The
182      *   restrictions will be checked sequentially and the first one to block will be considered.
183      * - notifies [userActionInteractor] about the action
184      * - logs it accordingly using [qsTileLogger] and [qsTileAnalytics]
185      *
186      * Subscribing to the result flow twice will result in doubling all actions, logs and analytics.
187      */
188     private fun userInputFlow(user: UserHandle): Flow<DataUpdateTrigger> =
189         userInputs
190             .filterFalseActions()
191             .filterByPolicy(user)
192             .throttle(CLICK_THROTTLE_DURATION, systemClock)
193             // Skip the input until there is some data
194             .mapNotNull { action ->
195                 val state: QSTileState = state.replayCache.lastOrNull() ?: return@mapNotNull null
196                 val data: DATA_TYPE = tileData.replayCache.lastOrNull() ?: return@mapNotNull null
197                 qsTileLogger.logUserActionPipeline(spec, action, state, data)
198                 qsTileAnalytics.trackUserAction(config, action)
199 
200                 DataUpdateTrigger.UserInput(QSTileInput(user, action, data))
201             }
202             .onEach { userActionInteractor().handleInput(it.input) }
203             .flowOn(backgroundDispatcher)
204 
205     /**
206      * The restrictions will be checked sequentially and the first one to block will be considered.
207      */
208     private fun Flow<QSTileUserAction>.filterByPolicy(user: UserHandle): Flow<QSTileUserAction> =
209         config.policy.let { policy ->
210             when (policy) {
211                 is QSTilePolicy.NoRestrictions -> this@filterByPolicy
212                 is QSTilePolicy.Restricted ->
213                     filter { action ->
214                         policy.userRestrictions.none {
215                             val result = disabledByPolicyInteractor.isDisabled(user, it)
216                             val handleResult = disabledByPolicyInteractor.handlePolicyResult(result)
217                             if (handleResult) {
218                                 qsTileLogger.logUserActionRejectedByPolicy(action, spec, it)
219                             }
220                             handleResult
221                         }
222                     }
223             }
224         }
225 
226     private fun Flow<QSTileUserAction>.filterFalseActions(): Flow<QSTileUserAction> =
227         filter { action ->
228             val isFalseAction =
229                 when (action) {
230                     is QSTileUserAction.Click ->
231                         falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)
232                     is QSTileUserAction.LongClick ->
233                         falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
234                 }
235             if (isFalseAction) {
236                 qsTileLogger.logUserActionRejectedByFalsing(action, spec)
237             }
238             !isFalseAction
239         }
240 
241     private companion object {
242         const val CLICK_THROTTLE_DURATION = 200L
243     }
244 }
245