diff options
| author | 2024-05-23 17:02:16 -0700 | |
|---|---|---|
| committer | 2024-05-29 14:37:04 -0700 | |
| commit | e65540dfd6d8eb7bb10c07eff04a65cc5e8d1427 (patch) | |
| tree | 5095eaa30630a4ce18537d117421adbd1530384d | |
| parent | fd5cf91931a124409ef1114b2902d96609ab78f4 (diff) | |
Allow Full Screen Glanceable Hub Swipe Entry on Lockscreen.
This changelist adds support for swiping in Glanceable Hub. When
enabled, horizontal swipe behaviors over the lockscreen under the
notification stack will drive the entry gesture into Glanceable
Hub.
Test: atest GlanceableHubContainerControllerTest#fullScreenSwipeGesture_doNotProcessTouchesInNotificationStack
Test atest MultiPointerDraggableTest#multiPointerSwipeDetectorInteraction
Flag: com.android.systemui.glanceable_hub_fullscreen_swipe
Bug: 339665673
Change-Id: Ib5d012e39b3ac4ea555b5cbb39c06cfc3c137fd9
12 files changed, 371 insertions, 36 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 1df9c88e48ac..d0f5d7b64dfb 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -997,6 +997,13 @@ flag { } flag { + name: "glanceable_hub_fullscreen_swipe" + namespace: "systemui" + description: "Increase swipe area for gestures to bring in glanceable hub" + bug: "339665673" +} + +flag { name: "glanceable_hub_shortcut_button" namespace: "systemui" description: "Shows a button over the dream and lock screen to open the glanceable hub" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index feb1f5b17bef..a90f82eda1af 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.CommunalSwipeDetector +import com.android.compose.animation.scene.DefaultSwipeDetector import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher @@ -35,6 +37,7 @@ import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.observableTransitionState import com.android.compose.animation.scene.transitions import com.android.systemui.Flags +import com.android.systemui.Flags.glanceableHubFullscreenSwipe import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys import com.android.systemui.communal.ui.compose.extensions.allowGestures @@ -108,6 +111,8 @@ fun CommunalContainer( ) } + val detector = remember { CommunalSwipeDetector() } + DisposableEffect(state) { val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope) dataSourceDelegator.setDelegate(dataSource) @@ -121,13 +126,25 @@ fun CommunalContainer( onDispose { viewModel.setTransitionState(null) } } + val swipeSourceDetector = + if (glanceableHubFullscreenSwipe()) { + detector + } else { + FixedSizeEdgeDetector(dimensionResource(id = R.dimen.communal_gesture_initiation_width)) + } + + val swipeDetector = + if (glanceableHubFullscreenSwipe()) { + detector + } else { + DefaultSwipeDetector + } + SceneTransitionLayout( state = state, modifier = modifier.fillMaxSize(), - swipeSourceDetector = - FixedSizeEdgeDetector( - dimensionResource(id = R.dimen.communal_gesture_initiation_width) - ), + swipeSourceDetector = swipeSourceDetector, + swipeDetector = swipeDetector, ) { scene( CommunalScenes.Blank, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt new file mode 100644 index 000000000000..7be34cabfaf8 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.animation.scene + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import kotlin.math.abs + +private const val TRAVEL_RATIO_THRESHOLD = .5f + +/** + * {@link CommunalSwipeDetector} provides an implementation of {@link SwipeDetector} and {@link + * SwipeSourceDetector} to enable fullscreen swipe handling to transition to and from the glanceable + * hub. + */ +class CommunalSwipeDetector(private var lastDirection: SwipeSource? = null) : + SwipeSourceDetector, SwipeDetector { + override fun source( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation + ): SwipeSource? { + return lastDirection + } + + override fun detectSwipe(change: PointerInputChange): Boolean { + if (change.positionChange().x > 0) { + lastDirection = Edge.Left + } else { + lastDirection = Edge.Right + } + + // Determine whether the ratio of the distance traveled horizontally to the distance + // traveled vertically exceeds the threshold. + return abs(change.positionChange().x / change.positionChange().y) > TRAVEL_RATIO_THRESHOLD + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 0fc0053ce4a1..3cc8431cd87e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -72,6 +72,7 @@ internal fun Modifier.multiPointerDraggable( enabled: () -> Boolean, startDragImmediately: (startedPosition: Offset) -> Boolean, onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + swipeDetector: SwipeDetector = DefaultSwipeDetector, ): Modifier = this.then( MultiPointerDraggableElement( @@ -79,6 +80,7 @@ internal fun Modifier.multiPointerDraggable( enabled, startDragImmediately, onDragStarted, + swipeDetector, ) ) @@ -88,6 +90,7 @@ private data class MultiPointerDraggableElement( private val startDragImmediately: (startedPosition: Offset) -> Boolean, private val onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + private val swipeDetector: SwipeDetector, ) : ModifierNodeElement<MultiPointerDraggableNode>() { override fun create(): MultiPointerDraggableNode = MultiPointerDraggableNode( @@ -95,6 +98,7 @@ private data class MultiPointerDraggableElement( enabled = enabled, startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, + swipeDetector = swipeDetector, ) override fun update(node: MultiPointerDraggableNode) { @@ -102,6 +106,7 @@ private data class MultiPointerDraggableElement( node.enabled = enabled node.startDragImmediately = startDragImmediately node.onDragStarted = onDragStarted + node.swipeDetector = swipeDetector } } @@ -111,6 +116,7 @@ internal class MultiPointerDraggableNode( var startDragImmediately: (startedPosition: Offset) -> Boolean, var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + var swipeDetector: SwipeDetector = DefaultSwipeDetector, ) : PointerInputModifierNode, DelegatingNode(), @@ -199,6 +205,7 @@ internal class MultiPointerDraggableNode( onDragCancel = { controller -> controller.onStop(velocity = 0f, canChangeScene = true) }, + swipeDetector = swipeDetector ) } catch (exception: CancellationException) { // If the coroutine scope is active, we can just restart the drag cycle. @@ -226,7 +233,8 @@ internal class MultiPointerDraggableNode( (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, onDrag: (controller: DragController, change: PointerInputChange, dragAmount: Float) -> Unit, onDragEnd: (controller: DragController) -> Unit, - onDragCancel: (controller: DragController) -> Unit + onDragCancel: (controller: DragController) -> Unit, + swipeDetector: SwipeDetector, ) { // Wait for a consumable event in [PointerEventPass.Main] pass val consumablePointer = awaitConsumableEvent().changes.first() @@ -238,8 +246,10 @@ internal class MultiPointerDraggableNode( consumablePointer } else { val onSlopReached = { change: PointerInputChange, over: Float -> - change.consume() - overSlop = over + if (swipeDetector.detectSwipe(change)) { + change.consume() + overSlop = over + } } // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once it diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 11e711ace971..cf8c5841f797 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -55,6 +55,7 @@ fun SceneTransitionLayout( state: SceneTransitionLayoutState, modifier: Modifier = Modifier, swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, + swipeDetector: SwipeDetector = DefaultSwipeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, scenes: SceneTransitionLayoutScope.() -> Unit, ) { @@ -62,6 +63,7 @@ fun SceneTransitionLayout( state, modifier, swipeSourceDetector, + swipeDetector, transitionInterceptionThreshold, onLayoutImpl = null, scenes, @@ -95,6 +97,7 @@ fun SceneTransitionLayout( transitions: SceneTransitions, modifier: Modifier = Modifier, swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, + swipeDetector: SwipeDetector = DefaultSwipeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, scenes: SceneTransitionLayoutScope.() -> Unit, @@ -111,6 +114,7 @@ fun SceneTransitionLayout( state, modifier, swipeSourceDetector, + swipeDetector, transitionInterceptionThreshold, scenes, ) @@ -467,6 +471,7 @@ internal fun SceneTransitionLayoutForTesting( state: SceneTransitionLayoutState, modifier: Modifier = Modifier, swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, + swipeDetector: SwipeDetector = DefaultSwipeDetector, transitionInterceptionThreshold: Float = 0f, onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null, scenes: SceneTransitionLayoutScope.() -> Unit, @@ -502,5 +507,5 @@ internal fun SceneTransitionLayoutForTesting( layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold } - layoutImpl.Content(modifier) + layoutImpl.Content(modifier, swipeDetector) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 7856498aa365..c614265e2ae1 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -185,14 +185,14 @@ internal class SceneTransitionLayoutImpl( } @Composable - internal fun Content(modifier: Modifier) { + internal fun Content(modifier: Modifier, swipeDetector: SwipeDetector) { Box( modifier // Handle horizontal and vertical swipes on this layout. // Note: order here is important and will give a slight priority to the vertical // swipes. - .swipeToScene(horizontalDraggableHandler) - .swipeToScene(verticalDraggableHandler) + .swipeToScene(horizontalDraggableHandler, swipeDetector) + .swipeToScene(verticalDraggableHandler, swipeDetector) .then(LayoutElement(layoutImpl = this)) ) { LookaheadScope { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt new file mode 100644 index 000000000000..54ee78366875 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.animation.scene + +import androidx.compose.runtime.Stable +import androidx.compose.ui.input.pointer.PointerInputChange + +/** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */ +@Stable +interface SwipeDetector { + /** + * Invoked on changes to pointer input. Returns {@code true} if a swipe has been recognized, + * {@code false} otherwise. + */ + fun detectSwipe(change: PointerInputChange): Boolean +} + +val DefaultSwipeDetector = PassthroughSwipeDetector() + +/** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */ +class PassthroughSwipeDetector : SwipeDetector { + override fun detectSwipe(change: PointerInputChange): Boolean { + // Simply accept all changes as a swipe + return true + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index b618369c2369..171e2430c004 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -31,14 +31,18 @@ import androidx.compose.ui.unit.IntSize * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state. */ @Stable -internal fun Modifier.swipeToScene(draggableHandler: DraggableHandlerImpl): Modifier { - return this.then(SwipeToSceneElement(draggableHandler)) +internal fun Modifier.swipeToScene( + draggableHandler: DraggableHandlerImpl, + swipeDetector: SwipeDetector +): Modifier { + return this.then(SwipeToSceneElement(draggableHandler, swipeDetector)) } private data class SwipeToSceneElement( val draggableHandler: DraggableHandlerImpl, + val swipeDetector: SwipeDetector ) : ModifierNodeElement<SwipeToSceneNode>() { - override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler) + override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler, swipeDetector) override fun update(node: SwipeToSceneNode) { node.draggableHandler = draggableHandler @@ -47,6 +51,7 @@ private data class SwipeToSceneElement( private class SwipeToSceneNode( draggableHandler: DraggableHandlerImpl, + swipeDetector: SwipeDetector, ) : DelegatingNode(), PointerInputModifierNode { private val delegate = delegate( @@ -55,6 +60,7 @@ private class SwipeToSceneNode( enabled = ::enabled, startDragImmediately = ::startDragImmediately, onDragStarted = draggableHandler::onDragStarted, + swipeDetector = swipeDetector, ) ) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index aa6d1130fc2a..4bb643f8b89e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalViewConfiguration @@ -346,4 +347,69 @@ class MultiPointerDraggableTest { continueDraggingDown() assertThat(stopped).isTrue() } + + @Test + fun multiPointerSwipeDetectorInteraction() { + val size = 200f + val middle = Offset(size / 2f, size / 2f) + + var started = false + + var capturedChange: PointerInputChange? = null + var swipeConsume = false + + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + Box( + Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .multiPointerDraggable( + orientation = Orientation.Vertical, + enabled = { true }, + startDragImmediately = { false }, + swipeDetector = + object : SwipeDetector { + override fun detectSwipe(change: PointerInputChange): Boolean { + capturedChange = change + return swipeConsume + } + }, + onDragStarted = { _, _, _ -> + started = true + object : DragController { + override fun onDrag(delta: Float) {} + + override fun onStop(velocity: Float, canChangeScene: Boolean) {} + } + }, + ) + ) {} + } + + fun startDraggingDown() { + rule.onRoot().performTouchInput { + down(middle) + moveBy(Offset(0f, touchSlop)) + } + } + + fun continueDraggingDown() { + rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) } + } + + startDraggingDown() + assertThat(capturedChange).isNotNull() + capturedChange = null + assertThat(started).isFalse() + + swipeConsume = true + continueDraggingDown() + assertThat(capturedChange).isNotNull() + capturedChange = null + + continueDraggingDown() + assertThat(capturedChange).isNull() + + assertThat(started).isTrue() + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 1d8b7e5b6155..bf0843b8fa4e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -20,10 +20,12 @@ import android.content.Context import android.graphics.Rect import android.os.PowerManager import android.os.SystemClock +import android.util.ArraySet import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner @@ -35,6 +37,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.Flags.glanceableHubFullscreenSwipe import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.communal.dagger.Communal @@ -52,10 +55,12 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.collectFlow +import java.util.function.Consumer import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @@ -77,11 +82,38 @@ constructor( private val communalColors: CommunalColors, private val ambientTouchComponentFactory: AmbientTouchComponent.Factory, private val communalContent: CommunalContent, - @Communal private val dataSourceDelegator: SceneDataSourceDelegator + @Communal private val dataSourceDelegator: SceneDataSourceDelegator, + private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController, ) : LifecycleOwner { + + private class CommunalWrapper(context: Context) : FrameLayout(context) { + private val consumers: MutableSet<Consumer<Boolean>> = ArraySet() + + override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { + consumers.forEach { it.accept(disallowIntercept) } + super.requestDisallowInterceptTouchEvent(disallowIntercept) + } + + fun dispatchTouchEvent( + ev: MotionEvent?, + disallowInterceptConsumer: Consumer<Boolean>? + ): Boolean { + disallowInterceptConsumer?.apply { consumers.add(this) } + + try { + return super.dispatchTouchEvent(ev) + } finally { + consumers.clear() + } + } + } + /** The container view for the hub. This will not be initialized until [initView] is called. */ private var communalContainerView: View? = null + /** Wrapper around the communal container to intercept touch events */ + private var communalContainerWrapper: CommunalWrapper? = null + /** * This lifecycle is used to control when the [touchMonitor] listens to touches. The lifecycle * should only be [Lifecycle.State.RESUMED] when the hub is showing and not covered by anything, @@ -271,9 +303,13 @@ constructor( ) collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it }) - communalContainerView = containerView - - return containerView + if (glanceableHubFullscreenSwipe()) { + communalContainerWrapper = CommunalWrapper(containerView.context) + communalContainerWrapper?.addView(communalContainerView) + return communalContainerWrapper!! + } else { + return containerView + } } /** @@ -306,6 +342,11 @@ constructor( lifecycleRegistry.currentState = Lifecycle.State.CREATED communalContainerView = null } + + communalContainerWrapper?.let { + (it.parent as ViewGroup).removeView(it) + communalContainerWrapper = null + } } /** @@ -319,6 +360,18 @@ constructor( */ fun onTouchEvent(ev: MotionEvent): Boolean { SceneContainerFlag.assertInLegacyMode() + + // In the case that we are handling full swipes on the lockscreen, are on the lockscreen, + // and the touch is within the horizontal notification band on the screen, do not process + // the touch. + if ( + glanceableHubFullscreenSwipe() && + !hubShowing && + !notificationStackScrollLayoutController.isBelowLastNotification(ev.x, ev.y) + ) { + return false + } + return communalContainerView?.let { handleTouchEventOnCommunalView(it, ev) } ?: false } @@ -330,12 +383,16 @@ constructor( val hubOccluded = anyBouncerShowing || shadeShowing if (isDown && !hubOccluded) { - val x = ev.rawX - val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth - if (inOpeningSwipeRegion || hubShowing) { - // Steal touch events when the hub is open, or if the touch started in the opening - // gesture region. + if (glanceableHubFullscreenSwipe()) { isTrackingHubTouch = true + } else { + val x = ev.rawX + val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth + if (inOpeningSwipeRegion || hubShowing) { + // Steal touch events when the hub is open, or if the touch started in the + // opening gesture region. + isTrackingHubTouch = true + } } } @@ -343,10 +400,7 @@ constructor( if (isUp || isCancel) { isTrackingHubTouch = false } - dispatchTouchEvent(view, ev) - // Return true regardless of dispatch result as some touches at the start of a gesture - // may return false from dispatchTouchEvent. - return true + return dispatchTouchEvent(view, ev) } return false @@ -356,13 +410,30 @@ constructor( * Dispatches the touch event to the communal container and sends a user activity event to reset * the screen timeout. */ - private fun dispatchTouchEvent(view: View, ev: MotionEvent) { - view.dispatchTouchEvent(ev) - powerManager.userActivity( - SystemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_TOUCH, - 0 - ) + private fun dispatchTouchEvent(view: View, ev: MotionEvent): Boolean { + try { + var handled = false + if (glanceableHubFullscreenSwipe()) { + communalContainerWrapper?.dispatchTouchEvent(ev) { + if (it) { + handled = true + } + } + return handled || hubShowing + } else { + view.dispatchTouchEvent(ev) + // Return true regardless of dispatch result as some touches at the start of a + // gesture + // may return false from dispatchTouchEvent. + return true + } + } finally { + powerManager.userActivity( + SystemClock.uptimeMillis(), + PowerManager.USER_ACTIVITY_EVENT_TOUCH, + 0 + ) + } } override val lifecycle: Lifecycle diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index bde1445acfa8..b8267a0e83d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.shade import android.graphics.Rect import android.os.PowerManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.ViewUtils @@ -30,6 +32,7 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE import com.android.systemui.SysuiTestCase import com.android.systemui.ambient.touch.TouchHandler import com.android.systemui.ambient.touch.TouchMonitor @@ -51,6 +54,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat @@ -64,9 +68,11 @@ import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @ExperimentalCoroutinesApi @@ -124,6 +130,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ambientTouchComponentFactory, communalContent, kosmos.sceneDataSourceDelegator, + kosmos.notificationStackScrollLayoutController ) } testableLooper = TestableLooper.get(this) @@ -166,6 +173,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ambientTouchComponentFactory, communalContent, kosmos.sceneDataSourceDelegator, + kosmos.notificationStackScrollLayoutController ) // First call succeeds. @@ -176,6 +184,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalClosed_doesNotIntercept() = with(kosmos) { @@ -187,6 +196,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_openGesture_interceptsTouches() = with(kosmos) { @@ -204,6 +214,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalTransitioning_interceptsTouches() = with(kosmos) { @@ -230,6 +241,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalOpen_interceptsTouches() = with(kosmos) { @@ -244,6 +256,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() = with(kosmos) { @@ -262,6 +275,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() = with(kosmos) { @@ -278,6 +292,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) @Test fun onTouchEvent_containerViewDisposed_doesNotIntercept() = with(kosmos) { @@ -310,6 +325,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ambientTouchComponentFactory, communalContent, kosmos.sceneDataSourceDelegator, + kosmos.notificationStackScrollLayoutController, ) assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) @@ -329,6 +345,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ambientTouchComponentFactory, communalContent, kosmos.sceneDataSourceDelegator, + kosmos.notificationStackScrollLayoutController, ) // Only initView without attaching a view as we don't want the flows to start collecting @@ -499,13 +516,30 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE) + fun fullScreenSwipeGesture_doNotProcessTouchesInNotificationStack() = + with(kosmos) { + testScope.runTest { + // Communal is closed. + goToScene(CommunalScenes.Blank) + `when`( + notificationStackScrollLayoutController.isBelowLastNotification( + anyFloat(), + anyFloat() + ) + ) + .thenReturn(false) + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + } + } + private fun initAndAttachContainerView() { containerView = View(context) parentView = FrameLayout(context) - parentView.addView(containerView) - underTest.initView(containerView) + parentView.addView(underTest.initView(containerView)) // Attach the view so that flows start collecting. ViewUtils.attachView(parentView, CONTAINER_WIDTH, CONTAINER_HEIGHT) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerKosmos.kt new file mode 100644 index 000000000000..569429f180df --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.notificationStackScrollLayoutController by + Kosmos.Fixture { mock<NotificationStackScrollLayoutController>() } |