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.wm.shell.windowdecor; 18 19 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 20 import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; 21 import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; 22 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; 23 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; 24 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_CONSUMER; 25 26 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; 27 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM; 28 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; 29 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; 30 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; 31 import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger; 32 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEdgeResizePermitted; 33 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEventFromTouchscreen; 34 35 import android.annotation.NonNull; 36 import android.app.ActivityManager.RunningTaskInfo; 37 import android.content.Context; 38 import android.graphics.Point; 39 import android.graphics.Rect; 40 import android.graphics.Region; 41 import android.hardware.input.InputManager; 42 import android.os.Binder; 43 import android.os.Handler; 44 import android.os.IBinder; 45 import android.os.RemoteException; 46 import android.os.Trace; 47 import android.util.Size; 48 import android.view.Choreographer; 49 import android.view.IWindowSession; 50 import android.view.InputChannel; 51 import android.view.InputEvent; 52 import android.view.InputEventReceiver; 53 import android.view.MotionEvent; 54 import android.view.PointerIcon; 55 import android.view.SurfaceControl; 56 import android.view.View; 57 import android.view.ViewConfiguration; 58 import android.window.InputTransferToken; 59 60 import com.android.internal.annotations.VisibleForTesting; 61 import com.android.internal.protolog.ProtoLog; 62 import com.android.wm.shell.common.DisplayController; 63 import com.android.wm.shell.common.DisplayLayout; 64 import com.android.wm.shell.common.ShellExecutor; 65 import com.android.wm.shell.desktopmode.DesktopModeEventLogger; 66 import com.android.wm.shell.shared.annotations.ShellBackgroundThread; 67 import com.android.wm.shell.shared.annotations.ShellMainThread; 68 69 import java.util.ArrayList; 70 import java.util.List; 71 import java.util.function.Consumer; 72 import java.util.function.Supplier; 73 74 /** 75 * An input event listener registered to InputDispatcher to receive input events on task edges and 76 * and corners. Converts them to drag resize requests. 77 * Task edges are for resizing with a mouse. 78 * Task corners are for resizing with touch input. 79 */ 80 class DragResizeInputListener implements AutoCloseable { 81 private static final String TAG = "DragResizeInputListener"; 82 private final IWindowSession mWindowSession; 83 private final TaskResizeInputEventReceiverFactory mEventReceiverFactory; 84 private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; 85 private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; 86 87 private final int mDisplayId; 88 89 @VisibleForTesting 90 final IBinder mClientToken; 91 92 private final SurfaceControl mDecorationSurface; 93 private InputChannel mInputChannel; 94 private TaskResizeInputEventReceiver mInputEventReceiver; 95 96 private final Context mContext; 97 private final @ShellBackgroundThread ShellExecutor mBgExecutor; 98 private final RunningTaskInfo mTaskInfo; 99 private final Handler mHandler; 100 private final Choreographer mChoreographer; 101 private SurfaceControl mInputSinkSurface; 102 @VisibleForTesting 103 final IBinder mSinkClientToken; 104 private InputChannel mSinkInputChannel; 105 private final DisplayController mDisplayController; 106 /** TODO: b/396490344 - this desktop-specific class should be abstracted out of here. */ 107 private final DesktopModeEventLogger mDesktopModeEventLogger; 108 private final DragPositioningCallback mDragPositioningCallback; 109 private final Region mTouchRegion = new Region(); 110 private final List<Runnable> mOnInitializedCallbacks = new ArrayList<>(); 111 112 private final Runnable mInitInputChannels; 113 private boolean mClosed = false; 114 DragResizeInputListener( Context context, IWindowSession windowSession, @ShellMainThread ShellExecutor mainExecutor, @ShellBackgroundThread ShellExecutor bgExecutor, TaskResizeInputEventReceiverFactory eventReceiverFactory, RunningTaskInfo taskInfo, Handler handler, Choreographer choreographer, int displayId, SurfaceControl decorationSurface, DragPositioningCallback callback, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, DisplayController displayController, DesktopModeEventLogger desktopModeEventLogger, InputChannel inputChannel, InputChannel sinkInputChannel)115 DragResizeInputListener( 116 Context context, 117 IWindowSession windowSession, 118 @ShellMainThread ShellExecutor mainExecutor, 119 @ShellBackgroundThread ShellExecutor bgExecutor, 120 TaskResizeInputEventReceiverFactory eventReceiverFactory, 121 RunningTaskInfo taskInfo, 122 Handler handler, 123 Choreographer choreographer, 124 int displayId, 125 SurfaceControl decorationSurface, 126 DragPositioningCallback callback, 127 Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, 128 Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, 129 DisplayController displayController, 130 DesktopModeEventLogger desktopModeEventLogger, 131 InputChannel inputChannel, 132 InputChannel sinkInputChannel) { 133 mContext = context; 134 mWindowSession = windowSession; 135 mBgExecutor = bgExecutor; 136 mEventReceiverFactory = eventReceiverFactory; 137 mTaskInfo = taskInfo; 138 mHandler = handler; 139 mChoreographer = choreographer; 140 mDisplayId = displayId; 141 // Creates a new SurfaceControl pointing the same underlying surface with decorationSurface 142 // to ensure that mDecorationSurface will not be released while it's used on the background 143 // thread. Note that the empty name will be overridden by the next copyFrom call. 144 mDecorationSurface = surfaceControlBuilderSupplier.get().setName("").build(); 145 mDecorationSurface.copyFrom(decorationSurface, "DragResizeInputListener"); 146 mDragPositioningCallback = callback; 147 mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier; 148 mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; 149 mDisplayController = displayController; 150 mDesktopModeEventLogger = desktopModeEventLogger; 151 mClientToken = new Binder(); 152 mSinkClientToken = new Binder(); 153 154 // Setting up input channels for both the resize listener and the input sink requires 155 // multiple blocking binder calls, so it's moved to a bg thread to keep the shell.main 156 // thread free. 157 // The input event receiver must be created back in the shell.main thread though because 158 // its geometry and util methods are updated/queried from the shell.main thread. 159 mInitInputChannels = () -> { 160 final InputSetUpResult result = setUpInputChannels(mDisplayId, mWindowSession, 161 mDecorationSurface, mClientToken, mSinkClientToken, 162 mSurfaceControlBuilderSupplier, 163 mSurfaceControlTransactionSupplier, inputChannel, sinkInputChannel); 164 mainExecutor.execute(() -> { 165 if (mClosed) { 166 result.mInputChannel.dispose(); 167 result.mSinkInputChannel.dispose(); 168 mSurfaceControlTransactionSupplier.get().remove( 169 result.mInputSinkSurface).apply(); 170 return; 171 } 172 mInputSinkSurface = result.mInputSinkSurface; 173 mInputChannel = result.mInputChannel; 174 mSinkInputChannel = result.mSinkInputChannel; 175 Trace.beginSection("DragResizeInputListener#ctor-initReceiver"); 176 mInputEventReceiver = mEventReceiverFactory.create( 177 mContext, 178 mTaskInfo, 179 mInputChannel, 180 mDragPositioningCallback, 181 mHandler, 182 mChoreographer, 183 () -> { 184 final DisplayLayout layout = 185 mDisplayController.getDisplayLayout(mDisplayId); 186 return new Size(layout.width(), layout.height()); 187 }, 188 this::updateSinkInputChannel, 189 mDesktopModeEventLogger); 190 mInputEventReceiver.setTouchSlop( 191 ViewConfiguration.get(mContext).getScaledTouchSlop()); 192 for (Runnable initCallback : mOnInitializedCallbacks) { 193 initCallback.run(); 194 } 195 mOnInitializedCallbacks.clear(); 196 Trace.endSection(); 197 }); 198 }; 199 bgExecutor.execute(mInitInputChannels); 200 } 201 DragResizeInputListener( Context context, IWindowSession windowSession, @ShellMainThread ShellExecutor mainExecutor, @ShellBackgroundThread ShellExecutor bgExecutor, RunningTaskInfo taskInfo, Handler handler, Choreographer choreographer, int displayId, SurfaceControl decorationSurface, DragPositioningCallback callback, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, DisplayController displayController, DesktopModeEventLogger desktopModeEventLogger)202 DragResizeInputListener( 203 Context context, 204 IWindowSession windowSession, 205 @ShellMainThread ShellExecutor mainExecutor, 206 @ShellBackgroundThread ShellExecutor bgExecutor, 207 RunningTaskInfo taskInfo, 208 Handler handler, 209 Choreographer choreographer, 210 int displayId, 211 SurfaceControl decorationSurface, 212 DragPositioningCallback callback, 213 Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, 214 Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, 215 DisplayController displayController, 216 DesktopModeEventLogger desktopModeEventLogger) { 217 this(context, windowSession, mainExecutor, bgExecutor, 218 new DefaultTaskResizeInputEventReceiverFactory(), taskInfo, 219 handler, choreographer, displayId, decorationSurface, callback, 220 surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, 221 displayController, desktopModeEventLogger, new InputChannel(), new InputChannel()); 222 } 223 DragResizeInputListener( Context context, IWindowSession windowSession, @ShellMainThread ShellExecutor mainExecutor, @ShellBackgroundThread ShellExecutor bgExecutor, RunningTaskInfo taskInfo, Handler handler, Choreographer choreographer, int displayId, SurfaceControl decorationSurface, DragPositioningCallback callback, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, DisplayController displayController)224 DragResizeInputListener( 225 Context context, 226 IWindowSession windowSession, 227 @ShellMainThread ShellExecutor mainExecutor, 228 @ShellBackgroundThread ShellExecutor bgExecutor, 229 RunningTaskInfo taskInfo, 230 Handler handler, 231 Choreographer choreographer, 232 int displayId, 233 SurfaceControl decorationSurface, 234 DragPositioningCallback callback, 235 Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, 236 Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, 237 DisplayController displayController) { 238 this(context, windowSession, mainExecutor, bgExecutor, taskInfo, 239 handler, choreographer, displayId, decorationSurface, callback, 240 surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, 241 displayController, new DesktopModeEventLogger()); 242 } 243 244 /** 245 * Registers a callback to be invoked when the input listener has finished initializing. If 246 * already finished, the callback will be invoked immediately. 247 */ addInitializedCallback(Runnable onReady)248 void addInitializedCallback(Runnable onReady) { 249 if (mInputEventReceiver != null) { 250 onReady.run(); 251 return; 252 } 253 mOnInitializedCallbacks.add(onReady); 254 } 255 256 @ShellBackgroundThread setUpInputChannels( int displayId, @NonNull IWindowSession windowSession, @NonNull SurfaceControl decorationSurface, @NonNull IBinder clientToken, @NonNull IBinder sinkClientToken, @NonNull Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, @NonNull Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, @NonNull InputChannel inputChannel, @NonNull InputChannel sinkInputChannel)257 private static InputSetUpResult setUpInputChannels( 258 int displayId, 259 @NonNull IWindowSession windowSession, 260 @NonNull SurfaceControl decorationSurface, 261 @NonNull IBinder clientToken, 262 @NonNull IBinder sinkClientToken, 263 @NonNull Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, 264 @NonNull Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, 265 @NonNull InputChannel inputChannel, 266 @NonNull InputChannel sinkInputChannel) { 267 Trace.beginSection("DragResizeInputListener#setUpInputChannels"); 268 final InputTransferToken inputTransferToken = new InputTransferToken(); 269 try { 270 windowSession.grantInputChannel( 271 displayId, 272 decorationSurface, 273 clientToken, 274 null /* hostInputToken */, 275 FLAG_NOT_FOCUSABLE, 276 PRIVATE_FLAG_TRUSTED_OVERLAY, 277 INPUT_FEATURE_SPY, 278 TYPE_APPLICATION, 279 null /* windowToken */, 280 inputTransferToken, 281 TAG + " of " + decorationSurface, 282 inputChannel); 283 } catch (RemoteException e) { 284 e.rethrowFromSystemServer(); 285 } 286 287 final SurfaceControl inputSinkSurface = surfaceControlBuilderSupplier.get() 288 .setName("TaskInputSink of " + decorationSurface) 289 .setContainerLayer() 290 .setParent(decorationSurface) 291 .setCallsite("DragResizeInputListener.setUpInputChannels") 292 .build(); 293 surfaceControlTransactionSupplier.get() 294 .setLayer(inputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER) 295 .show(inputSinkSurface) 296 .apply(); 297 try { 298 windowSession.grantInputChannel( 299 displayId, 300 inputSinkSurface, 301 sinkClientToken, 302 null /* hostInputToken */, 303 FLAG_NOT_FOCUSABLE, 304 0 /* privateFlags */, 305 INPUT_FEATURE_NO_INPUT_CHANNEL, 306 TYPE_INPUT_CONSUMER, 307 null /* windowToken */, 308 inputTransferToken, 309 "TaskInputSink of " + decorationSurface, 310 sinkInputChannel); 311 } catch (RemoteException e) { 312 e.rethrowFromSystemServer(); 313 } 314 Trace.endSection(); 315 return new InputSetUpResult(inputSinkSurface, inputChannel, sinkInputChannel); 316 } 317 318 /** 319 * Updates the geometry (the touch region) of this drag resize handler. 320 * 321 * @param incomingGeometry The geometry update to apply for this task's drag resize regions. 322 * @param touchSlop The distance in pixels user has to drag with touch for it to register 323 * as a resize action. 324 * @return whether the geometry has changed or not 325 */ setGeometry(@onNull DragResizeWindowGeometry incomingGeometry, int touchSlop)326 boolean setGeometry(@NonNull DragResizeWindowGeometry incomingGeometry, int touchSlop) { 327 DragResizeWindowGeometry geometry = mInputEventReceiver.getGeometry(); 328 if (incomingGeometry.equals(geometry)) { 329 // Geometry hasn't changed size so skip all updates. 330 return false; 331 } else { 332 geometry = incomingGeometry; 333 } 334 mInputEventReceiver.setTouchSlop(touchSlop); 335 336 mTouchRegion.setEmpty(); 337 // Apply the geometry to the touch region. 338 geometry.union(mTouchRegion); 339 mInputEventReceiver.setGeometry(geometry); 340 mInputEventReceiver.setTouchRegion(mTouchRegion); 341 342 try { 343 mWindowSession.updateInputChannel( 344 mInputChannel.getToken(), 345 mDisplayId, 346 mDecorationSurface, 347 FLAG_NOT_FOCUSABLE, 348 PRIVATE_FLAG_TRUSTED_OVERLAY, 349 INPUT_FEATURE_SPY, 350 mTouchRegion); 351 } catch (RemoteException e) { 352 e.rethrowFromSystemServer(); 353 } 354 355 final Size taskSize = geometry.getTaskSize(); 356 mSurfaceControlTransactionSupplier.get() 357 .setWindowCrop(mInputSinkSurface, taskSize.getWidth(), taskSize.getHeight()) 358 .apply(); 359 // The touch region of the TaskInputSink should be the touch region of this 360 // DragResizeInputHandler minus the task bounds. Pilfering events isn't enough to prevent 361 // input windows from handling down events, which will bring tasks in the back to front. 362 // 363 // Note not the entire touch region responds to both mouse and touchscreen events. 364 // Therefore, in the region that only responds to one of them, it would be a no-op to 365 // perform a gesture in the other type of events. We currently only have a mouse-only region 366 // out of the task bounds, and due to the roughness of touchscreen events, it's not a severe 367 // issue. However, were there touchscreen-only a region out of the task bounds, mouse 368 // gestures will become no-op in that region, even though the mouse gestures may appear to 369 // be performed on the input window behind the resize handle. 370 mTouchRegion.op(0, 0, taskSize.getWidth(), taskSize.getHeight(), Region.Op.DIFFERENCE); 371 updateSinkInputChannel(mTouchRegion); 372 return true; 373 } 374 375 /** 376 * Generate a Region that encapsulates all 4 corner handles and window edges. 377 */ getCornersRegion()378 @NonNull Region getCornersRegion() { 379 return mInputEventReceiver.getCornersRegion(); 380 } 381 updateSinkInputChannel(Region region)382 private void updateSinkInputChannel(Region region) { 383 try { 384 mWindowSession.updateInputChannel( 385 mSinkInputChannel.getToken(), 386 mDisplayId, 387 mInputSinkSurface, 388 FLAG_NOT_FOCUSABLE, 389 0 /* privateFlags */, 390 INPUT_FEATURE_NO_INPUT_CHANNEL, 391 region); 392 } catch (RemoteException ex) { 393 ex.rethrowFromSystemServer(); 394 } 395 } 396 shouldHandleEvent(@onNull MotionEvent e, @NonNull Point offset)397 boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { 398 return mInputEventReceiver != null && mInputEventReceiver.shouldHandleEvent(e, offset); 399 } 400 isHandlingDragResize()401 boolean isHandlingDragResize() { 402 return mInputEventReceiver != null && mInputEventReceiver.isHandlingEvents(); 403 } 404 405 @Override close()406 public void close() { 407 mClosed = true; 408 if (mInitInputChannels != null) { 409 mBgExecutor.removeCallbacks(mInitInputChannels); 410 } 411 if (mInputEventReceiver != null) { 412 mInputEventReceiver.dispose(); 413 } 414 if (mInputChannel != null) { 415 mInputChannel.dispose(); 416 } 417 if (mSinkInputChannel != null) { 418 mSinkInputChannel.dispose(); 419 } 420 421 if (mInputSinkSurface != null) { 422 mSurfaceControlTransactionSupplier.get() 423 .remove(mInputSinkSurface) 424 .apply(); 425 } 426 427 mBgExecutor.execute(() -> { 428 try { 429 mWindowSession.remove(mClientToken); 430 mWindowSession.remove(mSinkClientToken); 431 } catch (RemoteException e) { 432 e.rethrowFromSystemServer(); 433 } 434 // Removing this surface on the background thread to ensure that mInitInputChannels has 435 // already been finished. 436 // Do not |remove| the surface, the decoration might still be needed even if 437 // drag-resizing isn't. 438 mDecorationSurface.release(); 439 }); 440 } 441 442 private static class InputSetUpResult { 443 final @NonNull SurfaceControl mInputSinkSurface; 444 final @NonNull InputChannel mInputChannel; 445 final @NonNull InputChannel mSinkInputChannel; 446 InputSetUpResult(@onNull SurfaceControl inputSinkSurface, @NonNull InputChannel inputChannel, @NonNull InputChannel sinkInputChannel)447 InputSetUpResult(@NonNull SurfaceControl inputSinkSurface, 448 @NonNull InputChannel inputChannel, 449 @NonNull InputChannel sinkInputChannel) { 450 mInputSinkSurface = inputSinkSurface; 451 mInputChannel = inputChannel; 452 mSinkInputChannel = sinkInputChannel; 453 } 454 } 455 456 /** A factory that creates {@link TaskResizeInputEventReceiver}s. */ 457 interface TaskResizeInputEventReceiverFactory { 458 @NonNull create( @onNull Context context, @NonNull RunningTaskInfo taskInfo, @NonNull InputChannel inputChannel, @NonNull DragPositioningCallback callback, @NonNull Handler handler, @NonNull Choreographer choreographer, @NonNull Supplier<Size> displayLayoutSizeSupplier, @NonNull Consumer<Region> touchRegionConsumer, @NonNull DesktopModeEventLogger desktopModeEventLogger )459 TaskResizeInputEventReceiver create( 460 @NonNull Context context, 461 @NonNull RunningTaskInfo taskInfo, 462 @NonNull InputChannel inputChannel, 463 @NonNull DragPositioningCallback callback, 464 @NonNull Handler handler, 465 @NonNull Choreographer choreographer, 466 @NonNull Supplier<Size> displayLayoutSizeSupplier, 467 @NonNull Consumer<Region> touchRegionConsumer, 468 @NonNull DesktopModeEventLogger desktopModeEventLogger 469 ); 470 } 471 472 /** A default implementation of {@link TaskResizeInputEventReceiverFactory}. */ 473 static class DefaultTaskResizeInputEventReceiverFactory 474 implements TaskResizeInputEventReceiverFactory { 475 @Override 476 @NonNull create( @onNull Context context, @NonNull RunningTaskInfo taskInfo, @NonNull InputChannel inputChannel, @NonNull DragPositioningCallback callback, @NonNull Handler handler, @NonNull Choreographer choreographer, @NonNull Supplier<Size> displayLayoutSizeSupplier, @NonNull Consumer<Region> touchRegionConsumer, @NonNull DesktopModeEventLogger desktopModeEventLogger)477 public TaskResizeInputEventReceiver create( 478 @NonNull Context context, 479 @NonNull RunningTaskInfo taskInfo, 480 @NonNull InputChannel inputChannel, 481 @NonNull DragPositioningCallback callback, 482 @NonNull Handler handler, 483 @NonNull Choreographer choreographer, 484 @NonNull Supplier<Size> displayLayoutSizeSupplier, 485 @NonNull Consumer<Region> touchRegionConsumer, 486 @NonNull DesktopModeEventLogger desktopModeEventLogger) { 487 return new TaskResizeInputEventReceiver(context, taskInfo, inputChannel, callback, 488 handler, choreographer, displayLayoutSizeSupplier, touchRegionConsumer, 489 desktopModeEventLogger); 490 } 491 } 492 493 /** 494 * An input event receiver to handle motion events on the task's corners and edges for 495 * drag-resizing, as well as keeping the input sink updated. 496 */ 497 static class TaskResizeInputEventReceiver extends InputEventReceiver implements 498 DragDetector.MotionEventHandler { 499 @NonNull private final Context mContext; 500 @NonNull private final RunningTaskInfo mTaskInfo; 501 private final InputManager mInputManager; 502 @NonNull private final InputChannel mInputChannel; 503 @NonNull private final DragPositioningCallback mCallback; 504 @NonNull private final Choreographer mChoreographer; 505 @NonNull private final Runnable mConsumeBatchEventRunnable; 506 @NonNull private final DragDetector mDragDetector; 507 @NonNull private final Supplier<Size> mDisplayLayoutSizeSupplier; 508 @NonNull private final Consumer<Region> mTouchRegionConsumer; 509 @NonNull private final DesktopModeEventLogger mDesktopModeEventLogger; 510 private final Rect mTmpRect = new Rect(); 511 private boolean mConsumeBatchEventScheduled; 512 private DragResizeWindowGeometry mDragResizeWindowGeometry; 513 private Region mTouchRegion; 514 private boolean mShouldHandleEvents; 515 private int mLastCursorType = PointerIcon.TYPE_DEFAULT; 516 private Rect mDragStartTaskBounds; 517 // The id of the particular pointer in a MotionEvent that we are listening to for drag 518 // resize events. For example, if multiple fingers are touching the screen, then each one 519 // has a separate pointer id, but we only accept drag input from one. 520 private int mDragPointerId = -1; 521 // The type of resizing that is currently being done. Used to track the same resize trigger 522 // on start and end of the resizing action. 523 private ResizeTrigger mResizeTrigger = ResizeTrigger.UNKNOWN_RESIZE_TRIGGER; 524 // The last MotionEvent on ACTION_DOWN, used to track the input tool type and source for 525 // logging the start and end of the resizing action. 526 private MotionEvent mLastMotionEventOnDown; 527 TaskResizeInputEventReceiver(@onNull Context context, @NonNull RunningTaskInfo taskInfo, @NonNull InputChannel inputChannel, @NonNull DragPositioningCallback callback, @NonNull Handler handler, @NonNull Choreographer choreographer, @NonNull Supplier<Size> displayLayoutSizeSupplier, @NonNull Consumer<Region> touchRegionConsumer, @NonNull DesktopModeEventLogger desktopModeEventLogger)528 private TaskResizeInputEventReceiver(@NonNull Context context, 529 @NonNull RunningTaskInfo taskInfo, 530 @NonNull InputChannel inputChannel, 531 @NonNull DragPositioningCallback callback, @NonNull Handler handler, 532 @NonNull Choreographer choreographer, 533 @NonNull Supplier<Size> displayLayoutSizeSupplier, 534 @NonNull Consumer<Region> touchRegionConsumer, 535 @NonNull DesktopModeEventLogger desktopModeEventLogger) { 536 super(inputChannel, handler.getLooper()); 537 mContext = context; 538 mTaskInfo = taskInfo; 539 mInputManager = context.getSystemService(InputManager.class); 540 mInputChannel = inputChannel; 541 mCallback = callback; 542 mChoreographer = choreographer; 543 544 mConsumeBatchEventRunnable = () -> { 545 mConsumeBatchEventScheduled = false; 546 if (consumeBatchedInputEvents(mChoreographer.getFrameTimeNanos())) { 547 // If we consumed a batch here, we want to go ahead and schedule the 548 // consumption of batched input events on the next frame. Otherwise, we would 549 // wait until we have more input events pending and might get starved by other 550 // things occurring in the process. 551 scheduleConsumeBatchEvent(); 552 } 553 }; 554 555 mDragDetector = new DragDetector(this, 0 /* holdToDragMinDurationMs */, 556 ViewConfiguration.get(mContext).getScaledTouchSlop()); 557 mDisplayLayoutSizeSupplier = displayLayoutSizeSupplier; 558 mTouchRegionConsumer = touchRegionConsumer; 559 mDesktopModeEventLogger = desktopModeEventLogger; 560 } 561 562 /** 563 * Returns the geometry of the areas to drag resize. 564 */ getGeometry()565 DragResizeWindowGeometry getGeometry() { 566 return mDragResizeWindowGeometry; 567 } 568 569 /** 570 * Updates the geometry of the areas to drag resize. 571 */ setGeometry(@onNull DragResizeWindowGeometry dragResizeWindowGeometry)572 void setGeometry(@NonNull DragResizeWindowGeometry dragResizeWindowGeometry) { 573 mDragResizeWindowGeometry = dragResizeWindowGeometry; 574 } 575 576 /** 577 * Sets how much slop to allow for touches. 578 */ setTouchSlop(int touchSlop)579 void setTouchSlop(int touchSlop) { 580 mDragDetector.setTouchSlop(touchSlop); 581 } 582 583 /** 584 * Updates the region accepting input for drag resizing the task. 585 */ setTouchRegion(@onNull Region touchRegion)586 void setTouchRegion(@NonNull Region touchRegion) { 587 mTouchRegion = touchRegion; 588 } 589 590 /** 591 * Returns the union of all regions that can be touched for drag resizing; the corners and 592 * window edges. 593 */ getCornersRegion()594 @NonNull Region getCornersRegion() { 595 Region region = new Region(); 596 mDragResizeWindowGeometry.union(region); 597 return region; 598 } 599 600 @Override onBatchedInputEventPending(int source)601 public void onBatchedInputEventPending(int source) { 602 scheduleConsumeBatchEvent(); 603 } 604 scheduleConsumeBatchEvent()605 private void scheduleConsumeBatchEvent() { 606 if (mConsumeBatchEventScheduled) { 607 return; 608 } 609 mChoreographer.postCallback( 610 Choreographer.CALLBACK_INPUT, mConsumeBatchEventRunnable, null); 611 mConsumeBatchEventScheduled = true; 612 } 613 614 @Override onInputEvent(InputEvent inputEvent)615 public void onInputEvent(InputEvent inputEvent) { 616 finishInputEvent(inputEvent, handleInputEvent(inputEvent)); 617 } 618 isHandlingEvents()619 boolean isHandlingEvents() { 620 return mShouldHandleEvents; 621 } 622 handleInputEvent(InputEvent inputEvent)623 private boolean handleInputEvent(InputEvent inputEvent) { 624 if (!(inputEvent instanceof MotionEvent)) { 625 return false; 626 } 627 return mDragDetector.onMotionEvent((MotionEvent) inputEvent); 628 } 629 630 @Override handleMotionEvent(View v, MotionEvent e)631 public boolean handleMotionEvent(View v, MotionEvent e) { 632 boolean result = false; 633 634 // Check if this is a touch event vs mouse event. 635 // Touch events are tracked in four corners. Other events are tracked in resize edges. 636 switch (e.getActionMasked()) { 637 case MotionEvent.ACTION_DOWN: { 638 mShouldHandleEvents = mDragResizeWindowGeometry.shouldHandleEvent(mContext, e, 639 new Point() /* offset */); 640 if (mShouldHandleEvents) { 641 // Save the id of the pointer for this drag interaction; we will use the 642 // same pointer for all subsequent MotionEvents in this interaction. 643 mDragPointerId = e.getPointerId(0); 644 float x = e.getX(0); 645 float y = e.getY(0); 646 float rawX = e.getRawX(0); 647 float rawY = e.getRawY(0); 648 final int ctrlType = mDragResizeWindowGeometry.calculateCtrlType( 649 isEventFromTouchscreen(e), isEdgeResizePermitted(e), x, 650 y); 651 ProtoLog.d(WM_SHELL_DESKTOP_MODE, 652 "%s: Handling action down, update ctrlType to %d", TAG, ctrlType); 653 mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType, 654 e.getDisplayId(), rawX, rawY); 655 mLastMotionEventOnDown = e; 656 mResizeTrigger = (ctrlType == CTRL_TYPE_BOTTOM || ctrlType == CTRL_TYPE_TOP 657 || ctrlType == CTRL_TYPE_RIGHT || ctrlType == CTRL_TYPE_LEFT) 658 ? ResizeTrigger.EDGE : ResizeTrigger.CORNER; 659 mDesktopModeEventLogger.logTaskResizingStarted(mResizeTrigger, 660 DesktopModeEventLogger.getInputMethodFromMotionEvent(e), 661 mTaskInfo, mDragStartTaskBounds.width(), 662 mDragStartTaskBounds.height(), /* displayController= */ null, 663 /* displayLayoutSize= */ mDisplayLayoutSizeSupplier.get()); 664 // Increase the input sink region to cover the whole screen; this is to 665 // prevent input and focus from going to other tasks during a drag resize. 666 updateInputSinkRegionForDrag(mDragStartTaskBounds); 667 result = true; 668 } else { 669 ProtoLog.d(WM_SHELL_DESKTOP_MODE, 670 "%s: Handling action down, but ignore event", TAG); 671 } 672 break; 673 } 674 case MotionEvent.ACTION_MOVE: { 675 if (!mShouldHandleEvents) { 676 break; 677 } 678 mInputManager.pilferPointers(mInputChannel.getToken()); 679 final int dragPointerIndex = e.findPointerIndex(mDragPointerId); 680 if (dragPointerIndex < 0) { 681 ProtoLog.d(WM_SHELL_DESKTOP_MODE, 682 "%s: Handling action move, but ignore event due to invalid " 683 + "pointer index", 684 TAG); 685 break; 686 } 687 final float rawX = e.getRawX(dragPointerIndex); 688 final float rawY = e.getRawY(dragPointerIndex); 689 final Rect taskBounds = mCallback.onDragPositioningMove(e.getDisplayId(), 690 rawX, rawY); 691 updateInputSinkRegionForDrag(taskBounds); 692 result = true; 693 break; 694 } 695 case MotionEvent.ACTION_UP: 696 case MotionEvent.ACTION_CANCEL: { 697 if (mShouldHandleEvents) { 698 final int dragPointerIndex = e.findPointerIndex(mDragPointerId); 699 if (dragPointerIndex < 0) { 700 ProtoLog.d(WM_SHELL_DESKTOP_MODE, 701 "%s: Handling action %d, but ignore event due to invalid " 702 + "pointer index", 703 TAG, e.getActionMasked()); 704 break; 705 } 706 final Rect taskBounds = mCallback.onDragPositioningEnd(e.getDisplayId(), 707 e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex)); 708 // If taskBounds has changed, setGeometry will be called and update the 709 // sink region. Otherwise, we should revert it here. 710 if (taskBounds.equals(mDragStartTaskBounds)) { 711 mTouchRegionConsumer.accept(mTouchRegion); 712 } 713 714 mDesktopModeEventLogger.logTaskResizingEnded(mResizeTrigger, 715 DesktopModeEventLogger.getInputMethodFromMotionEvent( 716 mLastMotionEventOnDown), mTaskInfo, taskBounds.width(), 717 taskBounds.height(), 718 /* displayController= */ null, 719 /* displayLayoutSize= */ mDisplayLayoutSizeSupplier.get()); 720 } 721 mShouldHandleEvents = false; 722 mDragPointerId = -1; 723 result = true; 724 break; 725 } 726 case MotionEvent.ACTION_HOVER_ENTER: 727 case MotionEvent.ACTION_HOVER_MOVE: { 728 updateCursorType(e.getDisplayId(), e.getDeviceId(), 729 e.getPointerId(/*pointerIndex=*/0), e.getX(), e.getY()); 730 result = true; 731 break; 732 } 733 case MotionEvent.ACTION_HOVER_EXIT: 734 result = true; 735 break; 736 } 737 return result; 738 } 739 updateInputSinkRegionForDrag(Rect taskBounds)740 private void updateInputSinkRegionForDrag(Rect taskBounds) { 741 mTmpRect.set(taskBounds); 742 final Size displayLayoutSize = mDisplayLayoutSizeSupplier.get(); 743 final Region dragTouchRegion = new Region(-taskBounds.left, -taskBounds.top, 744 -taskBounds.left + displayLayoutSize.getWidth(), 745 -taskBounds.top + displayLayoutSize.getHeight()); 746 // Remove the localized task bounds from the touch region. 747 mTmpRect.offsetTo(0, 0); 748 dragTouchRegion.op(mTmpRect, Region.Op.DIFFERENCE); 749 mTouchRegionConsumer.accept(dragTouchRegion); 750 } 751 updateCursorType(int displayId, int deviceId, int pointerId, float x, float y)752 private void updateCursorType(int displayId, int deviceId, int pointerId, float x, 753 float y) { 754 // Since we are handling cursor, we know that this is not a touchscreen event, and 755 // that edge resizing should always be allowed. 756 @DragPositioningCallback.CtrlType int ctrlType = 757 mDragResizeWindowGeometry.calculateCtrlType(/* isTouchscreen= */ false, 758 /* isEdgeResizePermitted= */ true, x, y); 759 760 int cursorType = PointerIcon.TYPE_DEFAULT; 761 switch (ctrlType) { 762 case CTRL_TYPE_LEFT: 763 case CTRL_TYPE_RIGHT: 764 cursorType = PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; 765 break; 766 case CTRL_TYPE_TOP: 767 case CTRL_TYPE_BOTTOM: 768 cursorType = PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; 769 break; 770 case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: 771 case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: 772 cursorType = PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; 773 break; 774 case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: 775 case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: 776 cursorType = PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; 777 break; 778 } 779 // Only update the cursor type to default once so that views behind the decor container 780 // layer that aren't in the active resizing regions have chances to update the cursor 781 // type. We would like to enforce the cursor type by setting the cursor type multilple 782 // times in active regions because we shouldn't allow the views behind to change it, as 783 // we'll pilfer the gesture initiated in this area. This is necessary because 1) we 784 // should allow the views behind regions only for touches to set the cursor type; and 2) 785 // there is a small region out of each rounded corner that's inside the task bounds, 786 // where views in the task can receive input events because we can't set touch regions 787 // of input sinks to have rounded corners. 788 if (mLastCursorType != cursorType || cursorType != PointerIcon.TYPE_DEFAULT) { 789 ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: update pointer icon from %d to %d", 790 TAG, mLastCursorType, cursorType); 791 mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType), 792 displayId, deviceId, pointerId, mInputChannel.getToken()); 793 mLastCursorType = cursorType; 794 } 795 } 796 shouldHandleEvent(MotionEvent e, Point offset)797 private boolean shouldHandleEvent(MotionEvent e, Point offset) { 798 return mDragResizeWindowGeometry.shouldHandleEvent(mContext, e, offset); 799 } 800 } 801 } 802