diff options
| author | 2024-07-16 08:16:05 -0700 | |
|---|---|---|
| committer | 2024-07-24 20:33:15 +0000 | |
| commit | 6b73ee3ea1a7c5e34bb0a12cfa25e15a7335b46f (patch) | |
| tree | 11386c21e153da267802156a2bf72bb61e5c86ff | |
| parent | 9131cabb63f53ce3e5cb738322b70ed91f65e20a (diff) | |
Allow fullscreen swipes for shade and bouncer.
This changelist adds support for fullscreen swipes on the dream
and glanceable hub for bringing down the notification shade and
dragging up the keyguard bouncer.
Test: atest BouncerFullscreenSwipeTouchHandlerTest#testFullSwipe_notInitiatedWhenNotAvailable
Test: atest BouncerFullscreenSwipeTouchHandlerTest#testFullSwipe_initiatedWhenAvailable
Test: atest BouncerFullscreenSwipeTouchHandlerTest#testFullSwipe_motionCancelResetsTouchState
Test: atest BouncerFullscreenSwipeTouchHandlerTest#testFullSwipe_motionUpResetsTouchState
Test: atest ShadeTouchHandlerTest#testFullVerticalSwipe_initiatedWhenAvailable
Test: atest ShadeTouchHandlerTest#testFullVerticalSwipe_notInitiatedWhenNotAvailable
Test: atest ShadeTouchHandlerTest#testFullVerticalSwipe_resetsTouchStateOnCancell
Test: atest ShadeTouchHandlerTest#testFullVerticalSwipe_resetsTouchStateOnUp
Test: atest CommunalViewModelTest#glanceableTouchAvailable_availableWhenNestedScrollingWithoutConsumption
Fixes: 340177049
Flag: com.android.systemui.hubmode_fullscreen_vertical_swipe
Change-Id: Ib65987789785b0c3b5edca8cb8cfb858f22fc0a7
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, + ) + ) + } } } |