| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.dreams.touch; |
| |
| import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; |
| |
| import android.graphics.Rect; |
| import android.graphics.Region; |
| 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.android.systemui.util.display.DisplayHelper; |
| |
| import com.google.common.util.concurrent.ListenableFuture; |
| |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Set; |
| import java.util.concurrent.Executor; |
| import java.util.function.Consumer; |
| import java.util.stream.Collectors; |
| |
| import javax.inject.Inject; |
| |
| /** |
| * {@link DreamOverlayTouchMonitor} is responsible for monitoring touches and gestures over the |
| * dream overlay and redirecting them to a set of listeners. This monitor is in charge of figuring |
| * out when listeners are eligible for receiving touches and filtering the listener pool if |
| * touches are consumed. |
| */ |
| public class DreamOverlayTouchMonitor { |
| // This executor is used to protect {@code mActiveTouchSessions} from being modified |
| // concurrently. Any operation that adds or removes values should use this executor. |
| 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.getBounds(), |
| 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); |
| } |
| |
| if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) { |
| stopMonitoring(false); |
| } |
| }); |
| |
| return "DreamOverlayTouchMonitor::pop"; |
| }); |
| } |
| |
| private int getSessionCount() { |
| return mActiveTouchSessions.size(); |
| } |
| |
| /** |
| * {@link TouchSessionImpl} implements {@link DreamTouchHandler.TouchSession} for |
| * {@link DreamOverlayTouchMonitor}. It enables the monitor to access the associated listeners |
| * and provides the associated client with access to the monitor. |
| */ |
| private static class TouchSessionImpl implements DreamTouchHandler.TouchSession { |
| private final HashSet<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; |
| private final Rect mBounds; |
| |
| TouchSessionImpl(DreamOverlayTouchMonitor touchMonitor, Rect bounds, |
| TouchSessionImpl predecessor) { |
| mPredecessor = predecessor; |
| mTouchMonitor = touchMonitor; |
| mBounds = bounds; |
| } |
| |
| @Override |
| public void registerCallback(Callback callback) { |
| mCallbacks.add(callback); |
| } |
| |
| @Override |
| public boolean registerInputListener( |
| InputChannelCompat.InputEventListener inputEventListener) { |
| return mEventListeners.add(inputEventListener); |
| } |
| |
| @Override |
| public boolean registerGestureListener(GestureDetector.OnGestureListener gestureListener) { |
| return mGestureListeners.add(gestureListener); |
| } |
| |
| @Override |
| public ListenableFuture<DreamTouchHandler.TouchSession> push() { |
| return mTouchMonitor.push(this); |
| } |
| |
| @Override |
| public ListenableFuture<DreamTouchHandler.TouchSession> pop() { |
| return mTouchMonitor.pop(this); |
| } |
| |
| @Override |
| public int getActiveSessionCount() { |
| return mTouchMonitor.getSessionCount(); |
| } |
| |
| /** |
| * 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() { |
| mEventListeners.clear(); |
| mGestureListeners.clear(); |
| final Iterator<Callback> iter = mCallbacks.iterator(); |
| while (iter.hasNext()) { |
| final Callback callback = iter.next(); |
| callback.onRemoved(); |
| iter.remove(); |
| } |
| } |
| |
| @Override |
| public Rect getBounds() { |
| return mBounds; |
| } |
| } |
| |
| /** |
| * This lifecycle observer ensures touch monitoring only occurs while the overlay is "resumed". |
| * This concept is mapped over from the equivalent view definition: The {@link LifecycleOwner} |
| * will report the dream is not resumed when it is obscured (from the notification shade being |
| * expanded for example) or not active (such as when it is destroyed). |
| */ |
| private final LifecycleObserver mLifecycleObserver = new DefaultLifecycleObserver() { |
| @Override |
| public void onResume(@NonNull LifecycleOwner owner) { |
| startMonitoring(); |
| } |
| |
| @Override |
| public void onPause(@NonNull LifecycleOwner owner) { |
| stopMonitoring(false); |
| } |
| |
| @Override |
| public void onDestroy(LifecycleOwner owner) { |
| stopMonitoring(true); |
| } |
| }; |
| |
| /** |
| * When invoked, instantiates a new {@link InputSession} to monitor touch events. |
| */ |
| private void startMonitoring() { |
| stopMonitoring(true); |
| mCurrentInputSession = mInputSessionFactory.create( |
| "dreamOverlay", |
| mInputEventListener, |
| mOnGestureListener, |
| true) |
| .getInputSession(); |
| } |
| |
| /** |
| * Destroys any active {@link InputSession}. |
| */ |
| private void stopMonitoring(boolean force) { |
| if (mCurrentInputSession == null) { |
| return; |
| } |
| |
| if (!mActiveTouchSessions.isEmpty() && !force) { |
| mStopMonitoringPending = true; |
| return; |
| } |
| |
| // When we stop monitoring touches, we must ensure that all active touch sessions and |
| // descendants informed of the removal so any cleanup for active tracking can proceed. |
| mExecutor.execute(() -> mActiveTouchSessions.forEach(touchSession -> { |
| while (touchSession != null) { |
| touchSession.onRemoved(); |
| touchSession = touchSession.getPredecessor(); |
| } |
| })); |
| |
| mCurrentInputSession.dispose(); |
| mCurrentInputSession = null; |
| mStopMonitoringPending = false; |
| } |
| |
| |
| private final HashSet<TouchSessionImpl> mActiveTouchSessions = new HashSet<>(); |
| private final Collection<DreamTouchHandler> mHandlers; |
| private final DisplayHelper mDisplayHelper; |
| |
| private boolean mStopMonitoringPending; |
| |
| private InputChannelCompat.InputEventListener mInputEventListener = |
| new InputChannelCompat.InputEventListener() { |
| @Override |
| public void onInputEvent(InputEvent ev) { |
| // No Active sessions are receiving touches. Create sessions for each listener |
| if (mActiveTouchSessions.isEmpty()) { |
| final HashMap<DreamTouchHandler, DreamTouchHandler.TouchSession> sessionMap = |
| new HashMap<>(); |
| |
| for (DreamTouchHandler handler : mHandlers) { |
| final Rect maxBounds = mDisplayHelper.getMaxBounds(ev.getDisplayId(), |
| TYPE_APPLICATION_OVERLAY); |
| |
| final Region initiationRegion = Region.obtain(); |
| handler.getTouchInitiationRegion(maxBounds, initiationRegion); |
| |
| if (!initiationRegion.isEmpty()) { |
| // Initiation regions require a motion event to determine pointer location |
| // within the region. |
| if (!(ev instanceof MotionEvent)) { |
| continue; |
| } |
| |
| final MotionEvent motionEvent = (MotionEvent) ev; |
| |
| // If the touch event is outside the region, then ignore. |
| if (!initiationRegion.contains(Math.round(motionEvent.getX()), |
| Math.round(motionEvent.getY()))) { |
| continue; |
| } |
| } |
| |
| final TouchSessionImpl sessionStack = new TouchSessionImpl( |
| DreamOverlayTouchMonitor.this, maxBounds, null); |
| mActiveTouchSessions.add(sessionStack); |
| sessionMap.put(handler, sessionStack); |
| } |
| |
| // Informing handlers of new sessions is delayed until we have all created so the |
| // final session is correct. |
| sessionMap.forEach((dreamTouchHandler, touchSession) |
| -> dreamTouchHandler.onSessionStart(touchSession)); |
| } |
| |
| // Find active sessions and invoke on InputEvent. |
| mActiveTouchSessions.stream() |
| .map(touchSessionStack -> touchSessionStack.getEventListeners()) |
| .flatMap(Collection::stream) |
| .forEach(inputEventListener -> inputEventListener.onInputEvent(ev)); |
| } |
| }; |
| |
| /** |
| * 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, |
| DisplayHelper displayHelper, |
| Set<DreamTouchHandler> handlers) { |
| mHandlers = handlers; |
| mInputSessionFactory = inputSessionFactory; |
| mExecutor = executor; |
| mLifecycle = lifecycle; |
| mDisplayHelper = displayHelper; |
| } |
| |
| /** |
| * 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); |
| } |
| } |