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 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; 76 WaitForAnyEventPredicate(int mask) { 77 mMask = mask; 78 } 79 @Override 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 100 EventCollectingPredicate(int mask, List<AccessibilityEvent> events) { 101 mMask = mask; 102 mEventsList = events; 103 } 104 105 @Override 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; 124 WaitForAllEventPredicate(int mask) { 125 mMask = mask; 126 } 127 128 @Override 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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