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