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