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