• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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