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