1 /* 2 * Copyright (C) 2008 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 17 package android.view.cts; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.junit.Assert.fail; 22 23 import android.os.StrictMode; 24 import android.os.SystemClock; 25 import android.util.Log; 26 import android.view.InputDevice; 27 import android.view.MotionEvent; 28 import android.view.VelocityTracker; 29 30 import androidx.test.filters.SmallTest; 31 import androidx.test.runner.AndroidJUnit4; 32 33 import org.junit.After; 34 import org.junit.Before; 35 import org.junit.Test; 36 import org.junit.runner.RunWith; 37 38 import java.util.Set; 39 import java.util.function.Supplier; 40 41 /** 42 * Test {@link VelocityTracker}. 43 */ 44 @SmallTest 45 @RunWith(AndroidJUnit4.class) 46 public class VelocityTrackerTest { 47 private static final String TAG = "VelocityTrackerTest"; 48 49 // To enable these logs, run: 50 // 'adb shell setprop log.tag.VelocityTrackerTest DEBUG' (requires restart) 51 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 52 53 private static final float TOLERANCE_EXACT = 0.01f; 54 private static final float TOLERANCE_TIGHT = 0.05f; 55 private static final float TOLERANCE_WEAK = 0.1f; 56 private static final float TOLERANCE_VERY_WEAK = 0.2f; 57 58 private VelocityTracker mPlanarVelocityTracker; 59 private VelocityTracker mScrollVelocityTracker; 60 61 // Current axis value, velocity and acceleration. 62 private long mTime; 63 private long mLastTime; 64 private float mPx, mPy, mScroll; 65 private float mVx, mVy, mVscroll; 66 private float mAx, mAy, mAscroll; 67 68 private final StrictMode.ThreadPolicy mOldThreadPolicy = StrictMode.getThreadPolicy(); 69 private final StrictMode.VmPolicy mOldVmPolicy = StrictMode.getVmPolicy(); 70 71 @Before setup()72 public void setup() { 73 mPlanarVelocityTracker = VelocityTracker.obtain(); 74 mScrollVelocityTracker = VelocityTracker.obtain(); 75 76 mTime = 1000; 77 mLastTime = 0; 78 mPx = 300; 79 mPy = 600; 80 mVx = 0; 81 mVy = 0; 82 mAx = 0; 83 mAy = 0; 84 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() 85 .detectAll() 86 .penaltyLog() 87 .build()); 88 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() 89 .detectAll() 90 .penaltyLog() 91 .penaltyDeath() 92 .build()); 93 } 94 95 @After teardown()96 public void teardown() { 97 mPlanarVelocityTracker.recycle(); 98 mScrollVelocityTracker.recycle(); 99 StrictMode.setThreadPolicy(mOldThreadPolicy); 100 StrictMode.setVmPolicy(mOldVmPolicy); 101 } 102 103 @Test testNoMovement()104 public void testNoMovement() { 105 move(100, 10); 106 assertVelocity(TOLERANCE_EXACT, "Expect exact bound when no movement occurs."); 107 } 108 109 @Test testLinearMovement()110 public void testLinearMovement() { 111 mVx = 2.0f; 112 mVy = -4.0f; 113 mVscroll = 3.0f; 114 move(100, 10); 115 assertVelocity(TOLERANCE_TIGHT, "Expect tight bound for linear motion."); 116 } 117 118 @Test testAcceleratingMovement()119 public void testAcceleratingMovement() { 120 // A very good velocity tracking algorithm will produce a tight bound on 121 // simple acceleration. Certain alternate algorithms will fare less well but 122 // may be more stable in the presence of bad input data. 123 mVx = 2.0f; 124 mVy = -4.0f; 125 mVscroll = 3.0f; 126 mAx = 1.0f; 127 mAy = -0.5f; 128 mAscroll = 2.0f; 129 move(200, 10); 130 assertVelocity(TOLERANCE_WEAK, "Expect weak bound when there is acceleration."); 131 } 132 133 @Test testDeceleratingMovement()134 public void testDeceleratingMovement() { 135 // A very good velocity tracking algorithm will produce a tight bound on 136 // simple acceleration. Certain alternate algorithms will fare less well but 137 // may be more stable in the presence of bad input data. 138 mVx = 2.0f; 139 mVy = -4.0f; 140 mVscroll = 3.0f; 141 mAx = -1.0f; 142 mAy = 0.2f; 143 mAscroll = -0.5f; 144 move(200, 10); 145 assertVelocity(TOLERANCE_WEAK, "Expect weak bound when there is deceleration."); 146 } 147 148 @Test testLinearSharpDirectionChange()149 public void testLinearSharpDirectionChange() { 150 // After a sharp change of direction we expect the velocity to eventually 151 // converge but it might take a moment to get there. 152 mVx = 2.0f; 153 mVy = -4.0f; 154 mVscroll = 3.0f; 155 move(100, 10); 156 assertVelocity(TOLERANCE_TIGHT, "Expect tight bound for linear motion."); 157 mVx = -1.0f; 158 mVy = -3.0f; 159 mVscroll = -2.0f; 160 move(100, 10); 161 assertVelocity(TOLERANCE_WEAK, "Expect weak bound after 100ms of new direction."); 162 move(100, 10); 163 assertVelocity(TOLERANCE_TIGHT, "Expect tight bound after 200ms of new direction."); 164 } 165 166 @Test testLinearSharpDirectionChangeAfterALongPause()167 public void testLinearSharpDirectionChangeAfterALongPause() { 168 // Should be able to get a tighter bound if there is a pause before the 169 // change of direction. 170 mVx = 2.0f; 171 mVy = -4.0f; 172 mVscroll = 3.0f; 173 move(100, 10); 174 assertVelocity(TOLERANCE_TIGHT, "Expect tight bound for linear motion."); 175 pause(100); 176 mVx = -1.0f; 177 mVy = -3.0f; 178 mVscroll = -2.0f; 179 move(100, 10); 180 assertVelocity(TOLERANCE_TIGHT, 181 "Expect tight bound after a 100ms pause and 100ms of new direction."); 182 } 183 184 @Test testChangingAcceleration()185 public void testChangingAcceleration() { 186 // In real circumstances, the acceleration changes continuously throughout a 187 // gesture. Try to model this and see how the algorithm copes. 188 mVx = 2.0f; 189 mVy = -4.0f; 190 mVscroll = -2.0f; 191 for (float change : new float[] { 1, -2, -3, -1, 1 }) { 192 mAx += 1.0f * change; 193 mAy += -0.5f * change; 194 mAscroll += 2.0f * change; 195 move(30, 10); 196 } 197 assertVelocity(TOLERANCE_VERY_WEAK, 198 "Expect weak bound when there is changing acceleration."); 199 } 200 201 @Test testUsesRawCoordinates()202 public void testUsesRawCoordinates() { 203 VelocityTracker vt = VelocityTracker.obtain(); 204 final int numevents = 5; 205 206 final long downTime = SystemClock.uptimeMillis(); 207 for (int i = 0; i < numevents; i++) { 208 final long eventTime = downTime + i * 10; 209 int action = i == 0 ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_MOVE; 210 MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, 0, 0, 0); 211 // MotionEvent translation/offset is only applied to pointer sources, like touchscreens. 212 event.setSource(InputDevice.SOURCE_TOUCHSCREEN); 213 event.offsetLocation(i * 10, i * 10); 214 vt.addMovement(event); 215 } 216 vt.computeCurrentVelocity(1000); 217 float xVelocity = vt.getXVelocity(); 218 float yVelocity = vt.getYVelocity(); 219 if (xVelocity == 0 || yVelocity == 0) { 220 fail("VelocityTracker is using raw coordinates," 221 + " but it should be using adjusted coordinates"); 222 } 223 } 224 225 @Test testIsAxisSupported()226 public void testIsAxisSupported() { 227 Set<Integer> expectedSupportedAxes = 228 Set.of(MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_SCROLL); 229 VelocityTracker vt = VelocityTracker.obtain(); 230 // Note that we are testing up to the max possible axis value, plus 3 more values. We are 231 // going beyond the max value to add a bit more protection. "3" is chosen arbitrarily to 232 // cover a few more values beyond the max. 233 for (int axis = 0; axis <= getLargestDefinedMotionEventAxisValue() + 3; axis++) { 234 boolean expectedSupport = expectedSupportedAxes.contains(axis); 235 if (vt.isAxisSupported(axis) != expectedSupport) { 236 fail(String.format( 237 "Unexpected support found for axis %d (expected support=%b)", 238 axis, expectedSupport)); 239 } 240 } 241 } 242 243 @Test testVelocityCallsWithUnusedPointers()244 public void testVelocityCallsWithUnusedPointers() { 245 mVx = 2.0f; 246 mVy = -4.0f; 247 mVscroll = 3.0f; 248 249 move(100, 10); 250 251 assertThat(mPlanarVelocityTracker.getXVelocity(1)).isZero(); 252 assertThat(mPlanarVelocityTracker.getYVelocity(2)).isZero(); 253 assertThat(mScrollVelocityTracker.getAxisVelocity(MotionEvent.AXIS_SCROLL, 100)).isZero(); 254 } 255 256 @Test testVelocityCallsForUnsupportedAxis()257 public void testVelocityCallsForUnsupportedAxis() { 258 assertThat(mPlanarVelocityTracker.getAxisVelocity(MotionEvent.AXIS_DISTANCE)).isZero(); 259 assertThat(mPlanarVelocityTracker.getAxisVelocity(MotionEvent.AXIS_GENERIC_10)).isZero(); 260 } 261 move(long duration, long step)262 private void move(long duration, long step) { 263 addMovement(); 264 while (duration > 0) { 265 duration -= step; 266 mTime += step; 267 268 mPx += (mAx / 2 * step + mVx) * step; 269 mPy += (mAy / 2 * step + mVy) * step; 270 // Note that we are not incrementing the scroll-value. Instead, we are overriding the 271 // previous value with the new one. This is in accordance to the differential nature of 272 // the scroll axis. That is, the axis reports differential values since previous motion 273 // events, instead of absolute values. 274 mScroll = (mAscroll / 2 * step + mVscroll) * step; 275 276 mVx += mAx * step; 277 mVy += mAy * step; 278 mVscroll += mAscroll * step; 279 addMovement(); 280 } 281 } 282 pause(long duration)283 private void pause(long duration) { 284 mTime += duration; 285 } 286 createScrollMotionEvent()287 private MotionEvent createScrollMotionEvent() { 288 MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); 289 props.id = 0; 290 291 MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); 292 coords.setAxisValue(MotionEvent.AXIS_SCROLL, mScroll); 293 294 return MotionEvent.obtain(0 /* downTime */, 295 mTime, 296 MotionEvent.ACTION_SCROLL, 297 1 /* pointerCount */, 298 new MotionEvent.PointerProperties[] {props}, 299 new MotionEvent.PointerCoords[] {coords}, 300 0 /* metaState */, 301 0 /* buttonState */, 302 0 /* xPrecision */, 303 0 /* yPrecision */, 304 1 /* deviceId */, 305 0 /* edgeFlags */, 306 InputDevice.SOURCE_ROTARY_ENCODER, 307 0 /* flags */); 308 } 309 addMovement()310 private void addMovement() { 311 if (mTime > mLastTime) { 312 MotionEvent ev = MotionEvent.obtain(0L, mTime, MotionEvent.ACTION_MOVE, mPx, mPy, 0); 313 mPlanarVelocityTracker.addMovement(ev); 314 ev.recycle(); 315 316 ev = createScrollMotionEvent(); 317 mScrollVelocityTracker.addMovement(ev); 318 ev.recycle(); 319 320 mLastTime = mTime; 321 322 mPlanarVelocityTracker.computeCurrentVelocity(1); 323 mScrollVelocityTracker.computeCurrentVelocity(1); 324 325 final float estimatedVx = mPlanarVelocityTracker.getXVelocity(); 326 final float estimatedVy = mPlanarVelocityTracker.getYVelocity(); 327 final float estimatedVscroll = 328 mPlanarVelocityTracker.getAxisVelocity(MotionEvent.AXIS_SCROLL); 329 330 if (DEBUG) { 331 logTrackingInfo(MotionEvent.AXIS_X, mTime, mPx, mVx, estimatedVx, mAx); 332 logTrackingInfo(MotionEvent.AXIS_Y, mTime, mPy, mVy, estimatedVy, mAy); 333 logTrackingInfo(MotionEvent.AXIS_SCROLL, 334 mTime, mScroll, mVscroll, estimatedVscroll, mAscroll); 335 } 336 } 337 } 338 assertVelocity(float tolerance, String message)339 private void assertVelocity(float tolerance, String message) { 340 mPlanarVelocityTracker.computeCurrentVelocity(1); 341 mScrollVelocityTracker.computeCurrentVelocity(1); 342 343 assertVelocity(mPlanarVelocityTracker::getXVelocity, tolerance, mVx, message); 344 assertVelocity(mPlanarVelocityTracker, MotionEvent.AXIS_X, tolerance, mVx, message); 345 346 assertVelocity(mPlanarVelocityTracker::getYVelocity, tolerance, mVy, message); 347 assertVelocity(mPlanarVelocityTracker, MotionEvent.AXIS_Y, tolerance, mVy, message); 348 349 assertVelocity( 350 mScrollVelocityTracker, MotionEvent.AXIS_SCROLL, tolerance, mVscroll, message); 351 } 352 assertVelocity( VelocityTracker velocityTracker, int axis, float tolerance, float expectedVelocity, String message)353 private static void assertVelocity( 354 VelocityTracker velocityTracker, 355 int axis, 356 float tolerance, 357 float expectedVelocity, 358 String message) { 359 assertVelocity( 360 () -> velocityTracker.getAxisVelocity(axis), tolerance, expectedVelocity, message); 361 } 362 assertVelocity( Supplier<Float> estimatedVelocitySupplier, float tolerance, float expectedVelocity, String message)363 private static void assertVelocity( 364 Supplier<Float> estimatedVelocitySupplier, 365 float tolerance, 366 float expectedVelocity, 367 String message) { 368 final float estimatedVelociy = estimatedVelocitySupplier.get(); 369 float error = error(expectedVelocity, estimatedVelociy); 370 if (error > tolerance) { 371 fail(String.format("Velocity exceeds tolerance of %6.1f%%: " 372 + "expected=%6.1f. " 373 + "actual=%6.1f (%6.1f%%). %s", 374 tolerance * 100f, expectedVelocity, estimatedVelociy, error * 100f, message)); 375 } 376 } 377 error(float expected, float actual)378 private static float error(float expected, float actual) { 379 float absError = Math.abs(actual - expected); 380 if (absError < 0.001f) { 381 return 0; 382 } 383 if (Math.abs(expected) < 0.001f) { 384 return 1; 385 } 386 return absError / Math.abs(expected); 387 } 388 logTrackingInfo( int axis, long time, float val, float actualV, float estimatedV, float acc)389 private static void logTrackingInfo( 390 int axis, long time, float val, float actualV, float estimatedV, float acc) { 391 Log.d(TAG, String.format( 392 "[%d] %s: val=%6.1f, v=%6.1f, acc=%6.1f, estimatedV=%6.1f (%6.1f%%)", 393 time, MotionEvent.axisToString(axis), val, actualV, acc, 394 estimatedV, error(actualV, estimatedV) * 100f)); 395 } 396 getLargestDefinedMotionEventAxisValue()397 private static int getLargestDefinedMotionEventAxisValue() { 398 int i = 0; 399 while (!Integer.toString(i).equals(MotionEvent.axisToString(i))) { 400 i++; 401 } 402 return i - 1; 403 } 404 } 405