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 static com.android.launcher3.config.FeatureFlags.ENABLE_LSQ_VELOCITY_PROVIDER;
19 
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.util.Log;
23 import android.view.MotionEvent;
24 
25 import com.android.launcher3.Alarm;
26 import com.android.launcher3.R;
27 import com.android.launcher3.compat.AccessibilityManagerCompat;
28 import com.android.launcher3.testing.TestProtocol;
29 
30 /**
31  * Given positions along x- or y-axis, tracks velocity and acceleration and determines when there is
32  * a pause in motion.
33  */
34 public class MotionPauseDetector {
35 
36     // The percentage of the previous speed that determines whether this is a rapid deceleration.
37     // The bigger this number, the easier it is to trigger the first pause.
38     private static final float RAPID_DECELERATION_FACTOR = 0.6f;
39 
40     /** If no motion is added for this amount of time, assume the motion has paused. */
41     private static final long FORCE_PAUSE_TIMEOUT = 300;
42 
43     /**
44      * After {@link #mMakePauseHarderToTrigger}, must move slowly for this long to trigger a pause.
45      */
46     private static final long HARDER_TRIGGER_TIMEOUT = 400;
47 
48     private final float mSpeedVerySlow;
49     private final float mSpeedSlow;
50     private final float mSpeedSomewhatFast;
51     private final float mSpeedFast;
52     private final Alarm mForcePauseTimeout;
53     private final boolean mMakePauseHarderToTrigger;
54     private final Context mContext;
55     private final VelocityProvider mVelocityProvider;
56 
57     private Float mPreviousVelocity = null;
58 
59     private OnMotionPauseListener mOnMotionPauseListener;
60     private boolean mIsPaused;
61     // Bias more for the first pause to make it feel extra responsive.
62     private boolean mHasEverBeenPaused;
63     /** @see #setDisallowPause(boolean) */
64     private boolean mDisallowPause;
65     // Time at which speed became < mSpeedSlow (only used if mMakePauseHarderToTrigger == true).
66     private long mSlowStartTime;
67 
MotionPauseDetector(Context context)68     public MotionPauseDetector(Context context) {
69         this(context, false);
70     }
71 
72     /**
73      * @param makePauseHarderToTrigger Used for gestures that require a more explicit pause.
74      */
MotionPauseDetector(Context context, boolean makePauseHarderToTrigger)75     public MotionPauseDetector(Context context, boolean makePauseHarderToTrigger) {
76         this(context, makePauseHarderToTrigger, MotionEvent.AXIS_Y);
77     }
78 
79     /**
80      * @param makePauseHarderToTrigger Used for gestures that require a more explicit pause.
81      */
MotionPauseDetector(Context context, boolean makePauseHarderToTrigger, int axis)82     public MotionPauseDetector(Context context, boolean makePauseHarderToTrigger, int axis) {
83         mContext = context;
84         Resources res = context.getResources();
85         mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
86         mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow);
87         mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
88         mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
89         if (TestProtocol.sDebugTracing) {
90             Log.d(TestProtocol.PAUSE_NOT_DETECTED, "creating alarm");
91         }
92         mForcePauseTimeout = new Alarm();
93         mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
94         mMakePauseHarderToTrigger = makePauseHarderToTrigger;
95         mVelocityProvider = ENABLE_LSQ_VELOCITY_PROVIDER.get()
96                 ? new LSqVelocityProvider(axis) : new LinearVelocityProvider(axis);
97     }
98 
99     /**
100      * Get callbacks for when motion pauses and resumes.
101      */
setOnMotionPauseListener(OnMotionPauseListener listener)102     public void setOnMotionPauseListener(OnMotionPauseListener listener) {
103         mOnMotionPauseListener = listener;
104     }
105 
106     /**
107      * @param disallowPause If true, we will not detect any pauses until this is set to false again.
108      */
setDisallowPause(boolean disallowPause)109     public void setDisallowPause(boolean disallowPause) {
110         mDisallowPause = disallowPause;
111         updatePaused(mIsPaused);
112     }
113 
114     /**
115      * Computes velocity and acceleration to determine whether the motion is paused.
116      * @param ev The motion being tracked.
117      */
addPosition(MotionEvent ev)118     public void addPosition(MotionEvent ev) {
119         addPosition(ev, 0);
120     }
121 
122     /**
123      * Computes velocity and acceleration to determine whether the motion is paused.
124      * @param ev The motion being tracked.
125      * @param pointerIndex Index for the pointer being tracked in the motion event
126      */
addPosition(MotionEvent ev, int pointerIndex)127     public void addPosition(MotionEvent ev, int pointerIndex) {
128         if (TestProtocol.sDebugTracing) {
129             Log.d(TestProtocol.PAUSE_NOT_DETECTED, "setting alarm");
130         }
131         mForcePauseTimeout.setAlarm(mMakePauseHarderToTrigger
132                 ? HARDER_TRIGGER_TIMEOUT
133                 : FORCE_PAUSE_TIMEOUT);
134         Float newVelocity = mVelocityProvider.addMotionEvent(ev, pointerIndex);
135         if (newVelocity != null && mPreviousVelocity != null) {
136             checkMotionPaused(newVelocity, mPreviousVelocity, ev.getEventTime());
137         }
138         mPreviousVelocity = newVelocity;
139     }
140 
checkMotionPaused(float velocity, float prevVelocity, long time)141     private void checkMotionPaused(float velocity, float prevVelocity, long time) {
142         float speed = Math.abs(velocity);
143         float previousSpeed = Math.abs(prevVelocity);
144         boolean isPaused;
145         if (mIsPaused) {
146             // Continue to be paused until moving at a fast speed.
147             isPaused = speed < mSpeedFast || previousSpeed < mSpeedFast;
148         } else {
149             if (velocity < 0 != prevVelocity < 0) {
150                 // We're just changing directions, not necessarily stopping.
151                 isPaused = false;
152             } else {
153                 isPaused = speed < mSpeedVerySlow && previousSpeed < mSpeedVerySlow;
154                 if (!isPaused && !mHasEverBeenPaused) {
155                     // We want to be more aggressive about detecting the first pause to ensure it
156                     // feels as responsive as possible; getting two very slow speeds back to back
157                     // takes too long, so also check for a rapid deceleration.
158                     boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR;
159                     isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast;
160                 }
161                 if (mMakePauseHarderToTrigger) {
162                     if (speed < mSpeedSlow) {
163                         if (mSlowStartTime == 0) {
164                             mSlowStartTime = time;
165                         }
166                         isPaused = time - mSlowStartTime >= HARDER_TRIGGER_TIMEOUT;
167                     } else {
168                         mSlowStartTime = 0;
169                         isPaused = false;
170                     }
171                 }
172             }
173         }
174         updatePaused(isPaused);
175     }
176 
177     private void updatePaused(boolean isPaused) {
178         if (TestProtocol.sDebugTracing) {
179             Log.d(TestProtocol.PAUSE_NOT_DETECTED, "updatePaused: " + isPaused);
180         }
181         if (mDisallowPause) {
182             isPaused = false;
183         }
184         if (mIsPaused != isPaused) {
185             mIsPaused = isPaused;
186             if (mIsPaused) {
187                 AccessibilityManagerCompat.sendPauseDetectedEventToTest(mContext);
188                 mHasEverBeenPaused = true;
189             }
190             if (mOnMotionPauseListener != null) {
191                 mOnMotionPauseListener.onMotionPauseChanged(mIsPaused);
192             }
193         }
194     }
195 
196     public void clear() {
197         mVelocityProvider.clear();
198         mPreviousVelocity = null;
199         setOnMotionPauseListener(null);
200         mIsPaused = mHasEverBeenPaused = false;
201         mSlowStartTime = 0;
202         if (TestProtocol.sDebugTracing) {
203             Log.d(TestProtocol.PAUSE_NOT_DETECTED, "canceling alarm");
204         }
205         mForcePauseTimeout.cancelAlarm();
206     }
207 
208     public boolean isPaused() {
209         return mIsPaused;
210     }
211 
212     public interface OnMotionPauseListener {
213         void onMotionPauseChanged(boolean isPaused);
214     }
215 
216     /**
217      * Interface to abstract out velocity calculations
218      */
219     protected interface VelocityProvider {
220 
221         /**
222          * Adds a new motion events, and returns the velocity at this point, or null if
223          * the velocity is not available
224          */
225         Float addMotionEvent(MotionEvent ev, int pointer);
226 
227         /**
228          * Clears all stored motion event records
229          */
230         void clear();
231     }
232 
233     private static class LinearVelocityProvider implements VelocityProvider {
234 
235         private Long mPreviousTime = null;
236         private Float mPreviousPosition = null;
237 
238         private final int mAxis;
239 
240         LinearVelocityProvider(int axis) {
241             mAxis = axis;
242         }
243 
244         @Override
245         public Float addMotionEvent(MotionEvent ev, int pointer) {
246             long time = ev.getEventTime();
247             float position = ev.getAxisValue(mAxis, pointer);
248             Float velocity = null;
249 
250             if (mPreviousTime != null && mPreviousPosition != null) {
251                 long changeInTime = Math.max(1, time - mPreviousTime);
252                 float changeInPosition = position - mPreviousPosition;
253                 velocity = changeInPosition / changeInTime;
254             }
255             mPreviousTime = time;
256             mPreviousPosition = position;
257             return velocity;
258         }
259 
260         @Override
261         public void clear() {
262             mPreviousTime = null;
263             mPreviousPosition = null;
264         }
265     }
266 
267     /**
268      * Java implementation of {@link android.view.VelocityTracker} using the Least Square (deg 2)
269      * algorithm.
270      */
271     private static class LSqVelocityProvider implements VelocityProvider {
272 
273         // Maximum age of a motion event to be considered when calculating the velocity.
274         private static final long HORIZON_MS = 100;
275         // Number of samples to keep.
276         private static final int HISTORY_SIZE = 20;
277 
278         // Position history are stored in a circular array
279         private final float[] mHistoricTimes = new float[HISTORY_SIZE];
280         private final float[] mHistoricPos = new float[HISTORY_SIZE];
281         private int mHistoryCount = 0;
282         private int mHistoryStart = 0;
283 
284         private final int mAxis;
285 
286         LSqVelocityProvider(int axis) {
287             mAxis = axis;
288         }
289 
290         @Override
291         public void clear() {
292             mHistoryCount = mHistoryStart = 0;
293         }
294 
295         private void addPositionAndTime(float eventTime, float eventPosition) {
296             mHistoricTimes[mHistoryStart] = eventTime;
297             mHistoricPos[mHistoryStart] = eventPosition;
298             mHistoryStart++;
299             if (mHistoryStart >= HISTORY_SIZE) {
300                 mHistoryStart = 0;
301             }
302             mHistoryCount = Math.min(HISTORY_SIZE, mHistoryCount + 1);
303         }
304 
305         @Override
306         public Float addMotionEvent(MotionEvent ev, int pointer) {
307             // Add all historic points
308             int historyCount = ev.getHistorySize();
309             for (int i = 0; i < historyCount; i++) {
310                 addPositionAndTime(
311                         ev.getHistoricalEventTime(i), ev.getHistoricalAxisValue(mAxis, pointer, i));
312             }
313 
314             // Start index for the last position (about to be added)
315             int eventStartIndex = mHistoryStart;
316             addPositionAndTime(ev.getEventTime(), ev.getAxisValue(mAxis, pointer));
317             return solveUnweightedLeastSquaresDeg2(eventStartIndex);
318         }
319 
320         /**
321          * Solves the instantaneous velocity.
322          * Based on solveUnweightedLeastSquaresDeg2 in VelocityTracker.cpp
323          */
324         private Float solveUnweightedLeastSquaresDeg2(final int pointPos) {
325             final float eventTime = mHistoricTimes[pointPos];
326 
327             float sxi = 0, sxiyi = 0, syi = 0, sxi2 = 0, sxi3 = 0, sxi2yi = 0, sxi4 = 0;
328             int count = 0;
329             for (int i = 0; i < mHistoryCount; i++) {
330                 int index = pointPos - i;
331                 if (index < 0) {
332                     index += HISTORY_SIZE;
333                 }
334 
335                 float time = mHistoricTimes[index];
336                 float age = eventTime - time;
337                 if (age > HORIZON_MS) {
338                     break;
339                 }
340                 count++;
341                 float xi = -age;
342 
343                 float yi = mHistoricPos[index];
344                 float xi2 = xi * xi;
345                 float xi3 = xi2 * xi;
346                 float xi4 = xi3 * xi;
347                 float xiyi = xi * yi;
348                 float xi2yi = xi2 * yi;
349 
350                 sxi += xi;
351                 sxi2 += xi2;
352                 sxiyi += xiyi;
353                 sxi2yi += xi2yi;
354                 syi += yi;
355                 sxi3 += xi3;
356                 sxi4 += xi4;
357             }
358 
359             if (count < 3) {
360                 // Too few samples
361                 if (count == 2) {
362                     int endPos = pointPos - 1;
363                     if (endPos < 0) {
364                         endPos += HISTORY_SIZE;
365                     }
366                     float denominator = eventTime - mHistoricTimes[endPos];
367                     if (denominator != 0) {
368                         return (eventTime - mHistoricPos[endPos]) / denominator;
369 
370                     }
371                 }
372                 return null;
373             }
374 
375             float Sxx = sxi2 - sxi * sxi / count;
376             float Sxy = sxiyi - sxi * syi / count;
377             float Sxx2 = sxi3 - sxi * sxi2 / count;
378             float Sx2y = sxi2yi - sxi2 * syi / count;
379             float Sx2x2 = sxi4 - sxi2 * sxi2 / count;
380 
381             float denominator = Sxx * Sx2x2 - Sxx2 * Sxx2;
382             if (denominator == 0) {
383                 // division by 0 when computing velocity
384                 return null;
385             }
386             // Compute a
387             // float numerator = Sx2y*Sxx - Sxy*Sxx2;
388 
389             // Compute b
390             float numerator = Sxy * Sx2x2 - Sx2y * Sxx2;
391             float b = numerator / denominator;
392 
393             // Compute c
394             // float c = syi/count - b * sxi/count - a * sxi2/count;
395 
396             return b;
397         }
398     }
399 }
400