Merge "Dream overlay touch management."
This commit is contained in:
@@ -107,6 +107,7 @@ android_library {
|
||||
"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 @@ android_library {
|
||||
"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",
|
||||
|
||||
@@ -29,9 +29,12 @@ import androidx.lifecycle.LifecycleRegistry;
|
||||
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 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
|
||||
// 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 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
|
||||
|
||||
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 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
|
||||
setCurrentState(Lifecycle.State.DESTROYED);
|
||||
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
|
||||
windowManager.removeView(mWindow.getDecorView());
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModelStore;
|
||||
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 @@ public interface DreamOverlayComponent {
|
||||
|
||||
/** Builds a {@link androidx.lifecycle.LifecycleOwner} */
|
||||
LifecycleOwner getLifecycleOwner();
|
||||
|
||||
/** Builds a {@link DreamOverlayTouchMonitor} */
|
||||
DreamOverlayTouchMonitor getDreamOverlayTouchMonitor();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.os.Handler;
|
||||
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.broadcast.BroadcastDispatcher;
|
||||
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 javax.inject.Named;
|
||||
import dagger.Lazy;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoSet;
|
||||
|
||||
/** Dagger module for {@link DreamOverlayComponent}. */
|
||||
@Module
|
||||
@@ -140,4 +143,18 @@ public abstract class DreamOverlayModule {
|
||||
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 -> { };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -33,12 +33,12 @@ import android.view.WindowManagerImpl;
|
||||
|
||||
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 @@ public class DreamOverlayServiceTest extends SysuiTestCase {
|
||||
@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 @@ public class DreamOverlayServiceTest extends SysuiTestCase {
|
||||
@Mock
|
||||
DreamOverlayContainerViewController mDreamOverlayContainerViewController;
|
||||
|
||||
@Mock
|
||||
KeyguardUpdateMonitor mKeyguardUpdateMonitor;
|
||||
|
||||
@Mock
|
||||
DreamOverlayTouchMonitor mDreamOverlayTouchMonitor;
|
||||
|
||||
|
||||
DreamOverlayService mService;
|
||||
|
||||
@Before
|
||||
@@ -102,6 +105,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase {
|
||||
.thenReturn(mLifecycleOwner);
|
||||
when(mDreamOverlayComponent.getLifecycleRegistry())
|
||||
.thenReturn(mLifecycleRegistry);
|
||||
when(mDreamOverlayComponent.getDreamOverlayTouchMonitor())
|
||||
.thenReturn(mDreamOverlayTouchMonitor);
|
||||
when(mDreamOverlayComponentFactory
|
||||
.create(any(), any()))
|
||||
.thenReturn(mDreamOverlayComponent);
|
||||
@@ -109,7 +114,8 @@ public class DreamOverlayServiceTest extends SysuiTestCase {
|
||||
.thenReturn(mDreamOverlayContainerView);
|
||||
|
||||
mService = new DreamOverlayService(mContext, mMainExecutor,
|
||||
mDreamOverlayComponentFactory);
|
||||
mDreamOverlayComponentFactory,
|
||||
mKeyguardUpdateMonitor);
|
||||
final IBinder proxy = mService.onBind(new Intent());
|
||||
final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
|
||||
|
||||
|
||||
@@ -85,7 +85,9 @@ public class ComplicationCollectionLiveDataTest extends SysuiTestCase {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public class ComplicationLayoutParamsTest extends SysuiTestCase {
|
||||
final boolean properDirection = (invalidPosition & position) != invalidPosition;
|
||||
|
||||
try {
|
||||
final ComplicationLayoutParams params = new ComplicationLayoutParams(
|
||||
new ComplicationLayoutParams(
|
||||
100,
|
||||
100,
|
||||
position,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user