1 /* 2 * Copyright (C) 2021 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.csuite.core; 18 19 import android.service.dropbox.DropBoxManagerServiceDumpProto; 20 import android.service.dropbox.DropBoxManagerServiceDumpProto.Entry; 21 22 import com.android.tradefed.device.DeviceNotAvailableException; 23 import com.android.tradefed.device.DeviceRuntimeException; 24 import com.android.tradefed.device.ITestDevice; 25 import com.android.tradefed.log.LogUtil.CLog; 26 import com.android.tradefed.result.error.DeviceErrorIdentifier; 27 import com.android.tradefed.util.CommandResult; 28 import com.android.tradefed.util.CommandStatus; 29 import com.android.tradefed.util.IRunUtil; 30 import com.android.tradefed.util.RunUtil; 31 32 import com.google.common.annotations.VisibleForTesting; 33 import com.google.protobuf.InvalidProtocolBufferException; 34 35 import java.io.File; 36 import java.io.IOException; 37 import java.nio.file.Files; 38 import java.nio.file.Path; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Collections; 42 import java.util.Comparator; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.ListIterator; 46 import java.util.Random; 47 import java.util.Set; 48 import java.util.concurrent.TimeUnit; 49 import java.util.regex.Matcher; 50 import java.util.regex.Pattern; 51 import java.util.stream.Collectors; 52 53 /** A utility class that contains common methods to interact with the test device. */ 54 public class DeviceUtils { 55 @VisibleForTesting static final String UNKNOWN = "Unknown"; 56 @VisibleForTesting static final String VERSION_CODE_PREFIX = "versionCode="; 57 @VisibleForTesting static final String VERSION_NAME_PREFIX = "versionName="; 58 @VisibleForTesting static final String RESET_PACKAGE_COMMAND_PREFIX = "pm clear "; 59 public static final Set<String> DROPBOX_APP_CRASH_TAGS = 60 Set.of( 61 "SYSTEM_TOMBSTONE", 62 "system_app_anr", 63 "system_app_native_crash", 64 "system_app_crash", 65 "data_app_anr", 66 "data_app_native_crash", 67 "data_app_crash"); 68 69 private static final String VIDEO_PATH_ON_DEVICE_TEMPLATE = "/sdcard/screenrecord_%s.mp4"; 70 71 @VisibleForTesting 72 static final int WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS = 10 * 1000; 73 74 @VisibleForTesting static final int WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS = 500; 75 76 private final ITestDevice mDevice; 77 private final Sleeper mSleeper; 78 private final Clock mClock; 79 private final RunUtilProvider mRunUtilProvider; 80 private final TempFileSupplier mTempFileSupplier; 81 getInstance(ITestDevice device)82 public static DeviceUtils getInstance(ITestDevice device) { 83 return new DeviceUtils( 84 device, 85 duration -> { 86 Thread.sleep(duration); 87 }, 88 () -> System.currentTimeMillis(), 89 () -> RunUtil.getDefault(), 90 () -> Files.createTempFile(TestUtils.class.getName(), ".tmp")); 91 } 92 93 @VisibleForTesting DeviceUtils( ITestDevice device, Sleeper sleeper, Clock clock, RunUtilProvider runUtilProvider, TempFileSupplier tempFileSupplier)94 DeviceUtils( 95 ITestDevice device, 96 Sleeper sleeper, 97 Clock clock, 98 RunUtilProvider runUtilProvider, 99 TempFileSupplier tempFileSupplier) { 100 mDevice = device; 101 mSleeper = sleeper; 102 mClock = clock; 103 mRunUtilProvider = runUtilProvider; 104 mTempFileSupplier = tempFileSupplier; 105 } 106 107 /** 108 * A runnable that throws DeviceNotAvailableException. Use this interface instead of Runnable so 109 * that the DeviceNotAvailableException won't need to be handled inside the run() method. 110 */ 111 public interface RunnableThrowingDeviceNotAvailable { run()112 void run() throws DeviceNotAvailableException; 113 } 114 115 /** 116 * Get the current device timestamp in milliseconds. 117 * 118 * @return The device time 119 * @throws DeviceNotAvailableException When the device is not available. 120 * @throws DeviceRuntimeException When the command to get device time failed or failed to parse 121 * the timestamp. 122 */ currentTimeMillis()123 public DeviceTimestamp currentTimeMillis() 124 throws DeviceNotAvailableException, DeviceRuntimeException { 125 CommandResult result = mDevice.executeShellV2Command("echo ${EPOCHREALTIME:0:14}"); 126 if (result.getStatus() != CommandStatus.SUCCESS) { 127 throw new DeviceRuntimeException( 128 "Failed to get device time: " + result, 129 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 130 } 131 try { 132 return new DeviceTimestamp(Long.parseLong(result.getStdout().replace(".", "").trim())); 133 } catch (NumberFormatException e) { 134 CLog.e("Cannot parse device time string: " + result.getStdout()); 135 throw new DeviceRuntimeException( 136 "Cannot parse device time string: " + result.getStdout(), 137 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE); 138 } 139 } 140 141 /** 142 * Record the device screen while running a task. 143 * 144 * <p>This method will not throw exception when the screenrecord command failed unless the 145 * device is unresponsive. 146 * 147 * @param action A runnable job that throws DeviceNotAvailableException. 148 * @param handler A file handler that process the output screen record mp4 file located on the 149 * host. 150 * @throws DeviceNotAvailableException When the device is unresponsive. 151 */ runWithScreenRecording( RunnableThrowingDeviceNotAvailable action, ScreenrecordFileHandler handler)152 public void runWithScreenRecording( 153 RunnableThrowingDeviceNotAvailable action, ScreenrecordFileHandler handler) 154 throws DeviceNotAvailableException { 155 String videoPath = String.format(VIDEO_PATH_ON_DEVICE_TEMPLATE, new Random().nextInt()); 156 mDevice.deleteFile(videoPath); 157 158 // Start screen recording 159 Process recordingProcess = null; 160 try { 161 recordingProcess = 162 mRunUtilProvider 163 .get() 164 .runCmdInBackground( 165 String.format( 166 "adb -s %s shell screenrecord --time-limit 600" 167 + " %s", 168 mDevice.getSerialNumber(), videoPath) 169 .split("\\s+")); 170 } catch (IOException ioException) { 171 CLog.e("Exception is thrown when starting screen recording process: %s", ioException); 172 } 173 174 try { 175 long start = mClock.currentTimeMillis(); 176 // Wait for the recording to start since it may take time for the device to start 177 // recording 178 while (recordingProcess != null) { 179 CommandResult result = mDevice.executeShellV2Command("ls " + videoPath); 180 if (result.getStatus() == CommandStatus.SUCCESS) { 181 break; 182 } 183 184 CLog.d( 185 "Screenrecord not started yet. Waiting %s milliseconds.", 186 WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS); 187 188 try { 189 mSleeper.sleep(WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS); 190 } catch (InterruptedException e) { 191 throw new RuntimeException(e); 192 } 193 194 if (mClock.currentTimeMillis() - start 195 > WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS) { 196 CLog.e( 197 "Screenrecord did not start within %s milliseconds.", 198 WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS); 199 break; 200 } 201 } 202 203 action.run(); 204 } finally { 205 if (recordingProcess != null) { 206 mRunUtilProvider 207 .get() 208 .runTimedCmd( 209 WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS, 210 "kill", 211 "-SIGINT", 212 Long.toString(recordingProcess.pid())); 213 try { 214 recordingProcess.waitFor( 215 WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS, 216 TimeUnit.MILLISECONDS); 217 } catch (InterruptedException e) { 218 e.printStackTrace(); 219 recordingProcess.destroyForcibly(); 220 } 221 } 222 223 CommandResult result = mDevice.executeShellV2Command("ls -sh " + videoPath); 224 if (result != null && result.getStatus() == CommandStatus.SUCCESS) { 225 CLog.d("Completed screenrecord %s, video size: %s", videoPath, result.getStdout()); 226 } 227 // Try to pull, handle, and delete the video file from the device anyway. 228 handler.handleScreenRecordFile(mDevice.pullFile(videoPath)); 229 mDevice.deleteFile(videoPath); 230 } 231 } 232 233 /** A file handler for screen record results. */ 234 public interface ScreenrecordFileHandler { 235 /** 236 * Handles the screen record mp4 file located on the host. 237 * 238 * @param screenRecord The mp4 file located on the host. If screen record failed then the 239 * input could be null. 240 */ handleScreenRecordFile(File screenRecord)241 void handleScreenRecordFile(File screenRecord); 242 } 243 244 /** 245 * Freeze the screen rotation to the default orientation. 246 * 247 * @return True if succeed; False otherwise. 248 * @throws DeviceNotAvailableException 249 */ freezeRotation()250 public boolean freezeRotation() throws DeviceNotAvailableException { 251 CommandResult result = 252 mDevice.executeShellV2Command( 253 "content insert --uri content://settings/system --bind" 254 + " name:s:accelerometer_rotation --bind value:i:0"); 255 if (result.getStatus() != CommandStatus.SUCCESS || result.getExitCode() != 0) { 256 CLog.e("The command to disable auto screen rotation failed: %s", result); 257 return false; 258 } 259 260 return true; 261 } 262 263 /** 264 * Unfreeze the screen rotation to the default orientation. 265 * 266 * @return True if succeed; False otherwise. 267 * @throws DeviceNotAvailableException 268 */ unfreezeRotation()269 public boolean unfreezeRotation() throws DeviceNotAvailableException { 270 CommandResult result = 271 mDevice.executeShellV2Command( 272 "content insert --uri content://settings/system --bind" 273 + " name:s:accelerometer_rotation --bind value:i:1"); 274 if (result.getStatus() != CommandStatus.SUCCESS || result.getExitCode() != 0) { 275 CLog.e("The command to enable auto screen rotation failed: %s", result); 276 return false; 277 } 278 279 return true; 280 } 281 282 /** 283 * Launches a package on the device. 284 * 285 * @param packageName The package name to launch. 286 * @throws DeviceNotAvailableException When device was lost. 287 * @throws DeviceUtilsException When failed to launch the package. 288 */ launchPackage(String packageName)289 public void launchPackage(String packageName) 290 throws DeviceUtilsException, DeviceNotAvailableException { 291 CommandResult monkeyResult = 292 mDevice.executeShellV2Command( 293 String.format( 294 "monkey -p %s -c android.intent.category.LAUNCHER 1", packageName)); 295 if (monkeyResult.getStatus() == CommandStatus.SUCCESS) { 296 return; 297 } 298 CLog.w( 299 "Continuing to attempt using am command to launch the package %s after the monkey" 300 + " command failed: %s", 301 packageName, monkeyResult); 302 303 CommandResult pmResult = 304 mDevice.executeShellV2Command(String.format("pm dump %s", packageName)); 305 if (pmResult.getStatus() != CommandStatus.SUCCESS || pmResult.getExitCode() != 0) { 306 if (isPackageInstalled(packageName)) { 307 throw new DeviceUtilsException( 308 String.format( 309 "The command to dump package info for %s failed: %s", 310 packageName, pmResult)); 311 } else { 312 throw new DeviceUtilsException( 313 String.format("Package %s is not installed on the device.", packageName)); 314 } 315 } 316 317 String activity = getLaunchActivity(pmResult.getStdout()); 318 319 CommandResult amResult = 320 mDevice.executeShellV2Command(String.format("am start -n %s", activity)); 321 if (amResult.getStatus() != CommandStatus.SUCCESS 322 || amResult.getExitCode() != 0 323 || amResult.getStdout().contains("Error")) { 324 throw new DeviceUtilsException( 325 String.format( 326 "The command to start the package %s with activity %s failed: %s", 327 packageName, activity, amResult)); 328 } 329 } 330 331 /** 332 * Extracts the launch activity from a pm dump output. 333 * 334 * <p>This method parses the package manager dump, extracts the activities and filters them 335 * based on the categories and actions defined in the Android framework. The activities are 336 * sorted based on these attributes, and the first activity that is either the main action or a 337 * launcher category is returned. 338 * 339 * @param pmDump the pm dump output to parse. 340 * @return a activity that can be used to launch the package. 341 * @throws DeviceUtilsException if the launch activity cannot be found in the 342 * dump. @VisibleForTesting 343 */ 344 @VisibleForTesting getLaunchActivity(String pmDump)345 String getLaunchActivity(String pmDump) throws DeviceUtilsException { 346 class Activity { 347 String mName; 348 int mIndex; 349 List<String> mActions = new ArrayList<>(); 350 List<String> mCategories = new ArrayList<>(); 351 } 352 353 Pattern activityNamePattern = 354 Pattern.compile( 355 "([a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)+" 356 + "\\/([a-zA-Z][a-zA-Z0-9_]*)*(\\.[a-zA-Z][a-zA-Z0-9_]*)+)"); 357 Pattern actionPattern = 358 Pattern.compile( 359 "Action:([^a-zA-Z0-9_\\.]*)([a-zA-Z][a-zA-Z0-9_]*" 360 + "(\\.[a-zA-Z][a-zA-Z0-9_]*)+)"); 361 Pattern categoryPattern = 362 Pattern.compile( 363 "Category:([^a-zA-Z0-9_\\.]*)([a-zA-Z][a-zA-Z0-9_]*" 364 + "(\\.[a-zA-Z][a-zA-Z0-9_]*)+)"); 365 366 Matcher activityNameMatcher = activityNamePattern.matcher(pmDump); 367 368 List<Activity> activities = new ArrayList<>(); 369 while (activityNameMatcher.find()) { 370 Activity activity = new Activity(); 371 activity.mName = activityNameMatcher.group(0); 372 activity.mIndex = activityNameMatcher.start(); 373 activities.add(activity); 374 } 375 376 int endIdx = pmDump.length(); 377 ListIterator<Activity> iterator = activities.listIterator(activities.size()); 378 while (iterator.hasPrevious()) { 379 Activity activity = iterator.previous(); 380 Matcher actionMatcher = 381 actionPattern.matcher(pmDump.substring(activity.mIndex, endIdx)); 382 while (actionMatcher.find()) { 383 activity.mActions.add(actionMatcher.group(2)); 384 } 385 Matcher categoryMatcher = 386 categoryPattern.matcher(pmDump.substring(activity.mIndex, endIdx)); 387 while (categoryMatcher.find()) { 388 activity.mCategories.add(categoryMatcher.group(2)); 389 } 390 endIdx = activity.mIndex; 391 } 392 393 String categoryDefault = "android.intent.category.DEFAULT"; 394 String categoryLauncher = "android.intent.category.LAUNCHER"; 395 String actionMain = "android.intent.action.MAIN"; 396 397 class AndroidActivityComparator implements Comparator<Activity> { 398 @Override 399 public int compare(Activity a1, Activity a2) { 400 if (a1.mCategories.contains(categoryLauncher) 401 && !a2.mCategories.contains(categoryLauncher)) { 402 return -1; 403 } 404 if (!a1.mCategories.contains(categoryLauncher) 405 && a2.mCategories.contains(categoryLauncher)) { 406 return 1; 407 } 408 if (a1.mActions.contains(actionMain) && !a2.mActions.contains(actionMain)) { 409 return -1; 410 } 411 if (!a1.mActions.contains(actionMain) && a2.mActions.contains(actionMain)) { 412 return 1; 413 } 414 if (a1.mCategories.contains(categoryDefault) 415 && !a2.mCategories.contains(categoryDefault)) { 416 return -1; 417 } 418 if (!a1.mCategories.contains(categoryDefault) 419 && a2.mCategories.contains(categoryDefault)) { 420 return 1; 421 } 422 return Integer.compare(a2.mCategories.size(), a1.mCategories.size()); 423 } 424 } 425 426 Collections.sort(activities, new AndroidActivityComparator()); 427 List<Activity> filteredActivities = 428 activities.stream() 429 .filter( 430 activity -> 431 activity.mActions.contains(actionMain) 432 || activity.mCategories.contains(categoryLauncher)) 433 .collect(Collectors.toList()); 434 if (filteredActivities.isEmpty()) { 435 throw new DeviceUtilsException( 436 String.format( 437 "Cannot find an activity to launch the package. Number of activities" 438 + " parsed: %s", 439 activities.size())); 440 } 441 442 Activity res = filteredActivities.get(0); 443 444 if (!res.mCategories.contains(categoryLauncher)) { 445 CLog.d("Activity %s is not specified with a LAUNCHER category.", res.mName); 446 } 447 if (!res.mActions.contains(actionMain)) { 448 CLog.d("Activity %s is not specified with a MAIN action.", res.mName); 449 } 450 451 return res.mName; 452 } 453 454 /** 455 * Gets the version name of a package installed on the device. 456 * 457 * @param packageName The full package name to query 458 * @return The package version name, or 'Unknown' if the package doesn't exist or the adb 459 * command failed. 460 * @throws DeviceNotAvailableException 461 */ getPackageVersionName(String packageName)462 public String getPackageVersionName(String packageName) throws DeviceNotAvailableException { 463 CommandResult cmdResult = 464 mDevice.executeShellV2Command( 465 String.format("dumpsys package %s | grep versionName", packageName)); 466 467 if (cmdResult.getStatus() != CommandStatus.SUCCESS 468 || !cmdResult.getStdout().trim().startsWith(VERSION_NAME_PREFIX)) { 469 return UNKNOWN; 470 } 471 472 return cmdResult.getStdout().trim().substring(VERSION_NAME_PREFIX.length()); 473 } 474 475 /** 476 * Gets the version code of a package installed on the device. 477 * 478 * @param packageName The full package name to query 479 * @return The package version code, or 'Unknown' if the package doesn't exist or the adb 480 * command failed. 481 * @throws DeviceNotAvailableException 482 */ getPackageVersionCode(String packageName)483 public String getPackageVersionCode(String packageName) throws DeviceNotAvailableException { 484 CommandResult cmdResult = 485 mDevice.executeShellV2Command( 486 String.format("dumpsys package %s | grep versionCode", packageName)); 487 488 if (cmdResult.getStatus() != CommandStatus.SUCCESS 489 || !cmdResult.getStdout().trim().startsWith(VERSION_CODE_PREFIX)) { 490 return UNKNOWN; 491 } 492 493 return cmdResult.getStdout().trim().split(" ")[0].substring(VERSION_CODE_PREFIX.length()); 494 } 495 496 /** 497 * Stops a running package on the device. 498 * 499 * @param packageName 500 * @throws DeviceNotAvailableException 501 */ stopPackage(String packageName)502 public void stopPackage(String packageName) throws DeviceNotAvailableException { 503 mDevice.executeShellV2Command("am force-stop " + packageName); 504 } 505 506 /** 507 * Resets a package's data storage on the device. 508 * 509 * @param packageName The package name of an app to reset. 510 * @return True if the package exists and its data was reset; False otherwise. 511 * @throws DeviceNotAvailableException If the device was lost. 512 */ resetPackage(String packageName)513 public boolean resetPackage(String packageName) throws DeviceNotAvailableException { 514 return mDevice.executeShellV2Command(RESET_PACKAGE_COMMAND_PREFIX + packageName).getStatus() 515 == CommandStatus.SUCCESS; 516 } 517 518 /** 519 * Checks whether a package is installed on the device. 520 * 521 * @param packageName The name of the package to check 522 * @return True if the package is installed on the device; false otherwise. 523 * @throws DeviceUtilsException If the adb shell command failed. 524 * @throws DeviceNotAvailableException If the device was lost. 525 */ isPackageInstalled(String packageName)526 public boolean isPackageInstalled(String packageName) 527 throws DeviceUtilsException, DeviceNotAvailableException { 528 CommandResult commandResult = 529 executeShellCommandOrThrow( 530 String.format("pm list packages %s", packageName), 531 "Failed to execute pm command"); 532 533 if (commandResult.getStdout() == null) { 534 throw new DeviceUtilsException( 535 String.format( 536 "Failed to get pm command output: %s", commandResult.getStdout())); 537 } 538 539 return Arrays.asList(commandResult.getStdout().split("\\r?\\n")) 540 .contains(String.format("package:%s", packageName)); 541 } 542 executeShellCommandOrThrow(String command, String failureMessage)543 private CommandResult executeShellCommandOrThrow(String command, String failureMessage) 544 throws DeviceUtilsException, DeviceNotAvailableException { 545 CommandResult commandResult = mDevice.executeShellV2Command(command); 546 547 if (commandResult.getStatus() != CommandStatus.SUCCESS) { 548 throw new DeviceUtilsException( 549 String.format("%s; Command result: %s", failureMessage, commandResult)); 550 } 551 552 return commandResult; 553 } 554 555 /** 556 * Gets dropbox entries from the device filtered by the provided tags. 557 * 558 * @param tags Dropbox tags to query. 559 * @return A list of dropbox entries. 560 * @throws IOException when failed to dump or read the dropbox protos. 561 */ getDropboxEntries(Set<String> tags)562 public List<DropboxEntry> getDropboxEntries(Set<String> tags) throws IOException { 563 List<DropboxEntry> entries = new ArrayList<>(); 564 565 for (String tag : tags) { 566 Path dumpFile = mTempFileSupplier.get(); 567 568 CommandResult res = 569 mRunUtilProvider 570 .get() 571 .runTimedCmd( 572 6000, 573 "sh", 574 "-c", 575 String.format( 576 "adb -s %s shell dumpsys dropbox --proto %s > %s", 577 mDevice.getSerialNumber(), tag, dumpFile)); 578 579 if (res.getStatus() != CommandStatus.SUCCESS) { 580 throw new IOException("Dropbox dump command failed: " + res); 581 } 582 583 DropBoxManagerServiceDumpProto proto; 584 try { 585 proto = DropBoxManagerServiceDumpProto.parseFrom(Files.readAllBytes(dumpFile)); 586 } catch (InvalidProtocolBufferException e) { 587 // If dumping proto format is not supported such as in Android 10, the command will 588 // still succeed with exit code 0 and output strings instead of protobuf bytes, 589 // causing parse error. In this case we fallback to dumping dropbox --print option. 590 return getDropboxEntriesFromStdout(tags); 591 } 592 Files.delete(dumpFile); 593 594 for (Entry entry : proto.getEntriesList()) { 595 entries.add( 596 new DropboxEntry(entry.getTimeMs(), tag, entry.getData().toStringUtf8())); 597 } 598 } 599 600 return entries; 601 } 602 603 @VisibleForTesting getDropboxEntriesFromStdout(Set<String> tags)604 List<DropboxEntry> getDropboxEntriesFromStdout(Set<String> tags) throws IOException { 605 HashMap<String, DropboxEntry> entries = new HashMap<>(); 606 607 // The first step is to read the entry names and timestamps from the --file dump option 608 // output because the --print dump option does not contain timestamps. 609 CommandResult res; 610 Path fileDumpFile = mTempFileSupplier.get(); 611 res = 612 mRunUtilProvider 613 .get() 614 .runTimedCmd( 615 6000, 616 "sh", 617 "-c", 618 String.format( 619 "adb -s %s shell dumpsys dropbox --file > %s", 620 mDevice.getSerialNumber(), fileDumpFile)); 621 if (res.getStatus() != CommandStatus.SUCCESS) { 622 throw new IOException("Dropbox dump command failed: " + res); 623 } 624 625 String lastEntryName = null; 626 for (String line : Files.readAllLines(fileDumpFile)) { 627 if (DropboxEntry.isDropboxEntryName(line)) { 628 lastEntryName = line.trim(); 629 entries.put(lastEntryName, DropboxEntry.fromEntryName(line)); 630 } else if (DropboxEntry.isDropboxFilePath(line) && lastEntryName != null) { 631 entries.get(lastEntryName).parseTimeFromFilePath(line); 632 } 633 } 634 Files.delete(fileDumpFile); 635 636 // Then we get the entry data from the --print dump output. Entry names parsed from the 637 // --print dump output are verified against the entry names from the --file dump output to 638 // ensure correctness. 639 Path printDumpFile = mTempFileSupplier.get(); 640 res = 641 mRunUtilProvider 642 .get() 643 .runTimedCmd( 644 6000, 645 "sh", 646 "-c", 647 String.format( 648 "adb -s %s shell dumpsys dropbox --print > %s", 649 mDevice.getSerialNumber(), printDumpFile)); 650 if (res.getStatus() != CommandStatus.SUCCESS) { 651 throw new IOException("Dropbox dump command failed: " + res); 652 } 653 654 lastEntryName = null; 655 for (String line : Files.readAllLines(printDumpFile)) { 656 if (DropboxEntry.isDropboxEntryName(line)) { 657 lastEntryName = line.trim(); 658 } 659 660 if (lastEntryName != null && entries.containsKey(lastEntryName)) { 661 entries.get(lastEntryName).addData(line); 662 entries.get(lastEntryName).addData("\n"); 663 } 664 } 665 Files.delete(printDumpFile); 666 667 return entries.values().stream() 668 .filter(entry -> tags.contains(entry.getTag())) 669 .collect(Collectors.toList()); 670 } 671 672 /** A class that stores the information of a dropbox entry. */ 673 public static final class DropboxEntry { 674 private long mTime; 675 private String mTag; 676 private final StringBuilder mData = new StringBuilder(); 677 private static final Pattern ENTRY_NAME_PATTERN = 678 Pattern.compile( 679 "\\d{4}\\-\\d{2}\\-\\d{2} \\d{2}:\\d{2}:\\d{2} .+ \\(.+, [0-9]+ .+\\)"); 680 private static final Pattern DATE_PATTERN = 681 Pattern.compile("\\d{4}\\-\\d{2}\\-\\d{2} \\d{2}:\\d{2}:\\d{2}"); 682 private static final Pattern FILE_NAME_PATTERN = Pattern.compile(" +/.+@[0-9]+\\..+"); 683 684 /** Returns the entrt's time stamp on device. */ getTime()685 public long getTime() { 686 return mTime; 687 } 688 addData(String data)689 private void addData(String data) { 690 mData.append(data); 691 } 692 parseTimeFromFilePath(String input)693 private void parseTimeFromFilePath(String input) { 694 mTime = Long.parseLong(input.substring(input.indexOf('@') + 1, input.indexOf('.'))); 695 } 696 697 /** Returns the entrt's tag. */ getTag()698 public String getTag() { 699 return mTag; 700 } 701 702 /** Returns the entrt's data. */ getData()703 public String getData() { 704 return mData.toString(); 705 } 706 707 @VisibleForTesting DropboxEntry(long time, String tag, String data)708 DropboxEntry(long time, String tag, String data) { 709 mTime = time; 710 mTag = tag; 711 addData(data); 712 } 713 DropboxEntry()714 private DropboxEntry() { 715 // Intentionally left blank; 716 } 717 fromEntryName(String name)718 private static DropboxEntry fromEntryName(String name) { 719 DropboxEntry entry = new DropboxEntry(); 720 Matcher matcher = DATE_PATTERN.matcher(name); 721 if (!matcher.find()) { 722 throw new RuntimeException("Unexpected entry name: " + name); 723 } 724 entry.mTag = name.trim().substring(matcher.group().length()).trim().split(" ")[0]; 725 return entry; 726 } 727 isDropboxEntryName(String input)728 private static boolean isDropboxEntryName(String input) { 729 return ENTRY_NAME_PATTERN.matcher(input).find(); 730 } 731 isDropboxFilePath(String input)732 private static boolean isDropboxFilePath(String input) { 733 return FILE_NAME_PATTERN.matcher(input).find(); 734 } 735 } 736 737 /** A general exception class representing failed device utility operations. */ 738 public static final class DeviceUtilsException extends Exception { 739 /** 740 * Constructs a new {@link DeviceUtilsException} with a meaningful error message. 741 * 742 * @param message A error message describing the cause of the error. 743 */ DeviceUtilsException(String message)744 private DeviceUtilsException(String message) { 745 super(message); 746 } 747 748 /** 749 * Constructs a new {@link DeviceUtilsException} with a meaningful error message, and a 750 * cause. 751 * 752 * @param message A detailed error message. 753 * @param cause A {@link Throwable} capturing the original cause of the {@link 754 * DeviceUtilsException}. 755 */ DeviceUtilsException(String message, Throwable cause)756 private DeviceUtilsException(String message, Throwable cause) { 757 super(message, cause); 758 } 759 760 /** 761 * Constructs a new {@link DeviceUtilsException} with a cause. 762 * 763 * @param cause A {@link Throwable} capturing the original cause of the {@link 764 * DeviceUtilsException}. 765 */ DeviceUtilsException(Throwable cause)766 private DeviceUtilsException(Throwable cause) { 767 super(cause); 768 } 769 } 770 771 /** 772 * A class to contain a device timestamp. 773 * 774 * <p>Use this class instead of long to pass device timestamps so that they are less likely to 775 * be confused with host timestamps. 776 */ 777 public static class DeviceTimestamp { 778 private final long mTimestamp; 779 DeviceTimestamp(long timestamp)780 public DeviceTimestamp(long timestamp) { 781 mTimestamp = timestamp; 782 } 783 784 /** Gets the timestamp on a device. */ get()785 public long get() { 786 return mTimestamp; 787 } 788 } 789 790 @VisibleForTesting 791 interface Sleeper { sleep(long milliseconds)792 void sleep(long milliseconds) throws InterruptedException; 793 } 794 795 @VisibleForTesting 796 interface Clock { currentTimeMillis()797 long currentTimeMillis(); 798 } 799 800 @VisibleForTesting 801 interface RunUtilProvider { get()802 IRunUtil get(); 803 } 804 805 @VisibleForTesting 806 interface TempFileSupplier { get()807 Path get() throws IOException; 808 } 809 } 810