Merge "Dream overlay touch management."

This commit is contained in:
Bryce Lee
2022-01-27 19:22:18 +00:00
committed by Android (Google) Code Review
14 changed files with 1075 additions and 13 deletions

View File

@@ -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",

View File

@@ -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());

View File

@@ -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 {
}

View File

@@ -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();
}

View File

@@ -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 -> { };
}
}

View File

@@ -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);
}
}

View File

@@ -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) { }
}

View File

@@ -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();
}
}
}

View File

@@ -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";
}

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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));
}
}