• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.dreams.touch;
18 
19 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
20 
21 import android.graphics.Rect;
22 import android.graphics.Region;
23 import android.view.GestureDetector;
24 import android.view.InputEvent;
25 import android.view.MotionEvent;
26 
27 import androidx.annotation.NonNull;
28 import androidx.concurrent.futures.CallbackToFutureAdapter;
29 import androidx.lifecycle.DefaultLifecycleObserver;
30 import androidx.lifecycle.Lifecycle;
31 import androidx.lifecycle.LifecycleObserver;
32 import androidx.lifecycle.LifecycleOwner;
33 
34 import com.android.systemui.dagger.qualifiers.Main;
35 import com.android.systemui.dreams.touch.dagger.InputSessionComponent;
36 import com.android.systemui.shared.system.InputChannelCompat;
37 import com.android.systemui.util.display.DisplayHelper;
38 
39 import com.google.common.util.concurrent.ListenableFuture;
40 
41 import java.util.Collection;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.Set;
45 import java.util.concurrent.Executor;
46 import java.util.function.Consumer;
47 import java.util.stream.Collectors;
48 
49 import javax.inject.Inject;
50 
51 /**
52  * {@link DreamOverlayTouchMonitor} is responsible for monitoring touches and gestures over the
53  * dream overlay and redirecting them to a set of listeners. This monitor is in charge of figuring
54  * out when listeners are eligible for receiving touches and filtering the listener pool if
55  * touches are consumed.
56  */
57 public class DreamOverlayTouchMonitor {
58     // This executor is used to protect {@code mActiveTouchSessions} from being modified
59     // concurrently. Any operation that adds or removes values should use this executor.
60     private final Executor mExecutor;
61     private final Lifecycle mLifecycle;
62 
63     /**
64      * Adds a new {@link TouchSessionImpl} to participate in receiving future touches and gestures.
65      */
push( TouchSessionImpl touchSessionImpl)66     private ListenableFuture<DreamTouchHandler.TouchSession> push(
67             TouchSessionImpl touchSessionImpl) {
68         return CallbackToFutureAdapter.getFuture(completer -> {
69             mExecutor.execute(() -> {
70                 if (!mActiveTouchSessions.remove(touchSessionImpl)) {
71                     completer.set(null);
72                     return;
73                 }
74 
75                 final TouchSessionImpl touchSession =
76                         new TouchSessionImpl(this, touchSessionImpl.getBounds(),
77                                 touchSessionImpl);
78                 mActiveTouchSessions.add(touchSession);
79                 completer.set(touchSession);
80             });
81 
82             return "DreamOverlayTouchMonitor::push";
83         });
84     }
85 
86     /**
87      * Removes a {@link TouchSessionImpl} from receiving further updates.
88      */
89     private ListenableFuture<DreamTouchHandler.TouchSession> pop(
90             TouchSessionImpl touchSessionImpl) {
91         return CallbackToFutureAdapter.getFuture(completer -> {
92             mExecutor.execute(() -> {
93                 if (mActiveTouchSessions.remove(touchSessionImpl)) {
94                     touchSessionImpl.onRemoved();
95 
96                     final TouchSessionImpl predecessor = touchSessionImpl.getPredecessor();
97 
98                     if (predecessor != null) {
99                         mActiveTouchSessions.add(predecessor);
100                     }
101 
102                     completer.set(predecessor);
103                 }
104 
105                 if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) {
106                     stopMonitoring(false);
107                 }
108             });
109 
110             return "DreamOverlayTouchMonitor::pop";
111         });
112     }
113 
114     private int getSessionCount() {
115         return mActiveTouchSessions.size();
116     }
117 
118     /**
119      * {@link TouchSessionImpl} implements {@link DreamTouchHandler.TouchSession} for
120      * {@link DreamOverlayTouchMonitor}. It enables the monitor to access the associated listeners
121      * and provides the associated client with access to the monitor.
122      */
123     private static class TouchSessionImpl implements DreamTouchHandler.TouchSession {
124         private final HashSet<InputChannelCompat.InputEventListener> mEventListeners =
125                 new HashSet<>();
126         private final HashSet<GestureDetector.OnGestureListener> mGestureListeners =
127                 new HashSet<>();
128         private final HashSet<Callback> mCallbacks = new HashSet<>();
129 
130         private final TouchSessionImpl mPredecessor;
131         private final DreamOverlayTouchMonitor mTouchMonitor;
132         private final Rect mBounds;
133 
134         TouchSessionImpl(DreamOverlayTouchMonitor touchMonitor, Rect bounds,
135                 TouchSessionImpl predecessor) {
136             mPredecessor = predecessor;
137             mTouchMonitor = touchMonitor;
138             mBounds = bounds;
139         }
140 
141         @Override
142         public void registerCallback(Callback callback) {
143             mCallbacks.add(callback);
144         }
145 
146         @Override
147         public boolean registerInputListener(
148                 InputChannelCompat.InputEventListener inputEventListener) {
149             return mEventListeners.add(inputEventListener);
150         }
151 
152         @Override
153         public boolean registerGestureListener(GestureDetector.OnGestureListener gestureListener) {
154             return mGestureListeners.add(gestureListener);
155         }
156 
157         @Override
158         public ListenableFuture<DreamTouchHandler.TouchSession> push() {
159             return mTouchMonitor.push(this);
160         }
161 
162         @Override
163         public ListenableFuture<DreamTouchHandler.TouchSession> pop() {
164             return mTouchMonitor.pop(this);
165         }
166 
167         @Override
168         public int getActiveSessionCount() {
169             return mTouchMonitor.getSessionCount();
170         }
171 
172         /**
173          * Returns the active listeners to receive touch events.
174          */
175         public Collection<InputChannelCompat.InputEventListener> getEventListeners() {
176             return mEventListeners;
177         }
178 
179         /**
180          * Returns the active listeners to receive gesture events.
181          */
182         public Collection<GestureDetector.OnGestureListener> getGestureListeners() {
183             return mGestureListeners;
184         }
185 
186         /**
187          * Returns the {@link TouchSessionImpl} that preceded this current session. This will
188          * become the new active session when this session is popped.
189          */
190         private TouchSessionImpl getPredecessor() {
191             return mPredecessor;
192         }
193 
194         /**
195          * Called by the monitor when this session is removed.
196          */
197         private void onRemoved() {
198             mCallbacks.forEach(callback -> callback.onRemoved());
199         }
200 
201         @Override
202         public Rect getBounds() {
203             return mBounds;
204         }
205     }
206 
207     /**
208      * This lifecycle observer ensures touch monitoring only occurs while the overlay is "resumed".
209      * This concept is mapped over from the equivalent view definition: The {@link LifecycleOwner}
210      * will report the dream is not resumed when it is obscured (from the notification shade being
211      * expanded for example) or not active (such as when it is destroyed).
212      */
213     private final LifecycleObserver mLifecycleObserver = new DefaultLifecycleObserver() {
214         @Override
215         public void onResume(@NonNull LifecycleOwner owner) {
216             startMonitoring();
217         }
218 
219         @Override
220         public void onPause(@NonNull LifecycleOwner owner) {
221             stopMonitoring(false);
222         }
223 
224         @Override
225         public void onDestroy(LifecycleOwner owner) {
226             stopMonitoring(true);
227         }
228     };
229 
230     /**
231      * When invoked, instantiates a new {@link InputSession} to monitor touch events.
232      */
233     private void startMonitoring() {
234         stopMonitoring(true);
235         mCurrentInputSession = mInputSessionFactory.create(
236                 "dreamOverlay",
237                 mInputEventListener,
238                 mOnGestureListener,
239                 true)
240                 .getInputSession();
241     }
242 
243     /**
244      * Destroys any active {@link InputSession}.
245      */
246     private void stopMonitoring(boolean force) {
247         if (mCurrentInputSession == null) {
248             return;
249         }
250 
251         if (!mActiveTouchSessions.isEmpty() && !force) {
252             mStopMonitoringPending = true;
253             return;
254         }
255 
256         // When we stop monitoring touches, we must ensure that all active touch sessions and
257         // descendants informed of the removal so any cleanup for active tracking can proceed.
258         mExecutor.execute(() -> mActiveTouchSessions.forEach(touchSession -> {
259             while (touchSession != null) {
260                 touchSession.onRemoved();
261                 touchSession = touchSession.getPredecessor();
262             }
263         }));
264 
265         mCurrentInputSession.dispose();
266         mCurrentInputSession = null;
267         mStopMonitoringPending = false;
268     }
269 
270 
271     private final HashSet<TouchSessionImpl> mActiveTouchSessions = new HashSet<>();
272     private final Collection<DreamTouchHandler> mHandlers;
273     private final DisplayHelper mDisplayHelper;
274 
275     private boolean mStopMonitoringPending;
276 
277     private InputChannelCompat.InputEventListener mInputEventListener =
278             new InputChannelCompat.InputEventListener() {
279         @Override
280         public void onInputEvent(InputEvent ev) {
281             // No Active sessions are receiving touches. Create sessions for each listener
282             if (mActiveTouchSessions.isEmpty()) {
283                 final HashMap<DreamTouchHandler, DreamTouchHandler.TouchSession> sessionMap =
284                         new HashMap<>();
285 
286                 for (DreamTouchHandler handler : mHandlers) {
287                     final Rect maxBounds = mDisplayHelper.getMaxBounds(ev.getDisplayId(),
288                             TYPE_APPLICATION_OVERLAY);
289 
290                     final Region initiationRegion = Region.obtain();
291                     handler.getTouchInitiationRegion(maxBounds, initiationRegion);
292 
293                     if (!initiationRegion.isEmpty()) {
294                         // Initiation regions require a motion event to determine pointer location
295                         // within the region.
296                         if (!(ev instanceof MotionEvent)) {
297                             continue;
298                         }
299 
300                         final MotionEvent motionEvent = (MotionEvent) ev;
301 
302                         // If the touch event is outside the region, then ignore.
303                         if (!initiationRegion.contains(Math.round(motionEvent.getX()),
304                                 Math.round(motionEvent.getY()))) {
305                             continue;
306                         }
307                     }
308 
309                     final TouchSessionImpl sessionStack = new TouchSessionImpl(
310                             DreamOverlayTouchMonitor.this, maxBounds, null);
311                     mActiveTouchSessions.add(sessionStack);
312                     sessionMap.put(handler, sessionStack);
313                 }
314 
315                 // Informing handlers of new sessions is delayed until we have all created so the
316                 // final session is correct.
317                 sessionMap.forEach((dreamTouchHandler, touchSession)
318                         -> dreamTouchHandler.onSessionStart(touchSession));
319             }
320 
321             // Find active sessions and invoke on InputEvent.
322             mActiveTouchSessions.stream()
323                     .map(touchSessionStack -> touchSessionStack.getEventListeners())
324                     .flatMap(Collection::stream)
325                     .forEach(inputEventListener -> inputEventListener.onInputEvent(ev));
326         }
327     };
328 
329     /**
330      * The {@link Evaluator} interface allows for callers to inspect a listener from the
331      * {@link android.view.GestureDetector.OnGestureListener} set. This helps reduce duplicated
332      * iteration loops over this set.
333      */
334     private interface Evaluator {
335         boolean evaluate(GestureDetector.OnGestureListener listener);
336     }
337 
338     private GestureDetector.OnGestureListener mOnGestureListener =
339             new GestureDetector.OnGestureListener() {
340         private boolean evaluate(Evaluator evaluator) {
341             final Set<TouchSessionImpl> consumingSessions = new HashSet<>();
342 
343             // When a gesture is consumed, it is assumed that all touches for the current session
344             // should be directed only to those TouchSessions until those sessions are popped. All
345             // non-participating sessions are removed from receiving further updates with
346             // {@link DreamOverlayTouchMonitor#isolate}.
347             final boolean eventConsumed = mActiveTouchSessions.stream()
348                     .map(touchSession -> {
349                         boolean consume = touchSession.getGestureListeners()
350                                 .stream()
351                                 .map(listener -> evaluator.evaluate(listener))
352                                 .anyMatch(consumed -> consumed);
353 
354                         if (consume) {
355                             consumingSessions.add(touchSession);
356                         }
357                         return consume;
358                     }).anyMatch(consumed -> consumed);
359 
360             if (eventConsumed) {
361                 DreamOverlayTouchMonitor.this.isolate(consumingSessions);
362             }
363 
364             return eventConsumed;
365         }
366 
367         // This method is called for gesture events that cannot be consumed.
368         private void observe(Consumer<GestureDetector.OnGestureListener> consumer) {
369             mActiveTouchSessions.stream()
370                     .map(touchSession -> touchSession.getGestureListeners())
371                     .flatMap(Collection::stream)
372                     .forEach(listener -> consumer.accept(listener));
373         }
374 
375         @Override
376         public boolean onDown(MotionEvent e) {
377             return evaluate(listener -> listener.onDown(e));
378         }
379 
380         @Override
381         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
382             return evaluate(listener -> listener.onFling(e1, e2, velocityX, velocityY));
383         }
384 
385         @Override
386         public void onLongPress(MotionEvent e) {
387             observe(listener -> listener.onLongPress(e));
388         }
389 
390         @Override
391         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
392             return evaluate(listener -> listener.onScroll(e1, e2, distanceX, distanceY));
393         }
394 
395         @Override
396         public void onShowPress(MotionEvent e) {
397             observe(listener -> listener.onShowPress(e));
398         }
399 
400         @Override
401         public boolean onSingleTapUp(MotionEvent e) {
402             return evaluate(listener -> listener.onSingleTapUp(e));
403         }
404     };
405 
406     private InputSessionComponent.Factory mInputSessionFactory;
407     private InputSession mCurrentInputSession;
408 
409     /**
410      * Designated constructor for {@link DreamOverlayTouchMonitor}
411      * @param executor This executor will be used for maintaining the active listener list to avoid
412      *                 concurrent modification.
413      * @param lifecycle {@link DreamOverlayTouchMonitor} will listen to this lifecycle to determine
414      *                                                  whether touch monitoring should be active.
415      * @param inputSessionFactory This factory will generate the {@link InputSession} requested by
416      *                            the monitor. Each session should be unique and valid when
417      *                            returned.
418      * @param handlers This set represents the {@link DreamTouchHandler} instances that will
419      *                 participate in touch handling.
420      */
421     @Inject
422     public DreamOverlayTouchMonitor(
423             @Main Executor executor,
424             Lifecycle lifecycle,
425             InputSessionComponent.Factory inputSessionFactory,
426             DisplayHelper displayHelper,
427             Set<DreamTouchHandler> handlers) {
428         mHandlers = handlers;
429         mInputSessionFactory = inputSessionFactory;
430         mExecutor = executor;
431         mLifecycle = lifecycle;
432         mDisplayHelper = displayHelper;
433     }
434 
435     /**
436      * Initializes the monitor. should only be called once after creation.
437      */
438     public void init() {
439         mLifecycle.addObserver(mLifecycleObserver);
440     }
441 
442     private void isolate(Set<TouchSessionImpl> sessions) {
443         Collection<TouchSessionImpl> removedSessions = mActiveTouchSessions.stream()
444                 .filter(touchSession -> !sessions.contains(touchSession))
445                 .collect(Collectors.toCollection(HashSet::new));
446 
447         removedSessions.forEach(touchSession -> touchSession.onRemoved());
448 
449         mActiveTouchSessions.removeAll(removedSessions);
450     }
451 }
452