• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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