1/* 2 * Copyright (C) 2011 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 */ 16package com.android.chimpchat.adb; 17 18import com.google.common.annotations.VisibleForTesting; 19import com.google.common.base.Preconditions; 20import com.google.common.collect.Lists; 21import com.google.common.collect.Maps; 22 23import com.android.ddmlib.AdbCommandRejectedException; 24import com.android.ddmlib.IDevice; 25import com.android.ddmlib.InstallException; 26import com.android.ddmlib.ShellCommandUnresponsiveException; 27import com.android.ddmlib.TimeoutException; 28import com.android.chimpchat.ChimpManager; 29import com.android.chimpchat.adb.LinearInterpolator.Point; 30import com.android.chimpchat.core.ChimpRect; 31import com.android.chimpchat.core.IChimpImage; 32import com.android.chimpchat.core.IChimpDevice; 33import com.android.chimpchat.core.IChimpView; 34import com.android.chimpchat.core.IMultiSelector; 35import com.android.chimpchat.core.ISelector; 36import com.android.chimpchat.core.PhysicalButton; 37import com.android.chimpchat.core.TouchPressType; 38import com.android.chimpchat.hierarchyviewer.HierarchyViewer; 39 40import java.io.IOException; 41import java.net.InetAddress; 42import java.net.Socket; 43import java.net.UnknownHostException; 44import java.util.ArrayList; 45import java.util.Collection; 46import java.util.List; 47import java.util.Map; 48import java.util.Map.Entry; 49import java.util.concurrent.ExecutorService; 50import java.util.concurrent.Executors; 51import java.util.logging.Level; 52import java.util.logging.Logger; 53import java.util.regex.Matcher; 54import java.util.regex.Pattern; 55 56import javax.annotation.Nullable; 57 58public class AdbChimpDevice implements IChimpDevice { 59 private static final Logger LOG = Logger.getLogger(AdbChimpDevice.class.getName()); 60 61 private static final String[] ZERO_LENGTH_STRING_ARRAY = new String[0]; 62 private static final long MANAGER_CREATE_TIMEOUT_MS = 30 * 1000; // 30 seconds 63 private static final long MANAGER_CREATE_WAIT_TIME_MS = 1000; // wait 1 second 64 65 private final ExecutorService executor = Executors.newSingleThreadExecutor(); 66 67 private final IDevice device; 68 private ChimpManager manager; 69 70 public AdbChimpDevice(IDevice device) { 71 this.device = device; 72 this.manager = createManager("127.0.0.1", 12345); 73 74 Preconditions.checkNotNull(this.manager); 75 } 76 77 @Override 78 public ChimpManager getManager() { 79 return manager; 80 } 81 82 @Override 83 public void dispose() { 84 try { 85 manager.quit(); 86 } catch (IOException e) { 87 LOG.log(Level.SEVERE, "Error getting the manager to quit", e); 88 } 89 manager.close(); 90 executor.shutdown(); 91 manager = null; 92 } 93 94 @Override 95 public HierarchyViewer getHierarchyViewer() { 96 return new HierarchyViewer(device); 97 } 98 99 private void executeAsyncCommand(final String command, 100 final LoggingOutputReceiver logger) { 101 executor.submit(new Runnable() { 102 @Override 103 public void run() { 104 try { 105 device.executeShellCommand(command, logger); 106 } catch (TimeoutException e) { 107 LOG.log(Level.SEVERE, "Error starting command: " + command, e); 108 throw new RuntimeException(e); 109 } catch (AdbCommandRejectedException e) { 110 LOG.log(Level.SEVERE, "Error starting command: " + command, e); 111 throw new RuntimeException(e); 112 } catch (ShellCommandUnresponsiveException e) { 113 // This happens a lot 114 LOG.log(Level.INFO, "Error starting command: " + command, e); 115 throw new RuntimeException(e); 116 } catch (IOException e) { 117 LOG.log(Level.SEVERE, "Error starting command: " + command, e); 118 throw new RuntimeException(e); 119 } 120 } 121 }); 122 } 123 124 private ChimpManager createManager(String address, int port) { 125 try { 126 device.createForward(port, port); 127 } catch (TimeoutException e) { 128 LOG.log(Level.SEVERE, "Timeout creating adb port forwarding", e); 129 return null; 130 } catch (AdbCommandRejectedException e) { 131 LOG.log(Level.SEVERE, "Adb rejected adb port forwarding command: " + e.getMessage(), e); 132 return null; 133 } catch (IOException e) { 134 LOG.log(Level.SEVERE, "Unable to create adb port forwarding: " + e.getMessage(), e); 135 return null; 136 } 137 138 String command = "monkey --port " + port; 139 executeAsyncCommand(command, new LoggingOutputReceiver(LOG, Level.FINE)); 140 141 // Sleep for a second to give the command time to execute. 142 try { 143 Thread.sleep(1000); 144 } catch (InterruptedException e) { 145 LOG.log(Level.SEVERE, "Unable to sleep", e); 146 } 147 148 InetAddress addr; 149 try { 150 addr = InetAddress.getByName(address); 151 } catch (UnknownHostException e) { 152 LOG.log(Level.SEVERE, "Unable to convert address into InetAddress: " + address, e); 153 return null; 154 } 155 156 // We have a tough problem to solve here. "monkey" on the device gives us no indication 157 // when it has started up and is ready to serve traffic. If you try too soon, commands 158 // will fail. To remedy this, we will keep trying until a single command (in this case, 159 // wake) succeeds. 160 boolean success = false; 161 ChimpManager mm = null; 162 long start = System.currentTimeMillis(); 163 164 while (!success) { 165 long now = System.currentTimeMillis(); 166 long diff = now - start; 167 if (diff > MANAGER_CREATE_TIMEOUT_MS) { 168 LOG.severe("Timeout while trying to create chimp mananger"); 169 return null; 170 } 171 172 try { 173 Thread.sleep(MANAGER_CREATE_WAIT_TIME_MS); 174 } catch (InterruptedException e) { 175 LOG.log(Level.SEVERE, "Unable to sleep", e); 176 } 177 178 Socket monkeySocket; 179 try { 180 monkeySocket = new Socket(addr, port); 181 } catch (IOException e) { 182 LOG.log(Level.FINE, "Unable to connect socket", e); 183 success = false; 184 continue; 185 } 186 187 try { 188 mm = new ChimpManager(monkeySocket); 189 } catch (IOException e) { 190 LOG.log(Level.SEVERE, "Unable to open writer and reader to socket"); 191 continue; 192 } 193 194 try { 195 mm.wake(); 196 } catch (IOException e) { 197 LOG.log(Level.FINE, "Unable to wake up device", e); 198 success = false; 199 continue; 200 } 201 success = true; 202 } 203 204 return mm; 205 } 206 207 @Override 208 public IChimpImage takeSnapshot() { 209 try { 210 return new AdbChimpImage(device.getScreenshot()); 211 } catch (TimeoutException e) { 212 LOG.log(Level.SEVERE, "Unable to take snapshot", e); 213 return null; 214 } catch (AdbCommandRejectedException e) { 215 LOG.log(Level.SEVERE, "Unable to take snapshot", e); 216 return null; 217 } catch (IOException e) { 218 LOG.log(Level.SEVERE, "Unable to take snapshot", e); 219 return null; 220 } 221 } 222 223 @Override 224 public String getSystemProperty(String key) { 225 return device.getProperty(key); 226 } 227 228 @Override 229 public String getProperty(String key) { 230 try { 231 return manager.getVariable(key); 232 } catch (IOException e) { 233 LOG.log(Level.SEVERE, "Unable to get variable: " + key, e); 234 return null; 235 } 236 } 237 238 @Override 239 public Collection<String> getPropertyList() { 240 try { 241 return manager.listVariable(); 242 } catch (IOException e) { 243 LOG.log(Level.SEVERE, "Unable to get variable list", e); 244 return null; 245 } 246 } 247 248 @Override 249 public void wake() { 250 try { 251 manager.wake(); 252 } catch (IOException e) { 253 LOG.log(Level.SEVERE, "Unable to wake device (too sleepy?)", e); 254 } 255 } 256 257 private String shell(String... args) { 258 StringBuilder cmd = new StringBuilder(); 259 for (String arg : args) { 260 cmd.append(arg).append(" "); 261 } 262 return shell(cmd.toString()); 263 } 264 265 @Override 266 public String shell(String cmd) { 267 CommandOutputCapture capture = new CommandOutputCapture(); 268 try { 269 device.executeShellCommand(cmd, capture); 270 } catch (TimeoutException e) { 271 LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); 272 return null; 273 } catch (ShellCommandUnresponsiveException e) { 274 LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); 275 return null; 276 } catch (AdbCommandRejectedException e) { 277 LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); 278 return null; 279 } catch (IOException e) { 280 LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); 281 return null; 282 } 283 return capture.toString(); 284 } 285 286 @Override 287 public boolean installPackage(String path) { 288 try { 289 String result = device.installPackage(path, true); 290 if (result != null) { 291 LOG.log(Level.SEVERE, "Got error installing package: "+ result); 292 return false; 293 } 294 return true; 295 } catch (InstallException e) { 296 LOG.log(Level.SEVERE, "Error installing package: " + path, e); 297 return false; 298 } 299 } 300 301 @Override 302 public boolean removePackage(String packageName) { 303 try { 304 String result = device.uninstallPackage(packageName); 305 if (result != null) { 306 LOG.log(Level.SEVERE, "Got error uninstalling package "+ packageName + ": " + 307 result); 308 return false; 309 } 310 return true; 311 } catch (InstallException e) { 312 LOG.log(Level.SEVERE, "Error installing package: " + packageName, e); 313 return false; 314 } 315 } 316 317 @Override 318 public void press(String keyName, TouchPressType type) { 319 try { 320 switch (type) { 321 case DOWN_AND_UP: 322 manager.press(keyName); 323 break; 324 case DOWN: 325 manager.keyDown(keyName); 326 break; 327 case UP: 328 manager.keyUp(keyName); 329 break; 330 } 331 } catch (IOException e) { 332 LOG.log(Level.SEVERE, "Error sending press event: " + keyName + " " + type, e); 333 } 334 } 335 336 @Override 337 public void press(PhysicalButton key, TouchPressType type) { 338 press(key.getKeyName(), type); 339 } 340 341 @Override 342 public void type(String string) { 343 try { 344 manager.type(string); 345 } catch (IOException e) { 346 LOG.log(Level.SEVERE, "Error Typing: " + string, e); 347 } 348 } 349 350 @Override 351 public void touch(int x, int y, TouchPressType type) { 352 try { 353 switch (type) { 354 case DOWN: 355 manager.touchDown(x, y); 356 break; 357 case UP: 358 manager.touchUp(x, y); 359 break; 360 case DOWN_AND_UP: 361 manager.tap(x, y); 362 break; 363 } 364 } catch (IOException e) { 365 LOG.log(Level.SEVERE, "Error sending touch event: " + x + " " + y + " " + type, e); 366 } 367 } 368 369 @Override 370 public void reboot(String into) { 371 try { 372 device.reboot(into); 373 } catch (TimeoutException e) { 374 LOG.log(Level.SEVERE, "Unable to reboot device", e); 375 } catch (AdbCommandRejectedException e) { 376 LOG.log(Level.SEVERE, "Unable to reboot device", e); 377 } catch (IOException e) { 378 LOG.log(Level.SEVERE, "Unable to reboot device", e); 379 } 380 } 381 382 @Override 383 public void startActivity(String uri, String action, String data, String mimetype, 384 Collection<String> categories, Map<String, Object> extras, String component, 385 int flags) { 386 List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories, 387 extras, component, flags); 388 shell(Lists.asList("am", "start", 389 intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY)); 390 } 391 392 @Override 393 public void broadcastIntent(String uri, String action, String data, String mimetype, 394 Collection<String> categories, Map<String, Object> extras, String component, 395 int flags) { 396 List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories, 397 extras, component, flags); 398 shell(Lists.asList("am", "broadcast", 399 intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY)); 400 } 401 402 private static boolean isNullOrEmpty(@Nullable String string) { 403 return string == null || string.length() == 0; 404 } 405 406 private List<String> buildIntentArgString(String uri, String action, String data, String mimetype, 407 Collection<String> categories, Map<String, Object> extras, String component, 408 int flags) { 409 List<String> parts = Lists.newArrayList(); 410 411 // from adb docs: 412 //<INTENT> specifications include these flags: 413 // [-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>] 414 // [-c <CATEGORY> [-c <CATEGORY>] ...] 415 // [-e|--es <EXTRA_KEY> <EXTRA_STRING_VALUE> ...] 416 // [--esn <EXTRA_KEY> ...] 417 // [--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> ...] 418 // [-e|--ei <EXTRA_KEY> <EXTRA_INT_VALUE> ...] 419 // [-n <COMPONENT>] [-f <FLAGS>] 420 // [<URI>] 421 422 if (!isNullOrEmpty(action)) { 423 parts.add("-a"); 424 parts.add(action); 425 } 426 427 if (!isNullOrEmpty(data)) { 428 parts.add("-d"); 429 parts.add(data); 430 } 431 432 if (!isNullOrEmpty(mimetype)) { 433 parts.add("-t"); 434 parts.add(mimetype); 435 } 436 437 // Handle categories 438 for (String category : categories) { 439 parts.add("-c"); 440 parts.add(category); 441 } 442 443 // Handle extras 444 for (Entry<String, Object> entry : extras.entrySet()) { 445 // Extras are either boolean, string, or int. See which we have 446 Object value = entry.getValue(); 447 String valueString; 448 String arg; 449 if (value instanceof Integer) { 450 valueString = Integer.toString((Integer) value); 451 arg = "--ei"; 452 } else if (value instanceof Boolean) { 453 valueString = Boolean.toString((Boolean) value); 454 arg = "--ez"; 455 } else { 456 // treat is as a string. 457 valueString = value.toString(); 458 arg = "--es"; 459 } 460 parts.add(arg); 461 parts.add(entry.getKey()); 462 parts.add(valueString); 463 } 464 465 if (!isNullOrEmpty(component)) { 466 parts.add("-n"); 467 parts.add(component); 468 } 469 470 if (flags != 0) { 471 parts.add("-f"); 472 parts.add(Integer.toString(flags)); 473 } 474 475 if (!isNullOrEmpty(uri)) { 476 parts.add(uri); 477 } 478 479 return parts; 480 } 481 482 @Override 483 public Map<String, Object> instrument(String packageName, Map<String, Object> args) { 484 List<String> shellCmd = Lists.newArrayList("am", "instrument", "-w", "-r"); 485 for (Entry<String, Object> entry: args.entrySet()) { 486 final String key = entry.getKey(); 487 final Object value = entry.getValue(); 488 if (key != null && value != null) { 489 shellCmd.add("-e"); 490 shellCmd.add(key); 491 shellCmd.add(value.toString()); 492 } 493 } 494 shellCmd.add(packageName); 495 String result = shell(shellCmd.toArray(ZERO_LENGTH_STRING_ARRAY)); 496 return convertInstrumentResult(result); 497 } 498 499 /** 500 * Convert the instrumentation result into it's Map representation. 501 * 502 * @param result the result string 503 * @return the new map 504 */ 505 @VisibleForTesting 506 /* package */ static Map<String, Object> convertInstrumentResult(String result) { 507 Map<String, Object> map = Maps.newHashMap(); 508 Pattern pattern = Pattern.compile("^INSTRUMENTATION_(\\w+): ", Pattern.MULTILINE); 509 Matcher matcher = pattern.matcher(result); 510 511 int previousEnd = 0; 512 String previousWhich = null; 513 514 while (matcher.find()) { 515 if ("RESULT".equals(previousWhich)) { 516 String resultLine = result.substring(previousEnd, matcher.start()).trim(); 517 // Look for the = in the value, and split there 518 int splitIndex = resultLine.indexOf("="); 519 String key = resultLine.substring(0, splitIndex); 520 String value = resultLine.substring(splitIndex + 1); 521 522 map.put(key, value); 523 } 524 525 previousEnd = matcher.end(); 526 previousWhich = matcher.group(1); 527 } 528 if ("RESULT".equals(previousWhich)) { 529 String resultLine = result.substring(previousEnd, matcher.start()).trim(); 530 // Look for the = in the value, and split there 531 int splitIndex = resultLine.indexOf("="); 532 String key = resultLine.substring(0, splitIndex); 533 String value = resultLine.substring(splitIndex + 1); 534 535 map.put(key, value); 536 } 537 return map; 538 } 539 540 @Override 541 public void drag(int startx, int starty, int endx, int endy, int steps, long ms) { 542 final long iterationTime = ms / steps; 543 544 LinearInterpolator lerp = new LinearInterpolator(steps); 545 LinearInterpolator.Point start = new LinearInterpolator.Point(startx, starty); 546 LinearInterpolator.Point end = new LinearInterpolator.Point(endx, endy); 547 lerp.interpolate(start, end, new LinearInterpolator.Callback() { 548 @Override 549 public void step(Point point) { 550 try { 551 manager.touchMove(point.getX(), point.getY()); 552 } catch (IOException e) { 553 LOG.log(Level.SEVERE, "Error sending drag start event", e); 554 } 555 556 try { 557 Thread.sleep(iterationTime); 558 } catch (InterruptedException e) { 559 LOG.log(Level.SEVERE, "Error sleeping", e); 560 } 561 } 562 563 @Override 564 public void start(Point point) { 565 try { 566 manager.touchDown(point.getX(), point.getY()); 567 manager.touchMove(point.getX(), point.getY()); 568 } catch (IOException e) { 569 LOG.log(Level.SEVERE, "Error sending drag start event", e); 570 } 571 572 try { 573 Thread.sleep(iterationTime); 574 } catch (InterruptedException e) { 575 LOG.log(Level.SEVERE, "Error sleeping", e); 576 } 577 } 578 579 @Override 580 public void end(Point point) { 581 try { 582 manager.touchMove(point.getX(), point.getY()); 583 manager.touchUp(point.getX(), point.getY()); 584 } catch (IOException e) { 585 LOG.log(Level.SEVERE, "Error sending drag end event", e); 586 } 587 } 588 }); 589 } 590 591 592 @Override 593 public Collection<String> getViewIdList() { 594 try { 595 return manager.listViewIds(); 596 } catch(IOException e) { 597 LOG.log(Level.SEVERE, "Error retrieving view IDs", e); 598 return new ArrayList<String>(); 599 } 600 } 601 602 @Override 603 public IChimpView getView(ISelector selector) { 604 return selector.getView(manager); 605 } 606 607 @Override 608 public Collection<IChimpView> getViews(IMultiSelector selector) { 609 return selector.getViews(manager); 610 } 611 612 @Override 613 public IChimpView getRootView() { 614 try { 615 return manager.getRootView(); 616 } catch (IOException e) { 617 LOG.log(Level.SEVERE, "Error retrieving root view"); 618 return null; 619 } 620 } 621} 622