From 982ecde2d52c90260a178aa7f99b362d0e5e1819 Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Wed, 15 Mar 2023 18:18:29 +0000 Subject: [PATCH 1/4] Suppress transient bars over dreams. This changelist prevents transient bars over dream windows. This allows for suppressing these UI elements when other swipe affordances are provided by the SystemUI. Bug: 267565290 Test: atest DisplayPolicyTests#testTransientBarsSuppressedOnDreams Test: manual: swipe up and down from edges of device on a dream and observe now transient bars Change-Id: I5be342ed21174ff6e2c2cf87cea6202773e5f51a Merged-In: I5be342ed21174ff6e2c2cf87cea6202773e5f51a --- .../com/android/server/wm/DisplayPolicy.java | 8 +++++ .../android/server/wm/DisplayPolicyTests.java | 36 +++++++++++++++++++ .../android/server/wm/WindowTestsBase.java | 7 ++++ 3 files changed, 51 insertions(+) diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index aedd2c594b9dd..988e98f28d107 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -2148,6 +2148,14 @@ public class DisplayPolicy { return; } + if (controlTarget != null) { + final WindowState win = controlTarget.getWindow(); + + if (win != null && win.isActivityTypeDream()) { + return; + } + } + final @InsetsType int restorePositionTypes = (controlTarget.getRequestedVisibility(ITYPE_NAVIGATION_BAR) ? Type.navigationBars() : 0) diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index 73eb1273efa38..ac8dd465adbe2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -25,6 +25,7 @@ import static android.view.Surface.ROTATION_0; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; +import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND; import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; @@ -59,6 +60,7 @@ import android.platform.test.annotations.Presubmit; import android.view.DisplayInfo; import android.view.InsetsSource; import android.view.InsetsState; +import android.view.InsetsVisibilities; import android.view.PrivacyIndicatorBounds; import android.view.WindowInsets.Side; import android.view.WindowManager; @@ -85,6 +87,17 @@ public class DisplayPolicyTests extends WindowTestsBase { return win; } + private WindowState createDreamWindow() { + final WindowState win = createDreamWindow(null, TYPE_BASE_APPLICATION, "dream"); + final WindowManager.LayoutParams attrs = win.mAttrs; + attrs.width = MATCH_PARENT; + attrs.height = MATCH_PARENT; + attrs.flags = + FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR | FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; + attrs.format = PixelFormat.OPAQUE; + return win; + } + private WindowState createDimmingDialogWindow(boolean canBeImTarget) { final WindowState win = spy(createWindow(null, TYPE_APPLICATION, "dimmingDialog")); final WindowManager.LayoutParams attrs = win.mAttrs; @@ -381,4 +394,27 @@ public class DisplayPolicyTests extends WindowTestsBase { displayPolicy.requestTransientBars(windowState, true); verify(controlTarget).showInsets(anyInt(), anyBoolean()); } + + @UseTestDisplay(addWindows = { W_NAVIGATION_BAR }) + @Test + public void testTransientBarsSuppressedOnDreams() { + final WindowState win = createDreamWindow(); + + ((TestWindowManagerPolicy) mWm.mPolicy).mIsUserSetupComplete = true; + win.mAttrs.insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; + final InsetsVisibilities insetsVisibilities = new InsetsVisibilities(); + insetsVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, false); + insetsVisibilities.setVisibility(ITYPE_EXTRA_NAVIGATION_BAR, false); + win.setRequestedVisibilities(insetsVisibilities); + + final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy(); + displayPolicy.addWindowLw(mNavBarWindow, mNavBarWindow.mAttrs); + final InsetsSourceProvider navBarProvider = mNavBarWindow.getControllableInsetProvider(); + navBarProvider.updateControlForTarget(win, false); + navBarProvider.getSource().setVisible(false); + + displayPolicy.setCanSystemBarsBeShownByUser(true); + displayPolicy.requestTransientBars(mNavBarWindow, true); + assertFalse(mDisplayContent.getInsetsPolicy().isTransient(ITYPE_NAVIGATION_BAR)); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index f8b809463b2eb..57e7afd9f85a9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -17,6 +17,7 @@ package com.android.server.wm; import static android.app.AppOpsManager.OP_NONE; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; @@ -443,6 +444,12 @@ class WindowTestsBase extends SystemServiceTestsBase { return createWindow(null, type, activity, name); } + WindowState createDreamWindow(WindowState parent, int type, String name) { + final WindowToken token = createWindowToken( + mDisplayContent, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_DREAM, type); + return createWindow(parent, type, token, name); + } + // TODO: Move these calls to a builder? WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name, IWindow iwindow) { From 3d8957b153cd023de1dd7d31f781e0f44338771c Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Wed, 15 Mar 2023 11:32:16 -0700 Subject: [PATCH 2/4] Correct display dimensions considered. This changelist makes sure the full screen size is considered when accounting for the dream overlay. Previously, insets would be subtracted from the overall dimensions. Test: atest DreamOverlayTouchMonitorTest#testReportedDisplayBounds Fixes: 267565290 Change-Id: I48c072491efc62c49385ae5f4684d99585a05895 Merged-In: I48c072491efc62c49385ae5f4684d99585a05895 --- .../touch/BouncerSwipeTouchHandler.java | 26 ++++----- .../touch/DreamOverlayTouchMonitor.java | 29 ++++++++-- .../dreams/touch/DreamTouchHandler.java | 8 ++- .../systemui/touch/TouchInsetManager.java | 5 +- .../systemui/util/display/DisplayHelper.java | 53 +++++++++++++++++++ .../touch/BouncerSwipeTouchHandlerTest.java | 13 ++--- .../touch/DreamOverlayTouchMonitorTest.java | 44 +++++++++++++-- .../systemui/touch/TouchInsetManagerTest.java | 4 +- 8 files changed, 145 insertions(+), 37 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java index a7b3bbcbc37b6..2ea7bce664526 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java @@ -25,7 +25,6 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.graphics.Rect; import android.graphics.Region; -import android.util.DisplayMetrics; import android.util.Log; import android.view.GestureDetector; import android.view.InputEvent; @@ -89,8 +88,6 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { private final FlingAnimationUtils mFlingAnimationUtils; private final FlingAnimationUtils mFlingAnimationUtilsClosing; - private final DisplayMetrics mDisplayMetrics; - private Boolean mCapture; private Boolean mExpanded; @@ -161,7 +158,7 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { // (0). final float dragDownAmount = e2.getY() - e1.getY(); final float screenTravelPercentage = Math.abs(e1.getY() - e2.getY()) - / mCentralSurfaces.get().getDisplayHeight(); + / mTouchSession.getBounds().height(); setPanelExpansion(mBouncerInitiallyShowing ? screenTravelPercentage : 1 - screenTravelPercentage, dragDownAmount); return true; @@ -202,7 +199,6 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { @Inject public BouncerSwipeTouchHandler( - DisplayMetrics displayMetrics, ScrimManager scrimManager, Optional centralSurfaces, NotificationShadeWindowController notificationShadeWindowController, @@ -214,7 +210,6 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { FlingAnimationUtils flingAnimationUtilsClosing, @Named(SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage, UiEventLogger uiEventLogger) { - mDisplayMetrics = displayMetrics; mCentralSurfaces = centralSurfaces; mScrimManager = scrimManager; mNotificationShadeWindowController = notificationShadeWindowController; @@ -227,19 +222,20 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { } @Override - public void getTouchInitiationRegion(Region region) { + public void getTouchInitiationRegion(Rect bounds, Region region) { + final int width = bounds.width(); + final int height = bounds.height(); + if (mCentralSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) { - region.op(new Rect(0, 0, mDisplayMetrics.widthPixels, + region.op(new Rect(0, 0, width, Math.round( - mDisplayMetrics.heightPixels * mBouncerZoneScreenPercentage)), + height * mBouncerZoneScreenPercentage)), Region.Op.UNION); } else { region.op(new Rect(0, - Math.round( - mDisplayMetrics.heightPixels - * (1 - mBouncerZoneScreenPercentage)), - mDisplayMetrics.widthPixels, - mDisplayMetrics.heightPixels), + Math.round(height * (1 - mBouncerZoneScreenPercentage)), + width, + height), Region.Op.UNION); } } @@ -356,7 +352,7 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler { } // The animation utils deal in pixel units, rather than expansion height. - final float viewHeight = mCentralSurfaces.get().getDisplayHeight(); + final float viewHeight = mTouchSession.getBounds().height(); final float currentHeight = viewHeight * mCurrentExpansion; final float targetHeight = viewHeight * expansion; final float expansionHeight = targetHeight - currentHeight; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java index b8b459e1c68cb..43e4c62b60d68 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java @@ -16,6 +16,9 @@ package com.android.systemui.dreams.touch; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.graphics.Rect; import android.graphics.Region; import android.view.GestureDetector; import android.view.InputEvent; @@ -31,6 +34,7 @@ import androidx.lifecycle.LifecycleOwner; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.touch.dagger.InputSessionComponent; import com.android.systemui.shared.system.InputChannelCompat; +import com.android.systemui.util.display.DisplayHelper; import com.google.common.util.concurrent.ListenableFuture; @@ -69,7 +73,8 @@ public class DreamOverlayTouchMonitor { } final TouchSessionImpl touchSession = - new TouchSessionImpl(this, touchSessionImpl); + new TouchSessionImpl(this, touchSessionImpl.getBounds(), + touchSessionImpl); mActiveTouchSessions.add(touchSession); completer.set(touchSession); }); @@ -120,10 +125,13 @@ public class DreamOverlayTouchMonitor { private final TouchSessionImpl mPredecessor; private final DreamOverlayTouchMonitor mTouchMonitor; + private final Rect mBounds; - TouchSessionImpl(DreamOverlayTouchMonitor touchMonitor, TouchSessionImpl predecessor) { + TouchSessionImpl(DreamOverlayTouchMonitor touchMonitor, Rect bounds, + TouchSessionImpl predecessor) { mPredecessor = predecessor; mTouchMonitor = touchMonitor; + mBounds = bounds; } @Override @@ -185,6 +193,11 @@ public class DreamOverlayTouchMonitor { private void onRemoved() { mCallbacks.forEach(callback -> callback.onRemoved()); } + + @Override + public Rect getBounds() { + return mBounds; + } } /** @@ -242,6 +255,7 @@ public class DreamOverlayTouchMonitor { private final HashSet mActiveTouchSessions = new HashSet<>(); private final Collection mHandlers; + private final DisplayHelper mDisplayHelper; private InputChannelCompat.InputEventListener mInputEventListener = new InputChannelCompat.InputEventListener() { @@ -253,8 +267,11 @@ public class DreamOverlayTouchMonitor { new HashMap<>(); for (DreamTouchHandler handler : mHandlers) { + final Rect maxBounds = mDisplayHelper.getMaxBounds(ev.getDisplayId(), + TYPE_APPLICATION_OVERLAY); + final Region initiationRegion = Region.obtain(); - handler.getTouchInitiationRegion(initiationRegion); + handler.getTouchInitiationRegion(maxBounds, initiationRegion); if (!initiationRegion.isEmpty()) { // Initiation regions require a motion event to determine pointer location @@ -272,8 +289,8 @@ public class DreamOverlayTouchMonitor { } } - final TouchSessionImpl sessionStack = - new TouchSessionImpl(DreamOverlayTouchMonitor.this, null); + final TouchSessionImpl sessionStack = new TouchSessionImpl( + DreamOverlayTouchMonitor.this, maxBounds, null); mActiveTouchSessions.add(sessionStack); sessionMap.put(handler, sessionStack); } @@ -389,11 +406,13 @@ public class DreamOverlayTouchMonitor { @Main Executor executor, Lifecycle lifecycle, InputSessionComponent.Factory inputSessionFactory, + DisplayHelper displayHelper, Set handlers) { mHandlers = handlers; mInputSessionFactory = inputSessionFactory; mExecutor = executor; mLifecycle = lifecycle; + mDisplayHelper = displayHelper; } /** diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamTouchHandler.java index 8288fcfb54811..b37010cfc07bf 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamTouchHandler.java @@ -16,6 +16,7 @@ package com.android.systemui.dreams.touch; +import android.graphics.Rect; import android.graphics.Region; import android.view.GestureDetector; @@ -77,6 +78,11 @@ public interface DreamTouchHandler { * Returns the number of currently active sessions. */ int getActiveSessionCount(); + + /** + * Returns the bounds of the display the touch region. + */ + Rect getBounds(); } /** @@ -84,7 +90,7 @@ public interface DreamTouchHandler { * indicating the entire screen should be considered. * @param region A {@link Region} that is passed in to the target entry touch region. */ - default void getTouchInitiationRegion(Region region) { + default void getTouchInitiationRegion(Rect bounds, Region region) { } /** diff --git a/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java b/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java index 3d07491283c5d..a4f1ef4fa7560 100644 --- a/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java +++ b/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java @@ -19,6 +19,7 @@ package com.android.systemui.touch; import android.graphics.Rect; import android.graphics.Region; import android.view.View; +import android.view.ViewGroup; import android.view.ViewRootImpl; import androidx.concurrent.futures.CallbackToFutureAdapter; @@ -90,7 +91,9 @@ public class TouchInsetManager { mTrackedViews.stream().forEach(view -> { final Rect boundaries = new Rect(); - view.getBoundsOnScreen(boundaries); + view.getDrawingRect(boundaries); + ((ViewGroup) view.getRootView()).offsetDescendantRectToMyCoords(view, boundaries); + cumulativeRegion.op(boundaries, Region.Op.UNION); }); diff --git a/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java b/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java new file mode 100644 index 0000000000000..8acd6535e751e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 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.util.display; + +import android.content.Context; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.view.Display; +import android.view.WindowManager; + +import javax.inject.Inject; + +/** + * Utility class for working with displays. + */ +public class DisplayHelper { + private final Context mContext; + private final DisplayManager mDisplayManager; + + /** + * Default constructor. + */ + @Inject + public DisplayHelper(Context context, DisplayManager displayManager) { + mContext = context; + mDisplayManager = displayManager; + } + + + /** + * Returns the maximum display bounds for the given window context type. + */ + public Rect getMaxBounds(int displayId, int windowContextType) { + final Display display = mDisplayManager.getDisplay(displayId); + WindowManager windowManager = mContext.createDisplayContext(display) + .createWindowContext(windowContextType, null) + .getSystemService(WindowManager.class); + return windowManager.getMaximumWindowMetrics().getBounds(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java index d6dbd730368e2..1a89076741ef3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java @@ -31,7 +31,6 @@ import android.animation.ValueAnimator; import android.graphics.Rect; import android.graphics.Region; import android.testing.AndroidTestingRunner; -import android.util.DisplayMetrics; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.MotionEvent; @@ -101,20 +100,16 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { @Mock UiEventLogger mUiEventLogger; - final DisplayMetrics mDisplayMetrics = new DisplayMetrics(); - private static final float TOUCH_REGION = .3f; private static final int SCREEN_WIDTH_PX = 1024; private static final int SCREEN_HEIGHT_PX = 100; + private static final Rect SCREEN_BOUNDS = new Rect(0, 0, 1024, 100); + @Before public void setup() { - mDisplayMetrics.widthPixels = SCREEN_WIDTH_PX; - mDisplayMetrics.heightPixels = SCREEN_HEIGHT_PX; - MockitoAnnotations.initMocks(this); mTouchHandler = new BouncerSwipeTouchHandler( - mDisplayMetrics, mScrimManager, Optional.of(mCentralSurfaces), mNotificationShadeWindowController, @@ -127,10 +122,10 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { when(mScrimManager.getCurrentController()).thenReturn(mScrimController); when(mCentralSurfaces.isBouncerShowing()).thenReturn(false); - when(mCentralSurfaces.getDisplayHeight()).thenReturn((float) SCREEN_HEIGHT_PX); 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); } /** @@ -139,7 +134,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { @Test public void testSessionStart() { final Region region = Region.obtain(); - mTouchHandler.getTouchInitiationRegion(region); + mTouchHandler.getTouchInitiationRegion(SCREEN_BOUNDS, region); final Rect bounds = region.getBounds(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java index 178b9cc727269..7f6e2ba1c0f98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; @@ -44,6 +45,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.touch.dagger.InputSessionComponent; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.display.DisplayHelper; import com.android.systemui.util.time.FakeSystemClock; import com.google.common.util.concurrent.ListenableFuture; @@ -79,7 +81,9 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase { private final DefaultLifecycleObserver mLifecycleObserver; private final InputChannelCompat.InputEventListener mEventListener; private final GestureDetector.OnGestureListener mGestureListener; + private final DisplayHelper mDisplayHelper; private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); + private final Rect mDisplayBounds = Mockito.mock(Rect.class); Environment(Set handlers) { mLifecycle = Mockito.mock(Lifecycle.class); @@ -93,7 +97,11 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase { .thenReturn(inputComponent); when(inputComponent.getInputSession()).thenReturn(mInputSession); - mMonitor = new DreamOverlayTouchMonitor(mExecutor, mLifecycle, mInputFactory, handlers); + mDisplayHelper = Mockito.mock(DisplayHelper.class); + when(mDisplayHelper.getMaxBounds(anyInt(), anyInt())) + .thenReturn(mDisplayBounds); + mMonitor = new DreamOverlayTouchMonitor(mExecutor, mLifecycle, mInputFactory, + mDisplayHelper, handlers); mMonitor.init(); final ArgumentCaptor lifecycleObserverCaptor = @@ -117,6 +125,10 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase { mGestureListener = gestureListenerCaptor.getValue(); } + public Rect getDisplayBounds() { + return mDisplayBounds; + } + void executeAll() { mExecutor.runAllReady(); } @@ -139,16 +151,38 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase { } } + @Test + public void testReportedDisplayBounds() { + final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class); + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new))); + + final MotionEvent initialEvent = Mockito.mock(MotionEvent.class); + when(initialEvent.getX()).thenReturn(0.0f); + when(initialEvent.getY()).thenReturn(0.0f); + environment.publishInputEvent(initialEvent); + + // Verify display bounds passed into TouchHandler#getTouchInitiationRegion + verify(touchHandler).getTouchInitiationRegion(eq(environment.getDisplayBounds()), any()); + final ArgumentCaptor touchSessionArgumentCaptor = + ArgumentCaptor.forClass(DreamTouchHandler.TouchSession.class); + verify(touchHandler).onSessionStart(touchSessionArgumentCaptor.capture()); + + // Verify that display bounds provided from TouchSession#getBounds + assertThat(touchSessionArgumentCaptor.getValue().getBounds()) + .isEqualTo(environment.getDisplayBounds()); + } + @Test public void testEntryTouchZone() { final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class); final Rect touchArea = new Rect(4, 4, 8 , 8); doAnswer(invocation -> { - final Region region = (Region) invocation.getArguments()[0]; + final Region region = (Region) invocation.getArguments()[1]; region.set(touchArea); return null; - }).when(touchHandler).getTouchInitiationRegion(any()); + }).when(touchHandler).getTouchInitiationRegion(any(), any()); final Environment environment = new Environment(Stream.of(touchHandler) .collect(Collectors.toCollection(HashSet::new))); @@ -174,10 +208,10 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase { final DreamTouchHandler unzonedTouchHandler = Mockito.mock(DreamTouchHandler.class); doAnswer(invocation -> { - final Region region = (Region) invocation.getArguments()[0]; + final Region region = (Region) invocation.getArguments()[1]; region.set(touchArea); return null; - }).when(touchHandler).getTouchInitiationRegion(any()); + }).when(touchHandler).getTouchInitiationRegion(any(), any()); final Environment environment = new Environment(Stream.of(touchHandler, unzonedTouchHandler) .collect(Collectors.toCollection(HashSet::new))); diff --git a/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java index 14b9bfb1393f9..6e5af425de293 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java @@ -27,6 +27,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.testing.AndroidTestingRunner; import android.view.View; +import android.view.ViewGroup; import android.view.ViewRootImpl; import androidx.test.filters.SmallTest; @@ -47,7 +48,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) public class TouchInsetManagerTest extends SysuiTestCase { @Mock - private View mRootView; + private ViewGroup mRootView; @Mock private ViewRootImpl mRootViewImpl; @@ -193,6 +194,7 @@ public class TouchInsetManagerTest extends SysuiTestCase { private View createView(Rect bounds) { final Rect rect = new Rect(bounds); final View view = Mockito.mock(View.class); + when(view.getRootView()).thenReturn(mRootView); doAnswer(invocation -> { ((Rect) invocation.getArgument(0)).set(rect); return null; From f296aa6cea59cccae0303b3047426f11763654aa Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Thu, 16 Mar 2023 20:55:04 -0700 Subject: [PATCH 3/4] Allow in-progress touch sessions to continue outside resume. Currently, touch sessions are terminated outside the resume lifecycle state. This behavior causes issues when a touch behavior is directly leading to another state, such as dragging down the notification shade. This changelist allows in-progress touch sessions to continue until the session is popped. Note that lifecycle states here are used abstractly and do not correspond to Activity usage. Also, touch sessions are still forcefully ended in the case of resetting the touch monitor due to the destroy lifecycle state or from starting monitoring again. Test: atest DreamOverlayTouchMonitorTest#testPauseWithNoActiveSessions atest DreamOverlayTouchMonitorTest#testDeferredPauseWithActiveSessions atest DreamOverlayTouchMonitorTest#testDestroyWithActiveSessions Bug: 267565290 Change-Id: Ibac6e94bcf6b8c4d751349edfbcb04b2fe53285a Merged-In: Ibac6e94bcf6b8c4d751349edfbcb04b2fe53285a --- .../touch/DreamOverlayTouchMonitor.java | 23 ++++++- .../touch/DreamOverlayTouchMonitorTest.java | 63 ++++++++++++++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java index 43e4c62b60d68..7f44463f11916 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java @@ -101,6 +101,10 @@ public class DreamOverlayTouchMonitor { completer.set(predecessor); } + + if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) { + stopMonitoring(false); + } }); return "DreamOverlayTouchMonitor::pop"; @@ -214,7 +218,12 @@ public class DreamOverlayTouchMonitor { @Override public void onPause(@NonNull LifecycleOwner owner) { - stopMonitoring(); + stopMonitoring(false); + } + + @Override + public void onDestroy(LifecycleOwner owner) { + stopMonitoring(true); } }; @@ -222,7 +231,7 @@ public class DreamOverlayTouchMonitor { * When invoked, instantiates a new {@link InputSession} to monitor touch events. */ private void startMonitoring() { - stopMonitoring(); + stopMonitoring(true); mCurrentInputSession = mInputSessionFactory.create( "dreamOverlay", mInputEventListener, @@ -234,11 +243,16 @@ public class DreamOverlayTouchMonitor { /** * Destroys any active {@link InputSession}. */ - private void stopMonitoring() { + private void stopMonitoring(boolean force) { if (mCurrentInputSession == null) { return; } + if (!mActiveTouchSessions.isEmpty() && !force) { + mStopMonitoringPending = true; + return; + } + // When we stop monitoring touches, we must ensure that all active touch sessions and // descendants informed of the removal so any cleanup for active tracking can proceed. mExecutor.execute(() -> mActiveTouchSessions.forEach(touchSession -> { @@ -250,6 +264,7 @@ public class DreamOverlayTouchMonitor { mCurrentInputSession.dispose(); mCurrentInputSession = null; + mStopMonitoringPending = false; } @@ -257,6 +272,8 @@ public class DreamOverlayTouchMonitor { private final Collection mHandlers; private final DisplayHelper mDisplayHelper; + private boolean mStopMonitoringPending; + private InputChannelCompat.InputEventListener mInputEventListener = new InputChannelCompat.InputEventListener() { @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java index 7f6e2ba1c0f98..08427dab978b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java @@ -399,7 +399,21 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase { } @Test - public void testPause() { + public void testPauseWithNoActiveSessions() { + final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new))); + + environment.updateLifecycle(observerOwnerPair -> { + observerOwnerPair.first.onPause(observerOwnerPair.second); + }); + + environment.verifyInputSessionDispose(); + } + + @Test + public void testDeferredPauseWithActiveSessions() { final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class); final Environment environment = new Environment(Stream.of(touchHandler) @@ -417,13 +431,58 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase { environment.publishInputEvent(event); verify(eventListener).onInputEvent(eq(event)); + final ArgumentCaptor touchSessionArgumentCaptor = + ArgumentCaptor.forClass(DreamTouchHandler.TouchSession.class); + + verify(touchHandler).onSessionStart(touchSessionArgumentCaptor.capture()); + environment.updateLifecycle(observerOwnerPair -> { observerOwnerPair.first.onPause(observerOwnerPair.second); }); + verify(environment.mInputSession, never()).dispose(); + + // End session + touchSessionArgumentCaptor.getValue().pop(); + environment.executeAll(); + + // Check to make sure the input session is now disposed. environment.verifyInputSessionDispose(); } + @Test + public void testDestroyWithActiveSessions() { + final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new))); + + final InputEvent initialEvent = Mockito.mock(InputEvent.class); + environment.publishInputEvent(initialEvent); + + // Ensure session started + final InputChannelCompat.InputEventListener eventListener = + registerInputEventListener(touchHandler); + + // First event will be missed since we register after the execution loop, + final InputEvent event = Mockito.mock(InputEvent.class); + environment.publishInputEvent(event); + verify(eventListener).onInputEvent(eq(event)); + + final ArgumentCaptor touchSessionArgumentCaptor = + ArgumentCaptor.forClass(DreamTouchHandler.TouchSession.class); + + verify(touchHandler).onSessionStart(touchSessionArgumentCaptor.capture()); + + environment.updateLifecycle(observerOwnerPair -> { + observerOwnerPair.first.onDestroy(observerOwnerPair.second); + }); + + // Check to make sure the input session is now disposed. + environment.verifyInputSessionDispose(); + } + + @Test public void testPilfering() { final DreamTouchHandler touchHandler1 = Mockito.mock(DreamTouchHandler.class); @@ -476,7 +535,7 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase { environment.executeAll(); environment.updateLifecycle(observerOwnerPair -> { - observerOwnerPair.first.onPause(observerOwnerPair.second); + observerOwnerPair.first.onDestroy(observerOwnerPair.second); }); environment.executeAll(); From 76d6afe9dcb504499be4ffc22e434f3fb7e62db9 Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Thu, 16 Mar 2023 21:00:21 -0700 Subject: [PATCH 4/4] Allow swiping down notification shade over dream. This changelist enables swiping down the notification shade over the dream. NotificationShadeTouchHandler tracks swipes that originate from the status bar area of the dream and redirects these motion events to the NotificationPanelViewController. Test: atest NotificationShadeTouchHandlerTest Bug: 267565290 Change-Id: I72d54cf33601dacc0a2a6adfd2b7639615061c09 Merged-In: I72d54cf33601dacc0a2a6adfd2b7639615061c09 --- .../dreams/touch/ShadeTouchHandler.java | 92 ++++++++++++++ .../dreams/touch/dagger/DreamTouchModule.java | 1 + .../dreams/touch/dagger/ShadeModule.java | 62 ++++++++++ .../dreams/touch/ShadeTouchHandlerTest.java | 116 ++++++++++++++++++ 4 files changed, 271 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java new file mode 100644 index 0000000000000..58b70b02e84f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023 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.dreams.touch; + +import static com.android.systemui.dreams.touch.dagger.ShadeModule.NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT; + +import android.graphics.Rect; +import android.graphics.Region; +import android.view.GestureDetector; +import android.view.MotionEvent; + +import com.android.systemui.shade.NotificationPanelViewController; +import com.android.systemui.statusbar.phone.CentralSurfaces; + +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Named; + +/** + * {@link ShadeTouchHandler} is responsible for handling swipe down gestures over dream + * to bring down the shade. + */ +public class ShadeTouchHandler implements DreamTouchHandler { + private final Optional mSurfaces; + private final int mInitiationHeight; + + @Inject + ShadeTouchHandler(Optional centralSurfaces, + @Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) { + mSurfaces = centralSurfaces; + mInitiationHeight = initiationHeight; + } + + @Override + public void onSessionStart(TouchSession session) { + if (mSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) { + session.pop(); + return; + } + + session.registerInputListener(ev -> { + final NotificationPanelViewController viewController = + mSurfaces.map(CentralSurfaces::getNotificationPanelViewController).orElse(null); + + if (viewController != null) { + viewController.handleExternalTouch((MotionEvent) ev); + } + + if (ev instanceof MotionEvent) { + if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { + session.pop(); + } + } + }); + + session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + float distanceY) { + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return true; + } + }); + } + + @Override + public void getTouchInitiationRegion(Rect bounds, Region region) { + final Rect outBounds = new Rect(bounds); + outBounds.inset(0, 0, 0, outBounds.height() - mInitiationHeight); + region.op(outBounds, Region.Op.UNION); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java index 7338ecba8cf39..3facc4b603b22 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java @@ -24,6 +24,7 @@ import dagger.Module; @Module(includes = { BouncerSwipeModule.class, HideComplicationModule.class, + ShadeModule.class, }, subcomponents = { InputSessionComponent.class, }) diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java new file mode 100644 index 0000000000000..4ecc4a7ca3f57 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 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.dreams.touch.dagger; + +import android.content.res.Resources; + +import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dreams.touch.DreamTouchHandler; +import com.android.systemui.dreams.touch.ShadeTouchHandler; + +import javax.inject.Named; + +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoSet; + +/** + * Dependencies for swipe down to notification over dream. + */ +@Module +public class ShadeModule { + /** + * The height, defined in pixels, of the gesture initiation region at the top of the screen for + * swiping down notifications. + */ + public static final String NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT = + "notification_shade_gesture_initiation_height"; + + /** + * Provides {@link ShadeTouchHandler} to handle notification swipe down over dream. + */ + @Provides + @IntoSet + public static DreamTouchHandler providesNotificationShadeTouchHandler( + ShadeTouchHandler touchHandler) { + return touchHandler; + } + + /** + * Provides the height of the gesture area for notification swipe down. + */ + @Provides + @Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) + public static int providesNotificationShadeGestureRegionHeight(@Main Resources resources) { + return resources.getDimensionPixelSize(R.dimen.dream_overlay_status_bar_height); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java new file mode 100644 index 0000000000000..5704ef3f37dbd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2023 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.dreams.touch; + + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; +import android.view.GestureDetector; +import android.view.MotionEvent; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.shade.NotificationPanelViewController; +import com.android.systemui.shared.system.InputChannelCompat; +import com.android.systemui.statusbar.phone.CentralSurfaces; + +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.Optional; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ShadeTouchHandlerTest extends SysuiTestCase { + @Mock + CentralSurfaces mCentralSurfaces; + + @Mock + NotificationPanelViewController mNotificationPanelViewController; + + @Mock + DreamTouchHandler.TouchSession mTouchSession; + + ShadeTouchHandler mTouchHandler; + + private static final int TOUCH_HEIGHT = 20; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), + TOUCH_HEIGHT); + when(mCentralSurfaces.getNotificationPanelViewController()) + .thenReturn(mNotificationPanelViewController); + } + + /** + * Verify that touches aren't handled when the bouncer is showing. + */ + @Test + public void testInactiveOnBouncer() { + when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); + mTouchHandler.onSessionStart(mTouchSession); + verify(mTouchSession).pop(); + } + + /** + * Make sure {@link ShadeTouchHandler} + */ + @Test + public void testTouchPilferingOnScroll() { + final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class); + final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class); + + final ArgumentCaptor gestureListenerArgumentCaptor = + ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); + + mTouchHandler.onSessionStart(mTouchSession); + verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture()); + + assertThat(gestureListenerArgumentCaptor.getValue() + .onScroll(motionEvent1, motionEvent2, 1, 1)) + .isTrue(); + } + + /** + * Ensure touches are propagated to the {@link NotificationPanelViewController}. + */ + @Test + public void testEventPropagation() { + final MotionEvent motionEvent = Mockito.mock(MotionEvent.class); + + final ArgumentCaptor + inputEventListenerArgumentCaptor = + ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); + + mTouchHandler.onSessionStart(mTouchSession); + verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture()); + inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent); + verify(mNotificationPanelViewController).handleExternalTouch(motionEvent); + } + +}