1 /* 2 * Copyright (C) 2015 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 package com.android.compatibility.common.tradefed.targetprep; 17 18 import com.android.annotations.VisibleForTesting; 19 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 20 import com.android.compatibility.common.tradefed.util.DynamicConfigFileReader; 21 import com.android.ddmlib.IDevice; 22 import com.android.ddmlib.Log; 23 import com.android.tradefed.build.IBuildInfo; 24 import com.android.tradefed.config.Configuration; 25 import com.android.tradefed.config.Option; 26 import com.android.tradefed.config.OptionClass; 27 import com.android.tradefed.device.DeviceNotAvailableException; 28 import com.android.tradefed.device.ITestDevice; 29 import com.android.tradefed.invoker.TestInformation; 30 import com.android.tradefed.log.LogUtil; 31 import com.android.tradefed.log.LogUtil.CLog; 32 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 33 import com.android.tradefed.result.ITestInvocationListener; 34 import com.android.tradefed.result.TestDescription; 35 import com.android.tradefed.targetprep.BaseTargetPreparer; 36 import com.android.tradefed.targetprep.BuildError; 37 import com.android.tradefed.targetprep.TargetSetupError; 38 import com.android.tradefed.testtype.AndroidJUnitTest; 39 import com.android.tradefed.util.FileUtil; 40 import com.android.tradefed.util.ZipUtil; 41 42 import org.xmlpull.v1.XmlPullParserException; 43 44 import java.io.File; 45 import java.io.FileNotFoundException; 46 import java.io.IOException; 47 import java.io.InputStream; 48 import java.net.URL; 49 import java.net.URLConnection; 50 import java.util.HashMap; 51 import java.util.regex.Matcher; 52 import java.util.regex.Pattern; 53 import java.util.zip.ZipFile; 54 55 /** Ensures that the appropriate media files exist on the device */ 56 @OptionClass(alias = "media-preparer") 57 public class MediaPreparer extends BaseTargetPreparer { 58 59 @Option( 60 name = "local-media-path", 61 description = 62 "Absolute path of the media files directory, containing" 63 + "'bbb_short' and 'bbb_full' directories" 64 ) 65 private String mLocalMediaPath = null; 66 67 @Option( 68 name = "skip-media-download", 69 description = "Whether to skip the media files precondition" 70 ) 71 private boolean mSkipMediaDownload = false; 72 73 /** @deprecated do not use it. */ 74 @Deprecated 75 @Option( 76 name = "media-download-only", 77 description = 78 "Deprecated: Only download media files; do not run instrumentation or copy files" 79 ) 80 private boolean mMediaDownloadOnly = false; 81 82 @Option(name = "images-only", description = "Only push images files to the device") 83 private boolean mImagesOnly = false; 84 85 @Option( 86 name = "push-all", 87 description = 88 "Push everything downloaded to the device," 89 + " use 'media-folder-name' to specify the destination dir name." 90 ) 91 private boolean mPushAll = false; 92 93 @Option(name = "dynamic-config-module", 94 description = "For a target preparer, the 'module' of the configuration" + 95 " is the test suite.") 96 private String mDynamicConfigModule = "cts"; 97 98 @Option(name = "media-folder-name", 99 description = "The name of local directory into which media" + 100 " files will be downloaded, if option 'local-media-path' is not" + 101 " provided. This directory will live inside the temp directory." + 102 " If option 'push-all' is set, this is also the subdirectory name on device" + 103 " where media files are pushed to") 104 private String mMediaFolderName = MEDIA_FOLDER_NAME; 105 106 /* 107 * The pathnames of the device's directories that hold media files for the tests. 108 * These depend on the device's mount point, which is retrieved in the MediaPreparer's run 109 * method. 110 * 111 * These fields are exposed for unit testing 112 */ 113 protected String mBaseDeviceModuleDir; 114 protected String mBaseDeviceShortDir; 115 protected String mBaseDeviceFullDir; 116 protected String mBaseDeviceImagesDir; 117 118 /* 119 * Variables set by the MediaPreparerListener during retrieval of maximum media file 120 * resolution. After the MediaPreparerApp has been instrumented on the device: 121 * 122 * testMetrics contains the string representation of the resolution 123 * testFailures contains a stacktrace if retrieval of the resolution was unsuccessful 124 */ 125 protected Resolution mMaxRes = null; 126 protected String mFailureStackTrace = null; 127 128 /* 129 * The default name of local directory into which media files will be downloaded, if option 130 * "local-media-path" is not provided. This directory will live inside the temp directory. 131 */ 132 protected static final String MEDIA_FOLDER_NAME = "android-cts-media"; 133 134 /* The key used to retrieve the media files URL from the dynamic configuration */ 135 private static final String MEDIA_FILES_URL_KEY = "media_files_url"; 136 137 /* 138 * Info used to install and uninstall the MediaPreparerApp 139 */ 140 private static final String APP_APK = "CtsMediaPreparerApp.apk"; 141 private static final String APP_PKG_NAME = "android.mediastress.cts.preconditions.app"; 142 143 /* Key to retrieve resolution string in metrics upon MediaPreparerListener.testEnded() */ 144 private static final String RESOLUTION_STRING_KEY = "resolution"; 145 146 private static final String LOG_TAG = "MediaPreparer"; 147 148 /* 149 * In the case of MediaPreparer error, the default maximum resolution to push to the device. 150 * Pushing higher resolutions may lead to insufficient storage for installing test APKs. 151 * TODO(aaronholden): When the new detection of max resolution is proven stable, throw 152 * a TargetSetupError when detection results in error 153 */ 154 protected static final Resolution DEFAULT_MAX_RESOLUTION = new Resolution(480, 360); 155 156 protected static final Resolution[] RESOLUTIONS = { 157 new Resolution(176, 144), 158 new Resolution(480, 360), 159 new Resolution(720, 480), 160 new Resolution(1280, 720), 161 new Resolution(1920, 1080) 162 }; 163 164 /** Helper class for generating and retrieving width-height pairs */ 165 protected static final class Resolution { 166 // regex that matches a resolution string 167 private static final String PATTERN = "(\\d+)x(\\d+)"; 168 // group indices for accessing resolution width and height from a PATTERN-based Matcher 169 private static final int WIDTH_INDEX = 1; 170 private static final int HEIGHT_INDEX = 2; 171 172 private final int width; 173 private final int height; 174 Resolution(int width, int height)175 private Resolution(int width, int height) { 176 this.width = width; 177 this.height = height; 178 } 179 Resolution(String resolution)180 private Resolution(String resolution) { 181 Pattern pattern = Pattern.compile(PATTERN); 182 Matcher matcher = pattern.matcher(resolution); 183 matcher.find(); 184 this.width = Integer.parseInt(matcher.group(WIDTH_INDEX)); 185 this.height = Integer.parseInt(matcher.group(HEIGHT_INDEX)); 186 } 187 188 @Override toString()189 public String toString() { 190 return String.format("%dx%d", width, height); 191 } 192 193 /** Returns the width of the resolution. */ getWidth()194 public int getWidth() { 195 return width; 196 } 197 } 198 getDefaultMediaDir()199 public static File getDefaultMediaDir() { 200 return new File(System.getProperty("java.io.tmpdir"), MEDIA_FOLDER_NAME); 201 } 202 getMediaDir()203 protected File getMediaDir() { 204 return new File(System.getProperty("java.io.tmpdir"), mMediaFolderName); 205 } 206 207 /* 208 * Returns true if all necessary media files exist on the device, and false otherwise. 209 * 210 * This method is exposed for unit testing. 211 */ 212 @VisibleForTesting mediaFilesExistOnDevice(ITestDevice device)213 protected boolean mediaFilesExistOnDevice(ITestDevice device) 214 throws DeviceNotAvailableException { 215 if (mPushAll) { 216 return device.doesFileExist(mBaseDeviceModuleDir); 217 } else if (!mImagesOnly) { 218 for (Resolution resolution : RESOLUTIONS) { 219 if (resolution.width > mMaxRes.width) { 220 break; // no need to check for resolutions greater than this 221 } 222 String deviceShortFilePath = mBaseDeviceShortDir + resolution.toString(); 223 String deviceFullFilePath = mBaseDeviceFullDir + resolution.toString(); 224 if (!device.doesFileExist(deviceShortFilePath) 225 || !device.doesFileExist(deviceFullFilePath)) { 226 return false; 227 } 228 } 229 } 230 return device.doesFileExist(mBaseDeviceImagesDir); 231 } 232 233 /* 234 * After downloading and unzipping the media files, mLocalMediaPath must be the path to the 235 * directory containing 'bbb_short' and 'bbb_full' directories, as it is defined in its 236 * description as an option. 237 * After extraction, this directory exists one level below the the directory 'mediaFolder'. 238 * If the 'mediaFolder' contains anything other than exactly one subdirectory, a 239 * TargetSetupError is thrown. Otherwise, the mLocalMediaPath variable is set to the path of 240 * this subdirectory. 241 */ updateLocalMediaPath(ITestDevice device, File mediaFolder)242 private void updateLocalMediaPath(ITestDevice device, File mediaFolder) 243 throws TargetSetupError { 244 String[] subDirs = mediaFolder.list(); 245 if (subDirs.length != 1) { 246 throw new TargetSetupError(String.format( 247 "Unexpected contents in directory %s", mediaFolder.getAbsolutePath()), 248 device.getDeviceDescriptor()); 249 } 250 mLocalMediaPath = new File(mediaFolder, subDirs[0]).getAbsolutePath(); 251 } 252 253 /* 254 * Copies the media files to the host from a predefined URL. 255 * 256 * Synchronize this method so that multiple shards won't download/extract 257 * this file to the same location on the host. Only an issue in Android O and above, 258 * where MediaPreparer is used for multiple, shardable modules. 259 */ downloadMediaToHost(ITestDevice device, IBuildInfo buildInfo)260 private File downloadMediaToHost(ITestDevice device, IBuildInfo buildInfo) 261 throws TargetSetupError { 262 // Make sure the synchronization is on the class and not the object 263 synchronized (MediaPreparer.class) { 264 // Retrieve default directory for storing media files 265 File mediaFolder = getMediaDir(); 266 if (mediaFolder.exists() && mediaFolder.list().length > 0) { 267 // Folder has already been created and populated by previous MediaPreparer runs, 268 // assume all necessary media files exist inside. 269 return mediaFolder; 270 } 271 mediaFolder.mkdirs(); 272 URL url; 273 try { 274 // Get download URL from dynamic configuration service 275 String mediaUrlString = 276 DynamicConfigFileReader.getValueFromConfig( 277 buildInfo, mDynamicConfigModule, MEDIA_FILES_URL_KEY); 278 url = new URL(mediaUrlString); 279 } catch (IOException | XmlPullParserException e) { 280 throw new TargetSetupError( 281 "Trouble finding media file download location with " 282 + "dynamic configuration", 283 e, 284 device.getDeviceDescriptor()); 285 } 286 File mediaFolderZip = new File(mediaFolder.getAbsolutePath() + ".zip"); 287 try { 288 LogUtil.printLog( 289 Log.LogLevel.INFO, 290 LOG_TAG, 291 String.format("Downloading media files from %s", url.toString())); 292 URLConnection conn = url.openConnection(); 293 InputStream in = conn.getInputStream(); 294 mediaFolderZip.createNewFile(); 295 FileUtil.writeToFile(in, mediaFolderZip); 296 LogUtil.printLog(Log.LogLevel.INFO, LOG_TAG, "Unzipping media files"); 297 ZipUtil.extractZip(new ZipFile(mediaFolderZip), mediaFolder); 298 } catch (IOException e) { 299 FileUtil.recursiveDelete(mediaFolder); 300 throw new TargetSetupError( 301 String.format( 302 "Failed to download and open media files on host machine at '%s'." 303 + " These media files are required for compatibility tests.", 304 mediaFolderZip), 305 e, 306 device.getDeviceDescriptor(), 307 /* device side */ false); 308 } finally { 309 FileUtil.deleteFile(mediaFolderZip); 310 } 311 return mediaFolder; 312 } 313 } 314 315 /* 316 * Pushes directories containing media files to the device for all directories that: 317 * - are not already present on the device 318 * - contain video files of a resolution less than or equal to the device's 319 * max video playback resolution 320 * - contain image files 321 * 322 * This method is exposed for unit testing. 323 */ copyMediaFiles(ITestDevice device)324 protected void copyMediaFiles(ITestDevice device) throws DeviceNotAvailableException { 325 if (mPushAll) { 326 copyAll(device); 327 return; 328 } 329 if (!mImagesOnly) { 330 copyVideoFiles(device); 331 } 332 copyImagesFiles(device); 333 } 334 335 // copy video files of a resolution <= the device's maximum video playback resolution copyVideoFiles(ITestDevice device)336 protected void copyVideoFiles(ITestDevice device) throws DeviceNotAvailableException { 337 for (Resolution resolution : RESOLUTIONS) { 338 if (resolution.width > mMaxRes.width) { 339 CLog.i("Media file copying complete"); 340 return; 341 } 342 String deviceShortFilePath = mBaseDeviceShortDir + resolution.toString(); 343 String deviceFullFilePath = mBaseDeviceFullDir + resolution.toString(); 344 if (!device.doesFileExist(deviceShortFilePath) || 345 !device.doesFileExist(deviceFullFilePath)) { 346 CLog.i("Copying files of resolution %s to device", resolution.toString()); 347 String localShortDirName = "bbb_short/" + resolution.toString(); 348 String localFullDirName = "bbb_full/" + resolution.toString(); 349 File localShortDir = new File(mLocalMediaPath, localShortDirName); 350 File localFullDir = new File(mLocalMediaPath, localFullDirName); 351 // push short directory of given resolution, if not present on device 352 if(!device.doesFileExist(deviceShortFilePath)) { 353 device.pushDir(localShortDir, deviceShortFilePath); 354 } 355 // push full directory of given resolution, if not present on device 356 if(!device.doesFileExist(deviceFullFilePath)) { 357 device.pushDir(localFullDir, deviceFullFilePath); 358 } 359 } 360 } 361 } 362 363 // copy image files to the device copyImagesFiles(ITestDevice device)364 protected void copyImagesFiles(ITestDevice device) throws DeviceNotAvailableException { 365 if (!device.doesFileExist(mBaseDeviceImagesDir)) { 366 CLog.i("Copying images files to device"); 367 device.pushDir(new File(mLocalMediaPath, "images"), mBaseDeviceImagesDir); 368 } 369 } 370 371 // copy everything from the host directory to the device copyAll(ITestDevice device)372 protected void copyAll(ITestDevice device) throws DeviceNotAvailableException { 373 if (!device.doesFileExist(mBaseDeviceModuleDir)) { 374 CLog.i("Copying files to device"); 375 device.pushDir(new File(mLocalMediaPath), mBaseDeviceModuleDir); 376 } 377 } 378 379 // Initialize directory strings where media files live on device setMountPoint(ITestDevice device)380 protected void setMountPoint(ITestDevice device) { 381 String mountPoint = device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE); 382 mBaseDeviceModuleDir = String.format("%s/test/%s/", mountPoint, mMediaFolderName); 383 mBaseDeviceShortDir = String.format("%s/test/bbb_short/", mountPoint); 384 mBaseDeviceFullDir = String.format("%s/test/bbb_full/", mountPoint); 385 mBaseDeviceImagesDir = String.format("%s/test/images/", mountPoint); 386 } 387 388 @Override setUp(TestInformation testInfo)389 public void setUp(TestInformation testInfo) 390 throws TargetSetupError, BuildError, DeviceNotAvailableException { 391 ITestDevice device = testInfo.getDevice(); 392 IBuildInfo buildInfo = testInfo.getBuildInfo(); 393 if (mImagesOnly && mPushAll) { 394 throw new TargetSetupError( 395 "'images-only' and 'push-all' cannot be set to true together.", 396 device.getDeviceDescriptor()); 397 } 398 if (mSkipMediaDownload) { 399 CLog.i("Skipping media preparation"); 400 return; // skip this precondition 401 } 402 403 setMountPoint(device); 404 if (!mImagesOnly && !mPushAll) { 405 setMaxRes(testInfo); // max resolution only applies to video files 406 } 407 if (mediaFilesExistOnDevice(device)) { 408 // if files already on device, do nothing 409 CLog.i("Media files found on the device"); 410 return; 411 } 412 413 if (mLocalMediaPath == null) { 414 // Option 'local-media-path' has not been defined 415 // Get directory to store media files on this host 416 File mediaFolder = downloadMediaToHost(device, buildInfo); 417 // set mLocalMediaPath to extraction location of media files 418 updateLocalMediaPath(device, mediaFolder); 419 } 420 CLog.i("Media files located on host at: %s", mLocalMediaPath); 421 copyMediaFiles(device); 422 } 423 424 // Initialize maximum resolution of media files to copy setMaxRes(TestInformation testInfo)425 private void setMaxRes(TestInformation testInfo) throws DeviceNotAvailableException { 426 ITestInvocationListener listener = new MediaPreparerListener(); 427 ITestDevice device = testInfo.getDevice(); 428 IBuildInfo buildInfo = testInfo.getBuildInfo(); 429 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo); 430 File apkFile = null; 431 try { 432 apkFile = buildHelper.getTestFile(APP_APK); 433 if (!apkFile.exists()) { 434 // handle both missing tests dir and missing APK in catch block 435 throw new FileNotFoundException(); 436 } 437 } catch (FileNotFoundException e) { 438 mMaxRes = DEFAULT_MAX_RESOLUTION; 439 CLog.w( 440 "Cound not find %s to determine maximum resolution, copying up to %s", 441 APP_APK, DEFAULT_MAX_RESOLUTION.toString()); 442 return; 443 } 444 if (device.getAppPackageInfo(APP_PKG_NAME) != null) { 445 device.uninstallPackage(APP_PKG_NAME); 446 } 447 CLog.i("Instrumenting package %s:", APP_PKG_NAME); 448 AndroidJUnitTest instrTest = new AndroidJUnitTest(); 449 instrTest.setDevice(device); 450 instrTest.setInstallFile(apkFile); 451 instrTest.setPackageName(APP_PKG_NAME); 452 // AndroidJUnitTest requires a IConfiguration to work properly, add a stub to this 453 // implementation to avoid an NPE. 454 instrTest.setConfiguration(new Configuration("stub", "stub")); 455 instrTest.run(testInfo, listener); 456 if (mFailureStackTrace != null) { 457 mMaxRes = DEFAULT_MAX_RESOLUTION; 458 CLog.w("Retrieving maximum resolution failed with trace:\n%s", mFailureStackTrace); 459 CLog.w("Copying up to %s", DEFAULT_MAX_RESOLUTION.toString()); 460 } else if (mMaxRes == null) { 461 mMaxRes = DEFAULT_MAX_RESOLUTION; 462 CLog.w( 463 "Failed to pull resolution capabilities from device, copying up to %s", 464 DEFAULT_MAX_RESOLUTION.toString()); 465 } 466 } 467 468 /* Special listener for setting MediaPreparer instance variable values */ 469 private class MediaPreparerListener implements ITestInvocationListener { 470 471 @Override testEnded(TestDescription test, HashMap<String, Metric> metrics)472 public void testEnded(TestDescription test, HashMap<String, Metric> metrics) { 473 Metric resMetric = metrics.get(RESOLUTION_STRING_KEY); 474 if (resMetric != null) { 475 mMaxRes = new Resolution(resMetric.getMeasurements().getSingleString()); 476 } 477 } 478 479 @Override testFailed(TestDescription test, String trace)480 public void testFailed(TestDescription test, String trace) { 481 mFailureStackTrace = trace; 482 } 483 } 484 } 485