• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 package com.android.quickstep.inputconsumers;
17 
18 import static android.view.MotionEvent.ACTION_CANCEL;
19 import static android.view.MotionEvent.ACTION_DOWN;
20 import static android.view.MotionEvent.ACTION_MOVE;
21 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
22 import static android.view.MotionEvent.ACTION_POINTER_UP;
23 import static android.view.MotionEvent.ACTION_UP;
24 import static android.view.MotionEvent.INVALID_POINTER_ID;
25 
26 import static com.android.launcher3.PagedView.ACTION_MOVE_ALLOW_EASY_FLING;
27 import static com.android.launcher3.PagedView.DEBUG_FAILED_QUICKSWITCH;
28 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
29 import static com.android.launcher3.Utilities.squaredHypot;
30 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
31 import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
32 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
33 
34 import android.annotation.TargetApi;
35 import android.content.Context;
36 import android.content.ContextWrapper;
37 import android.content.Intent;
38 import android.graphics.PointF;
39 import android.os.Build;
40 import android.util.Log;
41 import android.view.MotionEvent;
42 import android.view.VelocityTracker;
43 import android.view.ViewConfiguration;
44 
45 import androidx.annotation.UiThread;
46 
47 import com.android.launcher3.R;
48 import com.android.launcher3.Utilities;
49 import com.android.launcher3.testing.TestLogging;
50 import com.android.launcher3.testing.shared.TestProtocol;
51 import com.android.launcher3.util.Preconditions;
52 import com.android.launcher3.util.TraceHelper;
53 import com.android.quickstep.AbsSwipeUpHandler;
54 import com.android.quickstep.AbsSwipeUpHandler.Factory;
55 import com.android.quickstep.BaseActivityInterface;
56 import com.android.quickstep.GestureState;
57 import com.android.quickstep.InputConsumer;
58 import com.android.quickstep.RecentsAnimationCallbacks;
59 import com.android.quickstep.RecentsAnimationController;
60 import com.android.quickstep.RecentsAnimationDeviceState;
61 import com.android.quickstep.RecentsAnimationTargets;
62 import com.android.quickstep.RotationTouchHelper;
63 import com.android.quickstep.TaskAnimationManager;
64 import com.android.quickstep.util.CachedEventDispatcher;
65 import com.android.quickstep.util.MotionPauseDetector;
66 import com.android.quickstep.util.NavBarPosition;
67 import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
68 import com.android.systemui.shared.system.InputMonitorCompat;
69 
70 import java.util.function.Consumer;
71 
72 /**
73  * Input consumer for handling events originating from an activity other than Launcher
74  */
75 @TargetApi(Build.VERSION_CODES.P)
76 public class OtherActivityInputConsumer extends ContextWrapper implements InputConsumer {
77 
78     public static final String DOWN_EVT = "OtherActivityInputConsumer.DOWN";
79     private static final String UP_EVT = "OtherActivityInputConsumer.UP";
80 
81     // Minimum angle of a gesture's coordinate where a release goes to overview.
82     public static final int OVERVIEW_MIN_DEGREES = 15;
83 
84     private final RecentsAnimationDeviceState mDeviceState;
85     private final NavBarPosition mNavBarPosition;
86     private final TaskAnimationManager mTaskAnimationManager;
87     private final GestureState mGestureState;
88     private final RotationTouchHelper mRotationTouchHelper;
89     private RecentsAnimationCallbacks mActiveCallbacks;
90     private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
91     private final InputMonitorCompat mInputMonitorCompat;
92     private final InputEventReceiver mInputEventReceiver;
93     private final BaseActivityInterface mActivityInterface;
94 
95     private final AbsSwipeUpHandler.Factory mHandlerFactory;
96 
97     private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
98     private final MotionPauseDetector mMotionPauseDetector;
99     private final float mMotionPauseMinDisplacement;
100 
101     private VelocityTracker mVelocityTracker;
102 
103     private AbsSwipeUpHandler mInteractionHandler;
104     private final FinishImmediatelyHandler mCleanupHandler = new FinishImmediatelyHandler();
105 
106     private final boolean mIsDeferredDownTarget;
107     private final PointF mDownPos = new PointF();
108     private final PointF mLastPos = new PointF();
109     private int mActivePointerId = INVALID_POINTER_ID;
110 
111     // Distance after which we start dragging the window.
112     private final float mTouchSlop;
113 
114     private final float mSquaredTouchSlop;
115     private final boolean mDisableHorizontalSwipe;
116 
117     // Slop used to check when we start moving window.
118     private boolean mPassedWindowMoveSlop;
119     // Slop used to determine when we say that the gesture has started.
120     private boolean mPassedPilferInputSlop;
121     // Same as mPassedPilferInputSlop, except when continuing a gesture mPassedPilferInputSlop is
122     // initially true while this one is false.
123     private boolean mPassedSlopOnThisGesture;
124 
125     // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
126     private float mStartDisplacement;
127 
OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState, TaskAnimationManager taskAnimationManager, GestureState gestureState, boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback, InputMonitorCompat inputMonitorCompat, InputEventReceiver inputEventReceiver, boolean disableHorizontalSwipe, Factory handlerFactory)128     public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState,
129             TaskAnimationManager taskAnimationManager, GestureState gestureState,
130             boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback,
131             InputMonitorCompat inputMonitorCompat, InputEventReceiver inputEventReceiver,
132             boolean disableHorizontalSwipe, Factory handlerFactory) {
133         super(base);
134         mDeviceState = deviceState;
135         mNavBarPosition = mDeviceState.getNavBarPosition();
136         mTaskAnimationManager = taskAnimationManager;
137         mGestureState = gestureState;
138         mHandlerFactory = handlerFactory;
139         mActivityInterface = mGestureState.getActivityInterface();
140 
141         mMotionPauseDetector = new MotionPauseDetector(base, false,
142                 mNavBarPosition.isLeftEdge() || mNavBarPosition.isRightEdge()
143                         ? MotionEvent.AXIS_X : MotionEvent.AXIS_Y);
144         mMotionPauseMinDisplacement = base.getResources().getDimension(
145                 R.dimen.motion_pause_detector_min_displacement_from_app);
146         mOnCompleteCallback = onCompleteCallback;
147         mVelocityTracker = VelocityTracker.obtain();
148         mInputMonitorCompat = inputMonitorCompat;
149         mInputEventReceiver = inputEventReceiver;
150 
151         boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
152         mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
153 
154         mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
155         mSquaredTouchSlop = mDeviceState.getSquaredTouchSlop();
156 
157         mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
158         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
159         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
160     }
161 
162     @Override
getType()163     public int getType() {
164         return TYPE_OTHER_ACTIVITY;
165     }
166 
167     @Override
isConsumerDetachedFromGesture()168     public boolean isConsumerDetachedFromGesture() {
169         return true;
170     }
171 
forceCancelGesture(MotionEvent ev)172     private void forceCancelGesture(MotionEvent ev) {
173         int action = ev.getAction();
174         ev.setAction(ACTION_CANCEL);
175         finishTouchTracking(ev);
176         ev.setAction(action);
177     }
178 
179     @Override
onMotionEvent(MotionEvent ev)180     public void onMotionEvent(MotionEvent ev) {
181         if (mVelocityTracker == null) {
182             return;
183         }
184 
185         // Proxy events to recents view
186         if (mPassedWindowMoveSlop && mInteractionHandler != null
187                 && !mRecentsViewDispatcher.hasConsumer()) {
188             mRecentsViewDispatcher.setConsumer(mInteractionHandler
189                     .getRecentsViewDispatcher(mNavBarPosition.getRotation()));
190             int action = ev.getAction();
191             ev.setAction(ACTION_MOVE_ALLOW_EASY_FLING);
192             mRecentsViewDispatcher.dispatchEvent(ev);
193             ev.setAction(action);
194         }
195         int edgeFlags = ev.getEdgeFlags();
196         ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
197 
198         if (mGestureState.isTrackpadGesture()) {
199             // Disable scrolling in RecentsView for 3-finger trackpad gesture. We don't know if a
200             // trackpad motion event is 3-finger or 4-finger with the U API until ACTION_MOVE (we
201             // skip ACTION_POINTER_UP events in TouchInteractionService), so in order to make sure
202             // that RecentsView always get a closed sequence of motion events and yet disable
203             // 3-finger scroll, we do the following (1) always dispatch ACTION_DOWN and ACTION_UP
204             // trackpad multi-finger motion events. (2) only dispatch 4-finger ACTION_MOVE motion
205             // events.
206             switch (ev.getActionMasked()) {
207                 case ACTION_MOVE -> {
208                     if (mGestureState.isFourFingerTrackpadGesture()) {
209                         mRecentsViewDispatcher.dispatchEvent(ev);
210                     }
211                 }
212                 default -> mRecentsViewDispatcher.dispatchEvent(ev);
213             }
214         } else {
215             mRecentsViewDispatcher.dispatchEvent(ev);
216         }
217         ev.setEdgeFlags(edgeFlags);
218 
219         mVelocityTracker.addMovement(ev);
220         if (ev.getActionMasked() == ACTION_POINTER_UP) {
221             mVelocityTracker.clear();
222             mMotionPauseDetector.clear();
223         }
224 
225         switch (ev.getActionMasked()) {
226             case ACTION_DOWN: {
227                 // Until we detect the gesture, handle events as we receive them
228                 mInputEventReceiver.setBatchingEnabled(false);
229 
230                 TraceHelper.INSTANCE.beginSection(DOWN_EVT);
231                 mActivePointerId = ev.getPointerId(0);
232                 mDownPos.set(ev.getX(), ev.getY());
233                 mLastPos.set(mDownPos);
234 
235                 // Start the window animation on down to give more time for launcher to draw if the
236                 // user didn't start the gesture over the back button
237                 if (!mIsDeferredDownTarget) {
238                     startTouchTrackingForWindowAnimation(ev.getEventTime());
239                 }
240 
241                 TraceHelper.INSTANCE.endSection();
242                 break;
243             }
244             case ACTION_POINTER_DOWN: {
245                 if (!mPassedPilferInputSlop) {
246                     // Cancel interaction in case of multi-touch interaction
247                     int ptrIdx = ev.getActionIndex();
248                     if (!mRotationTouchHelper.isInSwipeUpTouchRegion(ev, ptrIdx)) {
249                         forceCancelGesture(ev);
250                     }
251                 }
252                 break;
253             }
254             case ACTION_POINTER_UP: {
255                 int ptrIdx = ev.getActionIndex();
256                 int ptrId = ev.getPointerId(ptrIdx);
257                 if (ptrId == mActivePointerId) {
258                     final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
259                     mDownPos.set(
260                             ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
261                             ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
262                     mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
263                     mActivePointerId = ev.getPointerId(newPointerIdx);
264                 }
265                 break;
266             }
267             case ACTION_MOVE: {
268                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
269                 if (pointerIndex == INVALID_POINTER_ID) {
270                     break;
271                 }
272                 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
273                 float displacement = getDisplacement(ev);
274                 float displacementX = mLastPos.x - mDownPos.x;
275                 float displacementY = mLastPos.y - mDownPos.y;
276 
277                 if (!mPassedWindowMoveSlop) {
278                     if (!mIsDeferredDownTarget) {
279                         // Normal gesture, ensure we pass the drag slop before we start tracking
280                         // the gesture
281                         if (mGestureState.isTrackpadGesture() || Math.abs(displacement)
282                                 > mTouchSlop) {
283                             mPassedWindowMoveSlop = true;
284                             mStartDisplacement = Math.min(displacement, -mTouchSlop);
285                         }
286                     }
287                 }
288 
289                 float horizontalDist = Math.abs(displacementX);
290                 float upDist = -displacement;
291                 boolean passedSlop = mGestureState.isTrackpadGesture() || squaredHypot(
292                         displacementX, displacementY) >= mSquaredTouchSlop;
293 
294                 if (!mPassedSlopOnThisGesture && passedSlop) {
295                     mPassedSlopOnThisGesture = true;
296                 }
297                 // Until passing slop, we don't know what direction we're going, so assume
298                 // we're quick switching to avoid translating recents away when continuing
299                 // the gesture (in which case mPassedPilferInputSlop starts as true).
300                 boolean haveNotPassedSlopOnContinuedGesture =
301                         !mPassedSlopOnThisGesture && mPassedPilferInputSlop;
302                 double degrees = Math.toDegrees(Math.atan(upDist / horizontalDist));
303 
304                 // Regarding degrees >= -OVERVIEW_MIN_DEGREES - Trackpad gestures can start anywhere
305                 // on the screen, allowing downward swipes. We want to impose the same angle in that
306                 // scenario.
307                 boolean swipeWithinQuickSwitchRange = degrees <= OVERVIEW_MIN_DEGREES
308                         && (!mGestureState.isTrackpadGesture() || degrees >= -OVERVIEW_MIN_DEGREES);
309                 boolean isLikelyToStartNewTask =
310                         haveNotPassedSlopOnContinuedGesture || swipeWithinQuickSwitchRange;
311 
312                 if (!mPassedPilferInputSlop) {
313                     if (passedSlop) {
314                         // Horizontal gesture is not allowed in this region
315                         boolean isHorizontalSwipeWhenDisabled =
316                                 (mDisableHorizontalSwipe && Math.abs(displacementX) > Math.abs(
317                                         displacementY));
318                         // Do not allow quick switch for trackpad 3-finger gestures
319                         // TODO(b/261815244): might need to impose stronger conditions for the swipe
320                         //  angle
321                         boolean noQuickSwitchForThreeFingerGesture = isLikelyToStartNewTask
322                                 && mGestureState.isThreeFingerTrackpadGesture();
323                         boolean noQuickstepForFourFingerGesture = !isLikelyToStartNewTask
324                                 && mGestureState.isFourFingerTrackpadGesture();
325                         if (isHorizontalSwipeWhenDisabled || noQuickSwitchForThreeFingerGesture
326                                 || noQuickstepForFourFingerGesture) {
327                             forceCancelGesture(ev);
328                             break;
329                         }
330 
331                         mPassedPilferInputSlop = true;
332 
333                         if (mIsDeferredDownTarget) {
334                             // Deferred gesture, start the animation and gesture tracking once
335                             // we pass the actual touch slop
336                             startTouchTrackingForWindowAnimation(ev.getEventTime());
337                         }
338                         if (!mPassedWindowMoveSlop) {
339                             mPassedWindowMoveSlop = true;
340                             mStartDisplacement = Math.min(displacement, -mTouchSlop);
341                         }
342                         notifyGestureStarted(isLikelyToStartNewTask);
343                     }
344                 }
345 
346                 if (mInteractionHandler != null) {
347                     if (mPassedWindowMoveSlop) {
348                         // Move
349                         mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
350                     }
351 
352                     if (mDeviceState.isFullyGesturalNavMode()
353                             || mGestureState.isTrackpadGesture()) {
354                         boolean minSwipeMet = upDist >= Math.max(mMotionPauseMinDisplacement,
355                                 mInteractionHandler.getThresholdToAllowMotionPause());
356                         mInteractionHandler.setCanSlowSwipeGoHome(minSwipeMet);
357                         mMotionPauseDetector.setDisallowPause(!minSwipeMet
358                                 || isLikelyToStartNewTask);
359                         mMotionPauseDetector.addPosition(ev);
360                         mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask);
361                     }
362                 }
363                 break;
364             }
365             case ACTION_CANCEL:
366             case ACTION_UP: {
367                 if (DEBUG_FAILED_QUICKSWITCH && !mPassedWindowMoveSlop) {
368                     float displacementX = mLastPos.x - mDownPos.x;
369                     float displacementY = mLastPos.y - mDownPos.y;
370                     Log.d("Quickswitch", "mPassedWindowMoveSlop=false"
371                             + " disp=" + squaredHypot(displacementX, displacementY)
372                             + " slop=" + mSquaredTouchSlop);
373                 }
374                 finishTouchTracking(ev);
375                 break;
376             }
377         }
378     }
379 
notifyGestureStarted(boolean isLikelyToStartNewTask)380     private void notifyGestureStarted(boolean isLikelyToStartNewTask) {
381         if (mInteractionHandler == null) {
382             return;
383         }
384         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
385         mInputMonitorCompat.pilferPointers();
386         // Once we detect the gesture, we can enable batching to reduce further updates
387         mInputEventReceiver.setBatchingEnabled(true);
388 
389         // Notify the handler that the gesture has actually started
390         mInteractionHandler.onGestureStarted(isLikelyToStartNewTask);
391     }
392 
startTouchTrackingForWindowAnimation(long touchTimeMs)393     private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
394         mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs);
395         mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
396         mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler.getMotionPauseListener());
397         mInteractionHandler.initWhenReady();
398 
399         if (mTaskAnimationManager.isRecentsAnimationRunning()) {
400             mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState);
401             mActiveCallbacks.removeListener(mCleanupHandler);
402             mActiveCallbacks.addListener(mInteractionHandler);
403             mTaskAnimationManager.notifyRecentsAnimationState(mInteractionHandler);
404             notifyGestureStarted(true /*isLikelyToStartNewTask*/);
405         } else {
406             Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
407             intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
408             mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
409                     mInteractionHandler);
410         }
411     }
412 
413     /**
414      * Called when the gesture has ended. Does not correlate to the completion of the interaction as
415      * the animation can still be running.
416      */
finishTouchTracking(MotionEvent ev)417     private void finishTouchTracking(MotionEvent ev) {
418         TraceHelper.INSTANCE.beginSection(UP_EVT);
419 
420         boolean isCanceled = ev.getActionMasked() == ACTION_CANCEL;
421         if (mPassedWindowMoveSlop && mInteractionHandler != null) {
422             if (isCanceled) {
423                 mInteractionHandler.onGestureCancelled();
424             } else {
425                 mVelocityTracker.computeCurrentVelocity(PX_PER_MS);
426                 float velocityXPxPerMs = mVelocityTracker.getXVelocity(mActivePointerId);
427                 float velocityYPxPerMs = mVelocityTracker.getYVelocity(mActivePointerId);
428                 float velocityPxPerMs = mNavBarPosition.isRightEdge()
429                         ? velocityXPxPerMs
430                         : mNavBarPosition.isLeftEdge()
431                                 ? -velocityXPxPerMs
432                                 : velocityYPxPerMs;
433                 mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
434                 mInteractionHandler.onGestureEnded(velocityPxPerMs,
435                         new PointF(velocityXPxPerMs, velocityYPxPerMs));
436             }
437         } else {
438             // Since we start touch tracking on DOWN, we may reach this state without actually
439             // starting the gesture. In that case, we need to clean-up an unfinished or un-started
440             // animation.
441             if (mActiveCallbacks != null && mInteractionHandler != null) {
442                 if (mTaskAnimationManager.isRecentsAnimationRunning()) {
443                     // The animation started, but with no movement, in this case, there will be no
444                     // animateToProgress so we have to manually finish here. In the case of
445                     // ACTION_CANCEL, someone else may be doing something so finish synchronously.
446                     mTaskAnimationManager.finishRunningRecentsAnimation(false /* toHome */,
447                             isCanceled /* forceFinish */);
448                 } else {
449                     // The animation hasn't started yet, so insert a replacement handler into the
450                     // callbacks which immediately finishes the animation after it starts.
451                     mActiveCallbacks.addListener(mCleanupHandler);
452                 }
453             }
454             onConsumerAboutToBeSwitched();
455             onInteractionGestureFinished();
456         }
457         cleanupAfterGesture();
458         TraceHelper.INSTANCE.endSection();
459     }
460 
cleanupAfterGesture()461     private void cleanupAfterGesture() {
462         if (mVelocityTracker != null) {
463             mVelocityTracker.recycle();
464             mVelocityTracker = null;
465         }
466         mMotionPauseDetector.clear();
467     }
468 
469     @Override
notifyOrientationSetup()470     public void notifyOrientationSetup() {
471         mRotationTouchHelper.onStartGesture();
472     }
473 
474     @Override
onConsumerAboutToBeSwitched()475     public void onConsumerAboutToBeSwitched() {
476         Preconditions.assertUIThread();
477         if (mInteractionHandler != null) {
478             // The consumer is being switched while we are active. Set up the shared state to be
479             // used by the next animation
480             removeListener();
481             mInteractionHandler.onConsumerAboutToBeSwitched();
482         }
483     }
484 
485     @UiThread
onInteractionGestureFinished()486     private void onInteractionGestureFinished() {
487         Preconditions.assertUIThread();
488         removeListener();
489         mInteractionHandler = null;
490         cleanupAfterGesture();
491         mOnCompleteCallback.accept(this);
492     }
493 
removeListener()494     private void removeListener() {
495         if (mActiveCallbacks != null && mInteractionHandler != null) {
496             mActiveCallbacks.removeListener(mInteractionHandler);
497         }
498     }
499 
getDisplacement(MotionEvent ev)500     private float getDisplacement(MotionEvent ev) {
501         if (mNavBarPosition.isRightEdge()) {
502             return ev.getX() - mDownPos.x;
503         } else if (mNavBarPosition.isLeftEdge()) {
504             return mDownPos.x - ev.getX();
505         } else {
506             return ev.getY() - mDownPos.y;
507         }
508     }
509 
510     @Override
allowInterceptByParent()511     public boolean allowInterceptByParent() {
512         return !mPassedPilferInputSlop;
513     }
514 
515     /**
516      * A listener which just finishes the animation immediately after starting. Replaces
517      * AbsSwipeUpHandler if the gesture itself finishes before the animation even starts.
518      */
519     private static class FinishImmediatelyHandler
520             implements RecentsAnimationCallbacks.RecentsAnimationListener {
521 
onRecentsAnimationStart(RecentsAnimationController controller, RecentsAnimationTargets targets)522         public void onRecentsAnimationStart(RecentsAnimationController controller,
523                 RecentsAnimationTargets targets) {
524             Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
525                 controller.finish(false /* toRecents */, null);
526             });
527         }
528     }
529 }
530