1 /*
2  * Copyright (C) 2012 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 com.android.uiautomator.core;
18 
19 import android.accessibilityservice.AccessibilityService;
20 import android.app.UiAutomation;
21 import android.app.UiAutomation.AccessibilityEventFilter;
22 import android.graphics.Point;
23 import android.os.RemoteException;
24 import android.os.SystemClock;
25 import android.util.Log;
26 import android.view.InputDevice;
27 import android.view.InputEvent;
28 import android.view.KeyCharacterMap;
29 import android.view.KeyEvent;
30 import android.view.MotionEvent;
31 import android.view.MotionEvent.PointerCoords;
32 import android.view.MotionEvent.PointerProperties;
33 import android.view.accessibility.AccessibilityEvent;
34 
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.concurrent.TimeoutException;
38 
39 /**
40  * The InteractionProvider is responsible for injecting user events such as touch events
41  * (includes swipes) and text key events into the system. To do so, all it needs to know about
42  * are coordinates of the touch events and text for the text input events.
43  * The InteractionController performs no synchronization. It will fire touch and text input events
44  * as fast as it receives them. All idle synchronization is performed prior to querying the
45  * hierarchy. See {@link QueryController}
46  */
47 class InteractionController {
48 
49     private static final String LOG_TAG = InteractionController.class.getSimpleName();
50 
51     private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
52 
53     private final KeyCharacterMap mKeyCharacterMap =
54             KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
55 
56     private final UiAutomatorBridge mUiAutomatorBridge;
57 
58     private static final long REGULAR_CLICK_LENGTH = 100;
59 
60     private long mDownTime;
61 
62     // Inserted after each motion event injection.
63     private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
64 
InteractionController(UiAutomatorBridge bridge)65     public InteractionController(UiAutomatorBridge bridge) {
66         mUiAutomatorBridge = bridge;
67     }
68 
69     /**
70      * Predicate for waiting for any of the events specified in the mask
71      */
72     class WaitForAnyEventPredicate implements AccessibilityEventFilter {
73         int mMask;
WaitForAnyEventPredicate(int mask)74         WaitForAnyEventPredicate(int mask) {
75             mMask = mask;
76         }
77         @Override
accept(AccessibilityEvent t)78         public boolean accept(AccessibilityEvent t) {
79             // check current event in the list
80             if ((t.getEventType() & mMask) != 0) {
81                 return true;
82             }
83 
84             // no match yet
85             return false;
86         }
87     }
88 
89     /**
90      * Predicate for waiting for all the events specified in the mask and populating
91      * a ctor passed list with matching events. User of this Predicate must recycle
92      * all populated events in the events list.
93      */
94     class EventCollectingPredicate implements AccessibilityEventFilter {
95         int mMask;
96         List<AccessibilityEvent> mEventsList;
97 
EventCollectingPredicate(int mask, List<AccessibilityEvent> events)98         EventCollectingPredicate(int mask, List<AccessibilityEvent> events) {
99             mMask = mask;
100             mEventsList = events;
101         }
102 
103         @Override
accept(AccessibilityEvent t)104         public boolean accept(AccessibilityEvent t) {
105             // check current event in the list
106             if ((t.getEventType() & mMask) != 0) {
107                 // For the events you need, always store a copy when returning false from
108                 // predicates since the original will automatically be recycled after the call.
109                 mEventsList.add(AccessibilityEvent.obtain(t));
110             }
111 
112             // get more
113             return false;
114         }
115     }
116 
117     /**
118      * Predicate for waiting for every event specified in the mask to be matched at least once
119      */
120     class WaitForAllEventPredicate implements AccessibilityEventFilter {
121         int mMask;
WaitForAllEventPredicate(int mask)122         WaitForAllEventPredicate(int mask) {
123             mMask = mask;
124         }
125 
126         @Override
accept(AccessibilityEvent t)127         public boolean accept(AccessibilityEvent t) {
128             // check current event in the list
129             if ((t.getEventType() & mMask) != 0) {
130                 // remove from mask since this condition is satisfied
131                 mMask &= ~t.getEventType();
132 
133                 // Since we're waiting for all events to be matched at least once
134                 if (mMask != 0)
135                     return false;
136 
137                 // all matched
138                 return true;
139             }
140 
141             // no match yet
142             return false;
143         }
144     }
145 
146     /**
147      * Helper used by methods to perform actions and wait for any accessibility events and return
148      * predicated on predefined filter.
149      *
150      * @param command
151      * @param filter
152      * @param timeout
153      * @return
154      */
runAndWaitForEvents(Runnable command, AccessibilityEventFilter filter, long timeout)155     private AccessibilityEvent runAndWaitForEvents(Runnable command,
156             AccessibilityEventFilter filter, long timeout) {
157 
158         try {
159             return mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command, filter,
160                     timeout);
161         } catch (TimeoutException e) {
162             Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events");
163             return null;
164         } catch (Exception e) {
165             Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e);
166             return null;
167         }
168     }
169 
170     /**
171      * Send keys and blocks until the first specified accessibility event.
172      *
173      * Most key presses will cause some UI change to occur. If the device is busy, this will
174      * block until the device begins to process the key press at which point the call returns
175      * and normal wait for idle processing may begin. If no events are detected for the
176      * timeout period specified, the call will return anyway with false.
177      *
178      * @param keyCode
179      * @param metaState
180      * @param eventType
181      * @param timeout
182      * @return true if events is received, otherwise false.
183      */
sendKeyAndWaitForEvent(final int keyCode, final int metaState, final int eventType, long timeout)184     public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState,
185             final int eventType, long timeout) {
186         Runnable command = new Runnable() {
187             @Override
188             public void run() {
189                 final long eventTime = SystemClock.uptimeMillis();
190                 KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
191                         keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
192                         InputDevice.SOURCE_KEYBOARD);
193                 if (injectEventSync(downEvent)) {
194                     KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
195                             keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
196                             InputDevice.SOURCE_KEYBOARD);
197                     injectEventSync(upEvent);
198                 }
199             }
200         };
201 
202         return runAndWaitForEvents(command, new WaitForAnyEventPredicate(eventType), timeout)
203                 != null;
204     }
205 
206     /**
207      * Clicks at coordinates without waiting for device idle. This may be used for operations
208      * that require stressing the target.
209      * @param x
210      * @param y
211      * @return true if the click executed successfully
212      */
clickNoSync(int x, int y)213     public boolean clickNoSync(int x, int y) {
214         Log.d(LOG_TAG, "clickNoSync (" + x + ", " + y + ")");
215 
216         if (touchDown(x, y)) {
217             SystemClock.sleep(REGULAR_CLICK_LENGTH);
218             if (touchUp(x, y))
219                 return true;
220         }
221         return false;
222     }
223 
224     /**
225      * Click at coordinates and blocks until either accessibility event TYPE_WINDOW_CONTENT_CHANGED
226      * or TYPE_VIEW_SELECTED are received.
227      *
228      * @param x
229      * @param y
230      * @param timeout waiting for event
231      * @return true if events are received, else false if timeout.
232      */
clickAndSync(final int x, final int y, long timeout)233     public boolean clickAndSync(final int x, final int y, long timeout) {
234 
235         String logString = String.format("clickAndSync(%d, %d)", x, y);
236         Log.d(LOG_TAG, logString);
237 
238         return runAndWaitForEvents(clickRunnable(x, y), new WaitForAnyEventPredicate(
239                 AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED |
240                 AccessibilityEvent.TYPE_VIEW_SELECTED), timeout) != null;
241     }
242 
243     /**
244      * Clicks at coordinates and waits for for a TYPE_WINDOW_STATE_CHANGED event followed
245      * by TYPE_WINDOW_CONTENT_CHANGED. If timeout occurs waiting for TYPE_WINDOW_STATE_CHANGED,
246      * no further waits will be performed and the function returns.
247      * @param x
248      * @param y
249      * @param timeout waiting for event
250      * @return true if both events occurred in the expected order
251      */
clickAndWaitForNewWindow(final int x, final int y, long timeout)252     public boolean clickAndWaitForNewWindow(final int x, final int y, long timeout) {
253         String logString = String.format("clickAndWaitForNewWindow(%d, %d)", x, y);
254         Log.d(LOG_TAG, logString);
255 
256         return runAndWaitForEvents(clickRunnable(x, y), new WaitForAllEventPredicate(
257                 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED |
258                 AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED), timeout) != null;
259     }
260 
261     /**
262      * Returns a Runnable for use in {@link #runAndWaitForEvents(Runnable, AccessibilityEventFilter, long) to
263      * perform a click.
264      *
265      * @param x coordinate
266      * @param y coordinate
267      * @return Runnable
268      */
clickRunnable(final int x, final int y)269     private Runnable clickRunnable(final int x, final int y) {
270         return new Runnable() {
271             @Override
272             public void run() {
273                 if(touchDown(x, y)) {
274                     SystemClock.sleep(REGULAR_CLICK_LENGTH);
275                     touchUp(x, y);
276                 }
277             }
278         };
279     }
280 
281     /**
282      * Touches down for a long press at the specified coordinates.
283      *
284      * @param x
285      * @param y
286      * @return true if successful.
287      */
288     public boolean longTapNoSync(int x, int y) {
289         if (DEBUG) {
290             Log.d(LOG_TAG, "longTapNoSync (" + x + ", " + y + ")");
291         }
292 
293         if (touchDown(x, y)) {
294             SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime());
295             if(touchUp(x, y)) {
296                 return true;
297             }
298         }
299         return false;
300     }
301 
302     private boolean touchDown(int x, int y) {
303         if (DEBUG) {
304             Log.d(LOG_TAG, "touchDown (" + x + ", " + y + ")");
305         }
306         mDownTime = SystemClock.uptimeMillis();
307         MotionEvent event = MotionEvent.obtain(
308                 mDownTime, mDownTime, MotionEvent.ACTION_DOWN, x, y, 1);
309         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
310         return injectEventSync(event);
311     }
312 
313     private boolean touchUp(int x, int y) {
314         if (DEBUG) {
315             Log.d(LOG_TAG, "touchUp (" + x + ", " + y + ")");
316         }
317         final long eventTime = SystemClock.uptimeMillis();
318         MotionEvent event = MotionEvent.obtain(
319                 mDownTime, eventTime, MotionEvent.ACTION_UP, x, y, 1);
320         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
321         mDownTime = 0;
322         return injectEventSync(event);
323     }
324 
325     private boolean touchMove(int x, int y) {
326         if (DEBUG) {
327             Log.d(LOG_TAG, "touchMove (" + x + ", " + y + ")");
328         }
329         final long eventTime = SystemClock.uptimeMillis();
330         MotionEvent event = MotionEvent.obtain(
331                 mDownTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 1);
332         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
333         return injectEventSync(event);
334     }
335 
336     /**
337      * Handle swipes in any direction where the result is a scroll event. This call blocks
338      * until the UI has fired a scroll event or timeout.
339      * @param downX
340      * @param downY
341      * @param upX
342      * @param upY
343      * @param steps
344      * @return true if we are not at the beginning or end of the scrollable view.
345      */
346     public boolean scrollSwipe(final int downX, final int downY, final int upX, final int upY,
347             final int steps) {
348         Log.d(LOG_TAG, "scrollSwipe (" +  downX + ", " + downY + ", " + upX + ", "
349                 + upY + ", " + steps +")");
350 
351         Runnable command = new Runnable() {
352             @Override
353             public void run() {
354                 swipe(downX, downY, upX, upY, steps);
355             }
356         };
357 
358         // Collect all accessibility events generated during the swipe command and get the
359         // last event
360         ArrayList<AccessibilityEvent> events = new ArrayList<AccessibilityEvent>();
361         runAndWaitForEvents(command,
362                 new EventCollectingPredicate(AccessibilityEvent.TYPE_VIEW_SCROLLED, events),
363                 Configurator.getInstance().getScrollAcknowledgmentTimeout());
364 
365         AccessibilityEvent event = getLastMatchingEvent(events,
366                 AccessibilityEvent.TYPE_VIEW_SCROLLED);
367 
368         if (event == null) {
369             // end of scroll since no new scroll events received
370             recycleAccessibilityEvents(events);
371             return false;
372         }
373 
374         // AdapterViews have indices we can use to check for the beginning.
375         boolean foundEnd = false;
376         if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
377             foundEnd = event.getFromIndex() == 0 ||
378                     (event.getItemCount() - 1) == event.getToIndex();
379             Log.d(LOG_TAG, "scrollSwipe reached scroll end: " + foundEnd);
380         } else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
381             // Determine if we are scrolling vertically or horizontally.
382             if (downX == upX) {
383                 // Vertical
384                 foundEnd = event.getScrollY() == 0 ||
385                         event.getScrollY() == event.getMaxScrollY();
386                 Log.d(LOG_TAG, "Vertical scrollSwipe reached scroll end: " + foundEnd);
387             } else if (downY == upY) {
388                 // Horizontal
389                 foundEnd = event.getScrollX() == 0 ||
390                         event.getScrollX() == event.getMaxScrollX();
391                 Log.d(LOG_TAG, "Horizontal scrollSwipe reached scroll end: " + foundEnd);
392             }
393         }
394         recycleAccessibilityEvents(events);
395         return !foundEnd;
396     }
397 
398     private AccessibilityEvent getLastMatchingEvent(List<AccessibilityEvent> events, int type) {
399         for (int x = events.size(); x > 0; x--) {
400             AccessibilityEvent event = events.get(x - 1);
401             if (event.getEventType() == type)
402                 return event;
403         }
404         return null;
405     }
406 
407     private void recycleAccessibilityEvents(List<AccessibilityEvent> events) {
408         for (AccessibilityEvent event : events)
409             event.recycle();
410         events.clear();
411     }
412 
413     /**
414      * Handle swipes in any direction.
415      * @param downX
416      * @param downY
417      * @param upX
418      * @param upY
419      * @param steps
420      * @return true if the swipe executed successfully
421      */
422     public boolean swipe(int downX, int downY, int upX, int upY, int steps) {
423         return swipe(downX, downY, upX, upY, steps, false /*drag*/);
424     }
425 
426     /**
427      * Handle swipes/drags in any direction.
428      * @param downX
429      * @param downY
430      * @param upX
431      * @param upY
432      * @param steps
433      * @param drag when true, the swipe becomes a drag swipe
434      * @return true if the swipe executed successfully
435      */
436     public boolean swipe(int downX, int downY, int upX, int upY, int steps, boolean drag) {
437         boolean ret = false;
438         int swipeSteps = steps;
439         double xStep = 0;
440         double yStep = 0;
441 
442         // avoid a divide by zero
443         if(swipeSteps == 0)
444             swipeSteps = 1;
445 
446         xStep = ((double)(upX - downX)) / swipeSteps;
447         yStep = ((double)(upY - downY)) / swipeSteps;
448 
449         // first touch starts exactly at the point requested
450         ret = touchDown(downX, downY);
451         if (drag)
452             SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime());
453         for(int i = 1; i < swipeSteps; i++) {
454             ret &= touchMove(downX + (int)(xStep * i), downY + (int)(yStep * i));
455             if(ret == false)
456                 break;
457             // set some known constant delay between steps as without it this
458             // become completely dependent on the speed of the system and results
459             // may vary on different devices. This guarantees at minimum we have
460             // a preset delay.
461             SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
462         }
463         if (drag)
464             SystemClock.sleep(REGULAR_CLICK_LENGTH);
465         ret &= touchUp(upX, upY);
466         return(ret);
467     }
468 
469     /**
470      * Performs a swipe between points in the Point array.
471      * @param segments is Point array containing at least one Point object
472      * @param segmentSteps steps to inject between two Points
473      * @return true on success
474      */
475     public boolean swipe(Point[] segments, int segmentSteps) {
476         boolean ret = false;
477         int swipeSteps = segmentSteps;
478         double xStep = 0;
479         double yStep = 0;
480 
481         // avoid a divide by zero
482         if(segmentSteps == 0)
483             segmentSteps = 1;
484 
485         // must have some points
486         if(segments.length == 0)
487             return false;
488 
489         // first touch starts exactly at the point requested
490         ret = touchDown(segments[0].x, segments[0].y);
491         for(int seg = 0; seg < segments.length; seg++) {
492             if(seg + 1 < segments.length) {
493 
494                 xStep = ((double)(segments[seg+1].x - segments[seg].x)) / segmentSteps;
495                 yStep = ((double)(segments[seg+1].y - segments[seg].y)) / segmentSteps;
496 
497                 for(int i = 1; i < swipeSteps; i++) {
498                     ret &= touchMove(segments[seg].x + (int)(xStep * i),
499                             segments[seg].y + (int)(yStep * i));
500                     if(ret == false)
501                         break;
502                     // set some known constant delay between steps as without it this
503                     // become completely dependent on the speed of the system and results
504                     // may vary on different devices. This guarantees at minimum we have
505                     // a preset delay.
506                     SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
507                 }
508             }
509         }
510         ret &= touchUp(segments[segments.length - 1].x, segments[segments.length -1].y);
511         return(ret);
512     }
513 
514 
515     public boolean sendText(String text) {
516         if (DEBUG) {
517             Log.d(LOG_TAG, "sendText (" + text + ")");
518         }
519 
520         KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());
521 
522         if (events != null) {
523             long keyDelay = Configurator.getInstance().getKeyInjectionDelay();
524             for (KeyEvent event2 : events) {
525                 // We have to change the time of an event before injecting it because
526                 // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
527                 // time stamp and the system rejects too old events. Hence, it is
528                 // possible for an event to become stale before it is injected if it
529                 // takes too long to inject the preceding ones.
530                 KeyEvent event = KeyEvent.changeTimeRepeat(event2,
531                         SystemClock.uptimeMillis(), 0);
532                 if (!injectEventSync(event)) {
533                     return false;
534                 }
535                 SystemClock.sleep(keyDelay);
536             }
537         }
538         return true;
539     }
540 
541     public boolean sendKey(int keyCode, int metaState) {
542         if (DEBUG) {
543             Log.d(LOG_TAG, "sendKey (" + keyCode + ", " + metaState + ")");
544         }
545 
546         final long eventTime = SystemClock.uptimeMillis();
547         KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
548                 keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
549                 InputDevice.SOURCE_KEYBOARD);
550         if (injectEventSync(downEvent)) {
551             KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
552                     keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
553                     InputDevice.SOURCE_KEYBOARD);
554             if(injectEventSync(upEvent)) {
555                 return true;
556             }
557         }
558         return false;
559     }
560 
561     /**
562      * Rotates right and also freezes rotation in that position by
563      * disabling the sensors. If you want to un-freeze the rotation
564      * and re-enable the sensors see {@link #unfreezeRotation()}. Note
565      * that doing so may cause the screen contents to rotate
566      * depending on the current physical position of the test device.
567      * @throws RemoteException
568      */
569     public void setRotationRight() {
570         mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_270);
571     }
572 
573     /**
574      * Rotates left and also freezes rotation in that position by
575      * disabling the sensors. If you want to un-freeze the rotation
576      * and re-enable the sensors see {@link #unfreezeRotation()}. Note
577      * that doing so may cause the screen contents to rotate
578      * depending on the current physical position of the test device.
579      * @throws RemoteException
580      */
581     public void setRotationLeft() {
582         mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_90);
583     }
584 
585     /**
586      * Rotates up and also freezes rotation in that position by
587      * disabling the sensors. If you want to un-freeze the rotation
588      * and re-enable the sensors see {@link #unfreezeRotation()}. Note
589      * that doing so may cause the screen contents to rotate
590      * depending on the current physical position of the test device.
591      * @throws RemoteException
592      */
593     public void setRotationNatural() {
594         mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_0);
595     }
596 
597     /**
598      * Disables the sensors and freezes the device rotation at its
599      * current rotation state.
600      * @throws RemoteException
601      */
602     public void freezeRotation() {
603         mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_CURRENT);
604     }
605 
606     /**
607      * Re-enables the sensors and un-freezes the device rotation
608      * allowing its contents to rotate with the device physical rotation.
609      * @throws RemoteException
610      */
611     public void unfreezeRotation() {
612         mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_UNFREEZE);
613     }
614 
615     /**
616      * This method simply presses the power button if the screen is OFF else
617      * it does nothing if the screen is already ON.
618      * @return true if the device was asleep else false
619      * @throws RemoteException
620      */
621     public boolean wakeDevice() throws RemoteException {
622         if(!isScreenOn()) {
623             sendKey(KeyEvent.KEYCODE_POWER, 0);
624             return true;
625         }
626         return false;
627     }
628 
629     /**
630      * This method simply presses the power button if the screen is ON else
631      * it does nothing if the screen is already OFF.
632      * @return true if the device was awake else false
633      * @throws RemoteException
634      */
635     public boolean sleepDevice() throws RemoteException {
636         if(isScreenOn()) {
637             this.sendKey(KeyEvent.KEYCODE_POWER, 0);
638             return true;
639         }
640         return false;
641     }
642 
643     /**
644      * Checks the power manager if the screen is ON
645      * @return true if the screen is ON else false
646      * @throws RemoteException
647      */
648     public boolean isScreenOn() throws RemoteException {
649         return mUiAutomatorBridge.isScreenOn();
650     }
651 
652     private boolean injectEventSync(InputEvent event) {
653         return mUiAutomatorBridge.injectInputEvent(event, true);
654     }
655 
656     private int getPointerAction(int motionEnvent, int index) {
657         return motionEnvent + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
658     }
659 
660     /**
661      * Performs a multi-touch gesture
662      *
663      * Takes a series of touch coordinates for at least 2 pointers. Each pointer must have
664      * all of its touch steps defined in an array of {@link PointerCoords}. By having the ability
665      * to specify the touch points along the path of a pointer, the caller is able to specify
666      * complex gestures like circles, irregular shapes etc, where each pointer may take a
667      * different path.
668      *
669      * To create a single point on a pointer's touch path
670      * <code>
671      *       PointerCoords p = new PointerCoords();
672      *       p.x = stepX;
673      *       p.y = stepY;
674      *       p.pressure = 1;
675      *       p.size = 1;
676      * </code>
677      * @param touches each array of {@link PointerCoords} constitute a single pointer's touch path.
678      *        Multiple {@link PointerCoords} arrays constitute multiple pointers, each with its own
679      *        path. Each {@link PointerCoords} in an array constitute a point on a pointer's path.
680      * @return <code>true</code> if all points on all paths are injected successfully, <code>false
681      *        </code>otherwise
682      * @since API Level 18
683      */
684     public boolean performMultiPointerGesture(PointerCoords[] ... touches) {
685         boolean ret = true;
686         if (touches.length < 2) {
687             throw new IllegalArgumentException("Must provide coordinates for at least 2 pointers");
688         }
689 
690         // Get the pointer with the max steps to inject.
691         int maxSteps = 0;
692         for (int x = 0; x < touches.length; x++)
693             maxSteps = (maxSteps < touches[x].length) ? touches[x].length : maxSteps;
694 
695         // specify the properties for each pointer as finger touch
696         PointerProperties[] properties = new PointerProperties[touches.length];
697         PointerCoords[] pointerCoords = new PointerCoords[touches.length];
698         for (int x = 0; x < touches.length; x++) {
699             PointerProperties prop = new PointerProperties();
700             prop.id = x;
701             prop.toolType = MotionEvent.TOOL_TYPE_FINGER;
702             properties[x] = prop;
703 
704             // for each pointer set the first coordinates for touch down
705             pointerCoords[x] = touches[x][0];
706         }
707 
708         // Touch down all pointers
709         long downTime = SystemClock.uptimeMillis();
710         MotionEvent event;
711         event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 1,
712                 properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
713         ret &= injectEventSync(event);
714 
715         for (int x = 1; x < touches.length; x++) {
716             event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
717                     getPointerAction(MotionEvent.ACTION_POINTER_DOWN, x), x + 1, properties,
718                     pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
719             ret &= injectEventSync(event);
720         }
721 
722         // Move all pointers
723         for (int i = 1; i < maxSteps - 1; i++) {
724             // for each pointer
725             for (int x = 0; x < touches.length; x++) {
726                 // check if it has coordinates to move
727                 if (touches[x].length > i)
728                     pointerCoords[x] = touches[x][i];
729                 else
730                     pointerCoords[x] = touches[x][touches[x].length - 1];
731             }
732 
733             event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
734                     MotionEvent.ACTION_MOVE, touches.length, properties, pointerCoords, 0, 0, 1, 1,
735                     0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
736 
737             ret &= injectEventSync(event);
738             SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
739         }
740 
741         // For each pointer get the last coordinates
742         for (int x = 0; x < touches.length; x++)
743             pointerCoords[x] = touches[x][touches[x].length - 1];
744 
745         // touch up
746         for (int x = 1; x < touches.length; x++) {
747             event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
748                     getPointerAction(MotionEvent.ACTION_POINTER_UP, x), x + 1, properties,
749                     pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
750             ret &= injectEventSync(event);
751         }
752 
753         Log.i(LOG_TAG, "x " + pointerCoords[0].x);
754         // first to touch down is last up
755         event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 1,
756                 properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
757         ret &= injectEventSync(event);
758         return ret;
759     }
760 
761     /**
762      * Simulates a short press on the Recent Apps button.
763      *
764      * @return true if successful, else return false
765      * @since API Level 18
766      */
767     public boolean toggleRecentApps() {
768         return mUiAutomatorBridge.performGlobalAction(
769                 AccessibilityService.GLOBAL_ACTION_RECENTS);
770     }
771 
772     /**
773      * Opens the notification shade
774      *
775      * @return true if successful, else return false
776      * @since API Level 18
777      */
778     public boolean openNotification() {
779         return mUiAutomatorBridge.performGlobalAction(
780                 AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
781     }
782 
783     /**
784      * Opens the quick settings shade
785      *
786      * @return true if successful, else return false
787      * @since API Level 18
788      */
789     public boolean openQuickSettings() {
790         return mUiAutomatorBridge.performGlobalAction(
791                 AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS);
792     }
793 }
794