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..7f44463f11916 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); }); @@ -96,6 +101,10 @@ public class DreamOverlayTouchMonitor { completer.set(predecessor); } + + if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) { + stopMonitoring(false); + } }); return "DreamOverlayTouchMonitor::pop"; @@ -120,10 +129,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 +197,11 @@ public class DreamOverlayTouchMonitor { private void onRemoved() { mCallbacks.forEach(callback -> callback.onRemoved()); } + + @Override + public Rect getBounds() { + return mBounds; + } } /** @@ -201,7 +218,12 @@ public class DreamOverlayTouchMonitor { @Override public void onPause(@NonNull LifecycleOwner owner) { - stopMonitoring(); + stopMonitoring(false); + } + + @Override + public void onDestroy(LifecycleOwner owner) { + stopMonitoring(true); } }; @@ -209,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, @@ -221,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 -> { @@ -237,11 +264,15 @@ public class DreamOverlayTouchMonitor { mCurrentInputSession.dispose(); mCurrentInputSession = null; + mStopMonitoringPending = false; } private final HashSet mActiveTouchSessions = new HashSet<>(); private final Collection mHandlers; + private final DisplayHelper mDisplayHelper; + + private boolean mStopMonitoringPending; private InputChannelCompat.InputEventListener mInputEventListener = new InputChannelCompat.InputEventListener() { @@ -253,8 +284,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 +306,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 +423,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/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/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..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 @@ -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))); @@ -365,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) @@ -383,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); @@ -442,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(); 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); + } + +} 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; 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) {