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/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 dad0004613f6a..b719126adcf8c 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 @@ -23,6 +23,7 @@ import dagger.Module; */ @Module(includes = { BouncerSwipeModule.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..9e0ae4119f24e --- /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 dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoSet; + +import javax.inject.Named; + +/** + * 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/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(); 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/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 22dd0e5128e92..d31fe23bf2be3 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1981,6 +1981,14 @@ public class DisplayPolicy { return; } + if (controlTarget != null) { + final WindowState win = controlTarget.getWindow(); + + if (win != null && win.isActivityTypeDream()) { + return; + } + } + final @InsetsType int restorePositionTypes = (Type.statusBars() | Type.navigationBars()) & controlTarget.getRequestedVisibleTypes(); 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 20d410cce3706..2914de115efff 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -81,6 +81,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; @@ -384,4 +395,25 @@ public class DisplayPolicyTests extends WindowTestsBase { displayPolicy.requestTransientBars(mNavBarWindow, true); assertTrue(mDisplayContent.getInsetsPolicy().isTransient(navigationBars())); } + + @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; + win.setRequestedVisibleTypes(0, navigationBars()); + + 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(navigationBars())); + } } 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 0d7cdc851ddf7..7e3ec55f262a1 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; @@ -467,6 +468,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) {