1 /* 2 * Copyright (C) 2016 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 android.support.test.aupt; 18 19 import android.app.Service; 20 import android.content.Context; 21 import android.content.ContextWrapper; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.os.Bundle; 25 import android.os.Environment; 26 import android.os.IBinder; 27 import android.os.SystemClock; 28 import android.support.test.uiautomator.UiDevice; 29 import android.test.AndroidTestRunner; 30 import android.test.InstrumentationTestCase; 31 import android.test.InstrumentationTestRunner; 32 import android.util.Log; 33 34 import androidx.test.InstrumentationRegistry; 35 36 import junit.framework.AssertionFailedError; 37 import junit.framework.Test; 38 import junit.framework.TestCase; 39 import junit.framework.TestListener; 40 import junit.framework.TestResult; 41 import junit.framework.TestSuite; 42 43 import java.io.BufferedReader; 44 import java.io.File; 45 import java.io.FileInputStream; 46 import java.io.IOException; 47 import java.io.InputStreamReader; 48 import java.text.SimpleDateFormat; 49 import java.util.ArrayList; 50 import java.util.Arrays; 51 import java.util.Comparator; 52 import java.util.Date; 53 import java.util.HashMap; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.Random; 57 import java.util.concurrent.TimeUnit; 58 import java.util.concurrent.TimeoutException; 59 60 /** 61 * Ultra-fancy TestRunner to use when running AUPT: supports 62 * 63 * - Picking up tests from dexed JARs 64 * - Running tests for multiple iterations or in a custom order 65 * - Terminating tests after UI errors, timeouts, or when dependent processes die 66 * - Injecting additional information into custom TestCase subclasses 67 * - Passing through continuous metric-collection to a DataCollector instance 68 * - Collecting bugreports and heapdumps 69 * 70 */ 71 public class AuptTestRunner extends InstrumentationTestRunner { 72 /* Constants */ 73 private static final String LOG_TAG = AuptTestRunner.class.getSimpleName(); 74 private static final Long ANR_DELAY = 30000L; 75 private static final Long DEFAULT_SUITE_TIMEOUT = 0L; 76 private static final Long DEFAULT_TEST_TIMEOUT = 10L; 77 private static final SimpleDateFormat SCREENSHOT_DATE_FORMAT = 78 new SimpleDateFormat("dd-mm-yy:HH:mm:ss:SSS"); 79 80 /* Keep a pointer to our argument bundle around for testing */ 81 private Bundle mParams; 82 83 /* Primitive Parameters */ 84 private boolean mDeleteOldFiles; 85 private long mFileRetainCount; 86 private boolean mGenerateAnr; 87 private boolean mRecordMeminfo; 88 private long mIterations; 89 private long mSeed; 90 91 /* Dumpheap Parameters */ 92 private boolean mDumpheapEnabled; 93 private long mDumpheapInterval; 94 private long mDumpheapThreshold; 95 private long mMaxDumpheaps; 96 97 /* String Parameters */ 98 private List<String> mJars = new ArrayList<>(); 99 private List<String> mMemoryTrackedProcesses = new ArrayList<>(); 100 private List<String> mFinishCommands; 101 102 /* Other Parameters */ 103 private File mResultsDirectory; 104 105 /* Helpers */ 106 private Scheduler mScheduler; 107 private DataCollector mDataCollector; 108 private DexTestRunner mRunner; 109 110 /* Logging */ 111 private ProcessStatusTracker mProcessTracker; 112 private List<MemHealthRecord> mMemHealthRecords = new ArrayList<>(); 113 private Map<String, Long> mDumpheapCount = new HashMap<>(); 114 private Map<String, Long> mLastDumpheap = new HashMap<>(); 115 116 /* Test Initialization */ 117 @Override onCreate(Bundle params)118 public void onCreate(Bundle params) { 119 // Support the newer AndroidX test initialization. 120 InstrumentationRegistry.registerInstance(this, params); 121 122 mParams = params; 123 124 // Parse out primitive parameters 125 mIterations = parseLongParam("iterations", 1); 126 mRecordMeminfo = parseBoolParam("record_meminfo", false); 127 mDumpheapEnabled = parseBoolParam("enableDumpheap", false); 128 mDumpheapThreshold = parseLongParam("dumpheapThreshold", 200 * 1024 * 1024); 129 mDumpheapInterval = parseLongParam("dumpheapInterval", 60 * 60 * 1000); 130 mMaxDumpheaps = parseLongParam("maxDumpheaps", 5); 131 mSeed = parseLongParam("seed", new Random().nextLong()); 132 133 // Option: -e finishCommand 'a;b;c;d' 134 String finishCommandArg = parseStringParam("finishCommand", null); 135 mFinishCommands = 136 finishCommandArg == null 137 ? Arrays.<String>asList() 138 : Arrays.asList(finishCommandArg.split("\\s*;\\s*")); 139 140 // Option: -e shuffle true 141 mScheduler = parseBoolParam("shuffle", false) 142 ? Scheduler.shuffled(new Random(mSeed), mIterations) 143 : Scheduler.sequential(mIterations); 144 145 // Option: -e jars aupt-app-tests.jar:... 146 mJars.addAll(DexTestRunner.parseDexedJarPaths(parseStringParam("jars", ""))); 147 148 // Option: -e trackMemory com.pkg1,com.pkg2,... 149 String memoryTrackedProcesses = parseStringParam("trackMemory", null); 150 151 if (memoryTrackedProcesses != null) { 152 mMemoryTrackedProcesses = Arrays.asList(memoryTrackedProcesses.split(",")); 153 } else { 154 try { 155 // Deprecated approach: get tracked processes from a file. 156 String trackMemoryFileName = 157 Environment.getExternalStorageDirectory() + "/track_memory.txt"; 158 159 BufferedReader reader = new BufferedReader(new InputStreamReader( 160 new FileInputStream(new File(trackMemoryFileName)))); 161 162 mMemoryTrackedProcesses = Arrays.asList(reader.readLine().split(",")); 163 reader.close(); 164 } catch (NullPointerException | IOException ex) { 165 mMemoryTrackedProcesses = Arrays.asList(); 166 } 167 } 168 169 // Option: -e detectKill com.pkg1,...,com.pkg8 170 String processes = parseStringParam("detectKill", null); 171 172 if (processes != null) { 173 mProcessTracker = new ProcessStatusTracker(processes.split(",")); 174 } else { 175 mProcessTracker = new ProcessStatusTracker(new String[] {}); 176 } 177 178 // Option: -e outputLocation aupt_results 179 mResultsDirectory = new File(Environment.getExternalStorageDirectory(), 180 parseStringParam("outputLocation", "aupt_results")); 181 if (!mResultsDirectory.exists() && !mResultsDirectory.mkdirs()) { 182 Log.w(LOG_TAG, "Could not find or create output directory " + mResultsDirectory); 183 } 184 185 // Option: -e fileRetainCount 1 186 mFileRetainCount = parseLongParam("fileRetainCount", -1); 187 mDeleteOldFiles = (mFileRetainCount != -1); 188 189 // Primary logging infrastructure 190 mDataCollector = new DataCollector( 191 TimeUnit.MINUTES.toMillis(parseLongParam("bugreportInterval", 0)), 192 TimeUnit.MINUTES.toMillis(parseLongParam("jankInterval", 0)), 193 TimeUnit.MINUTES.toMillis(parseLongParam("meminfoInterval", 0)), 194 TimeUnit.MINUTES.toMillis(parseLongParam("cpuinfoInterval", 0)), 195 TimeUnit.MINUTES.toMillis(parseLongParam("fragmentationInterval", 0)), 196 TimeUnit.MINUTES.toMillis(parseLongParam("ionInterval", 0)), 197 TimeUnit.MINUTES.toMillis(parseLongParam("pagetypeinfoInterval", 0)), 198 TimeUnit.MINUTES.toMillis(parseLongParam("traceInterval", 0)), 199 TimeUnit.MINUTES.toMillis(parseLongParam("bugreportzInterval", 0)), 200 mResultsDirectory, this); 201 202 // Make our TestRunner and make sure we injectInstrumentation. 203 mRunner = new DexTestRunner(this, mScheduler, mJars, 204 TimeUnit.MINUTES.toMillis(parseLongParam("testCaseTimeout", DEFAULT_TEST_TIMEOUT)), 205 TimeUnit.MINUTES.toMillis(parseLongParam("suiteTimeout", DEFAULT_SUITE_TIMEOUT))) { 206 @Override 207 public void runTest(TestResult result) { 208 for (TestCase test: mTestCases) { 209 injectInstrumentation(test); 210 } 211 212 try { 213 super.runTest(result); 214 } finally { 215 mDataCollector.stop(); 216 } 217 } 218 }; 219 220 // Aupt's TestListeners 221 mRunner.addTestListener(new PeriodicHeapDumper()); 222 mRunner.addTestListener(new MemHealthRecorder()); 223 mRunner.addTestListener(new DcimCleaner()); 224 mRunner.addTestListener(new PidChecker()); 225 mRunner.addTestListener(new TimeoutStackDumper()); 226 mRunner.addTestListener(new MemInfoDumper()); 227 mRunner.addTestListener(new FinishCommandRunner()); 228 mRunner.addTestListenerIf(parseBoolParam("generateANR", false), new ANRTrigger()); 229 mRunner.addTestListenerIf(parseBoolParam("quitOnError", false), new QuitOnErrorListener()); 230 mRunner.addTestListenerIf(parseBoolParam("checkBattery", false), new BatteryChecker()); 231 mRunner.addTestListenerIf(parseBoolParam("screenshots", false), new Screenshotter()); 232 233 // Start our loggers 234 mDataCollector.start(); 235 236 // Start the test 237 super.onCreate(params); 238 } 239 240 /* Option-parsing helpers */ 241 parseLongParam(String key, long alternative)242 private long parseLongParam(String key, long alternative) throws NumberFormatException { 243 if (mParams.containsKey(key)) { 244 return Long.parseLong(mParams.getString(key)); 245 } else { 246 return alternative; 247 } 248 } 249 parseBoolParam(String key, boolean alternative)250 private boolean parseBoolParam(String key, boolean alternative) 251 throws NumberFormatException { 252 if (mParams.containsKey(key)) { 253 return Boolean.parseBoolean(mParams.getString(key)); 254 } else { 255 return alternative; 256 } 257 } 258 parseStringParam(String key, String alternative)259 private String parseStringParam(String key, String alternative) { 260 if (mParams.containsKey(key)) { 261 return mParams.getString(key); 262 } else { 263 return alternative; 264 } 265 } 266 267 /* Utility methods */ 268 269 /** 270 * Injects instrumentation into InstrumentationTestCase and AuptTestCase instances 271 */ injectInstrumentation(Test test)272 private void injectInstrumentation(Test test) { 273 if (InstrumentationTestCase.class.isAssignableFrom(test.getClass())) { 274 InstrumentationTestCase instrTest = (InstrumentationTestCase) test; 275 276 instrTest.injectInstrumentation(AuptTestRunner.this); 277 } 278 } 279 280 /* Passthrough to our DexTestRunner */ 281 @Override getAndroidTestRunner()282 protected AndroidTestRunner getAndroidTestRunner() { 283 return mRunner; 284 } 285 286 @Override getTargetContext()287 public Context getTargetContext() { 288 return new ContextWrapper(super.getTargetContext()) { 289 @Override 290 public ClassLoader getClassLoader() { 291 if(mRunner != null) { 292 return mRunner.getDexClassLoader(); 293 } else { 294 throw new RuntimeException("DexTestRunner not initialized!"); 295 } 296 } 297 }; 298 } 299 300 /** 301 * A simple abstract instantiation of TestListener 302 * 303 * Primarily meant to work around Java 7's lack of interface-default methods. 304 */ 305 abstract static class AuptListener implements TestListener { 306 /** Called when a test throws an exception. */ 307 public void addError(Test test, Throwable t) {} 308 309 /** Called when a test fails. */ 310 public void addFailure(Test test, AssertionFailedError t) {} 311 312 /** Called whenever a test ends. */ 313 public void endTest(Test test) {} 314 315 /** Called whenever a test begins. */ 316 public void startTest(Test test) {} 317 } 318 319 /** 320 * Periodically Heap-dump to assist with memory-leaks. 321 */ 322 private class PeriodicHeapDumper extends AuptListener { 323 private Thread mHeapDumpThread; 324 325 private class InternalHeapDumper implements Runnable { 326 private void recordDumpheap(String proc, long pss) throws IOException { 327 if (!mDumpheapEnabled) { 328 return; 329 } 330 Long count = mDumpheapCount.get(proc); 331 if (count == null) { 332 count = 0L; 333 } 334 Long lastDumpheap = mLastDumpheap.get(proc); 335 if (lastDumpheap == null) { 336 lastDumpheap = 0L; 337 } 338 long currentTime = SystemClock.uptimeMillis(); 339 if (pss > mDumpheapThreshold && count < mMaxDumpheaps && 340 currentTime - lastDumpheap > mDumpheapInterval) { 341 recordDumpheap(proc); 342 mDumpheapCount.put(proc, count + 1); 343 mLastDumpheap.put(proc, currentTime); 344 } 345 } 346 347 private void recordDumpheap(String proc) throws IOException { 348 long count = mDumpheapCount.get(proc); 349 350 String filename = String.format("dumpheap-%s-%d", proc, count); 351 String tempFilename = "/data/local/tmp/" + filename; 352 String finalFilename = mResultsDirectory + "/" + filename; 353 354 AuptTestRunner.this.getUiAutomation().executeShellCommand( 355 String.format("am dumpheap %s %s", proc, tempFilename)); 356 357 SystemClock.sleep(3000); 358 359 AuptTestRunner.this.getUiAutomation().executeShellCommand( 360 String.format("cp %s %s", tempFilename, finalFilename)); 361 } 362 363 public void run() { 364 try { 365 while (true) { 366 Thread.sleep(mDumpheapInterval); 367 368 for(String proc : mMemoryTrackedProcesses) { 369 recordDumpheap(proc); 370 } 371 } 372 } catch (InterruptedException iex) { 373 } catch (IOException ioex) { 374 Log.e(LOG_TAG, "Failed to write heap dump!", ioex); 375 } 376 } 377 } 378 379 @Override 380 public void startTest(Test test) { 381 mHeapDumpThread = new Thread(new InternalHeapDumper()); 382 mHeapDumpThread.start(); 383 } 384 385 @Override 386 public void endTest(Test test) { 387 try { 388 mHeapDumpThread.interrupt(); 389 mHeapDumpThread.join(); 390 } catch (InterruptedException iex) { } 391 } 392 } 393 394 /** 395 * Dump memory info on test start/stop 396 */ 397 private class MemInfoDumper extends AuptListener { 398 private void dumpMemInfo() { 399 if (mRecordMeminfo) { 400 FilesystemUtil.dumpMeminfo(AuptTestRunner.this, "MemInfoDumper"); 401 } 402 } 403 404 @Override 405 public void startTest(Test test) { 406 dumpMemInfo(); 407 } 408 409 @Override 410 public void endTest(Test test) { 411 dumpMemInfo(); 412 } 413 } 414 415 /** 416 * Record all of our MemHealthRecords 417 */ 418 private class MemHealthRecorder extends AuptListener { 419 @Override 420 public void startTest(Test test) { 421 recordMemHealth(); 422 } 423 424 @Override 425 public void endTest(Test test) { 426 recordMemHealth(); 427 428 try { 429 MemHealthRecord.saveVerbose(mMemHealthRecords, 430 new File(mResultsDirectory, "memory-health.txt").getPath()); 431 MemHealthRecord.saveCsv(mMemHealthRecords, 432 new File(mResultsDirectory, "memory-health-details.txt").getPath()); 433 434 mMemHealthRecords.clear(); 435 } catch (IOException ioex) { 436 Log.e(LOG_TAG, "Error writing MemHealthRecords", ioex); 437 } 438 } 439 440 private void recordMemHealth() { 441 try { 442 mMemHealthRecords.addAll(MemHealthRecord.get( 443 AuptTestRunner.this, 444 mMemoryTrackedProcesses, 445 System.currentTimeMillis(), 446 getForegroundProcs())); 447 } catch (IOException ioex) { 448 Log.e(LOG_TAG, "Error collecting MemHealthRecords", ioex); 449 } 450 } 451 452 private List<String> getForegroundProcs() { 453 List<String> foregroundProcs = new ArrayList<String>(); 454 try { 455 String compactMeminfo = MemHealthRecord.getProcessOutput(AuptTestRunner.this, 456 "dumpsys meminfo -c"); 457 458 for (String line : compactMeminfo.split("\\r?\\n")) { 459 if (line.contains("proc,fore")) { 460 String proc = line.split(",")[2]; 461 foregroundProcs.add(proc); 462 } 463 } 464 } catch (IOException e) { 465 Log.e(LOG_TAG, "Error while getting foreground process", e); 466 } finally { 467 return foregroundProcs; 468 } 469 } 470 } 471 472 /** 473 * Kills application and dumps UI Hierarchy on test error 474 */ 475 private class QuitOnErrorListener extends AuptListener { 476 @Override 477 public void addError(Test test, Throwable t) { 478 Log.e(LOG_TAG, "Caught exception from a test", t); 479 480 if ((t instanceof AuptTerminator)) { 481 throw (AuptTerminator)t; 482 } else { 483 484 // Check if our exception is caused by process dependency 485 if (test instanceof AuptTestCase) { 486 mProcessTracker.setUiAutomation(getUiAutomation()); 487 mProcessTracker.verifyRunningProcess(); 488 } 489 490 // If that didn't throw, then dump our hierarchy 491 Log.v(LOG_TAG, "Dumping UI hierarchy"); 492 try { 493 UiDevice.getInstance(AuptTestRunner.this).dumpWindowHierarchy( 494 new File("/data/local/tmp/error_dump.xml")); 495 } catch (IOException e) { 496 Log.w(LOG_TAG, "Failed to create UI hierarchy dump for UI error", e); 497 } 498 } 499 500 // Quit on an error 501 throw new AuptTerminator(t.getMessage(), t); 502 } 503 504 @Override 505 public void addFailure(Test test, AssertionFailedError t) { 506 // Quit on an error 507 throw new AuptTerminator(t.getMessage(), t); 508 } 509 } 510 511 /** 512 * Makes sure the processes this test requires are all alive 513 */ 514 private class PidChecker extends AuptListener { 515 @Override 516 public void startTest(Test test) { 517 mProcessTracker.setUiAutomation(getUiAutomation()); 518 mProcessTracker.verifyRunningProcess(); 519 } 520 521 @Override 522 public void endTest(Test test) { 523 mProcessTracker.verifyRunningProcess(); 524 } 525 } 526 527 /** 528 * Initialization for tests that touch the camera 529 */ 530 private class DcimCleaner extends AuptListener { 531 @Override 532 public void startTest(Test test) { 533 if (!mDeleteOldFiles) { 534 return; 535 } 536 537 File dcimFolder = new File(Environment.getExternalStorageDirectory(), "DCIM"); 538 File cameraFolder = new File(dcimFolder, "Camera"); 539 540 if (dcimFolder.exists()) { 541 if (cameraFolder.exists()) { 542 File[] allMediaFiles = cameraFolder.listFiles(); 543 Arrays.sort(allMediaFiles, new Comparator<File>() { 544 public int compare(File f1, File f2) { 545 return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified()); 546 } 547 }); 548 for (int i = 0; i < allMediaFiles.length - mFileRetainCount; i++) { 549 allMediaFiles[i].delete(); 550 } 551 } else { 552 Log.w(LOG_TAG, "No Camera folder found to delete from."); 553 } 554 } else { 555 Log.w(LOG_TAG, "No DCIM folder found to delete from."); 556 } 557 } 558 } 559 560 /** 561 * Makes sure the battery hasn't died before and after each test. 562 */ 563 private class BatteryChecker extends AuptListener { 564 private static final double BATTERY_THRESHOLD = 0.05; 565 566 private void checkBattery() { 567 Intent batteryIntent = getContext().registerReceiver(null, 568 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 569 int rawLevel = batteryIntent.getIntExtra("level", -1); 570 int scale = batteryIntent.getIntExtra("scale", -1); 571 572 if (rawLevel < 0 || scale <= 0) { 573 return; 574 } 575 576 double level = (double) rawLevel / (double) scale; 577 if (level < BATTERY_THRESHOLD) { 578 throw new AuptTerminator(String.format("Current battery level %f lower than %f", 579 level, 580 BATTERY_THRESHOLD)); 581 } 582 } 583 584 @Override 585 public void startTest(Test test) { 586 checkBattery(); 587 } 588 } 589 590 /** 591 * Generates heap dumps when a test times out 592 */ 593 private class TimeoutStackDumper extends AuptListener { 594 private String getStackTraces() { 595 StringBuilder sb = new StringBuilder(); 596 Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces(); 597 for (Thread t : stacks.keySet()) { 598 sb.append(t.toString()).append('\n'); 599 for (StackTraceElement ste : t.getStackTrace()) { 600 sb.append("\tat ").append(ste.toString()).append('\n'); 601 } 602 sb.append('\n'); 603 } 604 return sb.toString(); 605 } 606 607 @Override 608 public void addError(Test test, Throwable t) { 609 if (t instanceof TimeoutException) { 610 Log.d("THREAD_DUMP", getStackTraces()); 611 } 612 } 613 } 614 615 /** Generates ANRs when a test takes too long. */ 616 private class ANRTrigger extends AuptListener { 617 @Override 618 public void addError(Test test, Throwable t) { 619 if (t instanceof TimeoutException) { 620 Context ctx = getTargetContext(); 621 Log.d(LOG_TAG, "About to induce artificial ANR for debugging"); 622 ctx.startService(new Intent(ctx, AnrGenerator.class)); 623 624 try { 625 Thread.sleep(ANR_DELAY); 626 } catch (InterruptedException e) { 627 throw new RuntimeException("Interrupted while waiting for AnrGenerator..."); 628 } 629 } 630 } 631 632 /** Service that hangs to trigger an ANR. */ 633 private class AnrGenerator extends Service { 634 @Override 635 public IBinder onBind(Intent intent) { 636 return null; 637 } 638 639 @Override 640 public int onStartCommand(Intent intent, int flags, int id) { 641 Log.i(LOG_TAG, "in service start -- about to hang"); 642 try { 643 Thread.sleep(ANR_DELAY); 644 } catch (InterruptedException e) { 645 Log.wtf(LOG_TAG, e); 646 } 647 Log.i(LOG_TAG, "service hang finished -- stopping and returning"); 648 stopSelf(); 649 return START_NOT_STICKY; 650 } 651 } 652 } 653 654 /** 655 * Collect a screenshot on test failure. 656 */ 657 private class Screenshotter extends AuptListener { 658 private void collectScreenshot(Test test, String suffix) { 659 UiDevice device = UiDevice.getInstance(AuptTestRunner.this); 660 661 if (device == null) { 662 Log.w(LOG_TAG, "Couldn't collect screenshot on test failure"); 663 return; 664 } 665 666 String testName = 667 test instanceof TestCase 668 ? ((TestCase) test).getName() 669 : (test instanceof TestSuite ? ((TestSuite) test).getName() : test.toString()); 670 671 String fileName = 672 mResultsDirectory.getPath() 673 + "/" + testName.replace('.', '_') 674 + suffix + ".png"; 675 676 device.takeScreenshot(new File(fileName)); 677 } 678 679 @Override 680 public void addError(Test test, Throwable t) { 681 collectScreenshot(test, 682 "_failure_screenshot_" + SCREENSHOT_DATE_FORMAT.format(new Date())); 683 } 684 685 @Override 686 public void addFailure(Test test, AssertionFailedError t) { 687 collectScreenshot(test, 688 "_failure_screenshot_" + SCREENSHOT_DATE_FORMAT.format(new Date())); 689 } 690 } 691 692 /** Runs a command when a test finishes. */ 693 private class FinishCommandRunner extends AuptListener { 694 @Override 695 public void endTest(Test test) { 696 for (String command : mFinishCommands) { 697 AuptTestRunner.this.getUiAutomation().executeShellCommand(command); 698 } 699 } 700 } 701 } 702