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