Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1c173cfd authored by Bryce Lee's avatar Bryce Lee Committed by Android (Google) Code Review
Browse files

Merge changes Ib6598778,If52d5541 into main

* changes:
  Allow fullscreen swipes for shade and bouncer.
  Convert BouncerSwipeTouchHandler and ShadeTouchHandler to Kotlin.
parents 80462f89 6b73ee3e
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -1205,6 +1205,13 @@ flag {
  bug: "352600066"
}

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"
+45 −0
Original line number Diff line number Diff line
@@ -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 ->
+246 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+10 −1
Original line number Diff line number Diff line
@@ -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.
     */
+67 −3
Original line number Diff line number Diff line
@@ -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.
Loading