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