• 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.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