summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt45
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java246
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt70
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchModule.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt48
11 files changed, 547 insertions, 24 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index eb9d0ab9d42c..462db340bdaa 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1206,6 +1206,13 @@ flag {
}
flag {
+ name: "hubmode_fullscreen_vertical_swipe"
+ namespace: "systemui"
+ description: "Enables fullscreen vertical swiping in hub mode to bring up and down the bouncer and shade"
+ bug: "340177049"
+}
+
+flag {
namespace: "systemui"
name: "remove_update_listener_in_qs_icon_view_impl"
description: "Remove update listeners in QsIconViewImpl class to avoid memory leak."
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 650c2d3dd909..c381881c1782 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -42,6 +42,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -112,6 +113,11 @@ import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.input.pointer.changedToUp
+import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
@@ -138,6 +144,7 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
+import androidx.compose.ui.util.fastAll
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.layout.WindowMetricsCalculator
@@ -204,13 +211,51 @@ fun CommunalHub(
ScrollOnUpdatedLiveContentEffect(communalContent, gridState)
}
+ val nestedScrollConnection = remember {
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ // Begin tracking nested scrolling
+ viewModel.onNestedScrolling()
+ return super.onPreScroll(available, source)
+ }
+ }
+ }
+
Box(
modifier =
modifier
.semantics { testTagsAsResourceId = true }
.testTag(COMMUNAL_HUB_TEST_TAG)
.fillMaxSize()
+ .nestedScroll(nestedScrollConnection)
.pointerInput(gridState, contentOffset, contentListState) {
+ awaitPointerEventScope {
+ while (true) {
+ var event = awaitFirstDown(requireUnconsumed = false)
+ // Reset touch on first event.
+ viewModel.onResetTouchState()
+
+ // Process down event in case it's consumed immediately
+ if (event.isConsumed) {
+ viewModel.onHubTouchConsumed()
+ }
+
+ do {
+ var event = awaitPointerEvent()
+ for (change in event.changes) {
+ if (change.isConsumed) {
+ // Signal touch consumption on any consumed event.
+ viewModel.onHubTouchConsumed()
+ }
+ }
+ } while (
+ !event.changes.fastAll {
+ it.changedToUp() || it.changedToUpIgnoreConsumed()
+ }
+ )
+ }
+ }
+
// If not in edit mode, don't allow selecting items.
if (!viewModel.isEditMode) return@pointerInput
observeTaps { offset ->
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
new file mode 100644
index 000000000000..4850085c4b4e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.ambient.touch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.ValueAnimator;
+import android.content.pm.UserInfo;
+import android.graphics.Rect;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.systemui.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.ambient.touch.scrim.ScrimController;
+import com.android.systemui.ambient.touch.scrim.ScrimManager;
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.FakeUserTracker;
+import com.android.systemui.shared.system.InputChannelCompat;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.CentralSurfaces;
+import com.android.wm.shell.animation.FlingAnimationUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.Optional;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+@DisableFlags(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN)
+public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase {
+ private KosmosJavaAdapter mKosmos;
+
+ @Mock
+ CentralSurfaces mCentralSurfaces;
+
+ @Mock
+ ScrimManager mScrimManager;
+
+ @Mock
+ ScrimController mScrimController;
+
+ @Mock
+ NotificationShadeWindowController mNotificationShadeWindowController;
+
+ @Mock
+ FlingAnimationUtils mFlingAnimationUtils;
+
+ @Mock
+ FlingAnimationUtils mFlingAnimationUtilsClosing;
+
+ @Mock
+ TouchHandler.TouchSession mTouchSession;
+
+ BouncerSwipeTouchHandler mTouchHandler;
+
+ @Mock
+ BouncerSwipeTouchHandler.ValueAnimatorCreator mValueAnimatorCreator;
+
+ @Mock
+ ValueAnimator mValueAnimator;
+
+ @Mock
+ BouncerSwipeTouchHandler.VelocityTrackerFactory mVelocityTrackerFactory;
+
+ @Mock
+ VelocityTracker mVelocityTracker;
+
+ @Mock
+ UiEventLogger mUiEventLogger;
+
+ @Mock
+ LockPatternUtils mLockPatternUtils;
+
+ @Mock
+ ActivityStarter mActivityStarter;
+
+ @Mock
+ CommunalViewModel mCommunalViewModel;
+
+ FakeUserTracker mUserTracker;
+
+ private static final float TOUCH_REGION = .3f;
+ private static final float MIN_BOUNCER_HEIGHT = .05f;
+
+ private static final Rect SCREEN_BOUNDS = new Rect(0, 0, 1024, 100);
+ private static final UserInfo CURRENT_USER_INFO = new UserInfo(
+ 10,
+ /* name= */ "user10",
+ /* flags= */ 0
+ );
+
+ @Before
+ public void setup() {
+ mKosmos = new KosmosJavaAdapter(this);
+ MockitoAnnotations.initMocks(this);
+ mUserTracker = new FakeUserTracker();
+ mTouchHandler = new BouncerSwipeTouchHandler(
+ mKosmos.getTestScope(),
+ mScrimManager,
+ Optional.of(mCentralSurfaces),
+ mNotificationShadeWindowController,
+ mValueAnimatorCreator,
+ mVelocityTrackerFactory,
+ mLockPatternUtils,
+ mUserTracker,
+ mCommunalViewModel,
+ mFlingAnimationUtils,
+ mFlingAnimationUtilsClosing,
+ TOUCH_REGION,
+ MIN_BOUNCER_HEIGHT,
+ mUiEventLogger,
+ mActivityStarter);
+
+ when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
+ when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
+ when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker);
+ when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn(Float.MAX_VALUE);
+ when(mTouchSession.getBounds()).thenReturn(SCREEN_BOUNDS);
+ when(mLockPatternUtils.isSecure(CURRENT_USER_INFO.id)).thenReturn(true);
+
+ mUserTracker.set(Collections.singletonList(CURRENT_USER_INFO), 0);
+ }
+
+ /**
+ * Ensures expansion does not happen for full vertical swipes when touch is not available.
+ */
+ @Test
+ public void testFullSwipe_notInitiatedWhenNotAvailable() {
+ mTouchHandler.onGlanceableTouchAvailable(false);
+ mTouchHandler.onSessionStart(mTouchSession);
+ ArgumentCaptor<OnGestureListener> gestureListenerCaptor =
+ ArgumentCaptor.forClass(OnGestureListener.class);
+ verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+ // A touch within range at the bottom of the screen should trigger listening
+ assertThat(gestureListenerCaptor.getValue()
+ .onScroll(Mockito.mock(MotionEvent.class),
+ Mockito.mock(MotionEvent.class),
+ 1,
+ 2)).isFalse();
+ }
+
+ /**
+ * Ensures expansion only happens for full vertical swipes when touch is available.
+ */
+ @Test
+ public void testFullSwipe_initiatedWhenAvailable() {
+ mTouchHandler.onGlanceableTouchAvailable(true);
+ mTouchHandler.onSessionStart(mTouchSession);
+ ArgumentCaptor<OnGestureListener> gestureListenerCaptor =
+ ArgumentCaptor.forClass(OnGestureListener.class);
+ verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+ // A touch within range at the bottom of the screen should trigger listening
+ assertThat(gestureListenerCaptor.getValue()
+ .onScroll(Mockito.mock(MotionEvent.class),
+ Mockito.mock(MotionEvent.class),
+ 1,
+ 2)).isTrue();
+ }
+
+ @Test
+ public void testFullSwipe_motionUpResetsTouchState() {
+ mTouchHandler.onGlanceableTouchAvailable(true);
+ mTouchHandler.onSessionStart(mTouchSession);
+ ArgumentCaptor<OnGestureListener> gestureListenerCaptor =
+ ArgumentCaptor.forClass(OnGestureListener.class);
+ ArgumentCaptor<InputChannelCompat.InputEventListener> inputListenerCaptor =
+ ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
+ verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+ verify(mTouchSession).registerInputListener(inputListenerCaptor.capture());
+
+ // A touch within range at the bottom of the screen should trigger listening
+ assertThat(gestureListenerCaptor.getValue()
+ .onScroll(Mockito.mock(MotionEvent.class),
+ Mockito.mock(MotionEvent.class),
+ 1,
+ 2)).isTrue();
+
+ MotionEvent upEvent = Mockito.mock(MotionEvent.class);
+ when(upEvent.getAction()).thenReturn(MotionEvent.ACTION_UP);
+ inputListenerCaptor.getValue().onInputEvent(upEvent);
+ verify(mCommunalViewModel).onResetTouchState();
+ }
+
+ @Test
+ public void testFullSwipe_motionCancelResetsTouchState() {
+ mTouchHandler.onGlanceableTouchAvailable(true);
+ mTouchHandler.onSessionStart(mTouchSession);
+ ArgumentCaptor<OnGestureListener> gestureListenerCaptor =
+ ArgumentCaptor.forClass(OnGestureListener.class);
+ ArgumentCaptor<InputChannelCompat.InputEventListener> inputListenerCaptor =
+ ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
+ verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+ verify(mTouchSession).registerInputListener(inputListenerCaptor.capture());
+
+ // A touch within range at the bottom of the screen should trigger listening
+ assertThat(gestureListenerCaptor.getValue()
+ .onScroll(Mockito.mock(MotionEvent.class),
+ Mockito.mock(MotionEvent.class),
+ 1,
+ 2)).isTrue();
+
+ MotionEvent upEvent = Mockito.mock(MotionEvent.class);
+ when(upEvent.getAction()).thenReturn(MotionEvent.ACTION_CANCEL);
+ inputListenerCaptor.getValue().onInputEvent(upEvent);
+ verify(mCommunalViewModel).onResetTouchState();
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 7ebc224a00db..0e98b840942b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -50,6 +50,8 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.ambient.touch.scrim.ScrimController;
import com.android.systemui.ambient.touch.scrim.ScrimManager;
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.FakeUserTracker;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
@@ -72,7 +74,9 @@ import java.util.Optional;
@SmallTest
@RunWith(AndroidJUnit4.class)
+@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
+ private KosmosJavaAdapter mKosmos;
@Mock
CentralSurfaces mCentralSurfaces;
@@ -120,6 +124,9 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
@Mock
Region mRegion;
+ @Mock
+ CommunalViewModel mCommunalViewModel;
+
@Captor
ArgumentCaptor<Rect> mRectCaptor;
@@ -139,9 +146,11 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
@Before
public void setup() {
+ mKosmos = new KosmosJavaAdapter(this);
MockitoAnnotations.initMocks(this);
mUserTracker = new FakeUserTracker();
mTouchHandler = new BouncerSwipeTouchHandler(
+ mKosmos.getTestScope(),
mScrimManager,
Optional.of(mCentralSurfaces),
mNotificationShadeWindowController,
@@ -149,6 +158,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
mVelocityTrackerFactory,
mLockPatternUtils,
mUserTracker,
+ mCommunalViewModel,
mFlingAnimationUtils,
mFlingAnimationUtilsClosing,
TOUCH_REGION,
@@ -201,7 +211,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
2)).isTrue();
}
-
/**
* Ensures expansion only happens when touch down happens in valid part of the screen.
*/
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
index 7fd9ce20ab92..204d4b09f3ae 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
@@ -26,8 +26,10 @@ import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.ambient.touch.TouchHandler.TouchSession
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.testScope
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shared.system.InputChannelCompat
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -50,11 +52,11 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class ShadeTouchHandlerTest : SysuiTestCase() {
private var kosmos = testKosmos()
-
private var mCentralSurfaces = mock<CentralSurfaces>()
private var mShadeViewController = mock<ShadeViewController>()
private var mDreamManager = mock<DreamManager>()
private var mTouchSession = mock<TouchSession>()
+ private var communalViewModel = mock<CommunalViewModel>()
private lateinit var mTouchHandler: ShadeTouchHandler
@@ -65,9 +67,11 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
fun setup() {
mTouchHandler =
ShadeTouchHandler(
+ kosmos.testScope,
Optional.of(mCentralSurfaces),
mShadeViewController,
mDreamManager,
+ communalViewModel,
kosmos.communalSettingsInteractor,
TOUCH_HEIGHT
)
@@ -75,6 +79,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
// Verifies that a swipe down in the gesture region is captured by the shade touch handler.
@Test
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
fun testSwipeDown_captured() {
val captured = swipe(Direction.DOWN)
Truth.assertThat(captured).isTrue()
@@ -82,6 +87,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
// Verifies that a swipe in the upward direction is not captured.
@Test
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
fun testSwipeUp_notCaptured() {
val captured = swipe(Direction.UP)
@@ -91,6 +97,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
// Verifies that a swipe down forwards captured touches to central surfaces for handling.
@Test
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
fun testSwipeDown_communalEnabled_sentToCentralSurfaces() {
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -103,7 +110,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
// Verifies that a swipe down forwards captured touches to the shade view for handling.
@Test
- @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
+ @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
fun testSwipeDown_communalDisabled_sentToShadeView() {
swipe(Direction.DOWN)
@@ -114,6 +121,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
// Verifies that a swipe down while dreaming forwards captured touches to the shade view for
// handling.
@Test
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
fun testSwipeDown_dreaming_sentToShadeView() {
whenever(mDreamManager.isDreaming).thenReturn(true)
swipe(Direction.DOWN)
@@ -124,6 +132,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
// Verifies that a swipe up is not forwarded to central surfaces.
@Test
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
fun testSwipeUp_communalEnabled_touchesNotSent() {
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -137,7 +146,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
// Verifies that a swipe up is not forwarded to the shade view.
@Test
- @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
+ @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
fun testSwipeUp_communalDisabled_touchesNotSent() {
swipe(Direction.UP)
@@ -147,6 +156,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
fun testCancelMotionEvent_popsTouchSession() {
swipe(Direction.DOWN)
val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)
@@ -154,6 +164,60 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
verify(mTouchSession).pop()
}
+ @Test
+ @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ fun testFullVerticalSwipe_initiatedWhenAvailable() {
+ // Indicate touches are available
+ mTouchHandler.onGlanceableTouchAvailable(true)
+
+ // Verify swipe is handled
+ val captured = swipe(Direction.DOWN)
+ Truth.assertThat(captured).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ fun testFullVerticalSwipe_notInitiatedWhenNotAvailable() {
+ // Indicate touches aren't available
+ mTouchHandler.onGlanceableTouchAvailable(false)
+
+ // Verify swipe is not handled
+ val captured = swipe(Direction.DOWN)
+ Truth.assertThat(captured).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ fun testFullVerticalSwipe_resetsTouchStateOnUp() {
+ // Indicate touches are available
+ mTouchHandler.onGlanceableTouchAvailable(true)
+
+ // Verify swipe is handled
+ swipe(Direction.DOWN)
+
+ val upEvent: MotionEvent = mock()
+ whenever(upEvent.action).thenReturn(MotionEvent.ACTION_UP)
+ mInputListenerCaptor.lastValue.onInputEvent(upEvent)
+
+ verify(communalViewModel).onResetTouchState()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ fun testFullVerticalSwipe_resetsTouchStateOnCancel() {
+ // Indicate touches are available
+ mTouchHandler.onGlanceableTouchAvailable(true)
+
+ // Verify swipe is handled
+ swipe(Direction.DOWN)
+
+ val upEvent: MotionEvent = mock()
+ whenever(upEvent.action).thenReturn(MotionEvent.ACTION_CANCEL)
+ mInputListenerCaptor.lastValue.onInputEvent(upEvent)
+
+ verify(communalViewModel).onResetTouchState()
+ }
+
/**
* Simulates a swipe in the given direction and returns true if the touch was intercepted by the
* touch handler's gesture listener.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 86871edf98f5..7a41bc6da176 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -756,6 +756,17 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
verify(metricsLogger).logTapWidget("test_pkg/test_cls", rank = 10)
}
+ @Test
+ fun glanceableTouchAvailable_availableWhenNestedScrollingWithoutConsumption() =
+ testScope.runTest {
+ val touchAvailable by collectLastValue(underTest.glanceableTouchAvailable)
+ assertThat(touchAvailable).isTrue()
+ underTest.onHubTouchConsumed()
+ assertThat(touchAvailable).isFalse()
+ underTest.onNestedScrolling()
+ assertThat(touchAvailable).isTrue()
+ }
+
private suspend fun setIsMainUser(isMainUser: Boolean) {
val user = if (isMainUser) MAIN_USER_INFO else SECONDARY_USER_INFO
with(userRepository) {
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
index 00ca0082aacc..d5790a44a887 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
@@ -36,6 +36,7 @@ import com.android.systemui.ambient.touch.dagger.BouncerSwipeModule
import com.android.systemui.ambient.touch.scrim.ScrimController
import com.android.systemui.ambient.touch.scrim.ScrimManager
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.shade.ShadeExpansionChangeEvent
@@ -49,11 +50,14 @@ import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
/** Monitor for tracking touches on the DreamOverlay to bring up the bouncer. */
class BouncerSwipeTouchHandler
@Inject
constructor(
+ scope: CoroutineScope,
private val scrimManager: ScrimManager,
private val centralSurfaces: Optional<CentralSurfaces>,
private val notificationShadeWindowController: NotificationShadeWindowController,
@@ -61,6 +65,7 @@ constructor(
private val velocityTrackerFactory: VelocityTrackerFactory,
private val lockPatternUtils: LockPatternUtils,
private val userTracker: UserTracker,
+ private val communalViewModel: CommunalViewModel,
@param:Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING)
private val flingAnimationUtils: FlingAnimationUtils,
@param:Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING)
@@ -96,6 +101,10 @@ constructor(
currentScrimController = controller
}
+
+ /** Determines whether the touch handler should process touches in fullscreen swiping mode */
+ private var touchAvailable = false
+
private val onGestureListener: GestureDetector.OnGestureListener =
object : SimpleOnGestureListener() {
override fun onScroll(
@@ -107,7 +116,9 @@ constructor(
if (capture == null) {
capture =
if (Flags.dreamOverlayBouncerSwipeDirectionFiltering()) {
- (abs(distanceY.toDouble()) > abs(distanceX.toDouble()) && distanceY > 0)
+ (abs(distanceY.toDouble()) > abs(distanceX.toDouble()) &&
+ distanceY > 0) &&
+ if (Flags.hubmodeFullscreenVerticalSwipe()) touchAvailable else true
} else {
// If the user scrolling favors a vertical direction, begin capturing
// scrolls.
@@ -163,6 +174,21 @@ constructor(
}
}
+ init {
+ if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ scope.launch {
+ communalViewModel.glanceableTouchAvailable.collect {
+ onGlanceableTouchAvailable(it)
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ fun onGlanceableTouchAvailable(available: Boolean) {
+ touchAvailable = available
+ }
+
private fun setPanelExpansion(expansion: Float) {
currentExpansion = expansion
val event =
@@ -191,6 +217,12 @@ constructor(
val minAllowableBottom = Math.round(height * (1 - minBouncerZoneScreenPercentage))
val normalRegion =
Rect(0, Math.round(height * (1 - bouncerZoneScreenPercentage)), width, height)
+
+ if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ region.op(bounds, Region.Op.UNION)
+ exclusionRect?.apply { region.op(this, Region.Op.DIFFERENCE) }
+ }
+
if (exclusionRect != null) {
val lowestBottom =
min(max(0.0, exclusionRect.bottom.toDouble()), minAllowableBottom.toDouble())
@@ -233,6 +265,9 @@ constructor(
when (motionEvent.action) {
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
+ if (Flags.hubmodeFullscreenVerticalSwipe() && capture == true) {
+ communalViewModel.onResetTouchState()
+ }
touchSession?.apply { pop() }
// If we are not capturing any input, there is no need to consider animating to
// finish transition.
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
index 87f02aacdb82..06b41de12941 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
@@ -21,15 +21,20 @@ import android.graphics.Region
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.InputEvent
import android.view.MotionEvent
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Flags
import com.android.systemui.ambient.touch.TouchHandler.TouchSession
import com.android.systemui.ambient.touch.dagger.ShadeModule
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.phone.CentralSurfaces
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
import kotlin.math.abs
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
/**
* [ShadeTouchHandler] is responsible for handling swipe down gestures over dream to bring down the
@@ -38,9 +43,11 @@ import kotlin.math.abs
class ShadeTouchHandler
@Inject
constructor(
+ scope: CoroutineScope,
private val surfaces: Optional<CentralSurfaces>,
private val shadeViewController: ShadeViewController,
private val dreamManager: DreamManager,
+ private val communalViewModel: CommunalViewModel,
private val communalSettingsInteractor: CommunalSettingsInteractor,
@param:Named(ShadeModule.NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT)
private val initiationHeight: Int
@@ -50,6 +57,24 @@ constructor(
*/
private var capture: Boolean? = null
+ /** Determines whether the touch handler should process touches in fullscreen swiping mode */
+ private var touchAvailable = false
+
+ init {
+ if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ scope.launch {
+ communalViewModel.glanceableTouchAvailable.collect {
+ onGlanceableTouchAvailable(it)
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ fun onGlanceableTouchAvailable(available: Boolean) {
+ touchAvailable = available
+ }
+
override fun onSessionStart(session: TouchSession) {
if (surfaces.isEmpty) {
session.pop()
@@ -62,6 +87,9 @@ constructor(
sendTouchEvent(ev)
}
if (ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL) {
+ if (capture == true) {
+ communalViewModel.onResetTouchState()
+ }
session.pop()
}
}
@@ -77,7 +105,9 @@ constructor(
if (capture == null) {
// Only capture swipes that are going downwards.
capture =
- abs(distanceY.toDouble()) > abs(distanceX.toDouble()) && distanceY < 0
+ abs(distanceY.toDouble()) > abs(distanceX.toDouble()) &&
+ distanceY < 0 &&
+ if (Flags.hubmodeFullscreenVerticalSwipe()) touchAvailable else true
if (capture == true) {
// Send the initial touches over, as the input listener has already
// processed these touches.
@@ -112,7 +142,14 @@ constructor(
}
}
- override fun getTouchInitiationRegion(bounds: Rect, region: Region, exclusionRect: Rect) {
+ override fun getTouchInitiationRegion(bounds: Rect, region: Region, exclusionRect: Rect?) {
+ // If fullscreen swipe, use entire space minus exclusion region
+ if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ region.op(bounds, Region.Op.UNION)
+
+ exclusionRect?.apply { region.op(this, Region.Op.DIFFERENCE) }
+ }
+
val outBounds = Rect(bounds)
outBounds.inset(0, 0, 0, outBounds.height() - initiationHeight)
region.op(outBounds, Region.Op.UNION)
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchModule.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchModule.kt
index a4924d18e0c6..ae21e56eaf4d 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchModule.kt
@@ -17,12 +17,14 @@ package com.android.systemui.ambient.touch.dagger
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.coroutineScope
import com.android.systemui.ambient.dagger.AmbientModule
import com.android.systemui.ambient.touch.TouchHandler
import dagger.Module
import dagger.Provides
import dagger.multibindings.ElementsIntoSet
import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
@Module
interface AmbientTouchModule {
@@ -33,6 +35,12 @@ interface AmbientTouchModule {
return lifecycleOwner.lifecycle
}
+ @JvmStatic
+ @Provides
+ fun providesLifecycleScope(lifecycle: Lifecycle): CoroutineScope {
+ return lifecycle.coroutineScope
+ }
+
@Provides
@ElementsIntoSet
fun providesDreamTouchHandlers(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index b8601ec15132..4be93ccde1d5 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -29,9 +29,12 @@ import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOf
/** The base view model for the communal hub. */
@@ -57,6 +60,26 @@ abstract class BaseCommunalViewModel(
val selectedKey: StateFlow<String?>
get() = _selectedKey
+ private val _isTouchConsumed: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ /** Whether an element inside the lazy grid is actively consuming touches */
+ val isTouchConsumed: Flow<Boolean> = _isTouchConsumed.asStateFlow()
+
+ private val _isNestedScrolling: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ /** Whether the lazy grid is reporting scrolling within itself */
+ val isNestedScrolling: Flow<Boolean> = _isNestedScrolling.asStateFlow()
+
+ /**
+ * Whether touch is available to be consumed by a touch handler. Touch is available during
+ * nested scrolling as lazy grid reports this for all scroll directions that it detects. In the
+ * case that there is consumed scrolling on a nested element, such as an AndroidView, no nested
+ * scrolling will be reported. It is up to the flow consumer to determine whether the nested
+ * scroll can be applied. In the communal case, this would be identifying the scroll as
+ * vertical, which the lazy horizontal grid does not handle.
+ */
+ val glanceableTouchAvailable: Flow<Boolean> = anyOf(not(isTouchConsumed), isNestedScrolling)
+
/** Accessibility delegate to be set on CommunalAppWidgetHostView. */
open val widgetAccessibilityDelegate: View.AccessibilityDelegate? = null
@@ -200,4 +223,28 @@ abstract class BaseCommunalViewModel(
fun setSelectedKey(key: String?) {
_selectedKey.value = key
}
+
+ /** Invoked once touches inside the lazy grid are consumed */
+ fun onHubTouchConsumed() {
+ if (_isTouchConsumed.value) {
+ return
+ }
+
+ _isTouchConsumed.value = true
+ }
+
+ /** Invoked when nested scrolling begins on the lazy grid */
+ fun onNestedScrolling() {
+ if (_isNestedScrolling.value) {
+ return
+ }
+
+ _isNestedScrolling.value = true
+ }
+
+ /** Resets nested scroll and touch consumption state */
+ fun onResetTouchState() {
+ _isTouchConsumed.value = false
+ _isNestedScrolling.value = false
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index b468d0e75a7a..25d1cd17d092 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -39,6 +39,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
import com.android.systemui.Flags.glanceableHubBackGesture
import com.android.systemui.ambient.touch.TouchMonitor
import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
@@ -294,24 +295,37 @@ constructor(
}
containerView.systemGestureExclusionRects =
- listOf(
- // Only allow swipe up to bouncer and swipe down to shade in the very
- // top/bottom to avoid conflicting with widgets in the hub grid.
- Rect(
- insets.left,
- topEdgeSwipeRegionWidth,
- containerView.right - insets.right,
- containerView.bottom - bottomEdgeSwipeRegionWidth
- ),
- // Disable back gestures on the left side of the screen, to avoid
- // conflicting with scene transitions.
- Rect(
- 0,
- 0,
- insets.right,
- containerView.bottom,
+ if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ listOf(
+ // Disable back gestures on the left side of the screen, to avoid
+ // conflicting with scene transitions.
+ Rect(
+ 0,
+ 0,
+ insets.right,
+ containerView.bottom,
+ )
)
- )
+ } else {
+ listOf(
+ // Only allow swipe up to bouncer and swipe down to shade in the very
+ // top/bottom to avoid conflicting with widgets in the hub grid.
+ Rect(
+ insets.left,
+ topEdgeSwipeRegionWidth,
+ containerView.right - insets.right,
+ containerView.bottom - bottomEdgeSwipeRegionWidth
+ ),
+ // Disable back gestures on the left side of the screen, to avoid
+ // conflicting with scene transitions.
+ Rect(
+ 0,
+ 0,
+ insets.right,
+ containerView.bottom,
+ )
+ )
+ }
}
}