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