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