diff options
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, + ) + ) + } } } |