• 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.shade
18 
19 import android.content.Context
20 import android.content.res.Configuration
21 import android.graphics.Rect
22 import android.os.PowerManager
23 import android.util.ArraySet
24 import android.view.GestureDetector
25 import android.view.MotionEvent
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.WindowInsets
29 import android.widget.FrameLayout
30 import androidx.activity.OnBackPressedDispatcher
31 import androidx.activity.OnBackPressedDispatcherOwner
32 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
33 import androidx.compose.ui.platform.ComposeView
34 import androidx.core.view.updateMargins
35 import androidx.lifecycle.Lifecycle
36 import androidx.lifecycle.LifecycleEventObserver
37 import androidx.lifecycle.LifecycleObserver
38 import androidx.lifecycle.LifecycleOwner
39 import androidx.lifecycle.LifecycleRegistry
40 import androidx.lifecycle.lifecycleScope
41 import androidx.lifecycle.repeatOnLifecycle
42 import com.android.app.tracing.coroutines.launchTraced as launch
43 import com.android.compose.theme.PlatformTheme
44 import com.android.internal.annotations.VisibleForTesting
45 import com.android.keyguard.UserActivityNotifier
46 import com.android.systemui.Flags
47 import com.android.systemui.ambient.touch.TouchMonitor
48 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
49 import com.android.systemui.communal.dagger.Communal
50 import com.android.systemui.communal.domain.interactor.CommunalInteractor
51 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
52 import com.android.systemui.communal.ui.compose.CommunalContainer
53 import com.android.systemui.communal.ui.compose.CommunalContent
54 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
55 import com.android.systemui.communal.util.CommunalColors
56 import com.android.systemui.communal.util.UserTouchActivityNotifier
57 import com.android.systemui.dagger.SysUISingleton
58 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
59 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
60 import com.android.systemui.keyguard.shared.model.Edge
61 import com.android.systemui.keyguard.shared.model.KeyguardState
62 import com.android.systemui.lifecycle.repeatWhenAttached
63 import com.android.systemui.log.LogBuffer
64 import com.android.systemui.log.core.Logger
65 import com.android.systemui.log.dagger.CommunalTouchLog
66 import com.android.systemui.media.controls.ui.controller.KeyguardMediaController
67 import com.android.systemui.res.R
68 import com.android.systemui.scene.shared.flag.SceneContainerFlag
69 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
70 import com.android.systemui.shade.domain.interactor.ShadeInteractor
71 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
72 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
73 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
74 import com.android.systemui.util.kotlin.Quad
75 import com.android.systemui.util.kotlin.collectFlow
76 import java.util.function.Consumer
77 import javax.inject.Inject
78 import kotlinx.coroutines.flow.Flow
79 import kotlinx.coroutines.flow.combine
80 
81 /**
82  * Controller that's responsible for the glanceable hub container view and its touch handling.
83  *
84  * This will be used until the glanceable hub is integrated into Flexiglass.
85  */
86 @SysUISingleton
87 class GlanceableHubContainerController
88 @Inject
89 constructor(
90     private val communalInteractor: CommunalInteractor,
91     private val communalSettingsInteractor: CommunalSettingsInteractor,
92     private val communalViewModel: CommunalViewModel,
93     private val keyguardInteractor: KeyguardInteractor,
94     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
95     private val shadeInteractor: ShadeInteractor,
96     private val powerManager: PowerManager,
97     private val communalColors: CommunalColors,
98     private val ambientTouchComponentFactory: AmbientTouchComponent.Factory,
99     private val communalContent: CommunalContent,
100     @Communal private val dataSourceDelegator: SceneDataSourceDelegator,
101     private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
102     private val keyguardMediaController: KeyguardMediaController,
103     private val lockscreenSmartspaceController: LockscreenSmartspaceController,
104     private val userTouchActivityNotifier: UserTouchActivityNotifier,
105     @CommunalTouchLog logBuffer: LogBuffer,
106     private val userActivityNotifier: UserActivityNotifier,
107 ) : LifecycleOwner {
108     private val logger = Logger(logBuffer, TAG)
109 
110     private class CommunalWrapper(
111         context: Context,
112         private val communalSettingsInteractor: CommunalSettingsInteractor,
113     ) : FrameLayout(context) {
114         private val consumers: MutableSet<Consumer<Boolean>> = ArraySet()
115 
116         override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
117             consumers.forEach { it.accept(disallowIntercept) }
118             super.requestDisallowInterceptTouchEvent(disallowIntercept)
119         }
120 
121         fun dispatchTouchEvent(
122             ev: MotionEvent?,
123             disallowInterceptConsumer: Consumer<Boolean>?,
124         ): Boolean {
125             disallowInterceptConsumer?.apply { consumers.add(this) }
126 
127             try {
128                 return super.dispatchTouchEvent(ev)
129             } finally {
130                 consumers.clear()
131             }
132         }
133 
134         override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets {
135             if (
136                 !communalSettingsInteractor.isV2FlagEnabled() ||
137                     resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
138             ) {
139                 return super.onApplyWindowInsets(windowInsets)
140             }
141             val type = WindowInsets.Type.displayCutout()
142             val insets = windowInsets.getInsets(type)
143 
144             // Reset horizontal margins added by window insets, so hub can be edge to edge.
145             if (insets.left > 0 || insets.right > 0) {
146                 val lp = layoutParams as LayoutParams
147                 lp.updateMargins(0, lp.topMargin, 0, lp.bottomMargin)
148             }
149             return WindowInsets.CONSUMED
150         }
151     }
152 
153     /** The container view for the hub. This will not be initialized until [initView] is called. */
154     private var communalContainerView: View? = null
155 
156     /** Wrapper around the communal container to intercept touch events */
157     private var communalContainerWrapper: CommunalWrapper? = null
158 
159     /**
160      * This lifecycle is used to control when the [touchMonitor] listens to touches. The lifecycle
161      * should only be [Lifecycle.State.RESUMED] when the hub is showing and not covered by anything,
162      * such as the notification shade or bouncer.
163      */
164     private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
165 
166     /**
167      * This [TouchMonitor] listens for top and bottom swipe gestures globally when the hub is open.
168      * When a top or bottom swipe is detected, they will be intercepted and used to open the
169      * notification shade/bouncer.
170      */
171     private var touchMonitor: TouchMonitor? = null
172 
173     /**
174      * True if we are currently tracking a touch intercepted by the hub, either because the hub is
175      * open or being opened.
176      */
177     private var isTrackingHubTouch = false
178 
179     /**
180      * True if a touch gesture on the lock screen has been consumed by the shade/bouncer and thus
181      * should be ignored by the hub.
182      *
183      * This is necessary on the lock screen as gestures on an empty spot go through special touch
184      * handling logic in [NotificationShadeWindowViewController] that decides if they should go to
185      * the shade or bouncer. Once the shade or bouncer are moving, we don't get the typical cancel
186      * event so to play nice, we ignore touches once we see the shade or bouncer are opening.
187      */
188     private var touchTakenByKeyguardGesture = false
189 
190     /**
191      * True if the hub UI is fully open, meaning it should receive touch input.
192      *
193      * Tracks [CommunalInteractor.isCommunalShowing].
194      */
195     private var hubShowing = false
196 
197     /**
198      * True if we're transitioning to or from edit mode
199      *
200      * We block all touches and gestures when edit mode is open to prevent funky transition issues
201      * when entering and exiting edit mode because we delay exiting the hub scene when entering edit
202      * mode and enter the hub scene early when exiting edit mode to make for a smoother transition.
203      * Gestures during these transitions can result in broken and unexpected UI states.
204      *
205      * Tracks [CommunalInteractor.editActivityShowing] and the [KeyguardState.GONE] to
206      * [KeyguardState.GLANCEABLE_HUB] transition.
207      */
208     private var inEditModeTransition = false
209 
210     /**
211      * True if either the primary or alternate bouncer are open, meaning the hub should not receive
212      * any touch input.
213      */
214     private var anyBouncerShowing = false
215 
216     /**
217      * True if the shade is fully expanded and the user is not interacting with it anymore, meaning
218      * the hub should not receive any touch input.
219      *
220      * We need to not pause the touch handling lifecycle as soon as the shade opens because if the
221      * user swipes down, then back up without lifting their finger, the lifecycle will be paused
222      * then resumed, and resuming force-stops all active touch sessions. This means the shade will
223      * not receive the end of the gesture and will be stuck open.
224      *
225      * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting].
226      */
227     private var shadeShowingAndConsumingTouches = false
228 
229     /**
230      * True anytime the shade is processing user touches, regardless of expansion state.
231      *
232      * Based on [ShadeInteractor.isUserInteracting].
233      */
234     private var shadeConsumingTouches = false
235 
236     /**
237      * True if the shade is showing at all.
238      *
239      * Inverse of [ShadeInteractor.isShadeFullyCollapsed]
240      */
241     private var shadeShowing = false
242 
243     /** True if the keyguard transition state is finished on [KeyguardState.LOCKSCREEN]. */
244     private var onLockscreen = false
245 
246     /**
247      * True if the shade ever fully expands and the user isn't interacting with it (aka finger on
248      * screen dragging). In this case, the shade should handle all touch events until it has fully
249      * collapsed.
250      */
251     private var userNotInteractiveAtShadeFullyExpanded = false
252 
253     /**
254      * True if the device is dreaming, in which case we shouldn't do anything for top/bottom swipes
255      * and just let the dream overlay's touch handling deal with them.
256      *
257      * Tracks [KeyguardInteractor.isDreaming].
258      */
259     private var isDreaming = false
260 
261     /** True if we should allow swiping open the glanceable hub. */
262     private var swipeToHubEnabled = false
263 
264     /** Observes and logs state when the lifecycle that controls the [touchMonitor] updates. */
265     private val touchLifecycleLogger: LifecycleObserver = LifecycleEventObserver { _, event ->
266         logger.d({
267             "Touch handler lifecycle changed to $str1. hubShowing: $bool1, " +
268                 "shadeShowingAndConsumingTouches: $bool2, " +
269                 "anyBouncerShowing: $bool3, inEditModeTransition: $bool4"
270         }) {
271             str1 = event.toString()
272             bool1 = hubShowing
273             bool2 = shadeShowingAndConsumingTouches
274             bool3 = anyBouncerShowing
275             bool4 = inEditModeTransition
276         }
277     }
278 
279     /** Returns a flow that tracks whether communal hub is available. */
280     fun communalAvailable(): Flow<Boolean> =
281         anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
282 
283     /**
284      * Creates the container view containing the glanceable hub UI.
285      *
286      * @throws RuntimeException if the view is already initialized
287      */
288     fun initView(context: Context): View {
289         return initView(
290             ComposeView(context).apply {
291                 repeatWhenAttached {
292                     lifecycleScope.launch {
293                         repeatOnLifecycle(Lifecycle.State.CREATED) {
294                             setViewTreeOnBackPressedDispatcherOwner(
295                                 object : OnBackPressedDispatcherOwner {
296                                     override val onBackPressedDispatcher =
297                                         OnBackPressedDispatcher().apply {
298                                             setOnBackInvokedDispatcher(
299                                                 viewRootImpl.onBackInvokedDispatcher
300                                             )
301                                         }
302 
303                                     override val lifecycle: Lifecycle =
304                                         this@repeatWhenAttached.lifecycle
305                                 }
306                             )
307 
308                             setContent {
309                                 PlatformTheme {
310                                     CommunalContainer(
311                                         viewModel = communalViewModel,
312                                         colors = communalColors,
313                                         dataSourceDelegator = dataSourceDelegator,
314                                         content = communalContent,
315                                     )
316                                 }
317                             }
318                         }
319                     }
320                 }
321             }
322         )
323     }
324 
325     private fun resetTouchMonitor() {
326         touchMonitor?.apply {
327             destroy()
328             touchMonitor = null
329         }
330     }
331 
332     /** Override for testing. */
333     @VisibleForTesting
334     internal fun initView(containerView: View): View {
335         SceneContainerFlag.assertInLegacyMode()
336         if (communalContainerView != null) {
337             throw RuntimeException("Communal view has already been initialized")
338         }
339 
340         resetTouchMonitor()
341 
342         touchMonitor =
343             ambientTouchComponentFactory.create(this, HashSet(), TAG).getTouchMonitor().apply {
344                 init()
345             }
346 
347         lifecycleRegistry.addObserver(touchLifecycleLogger)
348         lifecycleRegistry.currentState = Lifecycle.State.CREATED
349 
350         communalContainerView = containerView
351 
352         if (!Flags.hubmodeFullscreenVerticalSwipeFix()) {
353             val topEdgeSwipeRegionWidth =
354                 containerView.resources.getDimensionPixelSize(
355                     R.dimen.communal_top_edge_swipe_region_height
356                 )
357             val bottomEdgeSwipeRegionWidth =
358                 containerView.resources.getDimensionPixelSize(
359                     R.dimen.communal_bottom_edge_swipe_region_height
360                 )
361 
362             // BouncerSwipeTouchHandler has a larger gesture area than we want, set an exclusion
363             // area so
364             // the gesture area doesn't overlap with widgets.
365             // TODO(b/323035776): adjust gesture area for portrait mode
366             containerView.repeatWhenAttached {
367                 // Run when the touch handling lifecycle is RESUMED, meaning the hub is visible and
368                 // not
369                 // occluded.
370                 lifecycleRegistry.repeatOnLifecycle(Lifecycle.State.RESUMED) {
371                     containerView.systemGestureExclusionRects =
372                         listOf(
373                             // Only allow swipe up to bouncer and swipe down to shade in the very
374                             // top/bottom to avoid conflicting with widgets in the hub grid.
375                             Rect(
376                                 0,
377                                 topEdgeSwipeRegionWidth,
378                                 containerView.right,
379                                 containerView.bottom - bottomEdgeSwipeRegionWidth,
380                             )
381                         )
382 
383                     logger.d({ "Insets updated: $str1" }) {
384                         str1 = containerView.systemGestureExclusionRects.toString()
385                     }
386                 }
387             }
388         }
389 
390         // Listen to bouncer visibility directly as these flows become true as soon as any portion
391         // of the bouncers are visible when the transition starts. The keyguard transition state
392         // only changes once transitions are fully finished, which would mean touches during a
393         // transition to the bouncer would be incorrectly intercepted by the hub.
394         collectFlow(
395             containerView,
396             anyOf(
397                 keyguardInteractor.primaryBouncerShowing,
398                 keyguardInteractor.alternateBouncerShowing,
399             ),
400             {
401                 anyBouncerShowing = it
402                 if (hubShowing) {
403                     logger.d({ "New value for anyBouncerShowing: $bool1" }) { bool1 = it }
404                 }
405                 updateTouchHandlingState()
406             },
407         )
408         collectFlow(
409             containerView,
410             keyguardTransitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN),
411             { onLockscreen = it },
412         )
413         collectFlow(
414             containerView,
415             communalInteractor.isCommunalVisible,
416             {
417                 hubShowing = it
418                 updateTouchHandlingState()
419             },
420         )
421         collectFlow(
422             containerView,
423             // When leaving edit mode, editActivityShowing is true until the edit mode activity
424             // finishes itself and the device locks, after which isInTransition will be true until
425             // we're fully on the hub.
426             anyOf(
427                 communalInteractor.editActivityShowing,
428                 keyguardTransitionInteractor.isInTransition(
429                     Edge.create(KeyguardState.GONE, KeyguardState.GLANCEABLE_HUB)
430                 ),
431             ),
432             {
433                 inEditModeTransition = it
434                 updateTouchHandlingState()
435             },
436         )
437         collectFlow(
438             containerView,
439             combine(
440                 shadeInteractor.isAnyFullyExpanded,
441                 shadeInteractor.isUserInteracting,
442                 shadeInteractor.isShadeFullyCollapsed,
443                 shadeInteractor.isQsExpanded,
444                 ::Quad,
445             ),
446             { (isFullyExpanded, isUserInteracting, isShadeFullyCollapsed, isQsExpanded) ->
447                 shadeConsumingTouches = isUserInteracting
448                 shadeShowing = isQsExpanded || !isShadeFullyCollapsed
449                 val expandedAndNotInteractive = isFullyExpanded && !isUserInteracting
450 
451                 // If we ever are fully expanded and not interacting, capture this state as we
452                 // should not handle touches until we fully collapse again
453                 userNotInteractiveAtShadeFullyExpanded =
454                     !isShadeFullyCollapsed &&
455                         (userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive)
456 
457                 // If the shade reaches full expansion without interaction, then we should allow it
458                 // to consume touches rather than handling it here until it disappears.
459                 shadeShowingAndConsumingTouches =
460                     (userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive).also {
461                         if (it != shadeShowingAndConsumingTouches && hubShowing) {
462                             logger.d({ "New value for shadeShowingAndConsumingTouches: $bool1" }) {
463                                 bool1 = it
464                             }
465                         }
466                     }
467                 updateTouchHandlingState()
468             },
469         )
470         collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
471         collectFlow(containerView, communalViewModel.swipeToHubEnabled, { swipeToHubEnabled = it })
472 
473         communalContainerWrapper =
474             CommunalWrapper(containerView.context, communalSettingsInteractor)
475         communalContainerWrapper?.addView(communalContainerView)
476         logger.d("Hub container initialized")
477         return communalContainerWrapper!!
478     }
479 
480     /**
481      * Updates the lifecycle stored by the [lifecycleRegistry] to control when the [touchMonitor]
482      * should listen for and intercept top and bottom swipes.
483      *
484      * Also clears gesture exclusion zones when the hub is occluded or gone.
485      */
486     private fun updateTouchHandlingState() {
487         // Only listen to gestures when we're settled in the hub keyguard state and the shade
488         // bouncer are not showing on top.
489         val shouldInterceptGestures =
490             hubShowing &&
491                 !(shadeShowingAndConsumingTouches || anyBouncerShowing || inEditModeTransition)
492         if (shouldInterceptGestures) {
493             lifecycleRegistry.currentState = Lifecycle.State.RESUMED
494         } else {
495             // Hub is either occluded or no longer showing, turn off touch handling.
496             lifecycleRegistry.currentState = Lifecycle.State.STARTED
497 
498             // Clear exclusion rects if the hub is not showing or is covered, so we don't interfere
499             // with back gestures when the bouncer or shade. We do this here instead of with
500             // repeatOnLifecycle as repeatOnLifecycle does not run when going from RESUMED back to
501             // STARTED, only when going from CREATED to STARTED.
502             communalContainerView!!.systemGestureExclusionRects = emptyList()
503         }
504     }
505 
506     /** Removes the container view from its parent. */
507     fun disposeView() {
508         SceneContainerFlag.assertInLegacyMode()
509         communalContainerView?.let {
510             (it.parent as ViewGroup).removeView(it)
511             lifecycleRegistry.currentState = Lifecycle.State.CREATED
512             communalContainerView = null
513         }
514 
515         communalContainerWrapper?.let {
516             (it.parent as ViewGroup).removeView(it)
517             communalContainerWrapper = null
518         }
519 
520         lifecycleRegistry.removeObserver(touchLifecycleLogger)
521 
522         resetTouchMonitor()
523 
524         logger.d("Hub container disposed")
525     }
526 
527     /**
528      * Notifies the hub container of a touch event. Returns true if it's determined that the touch
529      * should go to the hub container and no one else.
530      *
531      * Special handling is needed because the hub container sits at the lowest z-order in
532      * [NotificationShadeWindowView] and would not normally receive touches. We also cannot use a
533      * [GestureDetector] as the hub container's SceneTransitionLayout is a Compose view that expects
534      * to be fully in control of its own touch handling.
535      */
536     fun onTouchEvent(ev: MotionEvent): Boolean {
537         SceneContainerFlag.assertInLegacyMode()
538 
539         if (communalContainerView == null) {
540             // Return early so we don't log unnecessarily and fill up our LogBuffer.
541             return false
542         }
543 
544         // In the case that we are handling full swipes on the lockscreen, are on the lockscreen,
545         // and the touch is within the horizontal notification band on the screen, do not process
546         // the touch.
547         val touchOnNotifications =
548             !notificationStackScrollLayoutController.isBelowLastNotification(ev.x, ev.y)
549         val touchOnUmo = keyguardMediaController.isWithinMediaViewBounds(ev.x.toInt(), ev.y.toInt())
550         val touchOnSmartspace =
551             lockscreenSmartspaceController.isWithinSmartspaceBounds(ev.x.toInt(), ev.y.toInt())
552         val glanceableHubV2 = communalSettingsInteractor.isV2FlagEnabled()
553         if (
554             !hubShowing &&
555                 (touchOnNotifications || touchOnUmo || touchOnSmartspace || !swipeToHubEnabled)
556         ) {
557             logger.d({
558                 "Lockscreen touch ignored: touchOnNotifications: $bool1, touchOnUmo: $bool2, " +
559                     "touchOnSmartspace: $bool3, glanceableHubV2: $bool4"
560             }) {
561                 bool1 = touchOnNotifications
562                 bool2 = touchOnUmo
563                 bool3 = touchOnSmartspace
564                 bool4 = glanceableHubV2
565             }
566             return false
567         }
568 
569         return handleTouchEventOnCommunalView(ev)
570     }
571 
572     private fun handleTouchEventOnCommunalView(ev: MotionEvent): Boolean {
573         val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
574         val isUp = ev.actionMasked == MotionEvent.ACTION_UP
575         val isMove = ev.actionMasked == MotionEvent.ACTION_MOVE
576         val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL
577 
578         val hubOccluded = anyBouncerShowing || shadeConsumingTouches || shadeShowing
579 
580         if ((isDown || isMove) && !hubOccluded) {
581             if (isDown) {
582                 logger.d({
583                     "Touch started. x: $int1, y: $int2, hubShowing: $bool1, isDreaming: $bool2, " +
584                         "onLockscreen: $bool3"
585                 }) {
586                     int1 = ev.x.toInt()
587                     int2 = ev.y.toInt()
588                     bool1 = hubShowing
589                     bool2 = isDreaming
590                     bool3 = onLockscreen
591                 }
592             }
593             isTrackingHubTouch = true
594         }
595 
596         if (isTrackingHubTouch) {
597             // On the lock screen, our touch handlers are not active and we rely on the NSWVC's
598             // touch handling for gestures on blank areas, which can go up to show the bouncer or
599             // down to show the notification shade. We see the touches first and they are not
600             // consumed and cancelled like on the dream or hub so we have to gracefully ignore them
601             // if the shade or bouncer are handling them. This issue only applies to touches on the
602             // keyguard itself, once the bouncer or shade are fully open, our logic stops us from
603             // taking touches.
604             touchTakenByKeyguardGesture =
605                 (onLockscreen && (shadeConsumingTouches || anyBouncerShowing)).also {
606                     if (it != touchTakenByKeyguardGesture && it) {
607                         logger.d(
608                             "Lock screen touch consumed by shade or bouncer, ignoring " +
609                                 "subsequent touches"
610                         )
611                     }
612                 }
613             if (isUp || isCancel) {
614                 logger.d({
615                     val endReason = if (bool1) "up" else "cancel"
616                     "Touch ended with $endReason. x: $int1, y: $int2, " +
617                         "shadeConsumingTouches: $bool2, anyBouncerShowing: $bool3"
618                 }) {
619                     int1 = ev.x.toInt()
620                     int2 = ev.y.toInt()
621                     bool1 = isUp
622                     bool2 = shadeConsumingTouches
623                     bool3 = anyBouncerShowing
624                 }
625                 isTrackingHubTouch = false
626 
627                 // Clear out touch taken state to ensure the up/cancel event still gets dispatched
628                 // to the hub. This is necessary as the hub always receives at least the initial
629                 // down even if the shade or bouncer end up handling the touch.
630                 touchTakenByKeyguardGesture = false
631             }
632             return dispatchTouchEvent(ev)
633         }
634 
635         return false
636     }
637 
638     /**
639      * Dispatches the touch event to the communal container and sends a user activity event to reset
640      * the screen timeout.
641      */
642     private fun dispatchTouchEvent(ev: MotionEvent): Boolean {
643         if (inEditModeTransition) {
644             // Consume but ignore touches while we're transitioning to or from edit mode so that the
645             // user can't trigger another transition, such as by swiping the hub away, tapping a
646             // widget, or opening the shade/bouncer. Doing any of these while transitioning can
647             // result in broken states.
648             return true
649         }
650         var handled = hubShowing
651         try {
652             if (!touchTakenByKeyguardGesture) {
653                 communalContainerWrapper?.dispatchTouchEvent(ev) {
654                     if (it) {
655                         handled = true
656                     }
657                 }
658             }
659             return handled
660         } finally {
661             if (handled) {
662                 userTouchActivityNotifier.notifyActivity(ev)
663             }
664         }
665     }
666 
667     override val lifecycle: Lifecycle
668         get() = lifecycleRegistry
669 
670     companion object {
671         private const val TAG = "GlanceableHubContainer"
672     }
673 }
674