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