• 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 
34 import java.io.File;
35 import java.io.IOException;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.Random;
41 import java.util.Set;
42 
43 /** A utility class that contains common methods to interact with the test device. */
44 public class DeviceUtils {
45     @VisibleForTesting static final String UNKNOWN = "Unknown";
46     @VisibleForTesting static final String VERSION_CODE_PREFIX = "versionCode=";
47     @VisibleForTesting static final String VERSION_NAME_PREFIX = "versionName=";
48     @VisibleForTesting static final String RESET_PACKAGE_COMMAND_PREFIX = "pm clear ";
49     public static final Set<String> DROPBOX_APP_CRASH_TAGS =
50             Set.of(
51                     "SYSTEM_TOMBSTONE",
52                     "system_app_anr",
53                     "system_app_native_crash",
54                     "system_app_crash",
55                     "data_app_anr",
56                     "data_app_native_crash",
57                     "data_app_crash");
58 
59     @VisibleForTesting
60     static final String LAUNCH_PACKAGE_COMMAND_TEMPLATE =
61             "monkey -p %s -c android.intent.category.LAUNCHER 1";
62 
63     private static final String VIDEO_PATH_ON_DEVICE_TEMPLATE = "/sdcard/screenrecord_%s.mp4";
64     @VisibleForTesting static final int WAIT_FOR_SCREEN_RECORDING_START_TIMEOUT_MILLIS = 10 * 1000;
65     @VisibleForTesting static final int WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS = 500;
66 
67     private final ITestDevice mDevice;
68     private final Sleeper mSleeper;
69     private final Clock mClock;
70     private final RunUtilProvider mRunUtilProvider;
71     private final TempFileSupplier mTempFileSupplier;
72 
getInstance(ITestDevice device)73     public static DeviceUtils getInstance(ITestDevice device) {
74         return new DeviceUtils(
75                 device,
76                 duration -> {
77                     Thread.sleep(duration);
78                 },
79                 () -> System.currentTimeMillis(),
80                 () -> RunUtil.getDefault(),
81                 () -> Files.createTempFile(TestUtils.class.getName(), ".tmp"));
82     }
83 
84     @VisibleForTesting
DeviceUtils( ITestDevice device, Sleeper sleeper, Clock clock, RunUtilProvider runUtilProvider, TempFileSupplier tempFileSupplier)85     DeviceUtils(
86             ITestDevice device,
87             Sleeper sleeper,
88             Clock clock,
89             RunUtilProvider runUtilProvider,
90             TempFileSupplier tempFileSupplier) {
91         mDevice = device;
92         mSleeper = sleeper;
93         mClock = clock;
94         mRunUtilProvider = runUtilProvider;
95         mTempFileSupplier = tempFileSupplier;
96     }
97 
98     /**
99      * A runnable that throws DeviceNotAvailableException. Use this interface instead of Runnable so
100      * that the DeviceNotAvailableException won't need to be handled inside the run() method.
101      */
102     public interface RunnableThrowingDeviceNotAvailable {
run()103         void run() throws DeviceNotAvailableException;
104     }
105 
106     /**
107      * Get the current device timestamp in milliseconds.
108      *
109      * @return The device time
110      * @throws DeviceNotAvailableException When the device is not available.
111      * @throws DeviceRuntimeException When the command to get device time failed or failed to parse
112      *     the timestamp.
113      */
currentTimeMillis()114     public DeviceTimestamp currentTimeMillis()
115             throws DeviceNotAvailableException, DeviceRuntimeException {
116         CommandResult result = mDevice.executeShellV2Command("echo ${EPOCHREALTIME:0:14}");
117         if (result.getStatus() != CommandStatus.SUCCESS) {
118             throw new DeviceRuntimeException(
119                     "Failed to get device time: " + result,
120                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
121         }
122         try {
123             return new DeviceTimestamp(Long.parseLong(result.getStdout().replace(".", "").trim()));
124         } catch (NumberFormatException e) {
125             CLog.e("Cannot parse device time string: " + result.getStdout());
126             throw new DeviceRuntimeException(
127                     "Cannot parse device time string: " + result.getStdout(),
128                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
129         }
130     }
131 
132     /**
133      * Record the device screen while running a task.
134      *
135      * <p>This method will not throw exception when the screenrecord command failed unless the
136      * device is unresponsive.
137      *
138      * @param action A runnable job that throws DeviceNotAvailableException.
139      * @param handler A file handler that process the output screen record mp4 file located on the
140      *     host.
141      * @throws DeviceNotAvailableException When the device is unresponsive.
142      */
runWithScreenRecording( RunnableThrowingDeviceNotAvailable action, ScreenrecordFileHandler handler)143     public void runWithScreenRecording(
144             RunnableThrowingDeviceNotAvailable action, ScreenrecordFileHandler handler)
145             throws DeviceNotAvailableException {
146         String videoPath = String.format(VIDEO_PATH_ON_DEVICE_TEMPLATE, new Random().nextInt());
147         mDevice.deleteFile(videoPath);
148 
149         // Start screen recording
150         Process recordingProcess = null;
151         try {
152             recordingProcess =
153                     mRunUtilProvider
154                             .get()
155                             .runCmdInBackground(
156                                     String.format(
157                                                     "adb -s %s shell screenrecord %s",
158                                                     mDevice.getSerialNumber(), videoPath)
159                                             .split("\\s+"));
160         } catch (IOException ioException) {
161             CLog.e("Exception is thrown when starting screen recording process: %s", ioException);
162         }
163 
164         try {
165             long start = mClock.currentTimeMillis();
166             // Wait for the recording to start since it may take time for the device to start
167             // recording
168             while (recordingProcess != null) {
169                 CommandResult result = mDevice.executeShellV2Command("ls " + videoPath);
170                 if (result.getStatus() == CommandStatus.SUCCESS) {
171                     break;
172                 }
173 
174                 CLog.d(
175                         "Screenrecord not started yet. Waiting %s milliseconds.",
176                         WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS);
177 
178                 try {
179                     mSleeper.sleep(WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS);
180                 } catch (InterruptedException e) {
181                     throw new RuntimeException(e);
182                 }
183 
184                 if (mClock.currentTimeMillis() - start
185                         > WAIT_FOR_SCREEN_RECORDING_START_TIMEOUT_MILLIS) {
186                     CLog.e(
187                             "Screenrecord did not start within %s milliseconds.",
188                             WAIT_FOR_SCREEN_RECORDING_START_TIMEOUT_MILLIS);
189                     break;
190                 }
191             }
192 
193             action.run();
194         } finally {
195             if (recordingProcess != null) {
196                 recordingProcess.destroy();
197             }
198             // Try to pull, handle, and delete the video file from the device anyway.
199             handler.handleScreenRecordFile(mDevice.pullFile(videoPath));
200             mDevice.deleteFile(videoPath);
201         }
202     }
203 
204     /** A file handler for screen record results. */
205     public interface ScreenrecordFileHandler {
206         /**
207          * Handles the screen record mp4 file located on the host.
208          *
209          * @param screenRecord The mp4 file located on the host. If screen record failed then the
210          *     input could be null.
211          */
handleScreenRecordFile(File screenRecord)212         void handleScreenRecordFile(File screenRecord);
213     }
214 
215     /**
216      * Freeze the screen rotation to the default orientation.
217      *
218      * @return True if succeed; False otherwise.
219      * @throws DeviceNotAvailableException
220      */
freezeRotation()221     public boolean freezeRotation() throws DeviceNotAvailableException {
222         CommandResult result =
223                 mDevice.executeShellV2Command(
224                         "content insert --uri content://settings/system --bind"
225                                 + " name:s:accelerometer_rotation --bind value:i:0");
226         if (result.getStatus() != CommandStatus.SUCCESS || result.getExitCode() != 0) {
227             CLog.e("The command to disable auto screen rotation failed: %s", result);
228             return false;
229         }
230 
231         return true;
232     }
233 
234     /**
235      * Unfreeze the screen rotation to the default orientation.
236      *
237      * @return True if succeed; False otherwise.
238      * @throws DeviceNotAvailableException
239      */
unfreezeRotation()240     public boolean unfreezeRotation() throws DeviceNotAvailableException {
241         CommandResult result =
242                 mDevice.executeShellV2Command(
243                         "content insert --uri content://settings/system --bind"
244                                 + " name:s:accelerometer_rotation --bind value:i:1");
245         if (result.getStatus() != CommandStatus.SUCCESS || result.getExitCode() != 0) {
246             CLog.e("The command to enable auto screen rotation failed: %s", result);
247             return false;
248         }
249 
250         return true;
251     }
252 
253     /**
254      * Launches a package on the device.
255      *
256      * @param packageName The package name to launch.
257      * @throws DeviceNotAvailableException When device was lost.
258      * @throws DeviceUtilsException When failed to launch the package.
259      */
launchPackage(String packageName)260     public void launchPackage(String packageName)
261             throws DeviceUtilsException, DeviceNotAvailableException {
262         CommandResult result =
263                 mDevice.executeShellV2Command(
264                         String.format(LAUNCH_PACKAGE_COMMAND_TEMPLATE, packageName));
265         if (result.getStatus() != CommandStatus.SUCCESS || result.getExitCode() != 0) {
266             throw new DeviceUtilsException(
267                     String.format(
268                             "The command to launch package %s failed: %s", packageName, result));
269         }
270     }
271 
272     /**
273      * Gets the version name of a package installed on the device.
274      *
275      * @param packageName The full package name to query
276      * @return The package version name, or 'Unknown' if the package doesn't exist or the adb
277      *     command failed.
278      * @throws DeviceNotAvailableException
279      */
getPackageVersionName(String packageName)280     public String getPackageVersionName(String packageName) throws DeviceNotAvailableException {
281         CommandResult cmdResult =
282                 mDevice.executeShellV2Command(
283                         String.format("dumpsys package %s | grep versionName", packageName));
284 
285         if (cmdResult.getStatus() != CommandStatus.SUCCESS
286                 || !cmdResult.getStdout().trim().startsWith(VERSION_NAME_PREFIX)) {
287             return UNKNOWN;
288         }
289 
290         return cmdResult.getStdout().trim().substring(VERSION_NAME_PREFIX.length());
291     }
292 
293     /**
294      * Gets the version code of a package installed on the device.
295      *
296      * @param packageName The full package name to query
297      * @return The package version code, or 'Unknown' if the package doesn't exist or the adb
298      *     command failed.
299      * @throws DeviceNotAvailableException
300      */
getPackageVersionCode(String packageName)301     public String getPackageVersionCode(String packageName) throws DeviceNotAvailableException {
302         CommandResult cmdResult =
303                 mDevice.executeShellV2Command(
304                         String.format("dumpsys package %s | grep versionCode", packageName));
305 
306         if (cmdResult.getStatus() != CommandStatus.SUCCESS
307                 || !cmdResult.getStdout().trim().startsWith(VERSION_CODE_PREFIX)) {
308             return UNKNOWN;
309         }
310 
311         return cmdResult.getStdout().trim().split(" ")[0].substring(VERSION_CODE_PREFIX.length());
312     }
313 
314     /**
315      * Stops a running package on the device.
316      *
317      * @param packageName
318      * @throws DeviceNotAvailableException
319      */
stopPackage(String packageName)320     public void stopPackage(String packageName) throws DeviceNotAvailableException {
321         mDevice.executeShellV2Command("am force-stop " + packageName);
322     }
323 
324     /**
325      * Resets a package's data storage on the device.
326      *
327      * @param packageName The package name of an app to reset.
328      * @return True if the package exists and its data was reset; False otherwise.
329      * @throws DeviceNotAvailableException If the device was lost.
330      */
resetPackage(String packageName)331     public boolean resetPackage(String packageName) throws DeviceNotAvailableException {
332         return mDevice.executeShellV2Command(RESET_PACKAGE_COMMAND_PREFIX + packageName).getStatus()
333                 == CommandStatus.SUCCESS;
334     }
335 
336     /**
337      * Gets dropbox entries from the device filtered by the provided tags.
338      *
339      * @param tags Dropbox tags to query.
340      * @return A list of dropbox entries.
341      * @throws IOException when failed to dump or read the dropbox protos.
342      */
getDropboxEntries(Set<String> tags)343     public List<DropboxEntry> getDropboxEntries(Set<String> tags) throws IOException {
344         List<DropboxEntry> entries = new ArrayList<>();
345 
346         for (String tag : tags) {
347             Path dumpFile = mTempFileSupplier.get();
348 
349             CommandResult res =
350                     mRunUtilProvider
351                             .get()
352                             .runTimedCmd(
353                                     6000,
354                                     "sh",
355                                     "-c",
356                                     String.format(
357                                             "adb -s %s shell dumpsys dropbox --proto %s > %s",
358                                             mDevice.getSerialNumber(), tag, dumpFile));
359             if (res.getStatus() != CommandStatus.SUCCESS) {
360                 throw new IOException("Dropbox dump command failed: " + res);
361             }
362 
363             DropBoxManagerServiceDumpProto p =
364                     DropBoxManagerServiceDumpProto.parseFrom(Files.readAllBytes(dumpFile));
365             Files.delete(dumpFile);
366 
367             for (Entry entry : p.getEntriesList()) {
368                 entries.add(
369                         new DropboxEntry(entry.getTimeMs(), tag, entry.getData().toStringUtf8()));
370             }
371         }
372 
373         return entries;
374     }
375 
376     /** A class that stores the information of a dropbox entry. */
377     public static final class DropboxEntry {
378         private final long mTime;
379         private final String mTag;
380         private final String mData;
381 
382         /** Returns the entrt's time stamp on device. */
getTime()383         public long getTime() {
384             return mTime;
385         }
386 
387         /** Returns the entrt's tag. */
getTag()388         public String getTag() {
389             return mTag;
390         }
391 
392         /** Returns the entrt's data. */
getData()393         public String getData() {
394             return mData;
395         }
396 
397         @VisibleForTesting
DropboxEntry(long time, String tag, String data)398         DropboxEntry(long time, String tag, String data) {
399             mTime = time;
400             mTag = tag;
401             mData = data;
402         }
403     }
404 
405     /** A general exception class representing failed device utility operations. */
406     public static final class DeviceUtilsException extends Exception {
407         /**
408          * Constructs a new {@link DeviceUtilsException} with a meaningful error message.
409          *
410          * @param message A error message describing the cause of the error.
411          */
DeviceUtilsException(String message)412         private DeviceUtilsException(String message) {
413             super(message);
414         }
415 
416         /**
417          * Constructs a new {@link DeviceUtilsException} with a meaningful error message, and a
418          * cause.
419          *
420          * @param message A detailed error message.
421          * @param cause A {@link Throwable} capturing the original cause of the {@link
422          *     DeviceUtilsException}.
423          */
DeviceUtilsException(String message, Throwable cause)424         private DeviceUtilsException(String message, Throwable cause) {
425             super(message, cause);
426         }
427 
428         /**
429          * Constructs a new {@link DeviceUtilsException} with a cause.
430          *
431          * @param cause A {@link Throwable} capturing the original cause of the {@link
432          *     DeviceUtilsException}.
433          */
DeviceUtilsException(Throwable cause)434         private DeviceUtilsException(Throwable cause) {
435             super(cause);
436         }
437     }
438 
439     /**
440      * A class to contain a device timestamp.
441      *
442      * <p>Use this class instead of long to pass device timestamps so that they are less likely to
443      * be confused with host timestamps.
444      */
445     public static class DeviceTimestamp {
446         private final long mTimestamp;
447 
DeviceTimestamp(long timestamp)448         public DeviceTimestamp(long timestamp) {
449             mTimestamp = timestamp;
450         }
451 
452         /** Gets the timestamp on a device. */
get()453         public long get() {
454             return mTimestamp;
455         }
456     }
457 
458     @VisibleForTesting
459     interface Sleeper {
sleep(long milliseconds)460         void sleep(long milliseconds) throws InterruptedException;
461     }
462 
463     @VisibleForTesting
464     interface Clock {
currentTimeMillis()465         long currentTimeMillis();
466     }
467 
468     @VisibleForTesting
469     interface RunUtilProvider {
get()470         IRunUtil get();
471     }
472 
473     @VisibleForTesting
474     interface TempFileSupplier {
get()475         Path get() throws IOException;
476     }
477 }
478