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