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.scene.ui.viewmodel 18 19 import android.view.MotionEvent 20 import android.view.View 21 import androidx.compose.runtime.getValue 22 import androidx.compose.ui.unit.dp 23 import com.android.app.tracing.coroutines.launchTraced as launch 24 import com.android.compose.animation.scene.ContentKey 25 import com.android.compose.animation.scene.DefaultEdgeDetector 26 import com.android.compose.animation.scene.ObservableTransitionState 27 import com.android.compose.animation.scene.OverlayKey 28 import com.android.compose.animation.scene.SceneKey 29 import com.android.compose.animation.scene.SwipeSourceDetector 30 import com.android.compose.animation.scene.UserAction 31 import com.android.compose.animation.scene.UserActionResult 32 import com.android.systemui.classifier.Classifier 33 import com.android.systemui.classifier.domain.interactor.FalsingInteractor 34 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 35 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel 36 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel 37 import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel 38 import com.android.systemui.lifecycle.ExclusiveActivatable 39 import com.android.systemui.lifecycle.Hydrator 40 import com.android.systemui.power.domain.interactor.PowerInteractor 41 import com.android.systemui.scene.domain.interactor.SceneInteractor 42 import com.android.systemui.scene.shared.logger.SceneLogger 43 import com.android.systemui.scene.shared.model.Overlays 44 import com.android.systemui.scene.shared.model.Scenes 45 import com.android.systemui.scene.ui.composable.Overlay 46 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor 47 import com.android.systemui.shade.shared.model.ShadeMode 48 import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor 49 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer 50 import com.android.systemui.wallpapers.ui.viewmodel.WallpaperViewModel 51 import dagger.assisted.Assisted 52 import dagger.assisted.AssistedFactory 53 import dagger.assisted.AssistedInject 54 import kotlinx.coroutines.awaitCancellation 55 import kotlinx.coroutines.coroutineScope 56 import kotlinx.coroutines.flow.Flow 57 import kotlinx.coroutines.flow.StateFlow 58 import kotlinx.coroutines.flow.map 59 60 /** Models UI state for the scene container. */ 61 class SceneContainerViewModel 62 @AssistedInject 63 constructor( 64 private val sceneInteractor: SceneInteractor, 65 private val falsingInteractor: FalsingInteractor, 66 private val powerInteractor: PowerInteractor, 67 shadeModeInteractor: ShadeModeInteractor, 68 private val remoteInputInteractor: RemoteInputInteractor, 69 private val logger: SceneLogger, 70 hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory, 71 val lightRevealScrim: LightRevealScrimViewModel, 72 val wallpaperViewModel: WallpaperViewModel, 73 keyguardInteractor: KeyguardInteractor, 74 val burnIn: AodBurnInViewModel, 75 val clock: KeyguardClockViewModel, 76 @Assisted view: View, 77 @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, 78 ) : ExclusiveActivatable() { 79 80 /** The scene that should be rendered. */ 81 val currentScene: StateFlow<SceneKey> = sceneInteractor.currentScene 82 83 private val hydrator = Hydrator("SceneContainerViewModel.hydrator") 84 85 /** Whether the container is visible. */ 86 val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible) 87 88 val allContentKeys: List<ContentKey> = sceneInteractor.allContentKeys 89 90 val hapticsViewModel: SceneContainerHapticsViewModel = hapticsViewModelFactory.create(view) 91 92 /** 93 * The [SwipeSourceDetector] to use for defining which areas of the screen can be defined in the 94 * [UserAction]s for this container. 95 */ 96 val swipeSourceDetector: SwipeSourceDetector by 97 hydrator.hydratedStateOf( 98 traceName = "swipeSourceDetector", 99 initialValue = DefaultEdgeDetector, 100 source = 101 shadeModeInteractor.shadeMode.map { 102 if (it is ShadeMode.Dual) { 103 SceneContainerSwipeDetector(edgeSize = 40.dp) 104 } else { 105 DefaultEdgeDetector 106 } 107 }, 108 ) 109 110 /** Amount of color saturation for the Flexi ribbon. */ 111 val ribbonColorSaturation: Float by 112 hydrator.hydratedStateOf( 113 traceName = "ribbonColorSaturation", 114 source = keyguardInteractor.dozeAmount.map { 1 - it }, 115 initialValue = 1f, 116 ) 117 118 override suspend fun onActivated(): Nothing { 119 try { 120 // Sends a MotionEventHandler to the owner of the view-model so they can report 121 // MotionEvents into the view-model. 122 motionEventHandlerReceiver( 123 object : MotionEventHandler { 124 override fun onMotionEvent(motionEvent: MotionEvent) { 125 this@SceneContainerViewModel.onMotionEvent(motionEvent) 126 } 127 128 override fun onEmptySpaceMotionEvent(motionEvent: MotionEvent) { 129 this@SceneContainerViewModel.onEmptySpaceMotionEvent(motionEvent) 130 } 131 132 override fun onMotionEventComplete() { 133 this@SceneContainerViewModel.onMotionEventComplete() 134 } 135 } 136 ) 137 138 coroutineScope { 139 launch { hydrator.activate() } 140 launch("SceneContainerHapticsViewModel") { hapticsViewModel.activate() } 141 } 142 awaitCancellation() 143 } finally { 144 // Clears the previously-sent MotionEventHandler so the owner of the view-model releases 145 // their reference to it. 146 motionEventHandlerReceiver(null) 147 } 148 } 149 150 /** 151 * Binds the given flow so the system remembers it. 152 * 153 * Note that you must call this with `null` when the UI is done or risk a memory leak. 154 */ 155 fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { 156 sceneInteractor.setTransitionState(transitionState) 157 } 158 159 /** 160 * Notifies that a [MotionEvent] is first seen at the top of the scene container UI. This 161 * includes gestures on [SharedNotificationContainer] as well as the Composable scene container 162 * hierarchy. 163 * 164 * Call this before the [MotionEvent] starts to propagate through the UI hierarchy. 165 */ 166 fun onMotionEvent(event: MotionEvent) { 167 powerInteractor.onUserTouch() 168 falsingInteractor.onTouchEvent(event) 169 if ( 170 event.actionMasked == MotionEvent.ACTION_UP || 171 event.actionMasked == MotionEvent.ACTION_CANCEL 172 ) { 173 sceneInteractor.onUserInputFinished() 174 } 175 } 176 177 /** 178 * Notifies that a [MotionEvent] has propagated through the entire [SharedNotificationContainer] 179 * and Composable scene container hierarchy without being handled. 180 * 181 * Call this after the [MotionEvent] has finished propagating through the UI hierarchy. 182 */ 183 fun onEmptySpaceMotionEvent(event: MotionEvent) { 184 // check if the touch is outside the window and if remote input is active. 185 // If true, close any active remote inputs. 186 if ( 187 event.action == MotionEvent.ACTION_OUTSIDE && 188 (remoteInputInteractor.isRemoteInputActive as StateFlow).value 189 ) { 190 remoteInputInteractor.closeRemoteInputs() 191 } 192 } 193 194 /** 195 * Notifies that a scene container user interaction has begun. 196 * 197 * This is a user interaction that has reached the Composable hierarchy of the scene container, 198 * rather than being handled by [SharedNotificationContainer]. 199 */ 200 fun onSceneContainerUserInputStarted() { 201 sceneInteractor.onSceneContainerUserInputStarted() 202 } 203 204 /** 205 * Notifies that a [MotionEvent] that was previously sent to [onMotionEvent] has passed through 206 * the scene container UI. 207 * 208 * Call this after the [MotionEvent] propagates completely through the UI hierarchy. 209 */ 210 fun onMotionEventComplete() { 211 falsingInteractor.onMotionEventComplete() 212 } 213 214 /** 215 * Returns `true` if a change to [toScene] is currently allowed; `false` otherwise. 216 * 217 * This is invoked only for user-initiated transitions. The goal is to check with the falsing 218 * system whether the change from the current scene to the given scene should be rejected due to 219 * it being a false touch. 220 */ 221 fun canChangeScene(toScene: SceneKey): Boolean { 222 return isInteractionAllowedByFalsing(toScene).also { sceneChangeAllowed -> 223 if (sceneChangeAllowed) { 224 // A scene change is guaranteed; log it. 225 logger.logSceneChanged( 226 from = currentScene.value, 227 to = toScene, 228 sceneState = null, 229 reason = "user interaction", 230 isInstant = false, 231 ) 232 } else { 233 logger.logSceneChangeRejection( 234 from = currentScene.value, 235 to = toScene, 236 originalChangeReason = null, 237 rejectionReason = "Falsing: false touch detected", 238 ) 239 } 240 } 241 } 242 243 /** 244 * Returns `true` if showing the [newlyShown] overlay is currently allowed; `false` otherwise. 245 * 246 * This is invoked only for user-initiated transitions. The goal is to check with the falsing 247 * system whether the overlay change should be rejected due to it being a false touch. 248 */ 249 fun canShowOrReplaceOverlay( 250 newlyShown: OverlayKey, 251 beingReplaced: OverlayKey? = null, 252 ): Boolean { 253 return isInteractionAllowedByFalsing(newlyShown).also { 254 // An overlay change is guaranteed; log it. 255 logger.logOverlayChangeRequested( 256 from = beingReplaced, 257 to = newlyShown, 258 reason = "user interaction", 259 ) 260 } 261 } 262 263 /** 264 * Immediately resolves any scene families present in [actionResultMap] to their current 265 * resolution target. 266 */ 267 fun resolveSceneFamilies( 268 actionResultMap: Map<UserAction, UserActionResult> 269 ): Map<UserAction, UserActionResult> { 270 return actionResultMap.mapValues { (_, actionResult) -> 271 when (actionResult) { 272 is UserActionResult.ChangeScene -> { 273 sceneInteractor.resolveSceneFamilyOrNull(actionResult.toScene)?.value?.let { 274 toScene -> 275 UserActionResult( 276 toScene = toScene, 277 transitionKey = actionResult.transitionKey, 278 requiresFullDistanceSwipe = actionResult.requiresFullDistanceSwipe, 279 ) 280 } 281 } 282 // Overlay transitions don't use scene families, nothing to resolve. 283 is UserActionResult.ShowOverlay, 284 is UserActionResult.HideOverlay, 285 is UserActionResult.ReplaceByOverlay -> null 286 } ?: actionResult 287 } 288 } 289 290 /** 291 * Returns the [ContentKey] whose user actions should be active. 292 * 293 * @param overlayByKey Mapping of [Overlay] by [OverlayKey], ordered by z-order such that the 294 * last overlay is rendered on top of all other overlays. 295 */ 296 fun getActionableContentKey( 297 currentScene: SceneKey, 298 currentOverlays: Set<OverlayKey>, 299 overlayByKey: Map<OverlayKey, Overlay>, 300 ): ContentKey { 301 // Overlay actions take precedence over scene actions. 302 return when (currentOverlays.size) { 303 // No overlays, the scene is actionable. 304 0 -> currentScene 305 // Small optimization for the most common case. 306 1 -> currentOverlays.first() 307 // Find the top-most overlay by z-index. 308 else -> 309 checkNotNull(overlayByKey.asSequence().findLast { it.key in currentOverlays }?.key) 310 } 311 } 312 313 /** 314 * Returns a filtered version of [unfiltered], without action-result entries that would navigate 315 * to disabled scenes. 316 */ 317 fun filteredUserActions( 318 unfiltered: Flow<Map<UserAction, UserActionResult>> 319 ): Flow<Map<UserAction, UserActionResult>> { 320 return sceneInteractor.filteredUserActions(unfiltered) 321 } 322 323 /** 324 * Returns `true` if transitioning to [content] is permissible by the falsing system; `false` 325 * otherwise. 326 */ 327 private fun isInteractionAllowedByFalsing(content: ContentKey): Boolean { 328 val interactionTypeOrNull = 329 when (content) { 330 Overlays.Bouncer -> Classifier.BOUNCER_UNLOCK 331 Scenes.Gone -> Classifier.UNLOCK 332 Scenes.Shade, 333 Overlays.NotificationsShade -> Classifier.NOTIFICATION_DRAG_DOWN 334 Scenes.QuickSettings, 335 Overlays.QuickSettingsShade -> Classifier.QUICK_SETTINGS 336 else -> null 337 } 338 339 return interactionTypeOrNull?.let { interactionType -> 340 // It's important that the falsing system is always queried, even if no enforcement 341 // will occur. This helps build up the right signal in the system. 342 val isFalseTouch = falsingInteractor.isFalseTouch(interactionType) 343 344 // Only enforce falsing if moving from the lockscreen scene to new content. 345 val fromLockscreenScene = currentScene.value == Scenes.Lockscreen 346 347 !fromLockscreenScene || !isFalseTouch 348 } ?: true 349 } 350 351 /** Defines interface for classes that can handle externally-reported [MotionEvent]s. */ 352 interface MotionEventHandler { 353 /** Notifies that a [MotionEvent] has occurred. */ 354 fun onMotionEvent(motionEvent: MotionEvent) 355 356 /** Notifies that a [MotionEvent] has occurred outside the root window. */ 357 fun onEmptySpaceMotionEvent(motionEvent: MotionEvent) 358 359 /** 360 * Notifies that the previous [MotionEvent] reported by [onMotionEvent] has finished 361 * processing. 362 */ 363 fun onMotionEventComplete() 364 } 365 366 @AssistedFactory 367 interface Factory { 368 fun create( 369 view: View, 370 motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, 371 ): SceneContainerViewModel 372 } 373 } 374