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