• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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