• 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.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