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