• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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