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