• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.util;
17 
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.util.Log;
21 import android.view.MotionEvent;
22 import android.view.VelocityTracker;
23 
24 import com.android.launcher3.Alarm;
25 import com.android.launcher3.R;
26 import com.android.launcher3.Utilities;
27 import com.android.launcher3.compat.AccessibilityManagerCompat;
28 
29 /**
30  * Given positions along x- or y-axis, tracks velocity and acceleration and determines when there is
31  * a pause in motion.
32  */
33 public class MotionPauseDetector {
34 
35     private static final String TAG = "MotionPauseDetector";
36 
37     // The percentage of the previous speed that determines whether this is a rapid deceleration.
38     // The bigger this number, the easier it is to trigger the first pause.
39     private static final float RAPID_DECELERATION_FACTOR = 0.6f;
40 
41     /** If no motion is added for this amount of time, assume the motion has paused. */
42     private static final long FORCE_PAUSE_TIMEOUT = 300;
43 
44     /**
45      * After {@link #mMakePauseHarderToTrigger}, must move slowly for this long to trigger a pause.
46      */
47     private static final long HARDER_TRIGGER_TIMEOUT = 400;
48 
49     /**
50      * When running in a test harness, if no motion is added for this amount of time, assume the
51      * motion has paused. (We use an increased timeout since sometimes test devices can be slow.)
52      */
53     private static final long TEST_HARNESS_TRIGGER_TIMEOUT = 2000;
54 
55     private final float mSpeedVerySlow;
56     private final float mSpeedSlow;
57     private final float mSpeedSomewhatFast;
58     private final float mSpeedFast;
59     private final Alarm mForcePauseTimeout;
60     private final boolean mMakePauseHarderToTrigger;
61     private final Context mContext;
62     private final SystemVelocityProvider mVelocityProvider;
63 
64     private Float mPreviousVelocity = null;
65 
66     private OnMotionPauseListener mOnMotionPauseListener;
67     private boolean mIsPaused;
68     // Bias more for the first pause to make it feel extra responsive.
69     private boolean mHasEverBeenPaused;
70     /** @see #setDisallowPause(boolean) */
71     private boolean mDisallowPause;
72     // Time at which speed became < mSpeedSlow (only used if mMakePauseHarderToTrigger == true).
73     private long mSlowStartTime;
74 
MotionPauseDetector(Context context)75     public MotionPauseDetector(Context context) {
76         this(context, false);
77     }
78 
79     /**
80      * @param makePauseHarderToTrigger Used for gestures that require a more explicit pause.
81      */
MotionPauseDetector(Context context, boolean makePauseHarderToTrigger)82     public MotionPauseDetector(Context context, boolean makePauseHarderToTrigger) {
83         this(context, makePauseHarderToTrigger, MotionEvent.AXIS_Y);
84     }
85 
86     /**
87      * @param makePauseHarderToTrigger Used for gestures that require a more explicit pause.
88      */
MotionPauseDetector(Context context, boolean makePauseHarderToTrigger, int axis)89     public MotionPauseDetector(Context context, boolean makePauseHarderToTrigger, int axis) {
90         mContext = context;
91         Resources res = context.getResources();
92         mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
93         mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow);
94         mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
95         mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
96         mForcePauseTimeout = new Alarm();
97         mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */,
98                 "Force pause timeout after " +  alarm.getLastSetTimeout() + "ms" /* reason */));
99         mMakePauseHarderToTrigger = makePauseHarderToTrigger;
100         mVelocityProvider = new SystemVelocityProvider(axis);
101     }
102 
103     /**
104      * Get callbacks for when motion pauses and resumes.
105      */
setOnMotionPauseListener(OnMotionPauseListener listener)106     public void setOnMotionPauseListener(OnMotionPauseListener listener) {
107         mOnMotionPauseListener = listener;
108     }
109 
110     /**
111      * @param disallowPause If true, we will not detect any pauses until this is set to false again.
112      */
setDisallowPause(boolean disallowPause)113     public void setDisallowPause(boolean disallowPause) {
114         mDisallowPause = disallowPause;
115         updatePaused(mIsPaused, "Set disallowPause=" + disallowPause);
116     }
117 
118     /**
119      * Computes velocity and acceleration to determine whether the motion is paused.
120      * @param ev The motion being tracked.
121      */
addPosition(MotionEvent ev)122     public void addPosition(MotionEvent ev) {
123         addPosition(ev, 0);
124     }
125 
126     /**
127      * Computes velocity and acceleration to determine whether the motion is paused.
128      * @param ev The motion being tracked.
129      * @param pointerIndex Index for the pointer being tracked in the motion event
130      */
addPosition(MotionEvent ev, int pointerIndex)131     public void addPosition(MotionEvent ev, int pointerIndex) {
132         long timeoutMs = Utilities.isRunningInTestHarness()
133                 ? TEST_HARNESS_TRIGGER_TIMEOUT
134                 : mMakePauseHarderToTrigger
135                         ? HARDER_TRIGGER_TIMEOUT
136                         : FORCE_PAUSE_TIMEOUT;
137         mForcePauseTimeout.setAlarm(timeoutMs);
138         float newVelocity = mVelocityProvider.addMotionEvent(ev, ev.getPointerId(pointerIndex));
139         if (mPreviousVelocity != null) {
140             checkMotionPaused(newVelocity, mPreviousVelocity, ev.getEventTime());
141         }
142         mPreviousVelocity = newVelocity;
143     }
144 
checkMotionPaused(float velocity, float prevVelocity, long time)145     private void checkMotionPaused(float velocity, float prevVelocity, long time) {
146         float speed = Math.abs(velocity);
147         float previousSpeed = Math.abs(prevVelocity);
148         boolean isPaused;
149         String isPausedReason = "";
150         if (mIsPaused) {
151             // Continue to be paused until moving at a fast speed.
152             isPaused = speed < mSpeedFast || previousSpeed < mSpeedFast;
153             isPausedReason = "Was paused, but started moving at a fast speed";
154         } else {
155             if (velocity < 0 != prevVelocity < 0) {
156                 // We're just changing directions, not necessarily stopping.
157                 isPaused = false;
158                 isPausedReason = "Velocity changed directions";
159             } else {
160                 isPaused = speed < mSpeedVerySlow && previousSpeed < mSpeedVerySlow;
161                 isPausedReason = "Pause requires back to back slow speeds";
162                 if (!isPaused && !mHasEverBeenPaused) {
163                     // We want to be more aggressive about detecting the first pause to ensure it
164                     // feels as responsive as possible; getting two very slow speeds back to back
165                     // takes too long, so also check for a rapid deceleration.
166                     boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR;
167                     isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast;
168                     isPausedReason = "Didn't have back to back slow speeds, checking for rapid"
169                             + " deceleration on first pause only";
170                 }
171                 if (mMakePauseHarderToTrigger) {
172                     if (speed < mSpeedSlow) {
173                         if (mSlowStartTime == 0) {
174                             mSlowStartTime = time;
175                         }
176                         isPaused = time - mSlowStartTime >= HARDER_TRIGGER_TIMEOUT;
177                         isPausedReason = "Maintained slow speed for sufficient duration when making"
178                                 + " pause harder to trigger";
179                     } else {
180                         mSlowStartTime = 0;
181                         isPaused = false;
182                         isPausedReason = "Intentionally making pause harder to trigger";
183                     }
184                 }
185             }
186         }
187         updatePaused(isPaused, isPausedReason);
188     }
189 
190     private void updatePaused(boolean isPaused, String reason) {
191         if (mDisallowPause) {
192             reason = "Disallow pause; otherwise, would have been " + isPaused + " due to " + reason;
193             isPaused = false;
194         }
195         if (mIsPaused != isPaused) {
196             mIsPaused = isPaused;
197             String logString = "onMotionPauseChanged, paused=" + mIsPaused + " reason=" + reason;
198             if (Utilities.isRunningInTestHarness()) {
199                 Log.d(TAG, logString);
200             }
201             ActiveGestureLog.INSTANCE.addLog(logString);
202             boolean isFirstDetectedPause = !mHasEverBeenPaused && mIsPaused;
203             if (mIsPaused) {
204                 AccessibilityManagerCompat.sendPauseDetectedEventToTest(mContext);
205                 mHasEverBeenPaused = true;
206             }
207             if (mOnMotionPauseListener != null) {
208                 if (isFirstDetectedPause) {
209                     mOnMotionPauseListener.onMotionPauseDetected();
210                 }
211                 // Null check again as onMotionPauseDetected() maybe have called clear().
212                 if (mOnMotionPauseListener != null) {
213                     mOnMotionPauseListener.onMotionPauseChanged(mIsPaused);
214                 }
215             }
216         }
217     }
218 
219     public void clear() {
220         mVelocityProvider.clear();
221         mPreviousVelocity = null;
222         setOnMotionPauseListener(null);
223         mIsPaused = mHasEverBeenPaused = false;
224         mSlowStartTime = 0;
225         mForcePauseTimeout.cancelAlarm();
226     }
227 
228     public boolean isPaused() {
229         return mIsPaused;
230     }
231 
232     public interface OnMotionPauseListener {
233         /** Called only the first time motion pause is detected. */
234         void onMotionPauseDetected();
235         /** Called every time motion changes from paused to not paused and vice versa. */
236         default void onMotionPauseChanged(boolean isPaused) { }
237     }
238 
239     private static class SystemVelocityProvider {
240 
241         private final VelocityTracker mVelocityTracker;
242         private final int mAxis;
243 
244         SystemVelocityProvider(int axis) {
245             mVelocityTracker = VelocityTracker.obtain();
246             mAxis = axis;
247         }
248 
249         /**
250          * Adds a new motion events, and returns the velocity at this point, or null if
251          * the velocity is not available
252          */
253         public float addMotionEvent(MotionEvent ev, int pointer) {
254             mVelocityTracker.addMovement(ev);
255             mVelocityTracker.computeCurrentVelocity(1); // px / ms
256             return mAxis == MotionEvent.AXIS_X
257                     ? mVelocityTracker.getXVelocity(pointer)
258                     : mVelocityTracker.getYVelocity(pointer);
259         }
260 
261         /**
262          * Clears all stored motion event records
263          */
264         public void clear() {
265             mVelocityTracker.clear();
266         }
267     }
268 }
269