diff options
| author | 2024-05-30 17:52:48 +0000 | |
|---|---|---|
| committer | 2024-05-30 17:52:48 +0000 | |
| commit | df54137ce00a9f1839a10ba33f4747fd73be3d6c (patch) | |
| tree | f56833aa12fda6641bf31036096485f40f9d7ab1 | |
| parent | c5e362601572e175a13d52928bf737d5382e87c8 (diff) | |
| parent | e65540dfd6d8eb7bb10c07eff04a65cc5e8d1427 (diff) | |
Merge "Allow Full Screen Glanceable Hub Swipe Entry on Lockscreen." into main
12 files changed, 371 insertions, 36 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index dcca8455fe41..63a52d624ca7 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -990,6 +990,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>() } |