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