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