1 /*
2  * Copyright (C) 2017 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.accessibilityservice.cts.utils;
18 
19 import static org.junit.Assert.assertTrue;
20 
21 import android.accessibility.cts.common.InstrumentedAccessibilityService;
22 import android.accessibilityservice.AccessibilityService.GestureResultCallback;
23 import android.accessibilityservice.GestureDescription;
24 import android.accessibilityservice.GestureDescription.StrokeDescription;
25 import android.graphics.Path;
26 import android.graphics.PointF;
27 import android.view.MotionEvent;
28 import android.view.ViewConfiguration;
29 
30 import org.hamcrest.Description;
31 import org.hamcrest.Matcher;
32 import org.hamcrest.TypeSafeMatcher;
33 
34 import java.util.concurrent.CompletableFuture;
35 
36 public class GestureUtils {
37 
38     public static final long STROKE_TIME_GAP_MS = 40;
39 
40     public static final Matcher<MotionEvent> IS_ACTION_DOWN =
41             new MotionEventActionMatcher(MotionEvent.ACTION_DOWN);
42     public static final Matcher<MotionEvent> IS_ACTION_POINTER_DOWN =
43             new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_DOWN);
44     public static final Matcher<MotionEvent> IS_ACTION_UP =
45             new MotionEventActionMatcher(MotionEvent.ACTION_UP);
46     public static final Matcher<MotionEvent> IS_ACTION_POINTER_UP =
47             new MotionEventActionMatcher(MotionEvent.ACTION_POINTER_UP);
48     public static final Matcher<MotionEvent> IS_ACTION_CANCEL =
49             new MotionEventActionMatcher(MotionEvent.ACTION_CANCEL);
50     public static final Matcher<MotionEvent> IS_ACTION_MOVE =
51             new MotionEventActionMatcher(MotionEvent.ACTION_MOVE);
52 
GestureUtils()53     private GestureUtils() {}
54 
dispatchGesture( InstrumentedAccessibilityService service, GestureDescription gesture)55     public static CompletableFuture<Void> dispatchGesture(
56             InstrumentedAccessibilityService service,
57             GestureDescription gesture) {
58         CompletableFuture<Void> result = new CompletableFuture<>();
59         GestureResultCallback callback = new GestureResultCallback() {
60             @Override
61             public void onCompleted(GestureDescription gestureDescription) {
62                 result.complete(null);
63             }
64 
65             @Override
66             public void onCancelled(GestureDescription gestureDescription) {
67                 result.cancel(false);
68             }
69         };
70         service.runOnServiceSync(() -> {
71             if (!service.dispatchGesture(gesture, callback, null)) {
72                 result.completeExceptionally(new IllegalStateException());
73             }
74         });
75         return result;
76     }
77 
pointerDown(PointF point)78     public static StrokeDescription pointerDown(PointF point) {
79         return new StrokeDescription(path(point), 0, ViewConfiguration.getTapTimeout(), true);
80     }
81 
pointerUp(StrokeDescription lastStroke)82     public static StrokeDescription pointerUp(StrokeDescription lastStroke) {
83         return lastStroke.continueStroke(path(lastPointOf(lastStroke)),
84                 endTimeOf(lastStroke), ViewConfiguration.getTapTimeout(), false);
85     }
86 
lastPointOf(StrokeDescription stroke)87     public static PointF lastPointOf(StrokeDescription stroke) {
88         float[] p = stroke.getPath().approximate(0.3f);
89         return new PointF(p[p.length - 2], p[p.length - 1]);
90     }
91 
click(PointF point)92     public static StrokeDescription click(PointF point) {
93         return new StrokeDescription(path(point), 0, ViewConfiguration.getTapTimeout());
94     }
95 
longClick(PointF point)96     public static StrokeDescription longClick(PointF point) {
97         return new StrokeDescription(path(point), 0,
98                 ViewConfiguration.getLongPressTimeout() * 3);
99     }
100 
swipe(PointF from, PointF to)101     public static StrokeDescription swipe(PointF from, PointF to) {
102         return swipe(from, to, ViewConfiguration.getTapTimeout());
103     }
104 
swipe(PointF from, PointF to, long duration)105     public static StrokeDescription swipe(PointF from, PointF to, long duration) {
106         return new StrokeDescription(path(from, to), 0, duration);
107     }
108 
drag(StrokeDescription from, PointF to)109     public static StrokeDescription drag(StrokeDescription from, PointF to) {
110         return from.continueStroke(
111                 path(lastPointOf(from), to),
112                 endTimeOf(from), ViewConfiguration.getTapTimeout(), true);
113     }
114 
path(PointF first, PointF... rest)115     public static Path path(PointF first, PointF... rest) {
116         Path path = new Path();
117         path.moveTo(first.x, first.y);
118         for (PointF point : rest) {
119             path.lineTo(point.x, point.y);
120         }
121         return path;
122     }
123 
startingAt(long timeMs, StrokeDescription prototype)124     public static StrokeDescription startingAt(long timeMs, StrokeDescription prototype) {
125         return new StrokeDescription(
126                 prototype.getPath(), timeMs, prototype.getDuration(), prototype.willContinue());
127     }
128 
endTimeOf(StrokeDescription stroke)129     public static long endTimeOf(StrokeDescription stroke) {
130         return stroke.getStartTime() + stroke.getDuration();
131     }
132 
distance(PointF a, PointF b)133     public static float distance(PointF a, PointF b) {
134         if (a == null) throw new NullPointerException();
135         if (b == null) throw new NullPointerException();
136         return (float) Math.hypot(a.x - b.x, a.y - b.y);
137     }
138 
add(PointF a, float x, float y)139     public static PointF add(PointF a, float x, float y) {
140         return new PointF(a.x + x, a.y + y);
141     }
142 
add(PointF a, PointF b)143     public static PointF add(PointF a, PointF b) {
144         return add(a, b.x, b.y);
145     }
146 
diff(PointF a, PointF b)147     public static PointF diff(PointF a, PointF b) {
148         return add(a, -b.x, -b.y);
149     }
150 
negate(PointF p)151     public static PointF negate(PointF p) {
152         return times(-1, p);
153     }
154 
times(float mult, PointF p)155     public static PointF times(float mult, PointF p) {
156         return new PointF(p.x * mult, p.y * mult);
157     }
158 
length(PointF p)159     public static float length(PointF p) {
160         return (float) Math.hypot(p.x, p.y);
161     }
162 
ceil(PointF p)163     public static PointF ceil(PointF p) {
164         return new PointF((float) Math.ceil(p.x), (float) Math.ceil(p.y));
165     }
166 
doubleTap(PointF point)167     public static GestureDescription doubleTap(PointF point) {
168         return multiTap(point, 2);
169     }
170 
tripleTap(PointF point)171     public static GestureDescription tripleTap(PointF point) {
172         return multiTap(point, 3);
173     }
174 
multiTap(PointF point, int taps)175     public static GestureDescription multiTap(PointF point, int taps) {
176         return multiTap(point, taps, 0);
177         }
178 
multiTap(PointF point, int taps, int slop)179     public static GestureDescription multiTap(PointF point, int taps, int slop) {
180         GestureDescription.Builder builder = new GestureDescription.Builder();
181         long time = 0;
182         if (taps > 0) {
183             // Place first tap on the point itself.
184             // Subsequent taps will be offset somewhere within slop radius.
185             // If slop is 0 subsequent taps will also be on the point itself.
186             StrokeDescription stroke = click(point);
187             builder.addStroke(stroke);
188             time += stroke.getDuration() + STROKE_TIME_GAP_MS;
189             for (int i = 1; i < taps; i++) {
190                 stroke = click(getPointWithinSlop(point, slop));
191                 builder.addStroke(startingAt(time, stroke));
192                 time += stroke.getDuration() + STROKE_TIME_GAP_MS;
193             }
194         }
195         return builder.build();
196     }
197 
doubleTapAndHold(PointF point)198     public static GestureDescription doubleTapAndHold(PointF point) {
199         GestureDescription.Builder builder = new GestureDescription.Builder();
200         StrokeDescription tap1 = click(point);
201         StrokeDescription tap2 =
202                 startingAt(endTimeOf(tap1) + STROKE_TIME_GAP_MS, longClick(point));
203         builder.addStroke(tap1);
204         builder.addStroke(tap2);
205         return builder.build();
206     }
207 
getGestureBuilder( int displayId, StrokeDescription... strokes)208     public static GestureDescription.Builder getGestureBuilder(
209             int displayId, StrokeDescription... strokes) {
210         GestureDescription.Builder builder = new GestureDescription.Builder();
211         builder.setDisplayId(displayId);
212         for (StrokeDescription s : strokes) builder.addStroke(s);
213         return builder;
214     }
215 
216     private static class MotionEventActionMatcher extends TypeSafeMatcher<MotionEvent> {
217         int mAction;
218 
MotionEventActionMatcher(int action)219         MotionEventActionMatcher(int action) {
220             super();
221             mAction = action;
222         }
223 
224         @Override
matchesSafely(MotionEvent motionEvent)225         protected boolean matchesSafely(MotionEvent motionEvent) {
226             return motionEvent.getActionMasked() == mAction;
227         }
228 
229         @Override
describeTo(Description description)230         public void describeTo(Description description) {
231             description.appendText("Matching to action " + MotionEvent.actionToString(mAction));
232         }
233 
234         @Override
describeMismatchSafely(MotionEvent event, Description description)235         public void describeMismatchSafely(MotionEvent event, Description description) {
236             description.appendText(
237                     "received " + MotionEvent.actionToString(event.getActionMasked()));
238         }
239     }
240 
isAtPoint(final PointF point)241     public static Matcher<MotionEvent> isAtPoint(final PointF point) {
242         return isAtPoint(point, 0.01f);
243     }
244 
isAtPoint(final PointF point, final float tol)245     public static Matcher<MotionEvent> isAtPoint(final PointF point, final float tol) {
246         return new TypeSafeMatcher<MotionEvent>() {
247             @Override
248             protected boolean matchesSafely(MotionEvent event) {
249                 return Math.hypot(event.getX() - point.x, event.getY() - point.y) <= tol;
250             }
251 
252             @Override
253             public void describeTo(Description description) {
254                 description.appendText("Matching to point " + point);
255             }
256 
257             @Override
258             public void describeMismatchSafely(MotionEvent event, Description description) {
259                 description.appendText(
260                         "received (" + event.getX() + ", " + event.getY() + ")");
261             }
262         };
263     }
264 
265     public static Matcher<MotionEvent> isRawAtPoint(final PointF point) {
266         return isRawAtPoint(point, 0.01f);
267     }
268 
269     public static Matcher<MotionEvent> isRawAtPoint(final PointF point, final float tol) {
270         return new TypeSafeMatcher<MotionEvent>() {
271             @Override
272             protected boolean matchesSafely(MotionEvent event) {
273                 return Math.hypot(event.getRawX() - point.x, event.getRawY() - point.y) <= tol;
274             }
275 
276             @Override
277             public void describeTo(Description description) {
278                 description.appendText("Matching to point " + point);
279             }
280 
281             @Override
282             public void describeMismatchSafely(MotionEvent event, Description description) {
283                 description.appendText(
284                         "received (" + event.getRawX() + ", " + event.getRawY() + ")");
285             }
286         };
287     }
288 
289     public static PointF getPointWithinSlop(PointF point, int slop) {
290         return add(point, slop / 2, 0);
291     }
292 
293     /**
294      * Simulates a user placing one finger on the screen for a specified amount of time and then multi-tapping with a second finger.
295      * @param explorePoint Where to place the first finger.
296      * @param tapPoint Where to tap with the second finger.
297      * @param taps The number of second-finger taps.
298      * @param waitTime How long to hold the first finger before tapping with the second finger.
299      */
300     public static GestureDescription secondFingerMultiTap(
301             PointF explorePoint, PointF tapPoint, int taps, int waitTime) {
302         GestureDescription.Builder builder = new GestureDescription.Builder();
303         long time = waitTime;
304         for (int i = 0; i < taps; i++) {
305             StrokeDescription stroke = click(tapPoint);
306             builder.addStroke(startingAt(time, stroke));
307             time += stroke.getDuration();
308             time += ViewConfiguration.getDoubleTapTimeout() / 3;
309         }
310         builder.addStroke(swipe(explorePoint, explorePoint, time));
311         return builder.build();
312     }
313 
314     /**
315      * Simulates a user placing multiple fingers on the specified screen
316      * and then multi-tapping with these fingers.
317      *
318      * The location of fingers based on <code>basePoint<code/> are shifted by <code>delta<code/>.
319      * Like (baseX, baseY), (baseX + deltaX, baseY + deltaY), and so on.
320      *
321      * @param basePoint Where to place the first finger.
322      * @param delta Offset to basePoint where to place the 2nd or 3rd finger.
323      * @param fingerCount The number of fingers.
324      * @param tapCount The number of taps to fingers.
325      * @param slop Slop range the finger tapped.
326      * @param displayId Which display to dispatch the gesture.
327      */
328     public static GestureDescription multiFingerMultiTap(PointF basePoint, PointF delta,
329             int fingerCount, int tapCount, int slop, int displayId) {
330         assertTrue(fingerCount >= 2);
331         assertTrue(tapCount > 0);
332         final int strokeCount = fingerCount * tapCount;
333         final PointF[] pointers = new PointF[fingerCount];
334         final StrokeDescription[] strokes = new StrokeDescription[strokeCount];
335 
336         // The first tap
337         for (int i = 0; i < fingerCount; i++) {
338             pointers[i] = add(basePoint, times(i, delta));
339             strokes[i] = click(pointers[i]);
340         }
341         // The rest of taps
342         for (int tapIndex = 1; tapIndex < tapCount; tapIndex++) {
343             for (int i = 0; i < fingerCount; i++) {
344                 final StrokeDescription lastStroke = strokes[(tapIndex - 1) * fingerCount + i];
345                 final long nextStartTime = endTimeOf(lastStroke) + STROKE_TIME_GAP_MS;
346                 final int nextIndex = tapIndex * fingerCount + i;
347                 pointers[i] = getPointWithinSlop(pointers[i], slop);
348                 strokes[nextIndex] = startingAt(nextStartTime, click(pointers[i]));
349             }
350         }
351         return getGestureBuilder(displayId, strokes).build();
352     }
353 
354     /**
355      * Simulates a user placing multiple fingers on the specified screen
356      * and then multi-tapping and holding with these fingers.
357      *
358      * The location of fingers based on <code>basePoint<code/> are shifted by <code>delta<code/>.
359      * Like (baseX, baseY), (baseX + deltaX, baseY + deltaY), and so on.
360      *
361      * @param basePoint Where to place the first finger.
362      * @param delta Offset to basePoint where to place the 2nd or 3rd finger.
363      * @param fingerCount The number of fingers.
364      * @param tapCount The number of taps to fingers.
365      * @param slop Slop range the finger tapped.
366      * @param displayId Which display to dispatch the gesture.
367      */
368     public static GestureDescription multiFingerMultiTapAndHold(
369             PointF basePoint,
370             PointF delta,
371             int fingerCount,
372             int tapCount,
373             int slop,
374             int displayId) {
375         assertTrue(fingerCount >= 2);
376         assertTrue(tapCount > 0);
377         final int strokeCount = fingerCount * tapCount;
378         final PointF[] pointers = new PointF[fingerCount];
379         final StrokeDescription[] strokes = new StrokeDescription[strokeCount];
380 
381         // The first tap
382         for (int i = 0; i < fingerCount; i++) {
383             pointers[i] = add(basePoint, times(i, delta));
384             if(tapCount == 1) {
385                 strokes[i] = longClick(pointers[i]);
386             } else {
387                 strokes[i] = click(pointers[i]);
388             }
389         }
390         // The rest of taps
391         for (int tapIndex = 1; tapIndex < tapCount; tapIndex++) {
392             for (int i = 0; i < fingerCount; i++) {
393                 final StrokeDescription lastStroke = strokes[(tapIndex - 1) * fingerCount + i];
394                 final long nextStartTime = endTimeOf(lastStroke) + STROKE_TIME_GAP_MS;
395                 final int nextIndex = tapIndex * fingerCount + i;
396                 pointers[i] = getPointWithinSlop(pointers[i], slop);
397                 if (tapIndex + 1 == tapCount) {
398                     // Last tap so do long click.
399                     strokes[nextIndex] = startingAt(nextStartTime, longClick(pointers[i]));
400                 } else {
401                     strokes[nextIndex] = startingAt(nextStartTime, click(pointers[i]));
402                 }
403             }
404         }
405         return getGestureBuilder(displayId, strokes).build();
406     }
407 }
408