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