Dream overlay touch management.
This changelist introduces the DreamOverlayTouchMonitor, a component for observing touches and gestures over the dream overlay and reporting them to a set of consumers. DreamOverlayTouchMonitor is responsible for enabling/disabling touch listening based on the dream overlay service state. It enables listening entities to isolate touches to just their listeners when appropriate and handles resetting listening state across touches.
Bug: 211506329
Test: atest DreamOverlayTouchMonitorTest
Change-Id: I44bcba982dd3a73ebef8cfa52e9ad65216316c08
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 137a1fd..b1cfb11 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -107,6 +107,7 @@
"androidx.slice_slice-view",
"androidx.slice_slice-builders",
"androidx.arch.core_core-runtime",
+ "androidx.lifecycle_lifecycle-common-java8",
"androidx.lifecycle_lifecycle-extensions",
"androidx.dynamicanimation_dynamicanimation",
"androidx-constraintlayout_constraintlayout",
@@ -209,6 +210,7 @@
"androidx.slice_slice-view",
"androidx.slice_slice-builders",
"androidx.arch.core_core-runtime",
+ "androidx.lifecycle_lifecycle-common-java8",
"androidx.lifecycle_lifecycle-extensions",
"androidx.dynamicanimation_dynamicanimation",
"androidx-constraintlayout_constraintlayout",
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 16ed1fb..3ee0cad 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -29,9 +29,12 @@
import androidx.lifecycle.ViewModelStore;
import com.android.internal.policy.PhoneWindow;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
import java.util.concurrent.Executor;
@@ -53,6 +56,7 @@
// A controller for the dream overlay container view (which contains both the status bar and the
// content area).
private final DreamOverlayContainerViewController mDreamOverlayContainerViewController;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
// A reference to the {@link Window} used to hold the dream overlay.
private Window mWindow;
@@ -68,19 +72,40 @@
private ViewModelStore mViewModelStore = new ViewModelStore();
+ private DreamOverlayTouchMonitor mDreamOverlayTouchMonitor;
+
+ private final KeyguardUpdateMonitorCallback mKeyguardCallback =
+ new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onShadeExpandedChanged(boolean expanded) {
+ if (mLifecycleRegistry.getCurrentState() != Lifecycle.State.RESUMED
+ && mLifecycleRegistry.getCurrentState() != Lifecycle.State.STARTED) {
+ return;
+ }
+
+ mLifecycleRegistry.setCurrentState(
+ expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED);
+ }
+ };
+
@Inject
public DreamOverlayService(
Context context,
@Main Executor executor,
- DreamOverlayComponent.Factory dreamOverlayComponentFactory) {
+ DreamOverlayComponent.Factory dreamOverlayComponentFactory,
+ KeyguardUpdateMonitor keyguardUpdateMonitor) {
mContext = context;
mExecutor = executor;
+ mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
final DreamOverlayComponent component =
dreamOverlayComponentFactory.create(mViewModelStore, mHost);
mDreamOverlayContainerViewController = component.getDreamOverlayContainerViewController();
setCurrentState(Lifecycle.State.CREATED);
mLifecycleRegistry = component.getLifecycleRegistry();
+ mDreamOverlayTouchMonitor = component.getDreamOverlayTouchMonitor();
+ mDreamOverlayTouchMonitor.init();
}
private void setCurrentState(Lifecycle.State state) {
@@ -89,6 +114,7 @@
@Override
public void onDestroy() {
+ mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
setCurrentState(Lifecycle.State.DESTROYED);
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
windowManager.removeView(mWindow.getDecorView());
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index d5053a0..3d2f924 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -16,13 +16,18 @@
package com.android.systemui.dreams.dagger;
+import com.android.systemui.dreams.touch.dagger.DreamTouchModule;
+
import dagger.Module;
/**
* Dagger Module providing Communal-related functionality.
*/
-@Module(subcomponents = {
- DreamOverlayComponent.class,
-})
+@Module(includes = {
+ DreamTouchModule.class,
+ },
+ subcomponents = {
+ DreamOverlayComponent.class,
+ })
public interface DreamModule {
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
index f0ab696..05ab901 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
@@ -25,6 +25,7 @@
import com.android.systemui.dreams.DreamOverlayContainerViewController;
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.dreams.complication.dagger.ComplicationModule;
+import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -64,4 +65,7 @@
/** Builds a {@link androidx.lifecycle.LifecycleOwner} */
LifecycleOwner getLifecycleOwner();
+
+ /** Builds a {@link DreamOverlayTouchMonitor} */
+ DreamOverlayTouchMonitor getDreamOverlayTouchMonitor();
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index b56aa2c..503817a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -22,6 +22,7 @@
import android.view.LayoutInflater;
import android.view.ViewGroup;
+import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
@@ -33,6 +34,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayContainerView;
import com.android.systemui.dreams.DreamOverlayStatusBarView;
+import com.android.systemui.dreams.touch.DreamTouchHandler;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
@@ -42,6 +44,7 @@
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
+import dagger.multibindings.IntoSet;
/** Dagger module for {@link DreamOverlayComponent}. */
@Module
@@ -140,4 +143,18 @@
static LifecycleRegistry providesLifecycleRegistry(LifecycleOwner lifecycleOwner) {
return new LifecycleRegistry(lifecycleOwner);
}
+
+ @Provides
+ @DreamOverlayComponent.DreamOverlayScope
+ static Lifecycle providesLifecycle(LifecycleOwner lifecycleOwner) {
+ return lifecycleOwner.getLifecycle();
+ }
+
+ // TODO: This stub should be removed once there is a {@link DreamTouchHandler}
+ // implementation present.
+ @Provides
+ @IntoSet
+ static DreamTouchHandler provideDreamTouchHandler() {
+ return session -> { };
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java
new file mode 100644
index 0000000..3e5efb2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitor.java
@@ -0,0 +1,368 @@
+/*
+ * 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.view.GestureDetector;
+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.Main;
+import com.android.systemui.dreams.touch.dagger.InputSessionComponent;
+import com.android.systemui.shared.system.InputChannelCompat;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Collection;
+import java.util.HashSet;
+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.
+ private final Executor mExecutor;
+ private final Lifecycle mLifecycle;
+
+ /**
+ * Adds a new {@link TouchSessionImpl} to participate in receiving future touches and gestures.
+ */
+ private ListenableFuture<DreamTouchHandler.TouchSession> push(
+ TouchSessionImpl touchSessionImpl) {
+ return CallbackToFutureAdapter.getFuture(completer -> {
+ mExecutor.execute(() -> {
+ if (!mActiveTouchSessions.remove(touchSessionImpl)) {
+ completer.set(null);
+ return;
+ }
+
+ final TouchSessionImpl touchSession =
+ new TouchSessionImpl(this, touchSessionImpl);
+ mActiveTouchSessions.add(touchSession);
+ completer.set(touchSession);
+ });
+
+ return "DreamOverlayTouchMonitor::push";
+ });
+ }
+
+ /**
+ * Removes a {@link TouchSessionImpl} from receiving further updates.
+ */
+ private ListenableFuture<DreamTouchHandler.TouchSession> pop(
+ TouchSessionImpl touchSessionImpl) {
+ return CallbackToFutureAdapter.getFuture(completer -> {
+ mExecutor.execute(() -> {
+ if (mActiveTouchSessions.remove(touchSessionImpl)) {
+ touchSessionImpl.onRemoved();
+
+ final TouchSessionImpl predecessor = touchSessionImpl.getPredecessor();
+
+ if (predecessor != null) {
+ mActiveTouchSessions.add(predecessor);
+ }
+
+ completer.set(predecessor);
+ }
+ });
+
+ return "DreamOverlayTouchMonitor::pop";
+ });
+ }
+
+ /**
+ * {@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<InputChannelCompat.InputEventListener> mEventListeners =
+ new HashSet<>();
+ private final HashSet<GestureDetector.OnGestureListener> mGestureListeners =
+ new HashSet<>();
+ private final HashSet<Callback> mCallbacks = new HashSet<>();
+
+ private final TouchSessionImpl mPredecessor;
+ private final DreamOverlayTouchMonitor mTouchMonitor;
+
+ TouchSessionImpl(DreamOverlayTouchMonitor touchMonitor, TouchSessionImpl predecessor) {
+ mPredecessor = predecessor;
+ mTouchMonitor = touchMonitor;
+ }
+
+ @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<DreamTouchHandler.TouchSession> push() {
+ return mTouchMonitor.push(this);
+ }
+
+ @Override
+ public ListenableFuture<DreamTouchHandler.TouchSession> pop() {
+ return mTouchMonitor.pop(this);
+ }
+
+ /**
+ * Returns the active listeners to receive touch events.
+ */
+ public Collection<InputChannelCompat.InputEventListener> getEventListeners() {
+ return mEventListeners;
+ }
+
+ /**
+ * Returns the active listeners to receive gesture events.
+ */
+ public Collection<GestureDetector.OnGestureListener> 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() {
+ mCallbacks.forEach(callback -> callback.onRemoved());
+ }
+ }
+
+ /**
+ * 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();
+ }
+ };
+
+ /**
+ * When invoked, instantiates a new {@link InputSession} to monitor touch events.
+ */
+ private void startMonitoring() {
+ stopMonitoring();
+ mCurrentInputSession = mInputSessionFactory.create(
+ "dreamOverlay",
+ mInputEventListener,
+ mOnGestureListener,
+ true)
+ .getInputSession();
+ }
+
+ /**
+ * Destroys any active {@link InputSession}.
+ */
+ private void stopMonitoring() {
+ if (mCurrentInputSession == null) {
+ return;
+ }
+
+ mCurrentInputSession.dispose();
+ mCurrentInputSession = null;
+ }
+
+
+ private final HashSet<TouchSessionImpl> mActiveTouchSessions = new HashSet<>();
+ private final Collection<DreamTouchHandler> mHandlers;
+
+ 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()) {
+ for (DreamTouchHandler handler : mHandlers) {
+ final TouchSessionImpl sessionStack =
+ new TouchSessionImpl(DreamOverlayTouchMonitor.this, null);
+ mActiveTouchSessions.add(sessionStack);
+ handler.onSessionStart(sessionStack);
+ }
+ }
+
+ // Find active sessions and invoke on InputEvent.
+ mActiveTouchSessions.stream()
+ .map(touchSessionStack -> touchSessionStack.getEventListeners())
+ .flatMap(Collection::stream)
+ .forEach(inputEventListener -> inputEventListener.onInputEvent(ev));
+ }
+ };
+
+ /**
+ * 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<TouchSessionImpl> 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<GestureDetector.OnGestureListener> 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;
+
+ /**
+ * 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,
+ Lifecycle lifecycle,
+ InputSessionComponent.Factory inputSessionFactory,
+ Set<DreamTouchHandler> handlers) {
+ mHandlers = handlers;
+ mInputSessionFactory = inputSessionFactory;
+ mExecutor = executor;
+ mLifecycle = lifecycle;
+ }
+
+ /**
+ * Initializes the monitor. should only be called once after creation.
+ */
+ public void init() {
+ mLifecycle.addObserver(mLifecycleObserver);
+ }
+
+ private void isolate(Set<TouchSessionImpl> sessions) {
+ Collection<TouchSessionImpl> 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
new file mode 100644
index 0000000..c73ff73
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/DreamTouchHandler.java
@@ -0,0 +1,92 @@
+/*
+ * 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.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<TouchSession> 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<TouchSession> pop();
+ }
+
+ /**
+ * 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);
+
+ /**
+ * Invoked when a session has ended. This will be invoked for every session completion, even
+ * those that are removed through {@link TouchSession#pop()}.
+ * @param session
+ */
+ default void onSessionEnd(TouchSession session) { }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java
new file mode 100644
index 0000000..4382757
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java
@@ -0,0 +1,90 @@
+/*
+ * 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.android.systemui.dreams.touch.dagger.DreamTouchModule.INPUT_SESSION_NAME;
+import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.PILFER_ON_GESTURE_CONSUME;
+
+import android.os.Looper;
+import android.view.Choreographer;
+import android.view.Display;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+import com.android.systemui.shared.system.InputChannelCompat;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * {@link InputSession} encapsulates components behind input monitoring and handles their lifecycle.
+ * Sessions are meant to be disposable; actions such as exclusively capturing touch events is modal
+ * and destroying the sessions allows a reset. Additionally, {@link InputSession} is meant to have
+ * a single listener for input and gesture. Any broadcasting must be accomplished elsewhere.
+ */
+public class InputSession {
+ private final InputMonitorCompat mInputMonitor;
+ private final InputChannelCompat.InputEventReceiver mInputEventReceiver;
+ private final GestureDetector mGestureDetector;
+
+ /**
+ * Default session constructor.
+ * @param sessionName The session name that will be applied to the underlying
+ * {@link InputMonitorCompat}.
+ * @param inputEventListener A listener to receive input events.
+ * @param gestureListener A listener to receive gesture events.
+ * @param pilferOnGestureConsume Whether touch events should be pilfered after a gesture has
+ * been consumed.
+ */
+ @Inject
+ public InputSession(@Named(INPUT_SESSION_NAME) String sessionName,
+ InputChannelCompat.InputEventListener inputEventListener,
+ GestureDetector.OnGestureListener gestureListener,
+ @Named(PILFER_ON_GESTURE_CONSUME) boolean pilferOnGestureConsume) {
+ mInputMonitor = new InputMonitorCompat(sessionName, Display.DEFAULT_DISPLAY);
+ mGestureDetector = new GestureDetector(gestureListener);
+
+ mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(),
+ Choreographer.getInstance(),
+ ev -> {
+ // Process event. Since sometimes input may be a prerequisite for some
+ // gesture logic, process input first.
+ inputEventListener.onInputEvent(ev);
+
+ if (ev instanceof MotionEvent
+ && mGestureDetector.onTouchEvent((MotionEvent) ev)
+ && pilferOnGestureConsume) {
+ mInputMonitor.pilferPointers();
+ }
+ });
+ }
+
+ /**
+ * Destroys the {@link InputSession}, removing any component from listening to future touch
+ * events.
+ */
+ public void dispose() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ }
+
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java
new file mode 100644
index 0000000..7b77b59
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java
@@ -0,0 +1,30 @@
+/*
+ * 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.dagger;
+
+import dagger.Module;
+
+/**
+ * {@link DreamTouchModule} encapsulates dream touch-related components.
+ */
+@Module(subcomponents = {
+ InputSessionComponent.class,
+})
+public interface DreamTouchModule {
+ String INPUT_SESSION_NAME = "INPUT_SESSION_NAME";
+ String PILFER_ON_GESTURE_CONSUME = "PILFER_ON_GESTURE_CONSUME";
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionComponent.java
new file mode 100644
index 0000000..ad59a2e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionComponent.java
@@ -0,0 +1,51 @@
+/*
+ * 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.dagger;
+
+import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.INPUT_SESSION_NAME;
+import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.PILFER_ON_GESTURE_CONSUME;
+
+import android.view.GestureDetector;
+
+import com.android.systemui.dreams.touch.InputSession;
+import com.android.systemui.shared.system.InputChannelCompat;
+
+import javax.inject.Named;
+
+import dagger.BindsInstance;
+import dagger.Subcomponent;
+
+/**
+ * {@link InputSessionComponent} generates {@link InputSession} with specific instances bound for
+ * the session name and whether touches should be pilfered when consumed.
+ */
+@Subcomponent
+public interface InputSessionComponent {
+ /**
+ * Generates {@link InputSessionComponent}.
+ */
+ @Subcomponent.Factory
+ interface Factory {
+ InputSessionComponent create(@Named(INPUT_SESSION_NAME) @BindsInstance String name,
+ @BindsInstance InputChannelCompat.InputEventListener inputEventListener,
+ @BindsInstance GestureDetector.OnGestureListener gestureListener,
+ @Named(PILFER_ON_GESTURE_CONSUME) @BindsInstance boolean pilferOnGestureConsume);
+ }
+
+ /** */
+ InputSession getInputSession();
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index 6b156a4..b3b5fa5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -33,12 +33,12 @@
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
-import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.SysuiTestableContext;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import com.android.systemui.utils.leaks.LeakCheckedTest;
@@ -65,10 +65,6 @@
@Rule
public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
- @Rule
- public SysuiTestableContext mContext = new SysuiTestableContext(
- InstrumentationRegistry.getContext(), mLeakCheck);
-
WindowManager.LayoutParams mWindowParams = new WindowManager.LayoutParams();
@Mock
@@ -89,6 +85,13 @@
@Mock
DreamOverlayContainerViewController mDreamOverlayContainerViewController;
+ @Mock
+ KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+
+ @Mock
+ DreamOverlayTouchMonitor mDreamOverlayTouchMonitor;
+
+
DreamOverlayService mService;
@Before
@@ -102,6 +105,8 @@
.thenReturn(mLifecycleOwner);
when(mDreamOverlayComponent.getLifecycleRegistry())
.thenReturn(mLifecycleRegistry);
+ when(mDreamOverlayComponent.getDreamOverlayTouchMonitor())
+ .thenReturn(mDreamOverlayTouchMonitor);
when(mDreamOverlayComponentFactory
.create(any(), any()))
.thenReturn(mDreamOverlayComponent);
@@ -109,7 +114,8 @@
.thenReturn(mDreamOverlayContainerView);
mService = new DreamOverlayService(mContext, mMainExecutor,
- mDreamOverlayComponentFactory);
+ mDreamOverlayComponentFactory,
+ mKeyguardUpdateMonitor);
final IBinder proxy = mService.onBind(new Intent());
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java
index afc0309d..feeea5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationCollectionLiveDataTest.java
@@ -85,7 +85,9 @@
verify(observer).onChanged(collectionCaptor.capture());
- assertThat(collectionCaptor.getValue().equals(targetCollection)).isTrue();
+ final Collection collection = collectionCaptor.getValue();
+ assertThat(collection.containsAll(targetCollection)
+ && targetCollection.containsAll(collection)).isTrue();
Mockito.clearInvocations(observer);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
index d080bbc..967b30d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
@@ -81,7 +81,7 @@
final boolean properDirection = (invalidPosition & position) != invalidPosition;
try {
- final ComplicationLayoutParams params = new ComplicationLayoutParams(
+ new ComplicationLayoutParams(
100,
100,
position,
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
new file mode 100644
index 0000000..74b217b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/DreamOverlayTouchMonitorTest.java
@@ -0,0 +1,369 @@
+/*
+ * 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.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.util.Pair;
+import android.view.GestureDetector;
+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.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 FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+
+ Environment(Set<DreamTouchHandler> handlers) {
+ mLifecycle = Mockito.mock(Lifecycle.class);
+ mLifecycleOwner = Mockito.mock(LifecycleOwner.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);
+
+ mMonitor = new DreamOverlayTouchMonitor(mExecutor, mLifecycle, mInputFactory, handlers);
+ mMonitor.init();
+
+ final ArgumentCaptor<LifecycleObserver> 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<InputChannelCompat.InputEventListener> inputEventListenerCaptor =
+ ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
+ final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
+ ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+ verify(mInputFactory).create(any(), inputEventListenerCaptor.capture(),
+ gestureListenerCaptor.capture(),
+ eq(true));
+ mEventListener = inputEventListenerCaptor.getValue();
+ mGestureListener = gestureListenerCaptor.getValue();
+ }
+
+ void executeAll() {
+ mExecutor.runAllReady();
+ }
+
+ void publishInputEvent(InputEvent event) {
+ mEventListener.onInputEvent(event);
+ }
+
+ void publishGestureEvent(Consumer<GestureDetector.OnGestureListener> listenerConsumer) {
+ listenerConsumer.accept(mGestureListener);
+ }
+
+ void updateLifecycle(Consumer<Pair<DefaultLifecycleObserver, LifecycleOwner>> consumer) {
+ consumer.accept(Pair.create(mLifecycleObserver, mLifecycleOwner));
+ }
+
+ void verifyInputSessionDispose() {
+ verify(mInputSession).dispose();
+ Mockito.clearInvocations(mInputSession);
+ }
+ }
+
+ @Test
+ public void testInputEventPropagation() {
+ final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);
+
+ final Environment environment = new Environment(Stream.of(touchHandler)
+ .collect(Collectors.toCollection(HashSet::new)));
+
+ final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+ environment.publishInputEvent(initialEvent);
+
+ // Ensure session started
+ final InputChannelCompat.InputEventListener eventListener =
+ registerInputEventListener(touchHandler);
+
+ // First event will be missed since we register after the execution loop,
+ final InputEvent event = Mockito.mock(InputEvent.class);
+ environment.publishInputEvent(event);
+ verify(eventListener).onInputEvent(eq(event));
+ }
+
+ @Test
+ public void testInputGesturePropagation() {
+ final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);
+
+ final Environment environment = new Environment(Stream.of(touchHandler)
+ .collect(Collectors.toCollection(HashSet::new)));
+
+ final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+ environment.publishInputEvent(initialEvent);
+
+ // Ensure session started
+ final 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 = Mockito.mock(DreamTouchHandler.class);
+
+ final Environment environment = new Environment(Stream.of(touchHandler)
+ .collect(Collectors.toCollection(HashSet::new)));
+
+ final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+ environment.publishInputEvent(initialEvent);
+
+ // Ensure session started
+ final 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 = Mockito.mock(DreamTouchHandler.class);
+ final DreamTouchHandler touchHandler2 = Mockito.mock(DreamTouchHandler.class);
+
+ 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<InputChannelCompat.InputEventListener> 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 = Mockito.mock(DreamTouchHandler.class);
+
+ final Environment environment = new Environment(Stream.of(touchHandler)
+ .collect(Collectors.toCollection(HashSet::new)));
+
+ final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+ environment.publishInputEvent(initialEvent);
+
+ final DreamTouchHandler.TouchSession session = captureSession(touchHandler);
+ final InputChannelCompat.InputEventListener eventListener =
+ registerInputEventListener(session);
+
+ final ListenableFuture<DreamTouchHandler.TouchSession> 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<DreamTouchHandler.TouchSession> 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 testPause() {
+ final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);
+
+ final Environment environment = new Environment(Stream.of(touchHandler)
+ .collect(Collectors.toCollection(HashSet::new)));
+
+ final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+ environment.publishInputEvent(initialEvent);
+
+ // Ensure session started
+ final InputChannelCompat.InputEventListener eventListener =
+ registerInputEventListener(touchHandler);
+
+ // First event will be missed since we register after the execution loop,
+ final InputEvent event = Mockito.mock(InputEvent.class);
+ environment.publishInputEvent(event);
+ verify(eventListener).onInputEvent(eq(event));
+
+ environment.updateLifecycle(observerOwnerPair -> {
+ observerOwnerPair.first.onPause(observerOwnerPair.second);
+ });
+
+ environment.verifyInputSessionDispose();
+ }
+
+ @Test
+ public void testPilfering() {
+ final DreamTouchHandler touchHandler1 = Mockito.mock(DreamTouchHandler.class);
+ final DreamTouchHandler touchHandler2 = Mockito.mock(DreamTouchHandler.class);
+
+ 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));
+ }
+
+ public GestureDetector.OnGestureListener registerGestureListener(DreamTouchHandler handler) {
+ final GestureDetector.OnGestureListener gestureListener = Mockito.mock(
+ GestureDetector.OnGestureListener.class);
+ final ArgumentCaptor<DreamTouchHandler.TouchSession> 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<DreamTouchHandler.TouchSession> sessionCaptor =
+ ArgumentCaptor.forClass(DreamTouchHandler.TouchSession.class);
+ verify(handler).onSessionStart(sessionCaptor.capture());
+ return sessionCaptor.getValue();
+ }
+
+ public InputChannelCompat.InputEventListener registerInputEventListener(
+ DreamTouchHandler handler) {
+ return registerInputEventListener(captureSession(handler));
+ }
+}