From 1684cdbe5e6172d6bcaa32661fd3d902267d6d5c Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Thu, 11 Apr 2024 12:28:31 -0700 Subject: Rename Touch classes. This generalizes the names of touch related classes that were originally only for dreams. Flag: NA Test: atest TouchMonitorTest Bug: 333885071 Change-Id: Id34da5750d8cde8b855d78819fed8c625b780432 --- .../systemui/dreams/DreamOverlayServiceTest.java | 10 +- .../HideComplicationTouchHandlerTest.java | 4 +- .../dreams/touch/BouncerSwipeTouchHandlerTest.java | 8 +- .../dreams/touch/CommunalTouchHandlerTest.java | 2 +- .../dreams/touch/ShadeTouchHandlerTest.java | 2 +- .../ambient/touch/dagger/AmbientTouchComponent.kt | 10 +- .../ambient/touch/dagger/AmbientTouchModule.kt | 6 +- .../systemui/dreams/DreamOverlayService.java | 10 +- .../complication/HideComplicationTouchHandler.java | 12 +- .../dreams/touch/BouncerSwipeTouchHandler.java | 2 +- .../dreams/touch/CommunalTouchHandler.java | 4 +- .../dreams/touch/DreamOverlayTouchMonitor.java | 524 ----------------- .../systemui/dreams/touch/DreamTouchHandler.java | 119 ---- .../systemui/dreams/touch/ShadeTouchHandler.java | 2 +- .../systemui/dreams/touch/TouchHandler.java | 125 ++++ .../systemui/dreams/touch/TouchMonitor.java | 532 +++++++++++++++++ .../dreams/touch/dagger/BouncerSwipeModule.java | 4 +- .../systemui/dreams/touch/dagger/ShadeModule.java | 6 +- .../dreams/touch/DreamOverlayTouchMonitorTest.java | 643 --------------------- .../systemui/dreams/touch/TouchMonitorTest.java | 643 +++++++++++++++++++++ 20 files changed, 1341 insertions(+), 1327 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java delete mode 100644 packages/SystemUI/src/com/android/systemui/dreams/touch/DreamTouchHandler.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/touch/TouchHandler.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/touch/TouchMonitor.java delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/touch/TouchMonitorTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index 65d58f98a6bc..20f0932e3e4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -55,7 +55,7 @@ import com.android.systemui.complication.ComplicationLayoutEngine; import com.android.systemui.dreams.complication.HideComplicationTouchHandler; import com.android.systemui.dreams.complication.dagger.ComplicationComponent; import com.android.systemui.dreams.dagger.DreamOverlayComponent; -import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor; +import com.android.systemui.dreams.touch.TouchMonitor; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -143,7 +143,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock - DreamOverlayTouchMonitor mDreamOverlayTouchMonitor; + TouchMonitor mTouchMonitor; @Mock DreamOverlayStateController mStateController; @@ -187,8 +187,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase { .create(any(), any(), any())) .thenReturn(mDreamOverlayComponent); when(mAmbientTouchComponentFactory.create(any(), any())).thenReturn(mAmbientTouchComponent); - when(mAmbientTouchComponent.getDreamOverlayTouchMonitor()) - .thenReturn(mDreamOverlayTouchMonitor); + when(mAmbientTouchComponent.getTouchMonitor()) + .thenReturn(mTouchMonitor); when(mDreamOverlayContainerViewController.getContainerView()) .thenReturn(mDreamOverlayContainerView); @@ -515,7 +515,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { // Verify that new instances of overlay container view controller and overlay touch monitor // are created. verify(mDreamOverlayComponent).getDreamOverlayContainerViewController(); - verify(mAmbientTouchComponent).getDreamOverlayTouchMonitor(); + verify(mAmbientTouchComponent).getTouchMonitor(); } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/complication/HideComplicationTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/complication/HideComplicationTouchHandlerTest.java index a43415869580..95b83f29b6a5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/complication/HideComplicationTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/complication/HideComplicationTouchHandlerTest.java @@ -37,7 +37,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.complication.Complication; import com.android.systemui.dreams.DreamOverlayStateController; -import com.android.systemui.dreams.touch.DreamTouchHandler; +import com.android.systemui.dreams.touch.TouchHandler; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.touch.TouchInsetManager; @@ -74,7 +74,7 @@ public class HideComplicationTouchHandlerTest extends SysuiTestCase { MotionEvent mMotionEvent; @Mock - DreamTouchHandler.TouchSession mSession; + TouchHandler.TouchSession mSession; @Mock DreamOverlayStateController mStateController; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java index 9f52ae9a7406..fe91d1a84578 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandlerTest.java @@ -88,7 +88,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { FlingAnimationUtils mFlingAnimationUtilsClosing; @Mock - DreamTouchHandler.TouchSession mTouchSession; + TouchHandler.TouchSession mTouchSession; BouncerSwipeTouchHandler mTouchHandler; @@ -258,7 +258,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { } private static void onSessionStartHelper(BouncerSwipeTouchHandler touchHandler, - DreamTouchHandler.TouchSession touchSession, + TouchHandler.TouchSession touchSession, NotificationShadeWindowController notificationShadeWindowController) { touchHandler.onSessionStart(touchSession); verify(notificationShadeWindowController).setForcePluginOpen(eq(true), any()); @@ -677,8 +677,8 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { @Test public void testTouchSessionOnRemovedCalledTwice() { mTouchHandler.onSessionStart(mTouchSession); - ArgumentCaptor onRemovedCallbackCaptor = - ArgumentCaptor.forClass(DreamTouchHandler.TouchSession.Callback.class); + ArgumentCaptor onRemovedCallbackCaptor = + ArgumentCaptor.forClass(TouchHandler.TouchSession.Callback.class); verify(mTouchSession).registerCallback(onRemovedCallbackCaptor.capture()); onRemovedCallbackCaptor.getValue().onRemoved(); onRemovedCallbackCaptor.getValue().onRemoved(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java index 8dcf9032759b..464684ccbcdc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -54,7 +54,7 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { @Mock CentralSurfaces mCentralSurfaces; @Mock - DreamTouchHandler.TouchSession mTouchSession; + TouchHandler.TouchSession mTouchSession; CommunalTouchHandler mTouchHandler; @Mock Lifecycle mLifecycle; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java index 6aa821f15ab1..f4bba2f7906a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java @@ -52,7 +52,7 @@ public class ShadeTouchHandlerTest extends SysuiTestCase { ShadeViewController mShadeViewController; @Mock - DreamTouchHandler.TouchSession mTouchSession; + TouchHandler.TouchSession mTouchSession; ShadeTouchHandler mTouchHandler; diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchComponent.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchComponent.kt index fd1ef4447bf1..a75283445810 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchComponent.kt @@ -17,8 +17,8 @@ package com.android.systemui.ambient.touch.dagger import androidx.lifecycle.LifecycleOwner import com.android.systemui.ambient.dagger.AmbientModule.Companion.TOUCH_HANDLERS -import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor -import com.android.systemui.dreams.touch.DreamTouchHandler +import com.android.systemui.dreams.touch.TouchHandler +import com.android.systemui.dreams.touch.TouchMonitor import com.android.systemui.dreams.touch.dagger.BouncerSwipeModule import com.android.systemui.dreams.touch.dagger.ShadeModule import dagger.BindsInstance @@ -38,10 +38,10 @@ interface AmbientTouchComponent { @BindsInstance lifecycleOwner: LifecycleOwner, @BindsInstance @Named(TOUCH_HANDLERS) - dreamTouchHandlers: Set<@JvmSuppressWildcards DreamTouchHandler> + touchHandlers: Set<@JvmSuppressWildcards TouchHandler> ): AmbientTouchComponent } - /** Builds a [DreamOverlayTouchMonitor] */ - fun getDreamOverlayTouchMonitor(): DreamOverlayTouchMonitor + /** Builds a [TouchMonitor] */ + fun getTouchMonitor(): TouchMonitor } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchModule.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchModule.kt index 44e126008931..067ed311fc88 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchModule.kt +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/AmbientTouchModule.kt @@ -18,7 +18,7 @@ package com.android.systemui.ambient.touch.dagger import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import com.android.systemui.ambient.dagger.AmbientModule -import com.android.systemui.dreams.touch.DreamTouchHandler +import com.android.systemui.dreams.touch.TouchHandler import dagger.Module import dagger.Provides import dagger.multibindings.ElementsIntoSet @@ -37,8 +37,8 @@ interface AmbientTouchModule { @ElementsIntoSet fun providesDreamTouchHandlers( @Named(AmbientModule.TOUCH_HANDLERS) - touchHandlers: Set<@JvmSuppressWildcards DreamTouchHandler> - ): Set<@JvmSuppressWildcards DreamTouchHandler> { + touchHandlers: Set<@JvmSuppressWildcards TouchHandler> + ): Set<@JvmSuppressWildcards TouchHandler> { return touchHandlers } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index cf9606b2f545..b9b40337b6ca 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -48,7 +48,7 @@ import com.android.systemui.complication.Complication; import com.android.systemui.complication.dagger.ComplicationComponent; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.dagger.DreamOverlayComponent; -import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor; +import com.android.systemui.dreams.touch.TouchMonitor; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -105,7 +105,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private final DreamOverlayLifecycleOwner mLifecycleOwner; private final LifecycleRegistry mLifecycleRegistry; - private DreamOverlayTouchMonitor mDreamOverlayTouchMonitor; + private TouchMonitor mTouchMonitor; private final KeyguardUpdateMonitorCallback mKeyguardCallback = new KeyguardUpdateMonitorCallback() { @@ -244,8 +244,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mDreamOverlayContainerViewController = mDreamOverlayComponent.getDreamOverlayContainerViewController(); - mDreamOverlayTouchMonitor = mAmbientTouchComponent.getDreamOverlayTouchMonitor(); - mDreamOverlayTouchMonitor.init(); + mTouchMonitor = mAmbientTouchComponent.getTouchMonitor(); + mTouchMonitor.init(); mStateController.setShouldShowComplications(shouldShowComplications()); @@ -375,7 +375,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mStateController.setEntryAnimationsFinished(false); mDreamOverlayContainerViewController = null; - mDreamOverlayTouchMonitor = null; + mTouchMonitor = null; mWindow = null; mStarted = false; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java index d525ce36a061..e38b3d037e15 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java @@ -29,8 +29,8 @@ import androidx.annotation.Nullable; import com.android.systemui.complication.Complication; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.DreamOverlayStateController; -import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor; -import com.android.systemui.dreams.touch.DreamTouchHandler; +import com.android.systemui.dreams.touch.TouchHandler; +import com.android.systemui.dreams.touch.TouchMonitor; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -47,12 +47,12 @@ import javax.inject.Named; * {@link HideComplicationTouchHandler} is responsible for hiding the overlay complications from * visibility whenever there is touch interactions outside the overlay. The overlay interaction * scope includes touches to the complication plus any touch entry region for gestures as specified - * to the {@link DreamOverlayTouchMonitor}. + * to the {@link TouchMonitor}. * - * This {@link DreamTouchHandler} is also responsible for fading in the complications at the end - * of the {@link com.android.systemui.dreams.touch.DreamTouchHandler.TouchSession}. + * This {@link TouchHandler} is also responsible for fading in the complications at the end + * of the {@link TouchHandler.TouchSession}. */ -public class HideComplicationTouchHandler implements DreamTouchHandler { +public class HideComplicationTouchHandler implements TouchHandler { private static final String TAG = "HideComplicationHandler"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 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 66d413ab56b8..dc716bb9f9ca 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java @@ -55,7 +55,7 @@ import javax.inject.Named; /** * Monitor for tracking touches on the DreamOverlay to bring up the bouncer. */ -public class BouncerSwipeTouchHandler implements DreamTouchHandler { +public class BouncerSwipeTouchHandler implements TouchHandler { /** * An interface for creating ValueAnimators. */ diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java index 13588c2d45fe..c58351f4f7a4 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -36,8 +36,8 @@ import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Named; -/** {@link DreamTouchHandler} responsible for handling touches to open communal hub. **/ -public class CommunalTouchHandler implements DreamTouchHandler { +/** {@link TouchHandler} responsible for handling touches to open communal hub. **/ +public class CommunalTouchHandler implements TouchHandler { private final int mInitiationWidth; private final Optional mCentralSurfaces; private final Lifecycle mLifecycle; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java deleted file mode 100644 index 3b22b31de121..000000000000 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java +++ /dev/null @@ -1,524 +0,0 @@ -/* - * Copyright (C) 2022 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 android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - -import static com.android.systemui.shared.Flags.bouncerAreaExclusion; - -import android.graphics.Rect; -import android.graphics.Region; -import android.os.RemoteException; -import android.util.Log; -import android.view.GestureDetector; -import android.view.ISystemGestureExclusionListener; -import android.view.IWindowManager; -import android.view.InputEvent; -import android.view.MotionEvent; - -import androidx.annotation.NonNull; -import androidx.concurrent.futures.CallbackToFutureAdapter; -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.LifecycleOwner; - -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.DisplayId; -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; - -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.concurrent.Executor; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -/** - * {@link DreamOverlayTouchMonitor} is responsible for monitoring touches and gestures over the - * dream overlay and redirecting them to a set of listeners. This monitor is in charge of figuring - * out when listeners are eligible for receiving touches and filtering the listener pool if - * touches are consumed. - */ -public class DreamOverlayTouchMonitor { - // This executor is used to protect {@code mActiveTouchSessions} from being modified - // concurrently. Any operation that adds or removes values should use this executor. - public String TAG = "DreamOverlayTouchMonitor"; - private final Executor mMainExecutor; - private final Executor mBackgroundExecutor; - private final Lifecycle mLifecycle; - private Rect mExclusionRect = null; - - private ISystemGestureExclusionListener mGestureExclusionListener = - new ISystemGestureExclusionListener.Stub() { - @Override - public void onSystemGestureExclusionChanged(int displayId, - Region systemGestureExclusion, - Region systemGestureExclusionUnrestricted) { - mExclusionRect = systemGestureExclusion.getBounds(); - } - }; - - - - /** - * Adds a new {@link TouchSessionImpl} to participate in receiving future touches and gestures. - */ - private ListenableFuture push( - TouchSessionImpl touchSessionImpl) { - return CallbackToFutureAdapter.getFuture(completer -> { - mMainExecutor.execute(() -> { - if (!mActiveTouchSessions.remove(touchSessionImpl)) { - completer.set(null); - return; - } - - final TouchSessionImpl touchSession = - new TouchSessionImpl(this, touchSessionImpl.getBounds(), - touchSessionImpl); - mActiveTouchSessions.add(touchSession); - completer.set(touchSession); - }); - - return "DreamOverlayTouchMonitor::push"; - }); - } - - /** - * Removes a {@link TouchSessionImpl} from receiving further updates. - */ - private ListenableFuture pop( - TouchSessionImpl touchSessionImpl) { - return CallbackToFutureAdapter.getFuture(completer -> { - mMainExecutor.execute(() -> { - if (mActiveTouchSessions.remove(touchSessionImpl)) { - touchSessionImpl.onRemoved(); - - final TouchSessionImpl predecessor = touchSessionImpl.getPredecessor(); - - if (predecessor != null) { - mActiveTouchSessions.add(predecessor); - } - - completer.set(predecessor); - } - - if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) { - stopMonitoring(false); - } - }); - - return "DreamOverlayTouchMonitor::pop"; - }); - } - - private int getSessionCount() { - return mActiveTouchSessions.size(); - } - - /** - * {@link TouchSessionImpl} implements {@link DreamTouchHandler.TouchSession} for - * {@link DreamOverlayTouchMonitor}. It enables the monitor to access the associated listeners - * and provides the associated client with access to the monitor. - */ - private static class TouchSessionImpl implements DreamTouchHandler.TouchSession { - private final HashSet mEventListeners = - new HashSet<>(); - private final HashSet mGestureListeners = - new HashSet<>(); - private final HashSet mCallbacks = new HashSet<>(); - - private final TouchSessionImpl mPredecessor; - private final DreamOverlayTouchMonitor mTouchMonitor; - private final Rect mBounds; - - TouchSessionImpl(DreamOverlayTouchMonitor touchMonitor, Rect bounds, - TouchSessionImpl predecessor) { - mPredecessor = predecessor; - mTouchMonitor = touchMonitor; - mBounds = bounds; - } - - @Override - public void registerCallback(Callback callback) { - mCallbacks.add(callback); - } - - @Override - public boolean registerInputListener( - InputChannelCompat.InputEventListener inputEventListener) { - return mEventListeners.add(inputEventListener); - } - - @Override - public boolean registerGestureListener(GestureDetector.OnGestureListener gestureListener) { - return mGestureListeners.add(gestureListener); - } - - @Override - public ListenableFuture push() { - return mTouchMonitor.push(this); - } - - @Override - public ListenableFuture pop() { - return mTouchMonitor.pop(this); - } - - @Override - public int getActiveSessionCount() { - return mTouchMonitor.getSessionCount(); - } - - /** - * Returns the active listeners to receive touch events. - */ - public Collection getEventListeners() { - return mEventListeners; - } - - /** - * Returns the active listeners to receive gesture events. - */ - public Collection getGestureListeners() { - return mGestureListeners; - } - - /** - * Returns the {@link TouchSessionImpl} that preceded this current session. This will - * become the new active session when this session is popped. - */ - private TouchSessionImpl getPredecessor() { - return mPredecessor; - } - - /** - * Called by the monitor when this session is removed. - */ - private void onRemoved() { - mEventListeners.clear(); - mGestureListeners.clear(); - final Iterator iter = mCallbacks.iterator(); - while (iter.hasNext()) { - final Callback callback = iter.next(); - callback.onRemoved(); - iter.remove(); - } - } - - @Override - public Rect getBounds() { - return mBounds; - } - } - - /** - * This lifecycle observer ensures touch monitoring only occurs while the overlay is "resumed". - * This concept is mapped over from the equivalent view definition: The {@link LifecycleOwner} - * will report the dream is not resumed when it is obscured (from the notification shade being - * expanded for example) or not active (such as when it is destroyed). - */ - private final LifecycleObserver mLifecycleObserver = new DefaultLifecycleObserver() { - @Override - public void onResume(@NonNull LifecycleOwner owner) { - startMonitoring(); - } - - @Override - public void onPause(@NonNull LifecycleOwner owner) { - stopMonitoring(false); - } - - @Override - public void onDestroy(LifecycleOwner owner) { - stopMonitoring(true); - } - }; - - /** - * When invoked, instantiates a new {@link InputSession} to monitor touch events. - */ - private void startMonitoring() { - stopMonitoring(true); - if (bouncerAreaExclusion()) { - mBackgroundExecutor.execute(() -> { - try { - mWindowManagerService.registerSystemGestureExclusionListener( - mGestureExclusionListener, mDisplayId); - } catch (RemoteException e) { - // Handle the exception - Log.e(TAG, "Failed to register gesture exclusion listener", e); - } - }); - } - mCurrentInputSession = mInputSessionFactory.create( - "dreamOverlay", - mInputEventListener, - mOnGestureListener, - true) - .getInputSession(); - } - - /** - * Destroys any active {@link InputSession}. - */ - private void stopMonitoring(boolean force) { - mExclusionRect = null; - if (bouncerAreaExclusion()) { - mBackgroundExecutor.execute(() -> { - try { - mWindowManagerService.unregisterSystemGestureExclusionListener( - mGestureExclusionListener, mDisplayId); - } catch (RemoteException e) { - // Handle the exception - Log.e(TAG, "unregisterSystemGestureExclusionListener: failed", e); - } - }); - } - 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. - mMainExecutor.execute(() -> mActiveTouchSessions.forEach(touchSession -> { - while (touchSession != null) { - touchSession.onRemoved(); - touchSession = touchSession.getPredecessor(); - } - })); - - 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() { - @Override - public void onInputEvent(InputEvent ev) { - // No Active sessions are receiving touches. Create sessions for each listener - if (mActiveTouchSessions.isEmpty()) { - final HashMap sessionMap = - new HashMap<>(); - - for (DreamTouchHandler handler : mHandlers) { - if (!handler.isEnabled()) { - continue; - } - final Rect maxBounds = mDisplayHelper.getMaxBounds(ev.getDisplayId(), - TYPE_APPLICATION_OVERLAY); - final Region initiationRegion = Region.obtain(); - Rect exclusionRect = null; - if (bouncerAreaExclusion()) { - exclusionRect = getCurrentExclusionRect(); - } - handler.getTouchInitiationRegion( - maxBounds, initiationRegion, exclusionRect); - - if (!initiationRegion.isEmpty()) { - // Initiation regions require a motion event to determine pointer location - // within the region. - if (!(ev instanceof MotionEvent)) { - continue; - } - - final MotionEvent motionEvent = (MotionEvent) ev; - - // If the touch event is outside the region, then ignore. - if (!initiationRegion.contains(Math.round(motionEvent.getX()), - Math.round(motionEvent.getY()))) { - continue; - } - } - - final TouchSessionImpl sessionStack = new TouchSessionImpl( - DreamOverlayTouchMonitor.this, maxBounds, null); - mActiveTouchSessions.add(sessionStack); - sessionMap.put(handler, sessionStack); - } - - // Informing handlers of new sessions is delayed until we have all created so the - // final session is correct. - sessionMap.forEach((dreamTouchHandler, touchSession) - -> dreamTouchHandler.onSessionStart(touchSession)); - } - - // Find active sessions and invoke on InputEvent. - mActiveTouchSessions.stream() - .map(touchSessionStack -> touchSessionStack.getEventListeners()) - .flatMap(Collection::stream) - .forEach(inputEventListener -> inputEventListener.onInputEvent(ev)); - } - private Rect getCurrentExclusionRect() { - return mExclusionRect; - } - }; - - /** - * The {@link Evaluator} interface allows for callers to inspect a listener from the - * {@link android.view.GestureDetector.OnGestureListener} set. This helps reduce duplicated - * iteration loops over this set. - */ - private interface Evaluator { - boolean evaluate(GestureDetector.OnGestureListener listener); - } - - private GestureDetector.OnGestureListener mOnGestureListener = - new GestureDetector.OnGestureListener() { - private boolean evaluate(Evaluator evaluator) { - final Set consumingSessions = new HashSet<>(); - - // When a gesture is consumed, it is assumed that all touches for the current session - // should be directed only to those TouchSessions until those sessions are popped. All - // non-participating sessions are removed from receiving further updates with - // {@link DreamOverlayTouchMonitor#isolate}. - final boolean eventConsumed = mActiveTouchSessions.stream() - .map(touchSession -> { - boolean consume = touchSession.getGestureListeners() - .stream() - .map(listener -> evaluator.evaluate(listener)) - .anyMatch(consumed -> consumed); - - if (consume) { - consumingSessions.add(touchSession); - } - return consume; - }).anyMatch(consumed -> consumed); - - if (eventConsumed) { - DreamOverlayTouchMonitor.this.isolate(consumingSessions); - } - - return eventConsumed; - } - - // This method is called for gesture events that cannot be consumed. - private void observe(Consumer consumer) { - mActiveTouchSessions.stream() - .map(touchSession -> touchSession.getGestureListeners()) - .flatMap(Collection::stream) - .forEach(listener -> consumer.accept(listener)); - } - - @Override - public boolean onDown(MotionEvent e) { - return evaluate(listener -> listener.onDown(e)); - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - return evaluate(listener -> listener.onFling(e1, e2, velocityX, velocityY)); - } - - @Override - public void onLongPress(MotionEvent e) { - observe(listener -> listener.onLongPress(e)); - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - return evaluate(listener -> listener.onScroll(e1, e2, distanceX, distanceY)); - } - - @Override - public void onShowPress(MotionEvent e) { - observe(listener -> listener.onShowPress(e)); - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - return evaluate(listener -> listener.onSingleTapUp(e)); - } - }; - - private InputSessionComponent.Factory mInputSessionFactory; - private InputSession mCurrentInputSession; - private final int mDisplayId; - private final IWindowManager mWindowManagerService; - - - /** - * Designated constructor for {@link DreamOverlayTouchMonitor} - * @param executor This executor will be used for maintaining the active listener list to avoid - * concurrent modification. - * @param lifecycle {@link DreamOverlayTouchMonitor} will listen to this lifecycle to determine - * whether touch monitoring should be active. - * @param inputSessionFactory This factory will generate the {@link InputSession} requested by - * the monitor. Each session should be unique and valid when - * returned. - * @param handlers This set represents the {@link DreamTouchHandler} instances that will - * participate in touch handling. - */ - @Inject - public DreamOverlayTouchMonitor( - @Main Executor executor, - @Background Executor backgroundExecutor, - Lifecycle lifecycle, - InputSessionComponent.Factory inputSessionFactory, - DisplayHelper displayHelper, - Set handlers, - IWindowManager windowManagerService, - @DisplayId int displayId) { - mDisplayId = displayId; - mHandlers = handlers; - mInputSessionFactory = inputSessionFactory; - mMainExecutor = executor; - mBackgroundExecutor = backgroundExecutor; - mLifecycle = lifecycle; - mDisplayHelper = displayHelper; - mWindowManagerService = windowManagerService; - } - - /** - * Initializes the monitor. should only be called once after creation. - */ - public void init() { - mLifecycle.addObserver(mLifecycleObserver); - } - - private void isolate(Set sessions) { - Collection removedSessions = mActiveTouchSessions.stream() - .filter(touchSession -> !sessions.contains(touchSession)) - .collect(Collectors.toCollection(HashSet::new)); - - removedSessions.forEach(touchSession -> touchSession.onRemoved()); - - mActiveTouchSessions.removeAll(removedSessions); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamTouchHandler.java deleted file mode 100644 index 1ec000835ad2..000000000000 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamTouchHandler.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2022 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 android.graphics.Rect; -import android.graphics.Region; -import android.view.GestureDetector; - -import com.android.systemui.shared.system.InputChannelCompat; - -import com.google.common.util.concurrent.ListenableFuture; - -/** - * The {@link DreamTouchHandler} interface provides a way for dream overlay components to observe - * touch events and gestures with the ability to intercept the latter. Touch interaction sequences - * are abstracted as sessions. A session represents the time of first - * {@code android.view.MotionEvent.ACTION_DOWN} event to the last {@link DreamTouchHandler} - * stopping interception of gestures. If no gesture is intercepted, the session continues - * indefinitely. {@link DreamTouchHandler} have the ability to create a stack of sessions, which - * allows for motion logic to be captured in modal states. - */ -public interface DreamTouchHandler { - /** - * A touch session captures the interaction surface of a {@link DreamTouchHandler}. Clients - * register listeners as desired to participate in motion/gesture callbacks. - */ - interface TouchSession { - interface Callback { - void onRemoved(); - } - - void registerCallback(Callback callback); - - /** - * Adds a input event listener for the given session. - * @param inputEventListener - */ - boolean registerInputListener(InputChannelCompat.InputEventListener inputEventListener); - - /** - * Adds a gesture listener for the given session. - * @param gestureListener - */ - boolean registerGestureListener(GestureDetector.OnGestureListener gestureListener); - - /** - * Creates a new {@link TouchSession} that will receive any updates that would have been - * directed to this {@link TouchSession}. - * @return The future which will return a new {@link TouchSession} that will receive - * subsequent events. If the operation fails, {@code null} will be returned. - */ - ListenableFuture push(); - - /** - * Explicitly releases this {@link TouchSession}. The registered listeners will no longer - * receive any further updates. - * @return The future containing the {@link TouchSession} that will receive subsequent - * events. This session will be the direct predecessor of the popped session. {@code null} - * if the popped {@link TouchSession} was the initial session or has already been popped. - */ - ListenableFuture pop(); - - /** - * Returns the number of currently active sessions. - */ - int getActiveSessionCount(); - - /** - * Returns the bounds of the display the touch region. - */ - Rect getBounds(); - } - - /** - * Returns whether the handler is enabled to handle touch on dream. - * @return isEnabled state. By default it's true. - */ - default Boolean isEnabled() { - return true; - } - - /** - * Sets whether to enable the handler to handle touch on dream. - * @param enabled new value to be set whether to enable the handler. - */ - default void setIsEnabled(Boolean enabled){} - - /** - * Returns the region the touch handler is interested in. By default, no region is specified, - * 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(Rect bounds, Region region, Rect exclusionRect) { - } - - /** - * Informed a new touch session has begun. The first touch event will be delivered to any - * listener registered through - * {@link TouchSession#registerInputListener(InputChannelCompat.InputEventListener)} during this - * call. If there are no interactions with this touch session after this method returns, it will - * be dropped. - * @param session - */ - void onSessionStart(TouchSession session); -} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java index e0bf52e81875..63b09435d3e6 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java @@ -35,7 +35,7 @@ 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 { +public class ShadeTouchHandler implements TouchHandler { private final Optional mSurfaces; private final ShadeViewController mShadeViewController; private final int mInitiationHeight; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/TouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/TouchHandler.java new file mode 100644 index 000000000000..ee625b42d3e3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/TouchHandler.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 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 android.graphics.Rect; +import android.graphics.Region; +import android.view.GestureDetector; + +import com.android.systemui.shared.system.InputChannelCompat; + +import com.google.common.util.concurrent.ListenableFuture; + +/** + * The {@link TouchHandler} interface provides a way for dream overlay components to observe + * touch events and gestures with the ability to intercept the latter. Touch interaction sequences + * are abstracted as sessions. A session represents the time of first + * {@code android.view.MotionEvent.ACTION_DOWN} event to the last {@link TouchHandler} + * stopping interception of gestures. If no gesture is intercepted, the session continues + * indefinitely. {@link TouchHandler} have the ability to create a stack of sessions, which + * allows for motion logic to be captured in modal states. + */ +public interface TouchHandler { + /** + * A touch session captures the interaction surface of a {@link TouchHandler}. Clients + * register listeners as desired to participate in motion/gesture callbacks. + */ + interface TouchSession { + interface Callback { + /** + * Invoked when the session has been removed. + */ + void onRemoved(); + } + + /** + * Registers a callback to be notified when there are updates to the {@link TouchSession}. + */ + void registerCallback(Callback callback); + + /** + * Adds a input event listener for the given session. + * @param inputEventListener + */ + boolean registerInputListener(InputChannelCompat.InputEventListener inputEventListener); + + /** + * Adds a gesture listener for the given session. + * @param gestureListener + */ + boolean registerGestureListener(GestureDetector.OnGestureListener gestureListener); + + /** + * Creates a new {@link TouchSession} that will receive any updates that would have been + * directed to this {@link TouchSession}. + * @return The future which will return a new {@link TouchSession} that will receive + * subsequent events. If the operation fails, {@code null} will be returned. + */ + ListenableFuture push(); + + /** + * Explicitly releases this {@link TouchSession}. The registered listeners will no longer + * receive any further updates. + * @return The future containing the {@link TouchSession} that will receive subsequent + * events. This session will be the direct predecessor of the popped session. {@code null} + * if the popped {@link TouchSession} was the initial session or has already been popped. + */ + ListenableFuture pop(); + + /** + * Returns the number of currently active sessions. + */ + int getActiveSessionCount(); + + /** + * Returns the bounds of the display the touch region. + */ + Rect getBounds(); + } + + /** + * Returns whether the handler is enabled to handle touch on dream. + * @return isEnabled state. By default it's true. + */ + default Boolean isEnabled() { + return true; + } + + /** + * Sets whether to enable the handler to handle touch on dream. + * @param enabled new value to be set whether to enable the handler. + */ + default void setIsEnabled(Boolean enabled){} + + /** + * Returns the region the touch handler is interested in. By default, no region is specified, + * 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(Rect bounds, Region region, Rect exclusionRect) { + } + + /** + * Informed a new touch session has begun. The first touch event will be delivered to any + * listener registered through + * {@link TouchSession#registerInputListener(InputChannelCompat.InputEventListener)} during this + * call. If there are no interactions with this touch session after this method returns, it will + * be dropped. + * @param session + */ + void onSessionStart(TouchSession session); +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/TouchMonitor.java new file mode 100644 index 000000000000..7c50d6a5d895 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/TouchMonitor.java @@ -0,0 +1,532 @@ +/* + * Copyright (C) 2022 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 android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import static com.android.systemui.shared.Flags.bouncerAreaExclusion; + +import android.graphics.Rect; +import android.graphics.Region; +import android.os.RemoteException; +import android.util.Log; +import android.view.GestureDetector; +import android.view.ISystemGestureExclusionListener; +import android.view.IWindowManager; +import android.view.InputEvent; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.concurrent.futures.CallbackToFutureAdapter; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; + +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.DisplayId; +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; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +/** + * {@link TouchMonitor} is responsible for monitoring touches and gestures over the + * dream overlay and redirecting them to a set of listeners. This monitor is in charge of figuring + * out when listeners are eligible for receiving touches and filtering the listener pool if + * touches are consumed. + */ +public class TouchMonitor { + // This executor is used to protect {@code mActiveTouchSessions} from being modified + // concurrently. Any operation that adds or removes values should use this executor. + public String TAG = "DreamOverlayTouchMonitor"; + private final Executor mMainExecutor; + private final Executor mBackgroundExecutor; + private final Lifecycle mLifecycle; + private Rect mExclusionRect = null; + + private ISystemGestureExclusionListener mGestureExclusionListener = + new ISystemGestureExclusionListener.Stub() { + @Override + public void onSystemGestureExclusionChanged(int displayId, + Region systemGestureExclusion, + Region systemGestureExclusionUnrestricted) { + mExclusionRect = systemGestureExclusion.getBounds(); + } + }; + + + /** + * Adds a new {@link TouchSessionImpl} to participate in receiving future touches and gestures. + */ + private ListenableFuture push( + TouchSessionImpl touchSessionImpl) { + return CallbackToFutureAdapter.getFuture(completer -> { + mMainExecutor.execute(() -> { + if (!mActiveTouchSessions.remove(touchSessionImpl)) { + completer.set(null); + return; + } + + final TouchSessionImpl touchSession = + new TouchSessionImpl(this, touchSessionImpl.getBounds(), + touchSessionImpl); + mActiveTouchSessions.add(touchSession); + completer.set(touchSession); + }); + + return "DreamOverlayTouchMonitor::push"; + }); + } + + /** + * Removes a {@link TouchSessionImpl} from receiving further updates. + */ + private ListenableFuture pop( + TouchSessionImpl touchSessionImpl) { + return CallbackToFutureAdapter.getFuture(completer -> { + mMainExecutor.execute(() -> { + if (mActiveTouchSessions.remove(touchSessionImpl)) { + touchSessionImpl.onRemoved(); + + final TouchSessionImpl predecessor = touchSessionImpl.getPredecessor(); + + if (predecessor != null) { + mActiveTouchSessions.add(predecessor); + } + + completer.set(predecessor); + } + + if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) { + stopMonitoring(false); + } + }); + + return "DreamOverlayTouchMonitor::pop"; + }); + } + + private int getSessionCount() { + return mActiveTouchSessions.size(); + } + + /** + * {@link TouchSessionImpl} implements {@link TouchHandler.TouchSession} for + * {@link TouchMonitor}. It enables the monitor to access the associated listeners + * and provides the associated client with access to the monitor. + */ + private static class TouchSessionImpl implements TouchHandler.TouchSession { + private final HashSet mEventListeners = + new HashSet<>(); + private final HashSet mGestureListeners = + new HashSet<>(); + private final HashSet mCallbacks = new HashSet<>(); + + private final TouchSessionImpl mPredecessor; + private final TouchMonitor mTouchMonitor; + private final Rect mBounds; + + TouchSessionImpl(TouchMonitor touchMonitor, Rect bounds, + TouchSessionImpl predecessor) { + mPredecessor = predecessor; + mTouchMonitor = touchMonitor; + mBounds = bounds; + } + + @Override + public void registerCallback(Callback callback) { + mCallbacks.add(callback); + } + + @Override + public boolean registerInputListener( + InputChannelCompat.InputEventListener inputEventListener) { + return mEventListeners.add(inputEventListener); + } + + @Override + public boolean registerGestureListener(GestureDetector.OnGestureListener gestureListener) { + return mGestureListeners.add(gestureListener); + } + + @Override + public ListenableFuture push() { + return mTouchMonitor.push(this); + } + + @Override + public ListenableFuture pop() { + return mTouchMonitor.pop(this); + } + + @Override + public int getActiveSessionCount() { + return mTouchMonitor.getSessionCount(); + } + + /** + * Returns the active listeners to receive touch events. + */ + public Collection getEventListeners() { + return mEventListeners; + } + + /** + * Returns the active listeners to receive gesture events. + */ + public Collection getGestureListeners() { + return mGestureListeners; + } + + /** + * Returns the {@link TouchSessionImpl} that preceded this current session. This will + * become the new active session when this session is popped. + */ + private TouchSessionImpl getPredecessor() { + return mPredecessor; + } + + /** + * Called by the monitor when this session is removed. + */ + private void onRemoved() { + mEventListeners.clear(); + mGestureListeners.clear(); + final Iterator iter = mCallbacks.iterator(); + while (iter.hasNext()) { + final Callback callback = iter.next(); + callback.onRemoved(); + iter.remove(); + } + } + + @Override + public Rect getBounds() { + return mBounds; + } + } + + /** + * This lifecycle observer ensures touch monitoring only occurs while the overlay is "resumed". + * This concept is mapped over from the equivalent view definition: The {@link LifecycleOwner} + * will report the dream is not resumed when it is obscured (from the notification shade being + * expanded for example) or not active (such as when it is destroyed). + */ + private final LifecycleObserver mLifecycleObserver = new DefaultLifecycleObserver() { + @Override + public void onResume(@NonNull LifecycleOwner owner) { + startMonitoring(); + } + + @Override + public void onPause(@NonNull LifecycleOwner owner) { + stopMonitoring(false); + } + + @Override + public void onDestroy(LifecycleOwner owner) { + stopMonitoring(true); + } + }; + + /** + * When invoked, instantiates a new {@link InputSession} to monitor touch events. + */ + private void startMonitoring() { + stopMonitoring(true); + if (bouncerAreaExclusion()) { + mBackgroundExecutor.execute(() -> { + try { + mWindowManagerService.registerSystemGestureExclusionListener( + mGestureExclusionListener, mDisplayId); + } catch (RemoteException e) { + // Handle the exception + Log.e(TAG, "Failed to register gesture exclusion listener", e); + } + }); + } + mCurrentInputSession = mInputSessionFactory.create( + "dreamOverlay", + mInputEventListener, + mOnGestureListener, + true) + .getInputSession(); + } + + /** + * Destroys any active {@link InputSession}. + */ + private void stopMonitoring(boolean force) { + mExclusionRect = null; + if (bouncerAreaExclusion()) { + mBackgroundExecutor.execute(() -> { + try { + mWindowManagerService.unregisterSystemGestureExclusionListener( + mGestureExclusionListener, mDisplayId); + } catch (RemoteException e) { + // Handle the exception + Log.e(TAG, "unregisterSystemGestureExclusionListener: failed", e); + } + }); + } + 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. + mMainExecutor.execute(() -> mActiveTouchSessions.forEach(touchSession -> { + while (touchSession != null) { + touchSession.onRemoved(); + touchSession = touchSession.getPredecessor(); + } + })); + + 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() { + @Override + public void onInputEvent(InputEvent ev) { + // No Active sessions are receiving touches. Create sessions for each listener + if (mActiveTouchSessions.isEmpty()) { + final HashMap sessionMap = + new HashMap<>(); + + for (TouchHandler handler : mHandlers) { + if (!handler.isEnabled()) { + continue; + } + final Rect maxBounds = mDisplayHelper.getMaxBounds(ev.getDisplayId(), + TYPE_APPLICATION_OVERLAY); + final Region initiationRegion = Region.obtain(); + Rect exclusionRect = null; + if (bouncerAreaExclusion()) { + exclusionRect = getCurrentExclusionRect(); + } + handler.getTouchInitiationRegion( + maxBounds, initiationRegion, exclusionRect); + + if (!initiationRegion.isEmpty()) { + // Initiation regions require a motion event to determine pointer + // location + // within the region. + if (!(ev instanceof MotionEvent)) { + continue; + } + + final MotionEvent motionEvent = (MotionEvent) ev; + + // If the touch event is outside the region, then ignore. + if (!initiationRegion.contains(Math.round(motionEvent.getX()), + Math.round(motionEvent.getY()))) { + continue; + } + } + + final TouchSessionImpl sessionStack = new TouchSessionImpl( + TouchMonitor.this, maxBounds, null); + mActiveTouchSessions.add(sessionStack); + sessionMap.put(handler, sessionStack); + } + + // Informing handlers of new sessions is delayed until we have all + // created so the + // final session is correct. + sessionMap.forEach((dreamTouchHandler, touchSession) + -> dreamTouchHandler.onSessionStart(touchSession)); + } + + // Find active sessions and invoke on InputEvent. + mActiveTouchSessions.stream() + .map(touchSessionStack -> touchSessionStack.getEventListeners()) + .flatMap(Collection::stream) + .forEach(inputEventListener -> inputEventListener.onInputEvent(ev)); + } + + private Rect getCurrentExclusionRect() { + return mExclusionRect; + } + }; + + /** + * The {@link Evaluator} interface allows for callers to inspect a listener from the + * {@link android.view.GestureDetector.OnGestureListener} set. This helps reduce duplicated + * iteration loops over this set. + */ + private interface Evaluator { + boolean evaluate(GestureDetector.OnGestureListener listener); + } + + private GestureDetector.OnGestureListener mOnGestureListener = + new GestureDetector.OnGestureListener() { + private boolean evaluate(Evaluator evaluator) { + final Set consumingSessions = new HashSet<>(); + + // When a gesture is consumed, it is assumed that all touches for the current + // session + // should be directed only to those TouchSessions until those sessions are + // popped. All + // non-participating sessions are removed from receiving further updates with + // {@link DreamOverlayTouchMonitor#isolate}. + final boolean eventConsumed = mActiveTouchSessions.stream() + .map(touchSession -> { + boolean consume = touchSession.getGestureListeners() + .stream() + .map(listener -> evaluator.evaluate(listener)) + .anyMatch(consumed -> consumed); + + if (consume) { + consumingSessions.add(touchSession); + } + return consume; + }).anyMatch(consumed -> consumed); + + if (eventConsumed) { + TouchMonitor.this.isolate(consumingSessions); + } + + return eventConsumed; + } + + // This method is called for gesture events that cannot be consumed. + private void observe(Consumer consumer) { + mActiveTouchSessions.stream() + .map(touchSession -> touchSession.getGestureListeners()) + .flatMap(Collection::stream) + .forEach(listener -> consumer.accept(listener)); + } + + @Override + public boolean onDown(MotionEvent e) { + return evaluate(listener -> listener.onDown(e)); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return evaluate(listener -> listener.onFling(e1, e2, velocityX, velocityY)); + } + + @Override + public void onLongPress(MotionEvent e) { + observe(listener -> listener.onLongPress(e)); + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + float distanceY) { + return evaluate(listener -> listener.onScroll(e1, e2, distanceX, distanceY)); + } + + @Override + public void onShowPress(MotionEvent e) { + observe(listener -> listener.onShowPress(e)); + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return evaluate(listener -> listener.onSingleTapUp(e)); + } + }; + + private InputSessionComponent.Factory mInputSessionFactory; + private InputSession mCurrentInputSession; + private final int mDisplayId; + private final IWindowManager mWindowManagerService; + + + /** + * Designated constructor for {@link TouchMonitor} + * + * @param executor This executor will be used for maintaining the active listener + * list to avoid + * concurrent modification. + * @param lifecycle {@link TouchMonitor} will listen to this lifecycle to determine + * whether touch monitoring should be active. + * @param inputSessionFactory This factory will generate the {@link InputSession} requested by + * the monitor. Each session should be unique and valid when + * returned. + * @param handlers This set represents the {@link TouchHandler} instances that will + * participate in touch handling. + */ + @Inject + public TouchMonitor( + @Main Executor executor, + @Background Executor backgroundExecutor, + Lifecycle lifecycle, + InputSessionComponent.Factory inputSessionFactory, + DisplayHelper displayHelper, + Set handlers, + IWindowManager windowManagerService, + @DisplayId int displayId) { + mDisplayId = displayId; + mHandlers = handlers; + mInputSessionFactory = inputSessionFactory; + mMainExecutor = executor; + mBackgroundExecutor = backgroundExecutor; + mLifecycle = lifecycle; + mDisplayHelper = displayHelper; + mWindowManagerService = windowManagerService; + } + + /** + * Initializes the monitor. should only be called once after creation. + */ + public void init() { + mLifecycle.addObserver(mLifecycleObserver); + } + + private void isolate(Set sessions) { + Collection removedSessions = mActiveTouchSessions.stream() + .filter(touchSession -> !sessions.contains(touchSession)) + .collect(Collectors.toCollection(HashSet::new)); + + removedSessions.forEach(touchSession -> touchSession.onRemoved()); + + mActiveTouchSessions.removeAll(removedSessions); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/BouncerSwipeModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/BouncerSwipeModule.java index a5db2ff81f99..820135e6cd29 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/BouncerSwipeModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/BouncerSwipeModule.java @@ -23,7 +23,7 @@ import android.view.VelocityTracker; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.touch.BouncerSwipeTouchHandler; -import com.android.systemui.dreams.touch.DreamTouchHandler; +import com.android.systemui.dreams.touch.TouchHandler; import com.android.systemui.res.R; import com.android.systemui.shade.ShadeViewController; import com.android.wm.shell.animation.FlingAnimationUtils; @@ -66,7 +66,7 @@ public class BouncerSwipeModule { */ @Provides @IntoSet - public static DreamTouchHandler providesBouncerSwipeTouchHandler( + public static TouchHandler providesBouncerSwipeTouchHandler( BouncerSwipeTouchHandler touchHandler) { return touchHandler; } 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 index 0f08d376f37c..358023864bda 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java @@ -20,8 +20,8 @@ import android.content.res.Resources; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.touch.CommunalTouchHandler; -import com.android.systemui.dreams.touch.DreamTouchHandler; import com.android.systemui.dreams.touch.ShadeTouchHandler; +import com.android.systemui.dreams.touch.TouchHandler; import com.android.systemui.res.R; import dagger.Binds; @@ -52,13 +52,13 @@ public abstract class ShadeModule { */ @Binds @IntoSet - public abstract DreamTouchHandler providesNotificationShadeTouchHandler( + public abstract TouchHandler providesNotificationShadeTouchHandler( ShadeTouchHandler touchHandler); /** Provides {@link CommunalTouchHandler}. */ @Binds @IntoSet - public abstract DreamTouchHandler bindCommunalTouchHandler(CommunalTouchHandler touchHandler); + public abstract TouchHandler bindCommunalTouchHandler(CommunalTouchHandler touchHandler); /** * Provides the height of the gesture area for notification swipe down. 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 deleted file mode 100644 index a127631a536d..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java +++ /dev/null @@ -1,643 +0,0 @@ -/* - * Copyright (C) 2022 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.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; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.graphics.Rect; -import android.graphics.Region; -import android.testing.AndroidTestingRunner; -import android.util.Pair; -import android.view.GestureDetector; -import android.view.IWindowManager; -import android.view.InputEvent; -import android.view.MotionEvent; - -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.LifecycleOwner; -import androidx.test.filters.SmallTest; - -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; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class DreamOverlayTouchMonitorTest extends SysuiTestCase { - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - - private static class Environment { - private final InputSessionComponent.Factory mInputFactory; - private final InputSession mInputSession; - private final Lifecycle mLifecycle; - private final LifecycleOwner mLifecycleOwner; - private final DreamOverlayTouchMonitor mMonitor; - 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 FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock()); - private final Rect mDisplayBounds = Mockito.mock(Rect.class); - private final IWindowManager mIWindowManager; - - Environment(Set handlers) { - mLifecycle = Mockito.mock(Lifecycle.class); - mLifecycleOwner = Mockito.mock(LifecycleOwner.class); - mIWindowManager = Mockito.mock(IWindowManager.class); - - mInputFactory = Mockito.mock(InputSessionComponent.Factory.class); - final InputSessionComponent inputComponent = Mockito.mock(InputSessionComponent.class); - mInputSession = Mockito.mock(InputSession.class); - - when(mInputFactory.create(any(), any(), any(), anyBoolean())) - .thenReturn(inputComponent); - when(inputComponent.getInputSession()).thenReturn(mInputSession); - - mDisplayHelper = Mockito.mock(DisplayHelper.class); - when(mDisplayHelper.getMaxBounds(anyInt(), anyInt())) - .thenReturn(mDisplayBounds); - mMonitor = new DreamOverlayTouchMonitor(mExecutor, mBackgroundExecutor, - mLifecycle, mInputFactory, mDisplayHelper, handlers, mIWindowManager, 0); - mMonitor.init(); - - final ArgumentCaptor lifecycleObserverCaptor = - ArgumentCaptor.forClass(LifecycleObserver.class); - verify(mLifecycle).addObserver(lifecycleObserverCaptor.capture()); - assertThat(lifecycleObserverCaptor.getValue() instanceof DefaultLifecycleObserver) - .isTrue(); - mLifecycleObserver = (DefaultLifecycleObserver) lifecycleObserverCaptor.getValue(); - - updateLifecycle(observer -> observer.first.onResume(observer.second)); - - // Capture creation request. - final ArgumentCaptor inputEventListenerCaptor = - ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); - final ArgumentCaptor gestureListenerCaptor = - ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); - verify(mInputFactory).create(any(), inputEventListenerCaptor.capture(), - gestureListenerCaptor.capture(), - eq(true)); - mEventListener = inputEventListenerCaptor.getValue(); - mGestureListener = gestureListenerCaptor.getValue(); - } - - public Rect getDisplayBounds() { - return mDisplayBounds; - } - - void executeAll() { - mExecutor.runAllReady(); - } - - void publishInputEvent(InputEvent event) { - mEventListener.onInputEvent(event); - } - - void publishGestureEvent(Consumer listenerConsumer) { - listenerConsumer.accept(mGestureListener); - } - - void updateLifecycle(Consumer> consumer) { - consumer.accept(Pair.create(mLifecycleObserver, mLifecycleOwner)); - } - - void verifyInputSessionDispose() { - verify(mInputSession).dispose(); - Mockito.clearInvocations(mInputSession); - } - } - - @Test - public void testReportedDisplayBounds() { - final DreamTouchHandler touchHandler = createTouchHandler(); - 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(), 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 = createTouchHandler(); - final Rect touchArea = new Rect(4, 4, 8 , 8); - - doAnswer(invocation -> { - final Region region = (Region) invocation.getArguments()[1]; - region.set(touchArea); - return null; - }).when(touchHandler).getTouchInitiationRegion(any(), any(), any()); - - final Environment environment = new Environment(Stream.of(touchHandler) - .collect(Collectors.toCollection(HashSet::new))); - - // Ensure touch outside specified region is not delivered. - final MotionEvent initialEvent = Mockito.mock(MotionEvent.class); - when(initialEvent.getX()).thenReturn(0.0f); - when(initialEvent.getY()).thenReturn(1.0f); - environment.publishInputEvent(initialEvent); - verify(touchHandler, never()).onSessionStart(any()); - - // Make sure touch inside region causes session start. - when(initialEvent.getX()).thenReturn(5.0f); - when(initialEvent.getY()).thenReturn(5.0f); - environment.publishInputEvent(initialEvent); - verify(touchHandler).onSessionStart(any()); - } - - @Test - public void testSessionCount() { - final DreamTouchHandler touchHandler = createTouchHandler(); - final Rect touchArea = new Rect(4, 4, 8 , 8); - - final DreamTouchHandler unzonedTouchHandler = createTouchHandler(); - doAnswer(invocation -> { - final Region region = (Region) invocation.getArguments()[1]; - region.set(touchArea); - return null; - }).when(touchHandler).getTouchInitiationRegion(any(), any(), any()); - - final Environment environment = new Environment(Stream.of(touchHandler, unzonedTouchHandler) - .collect(Collectors.toCollection(HashSet::new))); - - // Ensure touch outside specified region is delivered to unzoned touch handler. - final MotionEvent initialEvent = Mockito.mock(MotionEvent.class); - when(initialEvent.getX()).thenReturn(0.0f); - when(initialEvent.getY()).thenReturn(1.0f); - environment.publishInputEvent(initialEvent); - - ArgumentCaptor touchSessionCaptor = ArgumentCaptor.forClass( - DreamTouchHandler.TouchSession.class); - - // Make sure only one active session. - { - verify(unzonedTouchHandler).onSessionStart(touchSessionCaptor.capture()); - final DreamTouchHandler.TouchSession touchSession = touchSessionCaptor.getValue(); - assertThat(touchSession.getActiveSessionCount()).isEqualTo(1); - touchSession.pop(); - environment.executeAll(); - } - - // Make sure touch inside the touch region. - when(initialEvent.getX()).thenReturn(5.0f); - when(initialEvent.getY()).thenReturn(5.0f); - environment.publishInputEvent(initialEvent); - - // Make sure there are two active sessions. - { - verify(touchHandler).onSessionStart(touchSessionCaptor.capture()); - final DreamTouchHandler.TouchSession touchSession = touchSessionCaptor.getValue(); - assertThat(touchSession.getActiveSessionCount()).isEqualTo(2); - touchSession.pop(); - } - } - - - @Test - public void testNoActiveSessionWhenHandlerDisabled() { - final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class); - // disable the handler - when(touchHandler.isEnabled()).thenReturn(false); - - final Environment environment = new Environment(Stream.of(touchHandler) - .collect(Collectors.toCollection(HashSet::new))); - final MotionEvent initialEvent = Mockito.mock(MotionEvent.class); - when(initialEvent.getX()).thenReturn(5.0f); - when(initialEvent.getY()).thenReturn(5.0f); - environment.publishInputEvent(initialEvent); - - // Make sure there is no active session. - verify(touchHandler, never()).onSessionStart(any()); - verify(touchHandler, never()).getTouchInitiationRegion(any(), any(), any()); - } - - @Test - public void testInputEventPropagation() { - final DreamTouchHandler touchHandler = createTouchHandler(); - - 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)); - } - - @Test - public void testInputEventPropagationAfterRemoval() { - final DreamTouchHandler touchHandler = createTouchHandler(); - - 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 DreamTouchHandler.TouchSession session = captureSession(touchHandler); - final InputChannelCompat.InputEventListener eventListener = - registerInputEventListener(session); - - session.pop(); - environment.executeAll(); - - final InputEvent event = Mockito.mock(InputEvent.class); - environment.publishInputEvent(event); - - verify(eventListener, never()).onInputEvent(eq(event)); - } - - @Test - public void testInputGesturePropagation() { - final DreamTouchHandler touchHandler = createTouchHandler(); - - 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 GestureDetector.OnGestureListener gestureListener = - registerGestureListener(touchHandler); - - final MotionEvent event = Mockito.mock(MotionEvent.class); - environment.publishGestureEvent(onGestureListener -> onGestureListener.onShowPress(event)); - verify(gestureListener).onShowPress(eq(event)); - } - - @Test - public void testGestureConsumption() { - final DreamTouchHandler touchHandler = createTouchHandler(); - - 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 GestureDetector.OnGestureListener gestureListener = - registerGestureListener(touchHandler); - - when(gestureListener.onDown(any())).thenReturn(true); - final MotionEvent event = Mockito.mock(MotionEvent.class); - environment.publishGestureEvent(onGestureListener -> { - assertThat(onGestureListener.onDown(event)).isTrue(); - }); - - verify(gestureListener).onDown(eq(event)); - } - - @Test - public void testBroadcast() { - final DreamTouchHandler touchHandler = createTouchHandler(); - final DreamTouchHandler touchHandler2 = createTouchHandler(); - when(touchHandler2.isEnabled()).thenReturn(true); - - final Environment environment = new Environment(Stream.of(touchHandler, touchHandler2) - .collect(Collectors.toCollection(HashSet::new))); - - final InputEvent initialEvent = Mockito.mock(InputEvent.class); - environment.publishInputEvent(initialEvent); - - final HashSet inputListeners = new HashSet<>(); - - inputListeners.add(registerInputEventListener(touchHandler)); - inputListeners.add(registerInputEventListener(touchHandler)); - inputListeners.add(registerInputEventListener(touchHandler2)); - - final MotionEvent event = Mockito.mock(MotionEvent.class); - environment.publishInputEvent(event); - - inputListeners - .stream() - .forEach(inputEventListener -> verify(inputEventListener).onInputEvent(event)); - } - - @Test - public void testPush() throws InterruptedException, ExecutionException { - final DreamTouchHandler touchHandler = createTouchHandler(); - - final Environment environment = new Environment(Stream.of(touchHandler) - .collect(Collectors.toCollection(HashSet::new))); - - final InputEvent initialEvent = Mockito.mock(InputEvent.class); - environment.publishInputEvent(initialEvent); - - final DreamTouchHandler.TouchSession session = captureSession(touchHandler); - final InputChannelCompat.InputEventListener eventListener = - registerInputEventListener(session); - - final ListenableFuture frontSessionFuture = session.push(); - environment.executeAll(); - final DreamTouchHandler.TouchSession frontSession = frontSessionFuture.get(); - final InputChannelCompat.InputEventListener frontEventListener = - registerInputEventListener(frontSession); - - final MotionEvent event = Mockito.mock(MotionEvent.class); - environment.publishInputEvent(event); - - verify(frontEventListener).onInputEvent(eq(event)); - verify(eventListener, never()).onInputEvent(any()); - - Mockito.clearInvocations(eventListener, frontEventListener); - - ListenableFuture sessionFuture = frontSession.pop(); - environment.executeAll(); - - DreamTouchHandler.TouchSession returnedSession = sessionFuture.get(); - assertThat(session == returnedSession).isTrue(); - - environment.executeAll(); - - final MotionEvent followupEvent = Mockito.mock(MotionEvent.class); - environment.publishInputEvent(followupEvent); - - verify(eventListener).onInputEvent(eq(followupEvent)); - verify(frontEventListener, never()).onInputEvent(any()); - } - - @Test - public void testPop() { - final DreamTouchHandler touchHandler = createTouchHandler(); - - final DreamTouchHandler.TouchSession.Callback callback = - Mockito.mock(DreamTouchHandler.TouchSession.Callback.class); - - final Environment environment = new Environment(Stream.of(touchHandler) - .collect(Collectors.toCollection(HashSet::new))); - - final InputEvent initialEvent = Mockito.mock(InputEvent.class); - environment.publishInputEvent(initialEvent); - - final DreamTouchHandler.TouchSession session = captureSession(touchHandler); - session.registerCallback(callback); - session.pop(); - environment.executeAll(); - - verify(callback).onRemoved(); - } - - @Test - public void testPauseWithNoActiveSessions() { - final DreamTouchHandler touchHandler = createTouchHandler(); - - 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 = createTouchHandler(); - - 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.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 = createTouchHandler(); - - 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 = createTouchHandler(); - final DreamTouchHandler touchHandler2 = createTouchHandler(); - final Environment environment = new Environment(Stream.of(touchHandler1, touchHandler2) - .collect(Collectors.toCollection(HashSet::new))); - - final InputEvent initialEvent = Mockito.mock(InputEvent.class); - environment.publishInputEvent(initialEvent); - - final DreamTouchHandler.TouchSession session1 = captureSession(touchHandler1); - final GestureDetector.OnGestureListener gestureListener1 = - registerGestureListener(session1); - - final DreamTouchHandler.TouchSession session2 = captureSession(touchHandler2); - final GestureDetector.OnGestureListener gestureListener2 = - registerGestureListener(session2); - when(gestureListener2.onDown(any())).thenReturn(true); - - final MotionEvent gestureEvent = Mockito.mock(MotionEvent.class); - environment.publishGestureEvent( - onGestureListener -> onGestureListener.onDown(gestureEvent)); - - Mockito.clearInvocations(gestureListener1, gestureListener2); - - final MotionEvent followupEvent = Mockito.mock(MotionEvent.class); - environment.publishGestureEvent( - onGestureListener -> onGestureListener.onDown(followupEvent)); - - verify(gestureListener1, never()).onDown(any()); - verify(gestureListener2).onDown(eq(followupEvent)); - } - - @Test - public void testOnRemovedCallbackOnStopMonitoring() { - final DreamTouchHandler touchHandler = createTouchHandler(); - - final DreamTouchHandler.TouchSession.Callback callback = - Mockito.mock(DreamTouchHandler.TouchSession.Callback.class); - - final Environment environment = new Environment(Stream.of(touchHandler) - .collect(Collectors.toCollection(HashSet::new))); - - final InputEvent initialEvent = Mockito.mock(InputEvent.class); - environment.publishInputEvent(initialEvent); - - final DreamTouchHandler.TouchSession session = captureSession(touchHandler); - session.registerCallback(callback); - - environment.executeAll(); - - environment.updateLifecycle(observerOwnerPair -> { - observerOwnerPair.first.onDestroy(observerOwnerPair.second); - }); - - environment.executeAll(); - - verify(callback).onRemoved(); - } - - public GestureDetector.OnGestureListener registerGestureListener(DreamTouchHandler handler) { - final GestureDetector.OnGestureListener gestureListener = Mockito.mock( - GestureDetector.OnGestureListener.class); - final ArgumentCaptor sessionCaptor = - ArgumentCaptor.forClass(DreamTouchHandler.TouchSession.class); - verify(handler).onSessionStart(sessionCaptor.capture()); - sessionCaptor.getValue().registerGestureListener(gestureListener); - - return gestureListener; - } - - public GestureDetector.OnGestureListener registerGestureListener( - DreamTouchHandler.TouchSession session) { - final GestureDetector.OnGestureListener gestureListener = Mockito.mock( - GestureDetector.OnGestureListener.class); - session.registerGestureListener(gestureListener); - - return gestureListener; - } - - public InputChannelCompat.InputEventListener registerInputEventListener( - DreamTouchHandler.TouchSession session) { - final InputChannelCompat.InputEventListener eventListener = Mockito.mock( - InputChannelCompat.InputEventListener.class); - session.registerInputListener(eventListener); - - return eventListener; - } - - public DreamTouchHandler.TouchSession captureSession(DreamTouchHandler handler) { - final ArgumentCaptor sessionCaptor = - ArgumentCaptor.forClass(DreamTouchHandler.TouchSession.class); - verify(handler).onSessionStart(sessionCaptor.capture()); - return sessionCaptor.getValue(); - } - - public InputChannelCompat.InputEventListener registerInputEventListener( - DreamTouchHandler handler) { - return registerInputEventListener(captureSession(handler)); - } - - private DreamTouchHandler createTouchHandler() { - final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class); - // enable the handler by default - when(touchHandler.isEnabled()).thenReturn(true); - return touchHandler; - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/TouchMonitorTest.java new file mode 100644 index 000000000000..ba275a129c4a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/TouchMonitorTest.java @@ -0,0 +1,643 @@ +/* + * Copyright (C) 2022 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.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; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Rect; +import android.graphics.Region; +import android.testing.AndroidTestingRunner; +import android.util.Pair; +import android.view.GestureDetector; +import android.view.IWindowManager; +import android.view.InputEvent; +import android.view.MotionEvent; + +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.test.filters.SmallTest; + +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; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class TouchMonitorTest extends SysuiTestCase { + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + private static class Environment { + private final InputSessionComponent.Factory mInputFactory; + private final InputSession mInputSession; + private final Lifecycle mLifecycle; + private final LifecycleOwner mLifecycleOwner; + private final TouchMonitor mMonitor; + 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 FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock()); + private final Rect mDisplayBounds = Mockito.mock(Rect.class); + private final IWindowManager mIWindowManager; + + Environment(Set handlers) { + mLifecycle = Mockito.mock(Lifecycle.class); + mLifecycleOwner = Mockito.mock(LifecycleOwner.class); + mIWindowManager = Mockito.mock(IWindowManager.class); + + mInputFactory = Mockito.mock(InputSessionComponent.Factory.class); + final InputSessionComponent inputComponent = Mockito.mock(InputSessionComponent.class); + mInputSession = Mockito.mock(InputSession.class); + + when(mInputFactory.create(any(), any(), any(), anyBoolean())) + .thenReturn(inputComponent); + when(inputComponent.getInputSession()).thenReturn(mInputSession); + + mDisplayHelper = Mockito.mock(DisplayHelper.class); + when(mDisplayHelper.getMaxBounds(anyInt(), anyInt())) + .thenReturn(mDisplayBounds); + mMonitor = new TouchMonitor(mExecutor, mBackgroundExecutor, + mLifecycle, mInputFactory, mDisplayHelper, handlers, mIWindowManager, 0); + mMonitor.init(); + + final ArgumentCaptor lifecycleObserverCaptor = + ArgumentCaptor.forClass(LifecycleObserver.class); + verify(mLifecycle).addObserver(lifecycleObserverCaptor.capture()); + assertThat(lifecycleObserverCaptor.getValue() instanceof DefaultLifecycleObserver) + .isTrue(); + mLifecycleObserver = (DefaultLifecycleObserver) lifecycleObserverCaptor.getValue(); + + updateLifecycle(observer -> observer.first.onResume(observer.second)); + + // Capture creation request. + final ArgumentCaptor inputEventListenerCaptor = + ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); + final ArgumentCaptor gestureListenerCaptor = + ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); + verify(mInputFactory).create(any(), inputEventListenerCaptor.capture(), + gestureListenerCaptor.capture(), + eq(true)); + mEventListener = inputEventListenerCaptor.getValue(); + mGestureListener = gestureListenerCaptor.getValue(); + } + + public Rect getDisplayBounds() { + return mDisplayBounds; + } + + void executeAll() { + mExecutor.runAllReady(); + } + + void publishInputEvent(InputEvent event) { + mEventListener.onInputEvent(event); + } + + void publishGestureEvent(Consumer listenerConsumer) { + listenerConsumer.accept(mGestureListener); + } + + void updateLifecycle(Consumer> consumer) { + consumer.accept(Pair.create(mLifecycleObserver, mLifecycleOwner)); + } + + void verifyInputSessionDispose() { + verify(mInputSession).dispose(); + Mockito.clearInvocations(mInputSession); + } + } + + @Test + public void testReportedDisplayBounds() { + final TouchHandler touchHandler = createTouchHandler(); + 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(), any()); + final ArgumentCaptor touchSessionArgumentCaptor = + ArgumentCaptor.forClass(TouchHandler.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 TouchHandler touchHandler = createTouchHandler(); + final Rect touchArea = new Rect(4, 4, 8 , 8); + + doAnswer(invocation -> { + final Region region = (Region) invocation.getArguments()[1]; + region.set(touchArea); + return null; + }).when(touchHandler).getTouchInitiationRegion(any(), any(), any()); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new))); + + // Ensure touch outside specified region is not delivered. + final MotionEvent initialEvent = Mockito.mock(MotionEvent.class); + when(initialEvent.getX()).thenReturn(0.0f); + when(initialEvent.getY()).thenReturn(1.0f); + environment.publishInputEvent(initialEvent); + verify(touchHandler, never()).onSessionStart(any()); + + // Make sure touch inside region causes session start. + when(initialEvent.getX()).thenReturn(5.0f); + when(initialEvent.getY()).thenReturn(5.0f); + environment.publishInputEvent(initialEvent); + verify(touchHandler).onSessionStart(any()); + } + + @Test + public void testSessionCount() { + final TouchHandler touchHandler = createTouchHandler(); + final Rect touchArea = new Rect(4, 4, 8 , 8); + + final TouchHandler unzonedTouchHandler = createTouchHandler(); + doAnswer(invocation -> { + final Region region = (Region) invocation.getArguments()[1]; + region.set(touchArea); + return null; + }).when(touchHandler).getTouchInitiationRegion(any(), any(), any()); + + final Environment environment = new Environment(Stream.of(touchHandler, unzonedTouchHandler) + .collect(Collectors.toCollection(HashSet::new))); + + // Ensure touch outside specified region is delivered to unzoned touch handler. + final MotionEvent initialEvent = Mockito.mock(MotionEvent.class); + when(initialEvent.getX()).thenReturn(0.0f); + when(initialEvent.getY()).thenReturn(1.0f); + environment.publishInputEvent(initialEvent); + + ArgumentCaptor touchSessionCaptor = ArgumentCaptor.forClass( + TouchHandler.TouchSession.class); + + // Make sure only one active session. + { + verify(unzonedTouchHandler).onSessionStart(touchSessionCaptor.capture()); + final TouchHandler.TouchSession touchSession = touchSessionCaptor.getValue(); + assertThat(touchSession.getActiveSessionCount()).isEqualTo(1); + touchSession.pop(); + environment.executeAll(); + } + + // Make sure touch inside the touch region. + when(initialEvent.getX()).thenReturn(5.0f); + when(initialEvent.getY()).thenReturn(5.0f); + environment.publishInputEvent(initialEvent); + + // Make sure there are two active sessions. + { + verify(touchHandler).onSessionStart(touchSessionCaptor.capture()); + final TouchHandler.TouchSession touchSession = touchSessionCaptor.getValue(); + assertThat(touchSession.getActiveSessionCount()).isEqualTo(2); + touchSession.pop(); + } + } + + + @Test + public void testNoActiveSessionWhenHandlerDisabled() { + final TouchHandler touchHandler = Mockito.mock(TouchHandler.class); + // disable the handler + when(touchHandler.isEnabled()).thenReturn(false); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new))); + final MotionEvent initialEvent = Mockito.mock(MotionEvent.class); + when(initialEvent.getX()).thenReturn(5.0f); + when(initialEvent.getY()).thenReturn(5.0f); + environment.publishInputEvent(initialEvent); + + // Make sure there is no active session. + verify(touchHandler, never()).onSessionStart(any()); + verify(touchHandler, never()).getTouchInitiationRegion(any(), any(), any()); + } + + @Test + public void testInputEventPropagation() { + final TouchHandler touchHandler = createTouchHandler(); + + 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)); + } + + @Test + public void testInputEventPropagationAfterRemoval() { + final TouchHandler touchHandler = createTouchHandler(); + + 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 TouchHandler.TouchSession session = captureSession(touchHandler); + final InputChannelCompat.InputEventListener eventListener = + registerInputEventListener(session); + + session.pop(); + environment.executeAll(); + + final InputEvent event = Mockito.mock(InputEvent.class); + environment.publishInputEvent(event); + + verify(eventListener, never()).onInputEvent(eq(event)); + } + + @Test + public void testInputGesturePropagation() { + final TouchHandler touchHandler = createTouchHandler(); + + 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 GestureDetector.OnGestureListener gestureListener = + registerGestureListener(touchHandler); + + final MotionEvent event = Mockito.mock(MotionEvent.class); + environment.publishGestureEvent(onGestureListener -> onGestureListener.onShowPress(event)); + verify(gestureListener).onShowPress(eq(event)); + } + + @Test + public void testGestureConsumption() { + final TouchHandler touchHandler = createTouchHandler(); + + 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 GestureDetector.OnGestureListener gestureListener = + registerGestureListener(touchHandler); + + when(gestureListener.onDown(any())).thenReturn(true); + final MotionEvent event = Mockito.mock(MotionEvent.class); + environment.publishGestureEvent(onGestureListener -> { + assertThat(onGestureListener.onDown(event)).isTrue(); + }); + + verify(gestureListener).onDown(eq(event)); + } + + @Test + public void testBroadcast() { + final TouchHandler touchHandler = createTouchHandler(); + final TouchHandler touchHandler2 = createTouchHandler(); + when(touchHandler2.isEnabled()).thenReturn(true); + + final Environment environment = new Environment(Stream.of(touchHandler, touchHandler2) + .collect(Collectors.toCollection(HashSet::new))); + + final InputEvent initialEvent = Mockito.mock(InputEvent.class); + environment.publishInputEvent(initialEvent); + + final HashSet inputListeners = new HashSet<>(); + + inputListeners.add(registerInputEventListener(touchHandler)); + inputListeners.add(registerInputEventListener(touchHandler)); + inputListeners.add(registerInputEventListener(touchHandler2)); + + final MotionEvent event = Mockito.mock(MotionEvent.class); + environment.publishInputEvent(event); + + inputListeners + .stream() + .forEach(inputEventListener -> verify(inputEventListener).onInputEvent(event)); + } + + @Test + public void testPush() throws InterruptedException, ExecutionException { + final TouchHandler touchHandler = createTouchHandler(); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new))); + + final InputEvent initialEvent = Mockito.mock(InputEvent.class); + environment.publishInputEvent(initialEvent); + + final TouchHandler.TouchSession session = captureSession(touchHandler); + final InputChannelCompat.InputEventListener eventListener = + registerInputEventListener(session); + + final ListenableFuture frontSessionFuture = session.push(); + environment.executeAll(); + final TouchHandler.TouchSession frontSession = frontSessionFuture.get(); + final InputChannelCompat.InputEventListener frontEventListener = + registerInputEventListener(frontSession); + + final MotionEvent event = Mockito.mock(MotionEvent.class); + environment.publishInputEvent(event); + + verify(frontEventListener).onInputEvent(eq(event)); + verify(eventListener, never()).onInputEvent(any()); + + Mockito.clearInvocations(eventListener, frontEventListener); + + ListenableFuture sessionFuture = frontSession.pop(); + environment.executeAll(); + + TouchHandler.TouchSession returnedSession = sessionFuture.get(); + assertThat(session == returnedSession).isTrue(); + + environment.executeAll(); + + final MotionEvent followupEvent = Mockito.mock(MotionEvent.class); + environment.publishInputEvent(followupEvent); + + verify(eventListener).onInputEvent(eq(followupEvent)); + verify(frontEventListener, never()).onInputEvent(any()); + } + + @Test + public void testPop() { + final TouchHandler touchHandler = createTouchHandler(); + + final TouchHandler.TouchSession.Callback callback = + Mockito.mock(TouchHandler.TouchSession.Callback.class); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new))); + + final InputEvent initialEvent = Mockito.mock(InputEvent.class); + environment.publishInputEvent(initialEvent); + + final TouchHandler.TouchSession session = captureSession(touchHandler); + session.registerCallback(callback); + session.pop(); + environment.executeAll(); + + verify(callback).onRemoved(); + } + + @Test + public void testPauseWithNoActiveSessions() { + final TouchHandler touchHandler = createTouchHandler(); + + 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 TouchHandler touchHandler = createTouchHandler(); + + 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(TouchHandler.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 TouchHandler touchHandler = createTouchHandler(); + + 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(TouchHandler.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 TouchHandler touchHandler1 = createTouchHandler(); + final TouchHandler touchHandler2 = createTouchHandler(); + final Environment environment = new Environment(Stream.of(touchHandler1, touchHandler2) + .collect(Collectors.toCollection(HashSet::new))); + + final InputEvent initialEvent = Mockito.mock(InputEvent.class); + environment.publishInputEvent(initialEvent); + + final TouchHandler.TouchSession session1 = captureSession(touchHandler1); + final GestureDetector.OnGestureListener gestureListener1 = + registerGestureListener(session1); + + final TouchHandler.TouchSession session2 = captureSession(touchHandler2); + final GestureDetector.OnGestureListener gestureListener2 = + registerGestureListener(session2); + when(gestureListener2.onDown(any())).thenReturn(true); + + final MotionEvent gestureEvent = Mockito.mock(MotionEvent.class); + environment.publishGestureEvent( + onGestureListener -> onGestureListener.onDown(gestureEvent)); + + Mockito.clearInvocations(gestureListener1, gestureListener2); + + final MotionEvent followupEvent = Mockito.mock(MotionEvent.class); + environment.publishGestureEvent( + onGestureListener -> onGestureListener.onDown(followupEvent)); + + verify(gestureListener1, never()).onDown(any()); + verify(gestureListener2).onDown(eq(followupEvent)); + } + + @Test + public void testOnRemovedCallbackOnStopMonitoring() { + final TouchHandler touchHandler = createTouchHandler(); + + final TouchHandler.TouchSession.Callback callback = + Mockito.mock(TouchHandler.TouchSession.Callback.class); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new))); + + final InputEvent initialEvent = Mockito.mock(InputEvent.class); + environment.publishInputEvent(initialEvent); + + final TouchHandler.TouchSession session = captureSession(touchHandler); + session.registerCallback(callback); + + environment.executeAll(); + + environment.updateLifecycle(observerOwnerPair -> { + observerOwnerPair.first.onDestroy(observerOwnerPair.second); + }); + + environment.executeAll(); + + verify(callback).onRemoved(); + } + + private GestureDetector.OnGestureListener registerGestureListener(TouchHandler handler) { + final GestureDetector.OnGestureListener gestureListener = Mockito.mock( + GestureDetector.OnGestureListener.class); + final ArgumentCaptor sessionCaptor = + ArgumentCaptor.forClass(TouchHandler.TouchSession.class); + verify(handler).onSessionStart(sessionCaptor.capture()); + sessionCaptor.getValue().registerGestureListener(gestureListener); + + return gestureListener; + } + + private GestureDetector.OnGestureListener registerGestureListener( + TouchHandler.TouchSession session) { + final GestureDetector.OnGestureListener gestureListener = Mockito.mock( + GestureDetector.OnGestureListener.class); + session.registerGestureListener(gestureListener); + + return gestureListener; + } + + private InputChannelCompat.InputEventListener registerInputEventListener( + TouchHandler.TouchSession session) { + final InputChannelCompat.InputEventListener eventListener = Mockito.mock( + InputChannelCompat.InputEventListener.class); + session.registerInputListener(eventListener); + + return eventListener; + } + + private TouchHandler.TouchSession captureSession(TouchHandler handler) { + final ArgumentCaptor sessionCaptor = + ArgumentCaptor.forClass(TouchHandler.TouchSession.class); + verify(handler).onSessionStart(sessionCaptor.capture()); + return sessionCaptor.getValue(); + } + + private InputChannelCompat.InputEventListener registerInputEventListener( + TouchHandler handler) { + return registerInputEventListener(captureSession(handler)); + } + + private TouchHandler createTouchHandler() { + final TouchHandler touchHandler = Mockito.mock(TouchHandler.class); + // enable the handler by default + when(touchHandler.isEnabled()).thenReturn(true); + return touchHandler; + } +} -- cgit v1.2.3-59-g8ed1b