| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.quickstep.inputconsumers; |
| |
| import static android.view.MotionEvent.ACTION_CANCEL; |
| import static android.view.MotionEvent.ACTION_DOWN; |
| import static android.view.MotionEvent.ACTION_MOVE; |
| import static android.view.MotionEvent.ACTION_POINTER_DOWN; |
| import static android.view.MotionEvent.ACTION_POINTER_UP; |
| import static android.view.MotionEvent.ACTION_UP; |
| import static android.view.MotionEvent.INVALID_POINTER_ID; |
| import static com.android.launcher3.Utilities.EDGE_NAV_BAR; |
| import static com.android.launcher3.Utilities.squaredHypot; |
| import static com.android.launcher3.util.RaceConditionTracker.ENTER; |
| import static com.android.launcher3.util.RaceConditionTracker.EXIT; |
| import static com.android.quickstep.TouchInteractionService.INTENT_EXTRA_LOG_TRACE_ID; |
| import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG; |
| import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync; |
| import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; |
| |
| import android.annotation.TargetApi; |
| import android.app.ActivityManager.RunningTaskInfo; |
| import android.content.Context; |
| import android.content.ContextWrapper; |
| import android.content.Intent; |
| import android.graphics.PointF; |
| import android.graphics.RectF; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.view.MotionEvent; |
| import android.view.VelocityTracker; |
| import android.view.ViewConfiguration; |
| import androidx.annotation.UiThread; |
| import com.android.launcher3.R; |
| import com.android.launcher3.util.Preconditions; |
| import com.android.launcher3.util.RaceConditionTracker; |
| import com.android.launcher3.util.TraceHelper; |
| import com.android.quickstep.BaseSwipeUpHandler; |
| import com.android.quickstep.BaseSwipeUpHandler.Factory; |
| import com.android.quickstep.OverviewCallbacks; |
| import com.android.quickstep.SwipeSharedState; |
| import com.android.quickstep.SysUINavigationMode; |
| import com.android.quickstep.SysUINavigationMode.Mode; |
| import com.android.quickstep.util.CachedEventDispatcher; |
| import com.android.quickstep.util.MotionPauseDetector; |
| import com.android.quickstep.util.NavBarPosition; |
| import com.android.quickstep.util.RecentsAnimationListenerSet; |
| import com.android.systemui.shared.system.ActivityManagerWrapper; |
| import com.android.systemui.shared.system.InputMonitorCompat; |
| import java.util.function.Consumer; |
| |
| /** |
| * Input consumer for handling events originating from an activity other than Launcher |
| */ |
| @TargetApi(Build.VERSION_CODES.P) |
| public class OtherActivityInputConsumer extends ContextWrapper implements InputConsumer { |
| |
| public static final String DOWN_EVT = "OtherActivityInputConsumer.DOWN"; |
| private static final String UP_EVT = "OtherActivityInputConsumer.UP"; |
| |
| // TODO: Move to quickstep contract |
| public static final float QUICKSTEP_TOUCH_SLOP_RATIO = 3; |
| |
| private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher(); |
| private final RunningTaskInfo mRunningTask; |
| private final OverviewCallbacks mOverviewCallbacks; |
| private final SwipeSharedState mSwipeSharedState; |
| private final InputMonitorCompat mInputMonitorCompat; |
| private final SysUINavigationMode.Mode mMode; |
| private final RectF mSwipeTouchRegion; |
| |
| private final BaseSwipeUpHandler.Factory mHandlerFactory; |
| |
| private final NavBarPosition mNavBarPosition; |
| |
| private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback; |
| private final MotionPauseDetector mMotionPauseDetector; |
| private final float mMotionPauseMinDisplacement; |
| private VelocityTracker mVelocityTracker; |
| |
| private BaseSwipeUpHandler mInteractionHandler; |
| |
| private final boolean mIsDeferredDownTarget; |
| private final PointF mDownPos = new PointF(); |
| private final PointF mLastPos = new PointF(); |
| private int mActivePointerId = INVALID_POINTER_ID; |
| |
| // Distance after which we start dragging the window. |
| private final float mTouchSlop; |
| |
| private final float mSquaredTouchSlop; |
| private final boolean mDisableHorizontalSwipe; |
| |
| // Slop used to check when we start moving window. |
| private boolean mPassedWindowMoveSlop; |
| // Slop used to determine when we say that the gesture has started. |
| private boolean mPassedPilferInputSlop; |
| |
| // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar. |
| private float mStartDisplacement; |
| |
| private Handler mMainThreadHandler; |
| private Runnable mCancelRecentsAnimationRunnable = () -> { |
| ActivityManagerWrapper.getInstance().cancelRecentsAnimation( |
| true /* restoreHomeStackPosition */); |
| }; |
| private int mLogId; |
| |
| public OtherActivityInputConsumer(Context base, RunningTaskInfo runningTaskInfo, |
| boolean isDeferredDownTarget, OverviewCallbacks overviewCallbacks, |
| Consumer<OtherActivityInputConsumer> onCompleteCallback, |
| SwipeSharedState swipeSharedState, InputMonitorCompat inputMonitorCompat, |
| RectF swipeTouchRegion, boolean disableHorizontalSwipe, |
| Factory handlerFactory, int logId) { |
| super(base); |
| mLogId = logId; |
| |
| mMainThreadHandler = new Handler(Looper.getMainLooper()); |
| mRunningTask = runningTaskInfo; |
| mMode = SysUINavigationMode.getMode(base); |
| mSwipeTouchRegion = swipeTouchRegion; |
| mHandlerFactory = handlerFactory; |
| |
| mMotionPauseDetector = new MotionPauseDetector(base); |
| mMotionPauseMinDisplacement = base.getResources().getDimension( |
| R.dimen.motion_pause_detector_min_displacement_from_app); |
| mOnCompleteCallback = onCompleteCallback; |
| mVelocityTracker = VelocityTracker.obtain(); |
| mInputMonitorCompat = inputMonitorCompat; |
| |
| boolean continuingPreviousGesture = swipeSharedState.getActiveListener() != null; |
| mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget; |
| mOverviewCallbacks = overviewCallbacks; |
| mSwipeSharedState = swipeSharedState; |
| |
| mNavBarPosition = new NavBarPosition(base); |
| mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop(); |
| |
| float slop = QUICKSTEP_TOUCH_SLOP_RATIO * mTouchSlop; |
| mSquaredTouchSlop = slop * slop; |
| |
| mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture; |
| mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe; |
| } |
| |
| @Override |
| public int getType() { |
| return TYPE_OTHER_ACTIVITY; |
| } |
| |
| private void forceCancelGesture(MotionEvent ev) { |
| int action = ev.getAction(); |
| ev.setAction(ACTION_CANCEL); |
| finishTouchTracking(ev); |
| ev.setAction(action); |
| } |
| |
| @Override |
| public void onMotionEvent(MotionEvent ev) { |
| if (mVelocityTracker == null) { |
| return; |
| } |
| |
| // Proxy events to recents view |
| if (mPassedWindowMoveSlop && mInteractionHandler != null |
| && !mRecentsViewDispatcher.hasConsumer()) { |
| mRecentsViewDispatcher.setConsumer(mInteractionHandler.getRecentsViewDispatcher( |
| mNavBarPosition.getRotationMode())); |
| } |
| int edgeFlags = ev.getEdgeFlags(); |
| ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR); |
| mRecentsViewDispatcher.dispatchEvent(ev); |
| ev.setEdgeFlags(edgeFlags); |
| |
| mVelocityTracker.addMovement(ev); |
| if (ev.getActionMasked() == ACTION_POINTER_UP) { |
| mVelocityTracker.clear(); |
| mMotionPauseDetector.clear(); |
| } |
| |
| switch (ev.getActionMasked()) { |
| case ACTION_DOWN: { |
| RaceConditionTracker.onEvent(DOWN_EVT, ENTER); |
| TraceHelper.beginSection("TouchInt"); |
| mActivePointerId = ev.getPointerId(0); |
| mDownPos.set(ev.getX(), ev.getY()); |
| mLastPos.set(mDownPos); |
| |
| // Start the window animation on down to give more time for launcher to draw if the |
| // user didn't start the gesture over the back button |
| if (!mIsDeferredDownTarget) { |
| startTouchTrackingForWindowAnimation(ev.getEventTime(), false); |
| } |
| |
| RaceConditionTracker.onEvent(DOWN_EVT, EXIT); |
| break; |
| } |
| case ACTION_POINTER_DOWN: { |
| if (!mPassedPilferInputSlop) { |
| // Cancel interaction in case of multi-touch interaction |
| int ptrIdx = ev.getActionIndex(); |
| if (!mSwipeTouchRegion.contains(ev.getX(ptrIdx), ev.getY(ptrIdx))) { |
| forceCancelGesture(ev); |
| } |
| } |
| break; |
| } |
| case ACTION_POINTER_UP: { |
| int ptrIdx = ev.getActionIndex(); |
| int ptrId = ev.getPointerId(ptrIdx); |
| if (ptrId == mActivePointerId) { |
| final int newPointerIdx = ptrIdx == 0 ? 1 : 0; |
| mDownPos.set( |
| ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), |
| ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); |
| mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); |
| mActivePointerId = ev.getPointerId(newPointerIdx); |
| } |
| break; |
| } |
| case ACTION_MOVE: { |
| int pointerIndex = ev.findPointerIndex(mActivePointerId); |
| if (pointerIndex == INVALID_POINTER_ID) { |
| break; |
| } |
| mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); |
| float displacement = getDisplacement(ev); |
| float displacementX = mLastPos.x - mDownPos.x; |
| |
| if (!mPassedWindowMoveSlop) { |
| if (!mIsDeferredDownTarget) { |
| // Normal gesture, ensure we pass the drag slop before we start tracking |
| // the gesture |
| if (Math.abs(displacement) > mTouchSlop) { |
| mPassedWindowMoveSlop = true; |
| mStartDisplacement = Math.min(displacement, -mTouchSlop); |
| } |
| } |
| } |
| |
| float horizontalDist = Math.abs(displacementX); |
| float upDist = -displacement; |
| boolean isLikelyToStartNewTask = horizontalDist > upDist; |
| |
| if (!mPassedPilferInputSlop) { |
| float displacementY = mLastPos.y - mDownPos.y; |
| if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) { |
| if (mDisableHorizontalSwipe |
| && Math.abs(displacementX) > Math.abs(displacementY)) { |
| // Horizontal gesture is not allowed in this region |
| forceCancelGesture(ev); |
| break; |
| } |
| |
| mPassedPilferInputSlop = true; |
| |
| if (mIsDeferredDownTarget) { |
| // Deferred gesture, start the animation and gesture tracking once |
| // we pass the actual touch slop |
| startTouchTrackingForWindowAnimation( |
| ev.getEventTime(), isLikelyToStartNewTask); |
| } |
| if (!mPassedWindowMoveSlop) { |
| mPassedWindowMoveSlop = true; |
| mStartDisplacement = Math.min(displacement, -mTouchSlop); |
| |
| } |
| notifyGestureStarted(); |
| } |
| } |
| |
| if (mInteractionHandler != null) { |
| if (mPassedWindowMoveSlop) { |
| // Move |
| mInteractionHandler.updateDisplacement(displacement - mStartDisplacement); |
| } |
| |
| if (mMode == Mode.NO_BUTTON) { |
| mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement |
| || isLikelyToStartNewTask); |
| mMotionPauseDetector.addPosition(displacement, ev.getEventTime()); |
| mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask); |
| } |
| } |
| break; |
| } |
| case ACTION_CANCEL: |
| case ACTION_UP: { |
| finishTouchTracking(ev); |
| break; |
| } |
| } |
| } |
| |
| private void notifyGestureStarted() { |
| TOUCH_INTERACTION_LOG.addLog("startQuickstep"); |
| if (mInteractionHandler == null) { |
| return; |
| } |
| mInputMonitorCompat.pilferPointers(); |
| |
| mOverviewCallbacks.closeAllWindows(); |
| ActivityManagerWrapper.getInstance().closeSystemWindows( |
| CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); |
| |
| // Notify the handler that the gesture has actually started |
| mInteractionHandler.onGestureStarted(); |
| } |
| |
| private void startTouchTrackingForWindowAnimation( |
| long touchTimeMs, boolean isLikelyToStartNewTask) { |
| TOUCH_INTERACTION_LOG.addLog("startRecentsAnimation"); |
| |
| RecentsAnimationListenerSet listenerSet = mSwipeSharedState.getActiveListener(); |
| final BaseSwipeUpHandler handler = mHandlerFactory.newHandler(mRunningTask, touchTimeMs, |
| listenerSet != null, isLikelyToStartNewTask); |
| |
| mInteractionHandler = handler; |
| handler.setGestureEndCallback(this::onInteractionGestureFinished); |
| mMotionPauseDetector.setOnMotionPauseListener(handler::onMotionPauseChanged); |
| handler.initWhenReady(); |
| |
| if (listenerSet != null) { |
| listenerSet.addListener(handler); |
| mSwipeSharedState.applyActiveRecentsAnimationState(handler); |
| notifyGestureStarted(); |
| } else { |
| RecentsAnimationListenerSet newListenerSet = |
| mSwipeSharedState.newRecentsAnimationListenerSet(); |
| newListenerSet.addListener(handler); |
| Intent intent = handler.getLaunchIntent(); |
| intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mLogId); |
| startRecentsActivityAsync(intent, newListenerSet); |
| } |
| } |
| |
| /** |
| * Called when the gesture has ended. Does not correlate to the completion of the interaction as |
| * the animation can still be running. |
| */ |
| private void finishTouchTracking(MotionEvent ev) { |
| RaceConditionTracker.onEvent(UP_EVT, ENTER); |
| TraceHelper.endSection("TouchInt"); |
| |
| if (mPassedWindowMoveSlop && mInteractionHandler != null) { |
| if (ev.getActionMasked() == ACTION_CANCEL) { |
| mInteractionHandler.onGestureCancelled(); |
| } else { |
| mVelocityTracker.computeCurrentVelocity(1000, |
| ViewConfiguration.get(this).getScaledMaximumFlingVelocity()); |
| float velocityX = mVelocityTracker.getXVelocity(mActivePointerId); |
| float velocityY = mVelocityTracker.getYVelocity(mActivePointerId); |
| float velocity = mNavBarPosition.isRightEdge() ? velocityX |
| : mNavBarPosition.isLeftEdge() ? -velocityX |
| : velocityY; |
| |
| mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement); |
| mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY), |
| mDownPos); |
| } |
| } else { |
| // Since we start touch tracking on DOWN, we may reach this state without actually |
| // starting the gesture. In that case, just cleanup immediately. |
| onConsumerAboutToBeSwitched(); |
| onInteractionGestureFinished(); |
| |
| // Cancel the recents animation if SysUI happens to handle UP before we have a chance |
| // to start the recents animation. In addition, workaround for b/126336729 by delaying |
| // the cancel of the animation for a period, in case SysUI is slow to handle UP and we |
| // handle DOWN & UP and move the home stack before SysUI can start the activity |
| mMainThreadHandler.removeCallbacks(mCancelRecentsAnimationRunnable); |
| mMainThreadHandler.postDelayed(mCancelRecentsAnimationRunnable, 100); |
| } |
| mVelocityTracker.recycle(); |
| mVelocityTracker = null; |
| mMotionPauseDetector.clear(); |
| RaceConditionTracker.onEvent(UP_EVT, EXIT); |
| } |
| |
| @Override |
| public void onConsumerAboutToBeSwitched() { |
| Preconditions.assertUIThread(); |
| mMainThreadHandler.removeCallbacks(mCancelRecentsAnimationRunnable); |
| if (mInteractionHandler != null) { |
| // The consumer is being switched while we are active. Set up the shared state to be |
| // used by the next animation |
| removeListener(); |
| mInteractionHandler.onConsumerAboutToBeSwitched(mSwipeSharedState); |
| } |
| } |
| |
| @UiThread |
| private void onInteractionGestureFinished() { |
| Preconditions.assertUIThread(); |
| removeListener(); |
| mInteractionHandler = null; |
| mOnCompleteCallback.accept(this); |
| } |
| |
| private void removeListener() { |
| RecentsAnimationListenerSet listenerSet = mSwipeSharedState.getActiveListener(); |
| if (listenerSet != null) { |
| listenerSet.removeListener(mInteractionHandler); |
| } |
| } |
| |
| private float getDisplacement(MotionEvent ev) { |
| if (mNavBarPosition.isRightEdge()) { |
| return ev.getX() - mDownPos.x; |
| } else if (mNavBarPosition.isLeftEdge()) { |
| return mDownPos.x - ev.getX(); |
| } else { |
| return ev.getY() - mDownPos.y; |
| } |
| } |
| |
| @Override |
| public boolean useSharedSwipeState() { |
| return mInteractionHandler != null; |
| } |
| |
| @Override |
| public boolean allowInterceptByParent() { |
| return !mPassedPilferInputSlop; |
| } |
| } |