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