summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Bryce Lee <brycelee@google.com> 2024-05-30 17:52:48 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-05-30 17:52:48 +0000
commitdf54137ce00a9f1839a10ba33f4747fd73be3d6c (patch)
treef56833aa12fda6641bf31036096485f40f9d7ab1
parentc5e362601572e175a13d52928bf737d5382e87c8 (diff)
parente65540dfd6d8eb7bb10c07eff04a65cc5e8d1427 (diff)
Merge "Allow Full Screen Glanceable Hub Swipe Entry on Lockscreen." into main
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt25
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt56
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt16
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt7
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt6
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt40
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt12
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt66
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt111
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt38
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerKosmos.kt23
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>() }