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 static com.android.tradefed.targetprep.UserHelper.getRunTestsAsUser;
19 
20 import com.android.annotations.VisibleForTesting;
21 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
22 import com.android.compatibility.common.tradefed.util.DynamicConfigFileReader;
23 import com.android.ddmlib.IDevice;
24 import com.android.tradefed.build.IBuildInfo;
25 import com.android.tradefed.config.Configuration;
26 import com.android.tradefed.config.IConfiguration;
27 import com.android.tradefed.config.IConfigurationReceiver;
28 import com.android.tradefed.config.IDeviceConfiguration;
29 import com.android.tradefed.config.Option;
30 import com.android.tradefed.config.OptionClass;
31 import com.android.tradefed.dependencies.ExternalDependency;
32 import com.android.tradefed.dependencies.IExternalDependency;
33 import com.android.tradefed.dependencies.connectivity.NetworkDependency;
34 import com.android.tradefed.device.DeviceNotAvailableException;
35 import com.android.tradefed.device.ITestDevice;
36 import com.android.tradefed.device.contentprovider.ContentProviderHandler;
37 import com.android.tradefed.invoker.TestInformation;
38 import com.android.tradefed.log.LogUtil.CLog;
39 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
40 import com.android.tradefed.result.ITestInvocationListener;
41 import com.android.tradefed.result.TestDescription;
42 import com.android.tradefed.result.error.DeviceErrorIdentifier;
43 import com.android.tradefed.result.error.InfraErrorIdentifier;
44 import com.android.tradefed.targetprep.BaseTargetPreparer;
45 import com.android.tradefed.targetprep.BuildError;
46 import com.android.tradefed.targetprep.ITargetPreparer;
47 import com.android.tradefed.targetprep.TargetSetupError;
48 import com.android.tradefed.testtype.AndroidJUnitTest;
49 import com.android.tradefed.util.FileUtil;
50 import com.android.tradefed.util.StreamUtil;
51 import com.android.tradefed.util.ZipUtil;
52 
53 import org.xmlpull.v1.XmlPullParserException;
54 
55 import java.io.BufferedReader;
56 import java.io.File;
57 import java.io.FileNotFoundException;
58 import java.io.FileReader;
59 import java.io.FileWriter;
60 import java.io.IOException;
61 import java.io.InputStream;
62 import java.net.URL;
63 import java.net.URLConnection;
64 import java.util.HashMap;
65 import java.util.HashSet;
66 import java.util.Set;
67 import java.util.regex.Matcher;
68 import java.util.regex.Pattern;
69 import java.util.zip.ZipFile;
70 
71 /** Ensures that the appropriate media files exist on the device */
72 @OptionClass(alias = "media-preparer")
73 public class MediaPreparer extends BaseTargetPreparer
74         implements IExternalDependency, IConfigurationReceiver {
75 
76     @Option(
77         name = "local-media-path",
78         description =
79                 "Absolute path of the media files directory, containing"
80                         + "'bbb_short' and 'bbb_full' directories"
81     )
82     private String mLocalMediaPath = null;
83 
84     @Option(
85         name = "skip-media-download",
86         description = "Whether to skip the media files precondition"
87     )
88     private boolean mSkipMediaDownload = false;
89 
90     @Option(
91             name = "simple-caching-semantics",
92             description = "Whether to use the original, simple MediaPreparer caching semantics")
93     private boolean mSimpleCachingSemantics = false;
94 
95     @Option(
96             name = "media-download-only",
97             description = "Only download media files; do not run instrumentation or copy files")
98     private boolean mMediaDownloadOnly = false;
99 
100     @Option(
101         name = "push-all",
102         description =
103                 "Push everything downloaded to the device,"
104                         + " use 'media-folder-name' to specify the destination dir name."
105     )
106     private boolean mPushAll = false;
107 
108     @Option(name = "dynamic-config-module",
109             description = "For a target preparer, the 'module' of the configuration" +
110             " is the test suite.")
111     private String mDynamicConfigModule = "cts";
112 
113     @Option(name = "media-folder-name",
114             description = "The name of local directory into which media" +
115             " files will be downloaded, if option 'local-media-path' is not" +
116             " provided. This directory will live inside the temp directory." +
117             " If option 'push-all' is set, this is also the subdirectory name on device" +
118             " where media files are pushed to")
119     private String mMediaFolderName = MEDIA_FOLDER_NAME;
120 
121     @Option(name = "use-legacy-folder-structure",
122             description = "Use legacy folder structure to store big buck bunny clips. When this " +
123             "is set to false, name specified in media-folder-name will be used. Default: true")
124     private boolean mUseLegacyFolderStructure = true;
125 
126     /*
127      * The pathnames of the device's directories that hold media files for the tests.
128      * These depend on the device's mount point, which is retrieved in the MediaPreparer's run
129      * method.
130      *
131      * These fields are exposed for unit testing
132      */
133     protected String mBaseDeviceModuleDir;
134     protected String mBaseDeviceShortDir;
135     protected String mBaseDeviceFullDir;
136 
137     /*
138      * Variables set by the MediaPreparerListener during retrieval of maximum media file
139      * resolution. After the MediaPreparerApp has been instrumented on the device:
140      *
141      * testMetrics contains the string representation of the resolution
142      * testFailures contains a stacktrace if retrieval of the resolution was unsuccessful
143      */
144     protected Resolution mMaxRes = null;
145     protected String mFailureStackTrace = null;
146 
147     /* User id that the test is running as. */
148     private int mUserId = -1;
149 
150     /** The module level configuration to check the target preparers. */
151     private IConfiguration mModuleConfiguration;
152 
153     /*
154      * The default name of local directory into which media files will be downloaded, if option
155      * "local-media-path" is not provided. This directory will live inside the temp directory.
156      */
157     protected static final String MEDIA_FOLDER_NAME = "android-cts-media";
158 
159     /* The key used to retrieve the media files URL from the dynamic configuration */
160     private static final String MEDIA_FILES_URL_KEY = "media_files_url";
161 
162     /*
163      * Info used to install and uninstall the MediaPreparerApp
164      */
165     private static final String APP_APK = "CtsMediaPreparerApp.apk";
166     private static final String APP_PKG_NAME = "android.mediastress.cts.preconditions.app";
167 
168     /* Key to retrieve resolution string in metrics upon MediaPreparerListener.testEnded() */
169     private static final String RESOLUTION_STRING_KEY = "resolution";
170 
171     protected static final Resolution[] RESOLUTIONS = {
172             new Resolution(176, 144),
173             new Resolution(480, 360),
174             new Resolution(720, 480),
175             new Resolution(1280, 720),
176             new Resolution(1920, 1080)
177     };
178 
179     /** {@inheritDoc} */
180     @Override
getDependencies()181     public Set<ExternalDependency> getDependencies() {
182         Set<ExternalDependency> dependencies = new HashSet<>();
183         if (!mSkipMediaDownload) {
184             dependencies.add(new NetworkDependency());
185         }
186         return dependencies;
187     }
188 
189     @Override
setConfiguration(IConfiguration configuration)190     public void setConfiguration(IConfiguration configuration) {
191         mModuleConfiguration = configuration;
192     }
193 
194     /** Helper class for generating and retrieving width-height pairs */
195     protected static final class Resolution {
196         // regex that matches a resolution string
197         private static final String PATTERN = "(\\d+)x(\\d+)";
198         // group indices for accessing resolution width and height from a PATTERN-based Matcher
199         private static final int WIDTH_INDEX = 1;
200         private static final int HEIGHT_INDEX = 2;
201 
202         private final int width;
203         private final int height;
204 
Resolution(int width, int height)205         private Resolution(int width, int height) {
206             this.width = width;
207             this.height = height;
208         }
209 
Resolution(String resolution)210         private Resolution(String resolution) {
211             Pattern pattern = Pattern.compile(PATTERN);
212             Matcher matcher = pattern.matcher(resolution);
213             matcher.find();
214             this.width = Integer.parseInt(matcher.group(WIDTH_INDEX));
215             this.height = Integer.parseInt(matcher.group(HEIGHT_INDEX));
216         }
217 
218         @Override
toString()219         public String toString() {
220             return String.format("%dx%d", width, height);
221         }
222 
223         /** Returns the width of the resolution. */
getWidth()224         public int getWidth() {
225             return width;
226         }
227     }
228 
getDefaultMediaDir()229     public static File getDefaultMediaDir() {
230         return new File(System.getProperty("java.io.tmpdir"), MEDIA_FOLDER_NAME);
231     }
232 
getMediaDir()233     protected File getMediaDir() {
234         return new File(System.getProperty("java.io.tmpdir"), mMediaFolderName);
235     }
236 
237     /*
238      * Returns true if all necessary media files exist on the device, and false otherwise.
239      *
240      * This method is exposed for unit testing.
241      */
242     @VisibleForTesting
mediaFilesExistOnDevice(ITestDevice device)243     protected boolean mediaFilesExistOnDevice(ITestDevice device)
244             throws DeviceNotAvailableException {
245         if (mPushAll) {
246             return device.doesFileExist(mBaseDeviceModuleDir, mUserId);
247         }
248         for (Resolution resolution : RESOLUTIONS) {
249             if (resolution.width > mMaxRes.width) {
250                 break; // no need to check for resolutions greater than this
251             }
252             String deviceShortFilePath = mBaseDeviceShortDir + resolution.toString();
253             String deviceFullFilePath = mBaseDeviceFullDir + resolution.toString();
254             if (!device.doesFileExist(deviceShortFilePath, mUserId)
255                     || !device.doesFileExist(deviceFullFilePath, mUserId)) {
256                 return false;
257             }
258         }
259         return true;
260     }
261 
262     protected static final String TOC_NAME = "contents.toc";
263 
264     /*
265      * After downloading and unzipping the media files, mLocalMediaPath must be the path to the
266      * directory containing 'bbb_short' and 'bbb_full' directories, as it is defined in its
267      * description as an option.
268      * After extraction, this directory exists one level below the the directory 'mediaFolder'.
269      * If the 'mediaFolder' contains anything other than exactly one subdirectory, a
270      * TargetSetupError is thrown. Otherwise, the mLocalMediaPath variable is set to the path of
271      * this subdirectory.
272      */
updateLocalMediaPath(ITestDevice device, File mediaFolder)273     private void updateLocalMediaPath(ITestDevice device, File mediaFolder)
274             throws TargetSetupError {
275         String[] entries = mediaFolder.list();
276 
277         // directory should contain:
278         // -- content subdirectory
279         // -- TOC (if we've run with the new caching semantics)
280         // if we've run new semantics, old semantics should ignore the TOC if present.
281         //
282         if (entries.length == 0) {
283             throw new TargetSetupError(
284                     String.format("Unexpectedly empty directory %s", mediaFolder.getAbsolutePath()),
285                     device.getDeviceDescriptor());
286         } else if (entries.length > 2) {
287             throw new TargetSetupError(String.format(
288                     "Unexpected contents in directory %s", mediaFolder.getAbsolutePath()),
289                     device.getDeviceDescriptor());
290         }
291 
292         // choose the entry that represents the contents to be sent, not the TOC
293         int slot = 0;
294         if (entries[slot].equals(TOC_NAME)) {
295             if (entries.length == 1) {
296                 throw new TargetSetupError(
297                         String.format(
298                                 "Missing contents in directory %s", mediaFolder.getAbsolutePath()),
299                         device.getDeviceDescriptor());
300             }
301             slot = 1;
302         }
303         mLocalMediaPath = new File(mediaFolder, entries[slot]).getAbsolutePath();
304     }
305 
generateDirectoryToc(FileWriter myWriter, File myFolder, String leadingPath)306     private void generateDirectoryToc(FileWriter myWriter, File myFolder, String leadingPath)
307             throws IOException {
308         String prefixPath;
309         if (leadingPath.equals("")) {
310             prefixPath = "";
311         } else {
312             prefixPath = leadingPath + File.separator;
313         }
314         for (String fileName : myFolder.list()) {
315             // list myself
316             myWriter.write(prefixPath + fileName + "\n");
317             // and recurse if i'm a directory
318             File oneFile = new File(myFolder, fileName);
319             if (oneFile.isDirectory()) {
320                 String newLeading = prefixPath + fileName;
321                 generateDirectoryToc(myWriter, oneFile, newLeading);
322             }
323         }
324     }
325 
326     /*
327      * Copies the media files to the host from a predefined URL.
328      *
329      * Synchronize this method so that multiple shards won't download/extract
330      * this file to the same location on the host. Only an issue in Android O and above,
331      * where MediaPreparer is used for multiple, shardable modules.
332      */
downloadMediaToHost(ITestDevice device, IBuildInfo buildInfo)333     private File downloadMediaToHost(ITestDevice device, IBuildInfo buildInfo)
334             throws TargetSetupError {
335 
336         // Make sure the synchronization is on the class and not the object
337         synchronized (MediaPreparer.class) {
338             // Retrieve default directory for storing media files
339             File mediaFolder = getMediaDir();
340 
341             // manage caching the content on the host side
342             //
343             if (mediaFolder.exists() && mediaFolder.list().length > 0) {
344                 // Folder has been created and populated by a previous MediaPreparer run.
345                 //
346 
347                 if (mSimpleCachingSemantics) {
348                     // old semantics: assumes all necessary media files exist inside
349                     CLog.i("old cache semantics: local directory exists, all is well");
350                     return mediaFolder;
351                 }
352 
353                 CLog.i("new cache semantics: verify against a TOC");
354                 // new caching semantics:
355                 // verify that the contents are still present.
356                 // use the TOC file generated when first downloaded/unpacked.
357                 // if TOC or any files are missing -- redownload.
358                 //
359                 // we're chatty about why we decide to re-download
360 
361                 boolean passing = true;
362                 BufferedReader tocReader = null;
363                 try {
364                     File tocFile = new File(mediaFolder, TOC_NAME);
365                     if (!tocFile.exists()) {
366                         passing = false;
367                         CLog.i(
368                                 "missing/inaccessible TOC: "
369                                         + mediaFolder
370                                         + File.separator
371                                         + TOC_NAME);
372                     } else {
373                         tocReader = new BufferedReader(new FileReader(tocFile));
374                         String line = tocReader.readLine();
375                         while (line != null) {
376                             File oneFile = new File(mediaFolder, line);
377                             if (!oneFile.exists()) {
378                                 CLog.i(
379                                         "missing TOC-listed file: "
380                                                 + mediaFolder
381                                                 + File.separator
382                                                 + line);
383                                 passing = false;
384                                 break;
385                             }
386                             line = tocReader.readLine();
387                         }
388                     }
389                 } catch (IOException | SecurityException | NullPointerException e) {
390                     CLog.i("TOC or contents missing, redownload");
391                     passing = false;
392                 } finally {
393                     StreamUtil.close(tocReader);
394                 }
395 
396                 if (passing) {
397                     CLog.i("Host-cached copy is complete in " + mediaFolder);
398                     return mediaFolder;
399                 }
400             }
401 
402             // uncached (or broken cache), so download again
403 
404             mediaFolder.mkdirs();
405             URL url;
406             try {
407                 // Get download URL from dynamic configuration service
408                 String mediaUrlString =
409                         DynamicConfigFileReader.getValueFromConfig(
410                                 buildInfo, mDynamicConfigModule, MEDIA_FILES_URL_KEY);
411                 url = new URL(mediaUrlString);
412             } catch (IOException | XmlPullParserException e) {
413                 throw new TargetSetupError(
414                         "Trouble finding media file download location with "
415                                 + "dynamic configuration",
416                         e,
417                         device.getDeviceDescriptor());
418             }
419             File mediaFolderZip = new File(mediaFolder.getAbsolutePath() + ".zip");
420             FileWriter tocWriter = null;
421             try {
422                 CLog.i("Downloading media files from %s", url.toString());
423                 URLConnection conn = url.openConnection();
424                 InputStream in = conn.getInputStream();
425                 mediaFolderZip.createNewFile();
426                 FileUtil.writeToFile(in, mediaFolderZip);
427                 CLog.i("Unzipping media files");
428                 ZipUtil.extractZip(new ZipFile(mediaFolderZip), mediaFolder);
429 
430                 // create the TOC when running the new caching scheme
431                 if (!mSimpleCachingSemantics) {
432                     // create a TOC, recursively listing all files/directories.
433                     // used to verify all files still exist before we re-use a prior copy
434                     CLog.i("Generating cache TOC");
435                     File tocFile = new File(mediaFolder, TOC_NAME);
436                     tocWriter = new FileWriter(tocFile, /*append*/ false);
437                     generateDirectoryToc(tocWriter, mediaFolder, "");
438                 }
439 
440             } catch (IOException e) {
441                 FileUtil.recursiveDelete(mediaFolder);
442                 throw new TargetSetupError(
443                         String.format(
444                                 "Failed to download and open media files on host machine at '%s'."
445                                     + " These media files are required for compatibility tests.",
446                                 mediaFolderZip),
447                         e,
448                         device.getDeviceDescriptor(),
449                         /* device side */ false);
450             } finally {
451                 FileUtil.deleteFile(mediaFolderZip);
452                 StreamUtil.close(tocWriter);
453             }
454             return mediaFolder;
455         }
456     }
457 
458     /*
459      * Pushes directories containing media files to the device for all directories that:
460      * - are not already present on the device
461      * - contain video files of a resolution less than or equal to the device's
462      *       max video playback resolution
463      *
464      * This method is exposed for unit testing.
465      */
copyMediaFiles(ITestDevice device)466     protected void copyMediaFiles(ITestDevice device) throws DeviceNotAvailableException {
467         if (mPushAll) {
468             copyAll(device);
469             return;
470         }
471         copyVideoFiles(device);
472     }
473 
474     // copy video files of a resolution <= the device's maximum video playback resolution
copyVideoFiles(ITestDevice device)475     protected void copyVideoFiles(ITestDevice device) throws DeviceNotAvailableException {
476         for (Resolution resolution : RESOLUTIONS) {
477             if (resolution.width > mMaxRes.width) {
478                 CLog.i("Media file copying complete");
479                 return;
480             }
481             String deviceShortFilePath = mBaseDeviceShortDir + resolution.toString();
482             String deviceFullFilePath = mBaseDeviceFullDir + resolution.toString();
483             if (!device.doesFileExist(deviceShortFilePath, mUserId)
484                     || !device.doesFileExist(deviceFullFilePath, mUserId)) {
485                 CLog.i("Copying files of resolution %s to device", resolution.toString());
486                 String localShortDirName = "bbb_short/" + resolution.toString();
487                 String localFullDirName = "bbb_full/" + resolution.toString();
488                 File localShortDir = new File(mLocalMediaPath, localShortDirName);
489                 File localFullDir = new File(mLocalMediaPath, localFullDirName);
490                 // push short directory of given resolution, if not present on device
491                 if (!device.doesFileExist(deviceShortFilePath, mUserId)) {
492                     device.pushDir(localShortDir, deviceShortFilePath, mUserId);
493                 }
494                 // push full directory of given resolution, if not present on device
495                 if (!device.doesFileExist(deviceFullFilePath, mUserId)) {
496                     device.pushDir(localFullDir, deviceFullFilePath, mUserId);
497                 }
498             }
499         }
500     }
501 
502     // copy everything from the host directory to the device
copyAll(ITestDevice device)503     protected void copyAll(ITestDevice device) throws DeviceNotAvailableException {
504         if (!device.doesFileExist(mBaseDeviceModuleDir, mUserId)) {
505             CLog.i("Copying files to device");
506             device.pushDir(new File(mLocalMediaPath), mBaseDeviceModuleDir, mUserId);
507         }
508     }
509 
510     // Initialize directory strings where media files live on device
setMountPoint(ITestDevice device)511     protected void setMountPoint(ITestDevice device) {
512         String mountPoint = device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
513         mBaseDeviceModuleDir = String.format("%s/test/%s/", mountPoint, mMediaFolderName);
514         if (mUseLegacyFolderStructure) {
515             mBaseDeviceShortDir = String.format("%s/test/bbb_short/", mountPoint);
516             mBaseDeviceFullDir = String.format("%s/test/bbb_full/", mountPoint);
517         } else {
518             mBaseDeviceShortDir = String.format("%s/test/%s/bbb_short/", mountPoint,
519                     mMediaFolderName);
520             mBaseDeviceFullDir = String.format("%s/test/%s/bbb_full/", mountPoint,
521                     mMediaFolderName);
522         }
523     }
524 
525     @Override
setUp(TestInformation testInfo)526     public void setUp(TestInformation testInfo)
527             throws TargetSetupError, BuildError, DeviceNotAvailableException {
528         ITestDevice device = testInfo.getDevice();
529         IBuildInfo buildInfo = testInfo.getBuildInfo();
530         mUserId = getRunTestsAsUser(testInfo);
531         if (mSkipMediaDownload) {
532             CLog.i("Skipping media preparation");
533             return; // skip this precondition
534         }
535 
536         if (!mMediaDownloadOnly) {
537             setMountPoint(device);
538             if (!mPushAll) {
539                 setMaxRes(testInfo); // max resolution only applies to video files
540             }
541             if (mediaFilesExistOnDevice(device)) {
542                 // if files already on device, do nothing
543                 CLog.i("Media files found on the device");
544                 return;
545             }
546         }
547 
548         if (mLocalMediaPath == null) {
549             // Option 'local-media-path' has not been defined
550             // Get directory to store media files on this host
551             File mediaFolder = downloadMediaToHost(device, buildInfo);
552             // set mLocalMediaPath to extraction location of media files
553             updateLocalMediaPath(device, mediaFolder);
554         }
555         CLog.i("Media files located on host at: " + mLocalMediaPath);
556         if (!mMediaDownloadOnly) {
557             copyMediaFiles(device);
558         }
559     }
560 
561     @VisibleForTesting
setUserId(int testUser)562     protected void setUserId(int testUser) {
563         mUserId = testUser;
564     }
565 
566     // Initialize maximum resolution of media files to copy
567     @VisibleForTesting
setMaxRes(TestInformation testInfo)568     protected void setMaxRes(TestInformation testInfo)
569             throws DeviceNotAvailableException, TargetSetupError {
570         ITestInvocationListener listener = new MediaPreparerListener();
571         ITestDevice device = testInfo.getDevice();
572         IBuildInfo buildInfo = testInfo.getBuildInfo();
573         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo);
574         File apkFile = null;
575         try {
576             apkFile = buildHelper.getTestFile(APP_APK);
577             if (!apkFile.exists()) {
578                 // handle both missing tests dir and missing APK in catch block
579                 throw new FileNotFoundException();
580             }
581         } catch (FileNotFoundException e) {
582             throw new TargetSetupError(
583                     String.format("Could not find '%s'", APP_APK),
584                     InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
585         }
586         if (device.getAppPackageInfo(APP_PKG_NAME) != null) {
587             device.uninstallPackage(APP_PKG_NAME);
588         }
589         CLog.i("Instrumenting package %s:", APP_PKG_NAME);
590         // We usually discourage from referencing the content provider utility
591         // but in this case, the helper needs it installed.
592         new ContentProviderHandler(device, mUserId).setUp();
593         AndroidJUnitTest instrTest = new AndroidJUnitTest();
594         instrTest.setDevice(device);
595         instrTest.setInstallFile(apkFile);
596         instrTest.setPackageName(APP_PKG_NAME);
597         String moduleName = getDynamicModuleName();
598         if (moduleName != null) {
599             instrTest.addInstrumentationArg("module-name", moduleName);
600         }
601         // AndroidJUnitTest requires a IConfiguration to work properly, add a stub to this
602         // implementation to avoid an NPE.
603         instrTest.setConfiguration(new Configuration("stub", "stub"));
604         instrTest.run(testInfo, listener);
605         if (mFailureStackTrace != null) {
606             throw new TargetSetupError(
607                     String.format(
608                             "Retrieving maximum resolution failed with trace:\n%s",
609                             mFailureStackTrace),
610                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
611         } else if (mMaxRes == null) {
612             throw new TargetSetupError(
613                     String.format("Failed to pull resolution capabilities from device"),
614                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
615         }
616     }
617 
618     /* Special listener for setting MediaPreparer instance variable values */
619     private class MediaPreparerListener implements ITestInvocationListener {
620 
621         @Override
testEnded(TestDescription test, HashMap<String, Metric> metrics)622         public void testEnded(TestDescription test, HashMap<String, Metric> metrics) {
623             Metric resMetric = metrics.get(RESOLUTION_STRING_KEY);
624             if (resMetric != null) {
625                 mMaxRes = new Resolution(resMetric.getMeasurements().getSingleString());
626             }
627         }
628 
629         @Override
testFailed(TestDescription test, String trace)630         public void testFailed(TestDescription test, String trace) {
631             mFailureStackTrace = trace;
632         }
633     }
634 
635     @VisibleForTesting
getDynamicModuleName()636     protected String getDynamicModuleName() throws TargetSetupError {
637         String moduleName = null;
638         boolean sameDevice = false;
639         for (IDeviceConfiguration deviceConfig : mModuleConfiguration.getDeviceConfig()) {
640             for (ITargetPreparer prep : deviceConfig.getTargetPreparers()) {
641                 if (prep instanceof DynamicConfigPusher) {
642                     moduleName = ((DynamicConfigPusher) prep).createModuleName();
643                     if (sameDevice) {
644                         throw new TargetSetupError(
645                                 "DynamicConfigPusher needs to be configured before MediaPreparer"
646                                         + " in your module configuration.",
647                                 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
648                     }
649                 }
650                 if (prep.equals(this)) {
651                     sameDevice = true;
652                     if (moduleName != null) {
653                         return moduleName;
654                     }
655                 }
656             }
657             moduleName = null;
658             sameDevice = false;
659         }
660         return null;
661     }
662 }
663