1 /*
2  * Copyright (C) 2024 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.ambient.touch;
18 
19 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
20 
21 import static com.android.systemui.shared.Flags.bouncerAreaExclusion;
22 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
23 
24 import android.graphics.Rect;
25 import android.graphics.Region;
26 import android.os.RemoteException;
27 import android.util.Log;
28 import android.view.GestureDetector;
29 import android.view.ISystemGestureExclusionListener;
30 import android.view.IWindowManager;
31 import android.view.InputEvent;
32 import android.view.MotionEvent;
33 
34 import androidx.annotation.NonNull;
35 import androidx.concurrent.futures.CallbackToFutureAdapter;
36 import androidx.lifecycle.DefaultLifecycleObserver;
37 import androidx.lifecycle.Lifecycle;
38 import androidx.lifecycle.LifecycleObserver;
39 import androidx.lifecycle.LifecycleOwner;
40 
41 import com.android.systemui.Flags;
42 import com.android.systemui.ambient.touch.dagger.InputSessionComponent;
43 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
44 import com.android.systemui.dagger.qualifiers.Background;
45 import com.android.systemui.dagger.qualifiers.DisplayId;
46 import com.android.systemui.dagger.qualifiers.Main;
47 import com.android.systemui.shared.system.InputChannelCompat;
48 import com.android.systemui.util.display.DisplayHelper;
49 
50 import com.google.common.util.concurrent.ListenableFuture;
51 
52 import kotlinx.coroutines.Job;
53 
54 import java.util.Collection;
55 import java.util.HashMap;
56 import java.util.HashSet;
57 import java.util.Iterator;
58 import java.util.Set;
59 import java.util.concurrent.CancellationException;
60 import java.util.concurrent.Executor;
61 import java.util.function.Consumer;
62 import java.util.stream.Collectors;
63 
64 import javax.inject.Inject;
65 
66 /**
67  * {@link TouchMonitor} is responsible for monitoring touches and gestures over the
68  * dream overlay and redirecting them to a set of listeners. This monitor is in charge of figuring
69  * out when listeners are eligible for receiving touches and filtering the listener pool if
70  * touches are consumed.
71  */
72 public class TouchMonitor {
73     // This executor is used to protect {@code mActiveTouchSessions} from being modified
74     // concurrently. Any operation that adds or removes values should use this executor.
75     public String TAG = "DreamOverlayTouchMonitor";
76     private final Executor mMainExecutor;
77     private final Executor mBackgroundExecutor;
78 
79     private final ConfigurationInteractor mConfigurationInteractor;
80 
81     private final Lifecycle mLifecycle;
82     private Rect mExclusionRect = null;
83 
84     private ISystemGestureExclusionListener mGestureExclusionListener;
85 
86     private Consumer<Rect> mMaxBoundsConsumer = rect -> mMaxBounds = rect;
87 
88 
89     /**
90      * Adds a new {@link TouchSessionImpl} to participate in receiving future touches and gestures.
91      */
push( TouchSessionImpl touchSessionImpl)92     private ListenableFuture<TouchHandler.TouchSession> push(
93             TouchSessionImpl touchSessionImpl) {
94         return CallbackToFutureAdapter.getFuture(completer -> {
95             mMainExecutor.execute(() -> {
96                 if (!mActiveTouchSessions.remove(touchSessionImpl)) {
97                     completer.set(null);
98                     return;
99                 }
100 
101                 final TouchSessionImpl touchSession =
102                         new TouchSessionImpl(this, touchSessionImpl.getBounds(),
103                                 touchSessionImpl);
104                 mActiveTouchSessions.add(touchSession);
105                 completer.set(touchSession);
106             });
107 
108             return "DreamOverlayTouchMonitor::push";
109         });
110     }
111 
112     /**
113      * Removes a {@link TouchSessionImpl} from receiving further updates.
114      */
115     private ListenableFuture<TouchHandler.TouchSession> pop(
116             TouchSessionImpl touchSessionImpl) {
117         return CallbackToFutureAdapter.getFuture(completer -> {
118             mMainExecutor.execute(() -> {
119                 if (mActiveTouchSessions.remove(touchSessionImpl)) {
120                     touchSessionImpl.onRemoved();
121 
122                     final TouchSessionImpl predecessor = touchSessionImpl.getPredecessor();
123 
124                     if (predecessor != null) {
125                         mActiveTouchSessions.add(predecessor);
126                     }
127 
128                     completer.set(predecessor);
129                 }
130 
131                 if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) {
132                     stopMonitoring(false);
133                 }
134             });
135 
136             return "DreamOverlayTouchMonitor::pop";
137         });
138     }
139 
140     private int getSessionCount() {
141         return mActiveTouchSessions.size();
142     }
143 
144     /**
145      * {@link TouchSessionImpl} implements {@link TouchHandler.TouchSession} for
146      * {@link TouchMonitor}. It enables the monitor to access the associated listeners
147      * and provides the associated client with access to the monitor.
148      */
149     private static class TouchSessionImpl implements TouchHandler.TouchSession {
150         private final HashSet<InputChannelCompat.InputEventListener> mEventListeners =
151                 new HashSet<>();
152         private final HashSet<GestureDetector.OnGestureListener> mGestureListeners =
153                 new HashSet<>();
154         private final HashSet<Callback> mCallbacks = new HashSet<>();
155 
156         private final TouchSessionImpl mPredecessor;
157         private final TouchMonitor mTouchMonitor;
158         private final Rect mBounds;
159 
160         TouchSessionImpl(TouchMonitor touchMonitor, Rect bounds,
161                 TouchSessionImpl predecessor) {
162             mPredecessor = predecessor;
163             mTouchMonitor = touchMonitor;
164             mBounds = bounds;
165         }
166 
167         @Override
168         public void registerCallback(Callback callback) {
169             mCallbacks.add(callback);
170         }
171 
172         @Override
173         public boolean registerInputListener(
174                 InputChannelCompat.InputEventListener inputEventListener) {
175             return mEventListeners.add(inputEventListener);
176         }
177 
178         @Override
179         public boolean registerGestureListener(GestureDetector.OnGestureListener gestureListener) {
180             return mGestureListeners.add(gestureListener);
181         }
182 
183         @Override
184         public ListenableFuture<TouchHandler.TouchSession> push() {
185             return mTouchMonitor.push(this);
186         }
187 
188         @Override
189         public ListenableFuture<TouchHandler.TouchSession> pop() {
190             return mTouchMonitor.pop(this);
191         }
192 
193         @Override
194         public int getActiveSessionCount() {
195             return mTouchMonitor.getSessionCount();
196         }
197 
198         /**
199          * Returns the active listeners to receive touch events.
200          */
201         public Collection<InputChannelCompat.InputEventListener> getEventListeners() {
202             return mEventListeners;
203         }
204 
205         /**
206          * Returns the active listeners to receive gesture events.
207          */
208         public Collection<GestureDetector.OnGestureListener> getGestureListeners() {
209             return mGestureListeners;
210         }
211 
212         /**
213          * Returns the {@link TouchSessionImpl} that preceded this current session. This will
214          * become the new active session when this session is popped.
215          */
216         private TouchSessionImpl getPredecessor() {
217             return mPredecessor;
218         }
219 
220         /**
221          * Called by the monitor when this session is removed.
222          */
223         private void onRemoved() {
224             mEventListeners.clear();
225             mGestureListeners.clear();
226             final Iterator<Callback> iter = mCallbacks.iterator();
227             while (iter.hasNext()) {
228                 final Callback callback = iter.next();
229                 callback.onRemoved();
230                 iter.remove();
231             }
232         }
233 
234         @Override
235         public Rect getBounds() {
236             return mBounds;
237         }
238     }
239 
240     /**
241      * This lifecycle observer ensures touch monitoring only occurs while the overlay is "resumed".
242      * This concept is mapped over from the equivalent view definition: The {@link LifecycleOwner}
243      * will report the dream is not resumed when it is obscured (from the notification shade being
244      * expanded for example) or not active (such as when it is destroyed).
245      */
246     private final LifecycleObserver mLifecycleObserver = new DefaultLifecycleObserver() {
247         @Override
248         public void onResume(@NonNull LifecycleOwner owner) {
249             startMonitoring();
250         }
251 
252         @Override
253         public void onPause(@NonNull LifecycleOwner owner) {
254             stopMonitoring(false);
255         }
256 
257         @Override
258         public void onDestroy(LifecycleOwner owner) {
259             stopMonitoring(true);
260         }
261     };
262 
263     /**
264      * When invoked, instantiates a new {@link InputSession} to monitor touch events.
265      */
266     private void startMonitoring() {
267         stopMonitoring(true);
268 
269         if (bouncerAreaExclusion()) {
270             mBackgroundExecutor.execute(() -> {
271                 try {
272                     mGestureExclusionListener = new ISystemGestureExclusionListener.Stub() {
273                         @Override
274                         public void onSystemGestureExclusionChanged(int displayId,
275                                 Region systemGestureExclusion,
276                                 Region systemGestureExclusionUnrestricted) {
277                             mExclusionRect = systemGestureExclusion.getBounds();
278                         }
279                     };
280                     mWindowManagerService.registerSystemGestureExclusionListener(
281                             mGestureExclusionListener, mDisplayId);
282                 } catch (RemoteException e) {
283                     // Handle the exception
284                     Log.e(TAG, "Failed to register gesture exclusion listener", e);
285                 }
286             });
287         }
288         mCurrentInputSession = mInputSessionFactory.create(
289                         "dreamOverlay",
290                         mInputEventListener,
291                         mOnGestureListener,
292                         true)
293                 .getInputSession();
294     }
295 
296     /**
297      * Destroys any active {@link InputSession}.
298      */
299     private void stopMonitoring(boolean force) {
300         mExclusionRect = null;
301         if (bouncerAreaExclusion()) {
302             mBackgroundExecutor.execute(() -> {
303                 try {
304                     if (mGestureExclusionListener != null) {
305                         mWindowManagerService.unregisterSystemGestureExclusionListener(
306                                 mGestureExclusionListener, mDisplayId);
307                         mGestureExclusionListener = null;
308                     }
309                 } catch (RemoteException e) {
310                     // Handle the exception
311                     Log.e(TAG, "unregisterSystemGestureExclusionListener: failed", e);
312                 }
313             });
314         }
315         if (mCurrentInputSession == null) {
316             return;
317         }
318 
319         if (!mActiveTouchSessions.isEmpty() && !force) {
320             mStopMonitoringPending = true;
321             return;
322         }
323 
324         // When we stop monitoring touches, we must ensure that all active touch sessions and
325         // descendants informed of the removal so any cleanup for active tracking can proceed.
326         mMainExecutor.execute(() -> mActiveTouchSessions.forEach(touchSession -> {
327             while (touchSession != null) {
328                 touchSession.onRemoved();
329                 touchSession = touchSession.getPredecessor();
330             }
331         }));
332 
333         mCurrentInputSession.dispose();
334         mCurrentInputSession = null;
335         mStopMonitoringPending = false;
336     }
337 
338 
339     private final HashSet<TouchSessionImpl> mActiveTouchSessions = new HashSet<>();
340     private final Collection<TouchHandler> mHandlers;
341     private final DisplayHelper mDisplayHelper;
342 
343     private boolean mStopMonitoringPending;
344 
345     private InputChannelCompat.InputEventListener mInputEventListener =
346             new InputChannelCompat.InputEventListener() {
347                 @Override
348                 public void onInputEvent(InputEvent ev) {
349                     // No Active sessions are receiving touches. Create sessions for each listener
350                     if (mActiveTouchSessions.isEmpty()) {
351                         final HashMap<TouchHandler, TouchHandler.TouchSession> sessionMap =
352                                 new HashMap<>();
353 
354                         for (TouchHandler handler : mHandlers) {
355                             if (!handler.isEnabled()) {
356                                 continue;
357                             }
358 
359                             final Rect maxBounds =
360                                     Flags.ambientTouchMonitorListenToDisplayChanges()
361                                             ? mMaxBounds
362                                             : mDisplayHelper.getMaxBounds(ev.getDisplayId(),
363                                                     TYPE_APPLICATION_OVERLAY);
364 
365                             final Region initiationRegion = Region.obtain();
366                             Rect exclusionRect = null;
367                             if (bouncerAreaExclusion()) {
368                                 exclusionRect = getCurrentExclusionRect();
369                             }
370                             handler.getTouchInitiationRegion(
371                                     maxBounds, initiationRegion, exclusionRect);
372 
373                             if (!initiationRegion.isEmpty()) {
374                                 // Initiation regions require a motion event to determine pointer
375                                 // location
376                                 // within the region.
377                                 if (!(ev instanceof MotionEvent)) {
378                                     continue;
379                                 }
380 
381                                 final MotionEvent motionEvent = (MotionEvent) ev;
382 
383                                 // If the touch event is outside the region, then ignore.
384                                 if (!initiationRegion.contains(Math.round(motionEvent.getX()),
385                                         Math.round(motionEvent.getY()))) {
386                                     continue;
387                                 }
388                             }
389 
390                             final TouchSessionImpl sessionStack = new TouchSessionImpl(
391                                     TouchMonitor.this, maxBounds, null);
392                             mActiveTouchSessions.add(sessionStack);
393                             sessionMap.put(handler, sessionStack);
394                         }
395 
396                         // Informing handlers of new sessions is delayed until we have all
397                         // created so the
398                         // final session is correct.
399                         sessionMap.forEach((dreamTouchHandler, touchSession)
400                                 -> dreamTouchHandler.onSessionStart(touchSession));
401                     }
402 
403                     // Find active sessions and invoke on InputEvent.
404                     mActiveTouchSessions.stream()
405                             .map(touchSessionStack -> touchSessionStack.getEventListeners())
406                             .flatMap(Collection::stream)
407                             .forEach(inputEventListener -> inputEventListener.onInputEvent(ev));
408                 }
409 
410                 private Rect getCurrentExclusionRect() {
411                     return mExclusionRect;
412                 }
413             };
414 
415     /**
416      * The {@link Evaluator} interface allows for callers to inspect a listener from the
417      * {@link android.view.GestureDetector.OnGestureListener} set. This helps reduce duplicated
418      * iteration loops over this set.
419      */
420     private interface Evaluator {
421         boolean evaluate(GestureDetector.OnGestureListener listener);
422     }
423 
424     private GestureDetector.OnGestureListener mOnGestureListener =
425             new GestureDetector.OnGestureListener() {
426                 private boolean evaluate(Evaluator evaluator) {
427                     final Set<TouchSessionImpl> consumingSessions = new HashSet<>();
428 
429                     // When a gesture is consumed, it is assumed that all touches for the current
430                     // session
431                     // should be directed only to those TouchSessions until those sessions are
432                     // popped. All
433                     // non-participating sessions are removed from receiving further updates with
434                     // {@link DreamOverlayTouchMonitor#isolate}.
435                     final boolean eventConsumed = mActiveTouchSessions.stream()
436                             .map(touchSession -> {
437                                 boolean consume = touchSession.getGestureListeners()
438                                         .stream()
439                                         .map(listener -> evaluator.evaluate(listener))
440                                         .anyMatch(consumed -> consumed);
441 
442                                 if (consume) {
443                                     consumingSessions.add(touchSession);
444                                 }
445                                 return consume;
446                             }).anyMatch(consumed -> consumed);
447 
448                     if (eventConsumed) {
449                         TouchMonitor.this.isolate(consumingSessions);
450                     }
451 
452                     return eventConsumed;
453                 }
454 
455                 // This method is called for gesture events that cannot be consumed.
456                 private void observe(Consumer<GestureDetector.OnGestureListener> consumer) {
457                     mActiveTouchSessions.stream()
458                             .map(touchSession -> touchSession.getGestureListeners())
459                             .flatMap(Collection::stream)
460                             .forEach(listener -> consumer.accept(listener));
461                 }
462 
463                 @Override
464                 public boolean onDown(MotionEvent e) {
465                     return evaluate(listener -> listener.onDown(e));
466                 }
467 
468                 @Override
469                 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
470                         float velocityY) {
471                     return evaluate(listener -> listener.onFling(e1, e2, velocityX, velocityY));
472                 }
473 
474                 @Override
475                 public void onLongPress(MotionEvent e) {
476                     observe(listener -> listener.onLongPress(e));
477                 }
478 
479                 @Override
480                 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
481                         float distanceY) {
482                     return evaluate(listener -> listener.onScroll(e1, e2, distanceX, distanceY));
483                 }
484 
485                 @Override
486                 public void onShowPress(MotionEvent e) {
487                     observe(listener -> listener.onShowPress(e));
488                 }
489 
490                 @Override
491                 public boolean onSingleTapUp(MotionEvent e) {
492                     return evaluate(listener -> listener.onSingleTapUp(e));
493                 }
494             };
495 
496     private InputSessionComponent.Factory mInputSessionFactory;
497     private InputSession mCurrentInputSession;
498     private final int mDisplayId;
499     private final IWindowManager mWindowManagerService;
500 
501     private Rect mMaxBounds;
502 
503     private Job mBoundsFlow;
504 
505     private boolean mInitialized;
506 
507 
508     /**
509      * Designated constructor for {@link TouchMonitor}
510      *
511      * @param executor            This executor will be used for maintaining the active listener
512      *                            list to avoid
513      *                            concurrent modification.
514      * @param lifecycle           {@link TouchMonitor} will listen to this lifecycle to determine
515      *                            whether touch monitoring should be active.
516      * @param inputSessionFactory This factory will generate the {@link InputSession} requested by
517      *                            the monitor. Each session should be unique and valid when
518      *                            returned.
519      * @param handlers            This set represents the {@link TouchHandler} instances that will
520      *                            participate in touch handling.
521      */
522     @Inject
523     public TouchMonitor(
524             @Main Executor executor,
525             @Background Executor backgroundExecutor,
526             Lifecycle lifecycle,
527             InputSessionComponent.Factory inputSessionFactory,
528             DisplayHelper displayHelper,
529             ConfigurationInteractor configurationInteractor,
530             Set<TouchHandler> handlers,
531             IWindowManager windowManagerService,
532             @DisplayId int displayId) {
533         mDisplayId = displayId;
534         mHandlers = handlers;
535         mInputSessionFactory = inputSessionFactory;
536         mMainExecutor = executor;
537         mBackgroundExecutor = backgroundExecutor;
538         mLifecycle = lifecycle;
539         mDisplayHelper = displayHelper;
540         mWindowManagerService = windowManagerService;
541         mConfigurationInteractor = configurationInteractor;
542     }
543 
544     /**
545      * Initializes the monitor. should only be called once after creation.
546      */
547     public void init() {
548         if (mInitialized) {
549             throw new IllegalStateException("TouchMonitor already initialized");
550         }
551 
552         mLifecycle.addObserver(mLifecycleObserver);
553         if (Flags.ambientTouchMonitorListenToDisplayChanges()) {
554             mBoundsFlow = collectFlow(mLifecycle, mConfigurationInteractor.getMaxBounds(),
555                     mMaxBoundsConsumer);
556         }
557 
558         mInitialized = true;
559     }
560 
561     /**
562      * Called when the TouchMonitor should be discarded and will not be used anymore.
563      */
564     public void destroy() {
565         if (!mInitialized) {
566             throw new IllegalStateException("TouchMonitor not initialized");
567         }
568 
569         stopMonitoring(true);
570 
571         mLifecycle.removeObserver(mLifecycleObserver);
572         if (Flags.ambientTouchMonitorListenToDisplayChanges()) {
573             mBoundsFlow.cancel(new CancellationException());
574         }
575 
576         mInitialized = false;
577     }
578 
579     private void isolate(Set<TouchSessionImpl> sessions) {
580         Collection<TouchSessionImpl> removedSessions = mActiveTouchSessions.stream()
581                 .filter(touchSession -> !sessions.contains(touchSession))
582                 .collect(Collectors.toCollection(HashSet::new));
583 
584         removedSessions.forEach(touchSession -> touchSession.onRemoved());
585 
586         mActiveTouchSessions.removeAll(removedSessions);
587     }
588 }
589