1 /*
2  * Copyright (C) 2019 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.tradefed.targetprep.multi;
17 
18 import com.android.tradefed.build.IBuildInfo;
19 import com.android.tradefed.build.IBuildInfo.BuildInfoProperties;
20 import com.android.tradefed.build.IDeviceBuildInfo;
21 import com.android.tradefed.config.Option;
22 import com.android.tradefed.config.OptionClass;
23 import com.android.tradefed.device.DeviceNotAvailableException;
24 import com.android.tradefed.device.ITestDevice;
25 import com.android.tradefed.invoker.IInvocationContext;
26 import com.android.tradefed.invoker.TestInformation;
27 import com.android.tradefed.log.LogUtil.CLog;
28 import com.android.tradefed.result.error.InfraErrorIdentifier;
29 import com.android.tradefed.targetprep.BuildError;
30 import com.android.tradefed.targetprep.TargetSetupError;
31 import com.android.tradefed.util.CommandResult;
32 import com.android.tradefed.util.CommandStatus;
33 import com.android.tradefed.util.FileUtil;
34 import com.android.tradefed.util.IRunUtil;
35 import com.android.tradefed.util.RunUtil;
36 import com.android.tradefed.util.StreamUtil;
37 import com.android.tradefed.util.ZipUtil;
38 import com.google.common.annotations.VisibleForTesting;
39 import java.io.BufferedInputStream;
40 import java.io.BufferedOutputStream;
41 import java.io.ByteArrayInputStream;
42 import java.io.File;
43 import java.io.FileInputStream;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.OutputStream;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collection;
51 import java.util.Enumeration;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Map.Entry;
56 import java.util.Set;
57 import java.util.TreeSet;
58 import java.util.function.Predicate;
59 import java.util.zip.Deflater;
60 import java.util.zip.ZipEntry;
61 import java.util.zip.ZipFile;
62 import java.util.zip.ZipOutputStream;
63 
64 /** An {@link IMultiTargetPreparer} that mixes a system build's images in a device build. */
65 @OptionClass(alias = "mix-image-zip")
66 public class MixImageZipPreparer extends BaseMultiTargetPreparer {
67 
68     @Option(name = "device-label", description = "the label for the device.")
69     private String mDeviceLabel = "device";
70 
71     @Option(
72         name = "system-label",
73         description = "the label for the null-device used to store the system image information."
74     )
75     private String mSystemLabel = "system";
76 
77     @Option(
78         name = "resource-label",
79         description = "the label for the null-device used to store the extra build information."
80     )
81     private String mResourceLabel = "resource";
82 
83     @Option(
84         name = "extra-build-test-resource-name",
85         description =
86                 "the name of the extra build file copied to device build. " + "Can be repeated."
87     )
88     private Set<String> mExtraBuildResourceFiles = new TreeSet<>();
89 
90     @Option(
91             name = "system-build-file-name",
92             description =
93                     "the name of the image file copied from system build to device build. "
94                             + "Can be repeated.")
95     private Set<String> mSystemFileNames = new TreeSet<>();
96 
97     @Option(
98             name = "system-build-file-name-map",
99             description =
100                     "the file name in the device image zip to be replaced with the file with name "
101                             + "in the system build image zip. For example boot.img=boot-5.4.img. "
102                             + "Can be repeated.")
103     private Map<String, String> mSystemFileNameMaps = new HashMap<>();
104 
105     @Option(
106             name = "stub-file-name",
107             description =
108                     "the name of the image file to be replaced with a small stub file. "
109                             + "Can be repeated. This option is used when the generic system "
110                             + "image is too large for the device's dynamic partition. "
111                             + "As GSI doesn't use product partition, the product image can be "
112                             + "replaced with a stub file so as to free up space for GSI.")
113     private Set<String> mStubFileNames = new TreeSet<>();
114 
115     @Option(
116         name = "compression-level",
117         description =
118                 "the compression level of the mixed image zip. It is an integer between 0 "
119                         + "and 9. Larger value indicates longer time and smaller output."
120     )
121     private int mCompressionLevel = Deflater.DEFAULT_COMPRESSION;
122 
123     @Option(
124             name = "misc-info-path",
125             description =
126                     "the misc info file for repacking super image. By default, this preparer "
127                             + "retrieves the file from device build.")
128     private File mMiscInfoFile = null;
129 
130     @Option(
131             name = "ota-tools-path",
132             description =
133                     "the zip file containing the tools for repacking super image. By default, "
134                             + "this preparer retrieves the file from system build.")
135     private File mOtaToolsZip = null;
136 
137     @Option(
138             name = "repack-super-image-path",
139             description =
140                     "the script that repacks the super image. By default, this preparer "
141                             + "retrieves the file from system build. In build environment, the "
142                             + "script is located at development/gsi/repack_super_image, and can "
143                             + "be built by `make repack_super_image` or `make dist gsi_utils`.")
144     private File mRepackSuperImageFile = null;
145 
146     // Build info file keys.
147     private static final String SUPER_IMAGE_NAME = "super.img";
148     private static final String OTATOOLS_ZIP_NAME = "otatools.zip";
149     private static final String MISC_INFO_FILE_NAME = "misc_info.txt";
150     private static final String REPACK_SUPER_IMAGE_FILE_NAME = "repack_super_image";
151 
152     /** The interface that creates {@link InputStream} from a file or a compressed file. */
153     @VisibleForTesting
154     static interface InputStreamFactory {
155         /** Create a new stream. The caller should close it. */
createInputStream()156         InputStream createInputStream() throws IOException;
157 
158         /** Return the uncompressed size of the data. */
getSize()159         long getSize();
160 
161         /** Return the CRC32 of the data. */
getCrc32()162         long getCrc32() throws IOException;
163     }
164 
165     private static class FileInputStreamFactory implements InputStreamFactory {
166         private File mFile;
167 
FileInputStreamFactory(File file)168         FileInputStreamFactory(File file) {
169             mFile = file;
170         }
171 
172         @Override
createInputStream()173         public InputStream createInputStream() throws IOException {
174             return new FileInputStream(mFile);
175         }
176 
177         @Override
getSize()178         public long getSize() {
179             return mFile.length();
180         }
181 
182         @Override
getCrc32()183         public long getCrc32() throws IOException {
184             return FileUtil.calculateCrc32(mFile);
185         }
186     }
187 
188     @Override
setUp(TestInformation testInformation)189     public void setUp(TestInformation testInformation)
190             throws TargetSetupError, BuildError, DeviceNotAvailableException {
191         IInvocationContext context = testInformation.getContext();
192 
193         ITestDevice device = context.getDevice(mDeviceLabel);
194         IDeviceBuildInfo deviceBuildInfo = (IDeviceBuildInfo) context.getBuildInfo(device);
195 
196         ITestDevice systemNullDevice = context.getDevice(mSystemLabel);
197         IDeviceBuildInfo systemBuildInfo =
198                 (IDeviceBuildInfo) context.getBuildInfo(systemNullDevice);
199 
200         IBuildInfo resourceBuildInfo = null;
201         if (!mExtraBuildResourceFiles.isEmpty()) {
202             ITestDevice resourceNullDevice = context.getDevice(mResourceLabel);
203             resourceBuildInfo = context.getBuildInfo(resourceNullDevice);
204         }
205 
206         ZipFile deviceImageZip = null;
207         ZipFile systemImageZip = null;
208         File mixedSuperImage = null;
209         File mixedImageZip = null;
210         try {
211             deviceImageZip = new ZipFile(deviceBuildInfo.getDeviceImageFile());
212 
213             // Get all files from device build.
214             Map<String, InputStreamFactory> files =
215                     getInputStreamFactoriesFromImageZip(deviceImageZip, file -> true);
216             Map<String, InputStreamFactory> filesNotInDeviceBuild =
217                     new HashMap<String, InputStreamFactory>();
218 
219             // Map system build file names to contents by file name.
220             systemImageZip = new ZipFile(systemBuildInfo.getDeviceImageFile());
221             Map<String, InputStreamFactory> systemFiles =
222                     getInputStreamFactoriesFromImageZip(
223                             systemImageZip, file -> mSystemFileNames.contains(file));
224 
225             // Map system build file names to contents by file name map values
226             Map<String, InputStreamFactory> extraSystemFiles =
227                     getInputStreamFactoriesFromImageZip(
228                             systemImageZip, file -> mSystemFileNameMaps.containsValue(file));
229             // Map device build file names to contents.
230             for (Entry<String, String> entry : mSystemFileNameMaps.entrySet()) {
231                 InputStreamFactory value = extraSystemFiles.get(entry.getValue());
232                 if (value == null) {
233                     throw new BuildError(
234                             "Cannot find " + entry.getValue() + " in system build image zip.",
235                             device.getDeviceDescriptor(),
236                             InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
237                 }
238                 systemFiles.put(entry.getKey(), value);
239             }
240 
241             // Replace files in device build.
242             systemFiles = replaceExistingEntries(systemFiles, files);
243             filesNotInDeviceBuild.putAll(systemFiles);
244 
245             // Generate specified stub files and replace those in device build.
246             Map<String, InputStreamFactory> stubFiles =
247                     createStubInputStreamFactories(mStubFileNames);
248             Map<String, InputStreamFactory> stubFilesNotInDeviceBuild =
249                     replaceExistingEntries(stubFiles, files);
250             // The purpose of the stub files is to make fastboot shrink product partition.
251             // Some devices don't have product partition and image. If the stub file names are not
252             // found in device build, they are ignored so that devices with and without product
253             // partition can share configurations.
254             // This preparer does not generate stub files in super image because
255             // build_super_image cannot handle unformatted files.
256             if (!stubFilesNotInDeviceBuild.isEmpty()) {
257                 CLog.w(
258                         "Skip creating stub images: %s",
259                         String.join(",", stubFilesNotInDeviceBuild.keySet()));
260             }
261 
262             if (resourceBuildInfo != null) {
263                 // Get specified files from resource build and replace those in device build.
264                 Map<String, InputStreamFactory> resourceFiles =
265                         getBuildFiles(resourceBuildInfo, mExtraBuildResourceFiles);
266                 resourceFiles = replaceExistingEntries(resourceFiles, files);
267                 filesNotInDeviceBuild.putAll(resourceFiles);
268             }
269 
270             if (files.containsKey(SUPER_IMAGE_NAME) && !filesNotInDeviceBuild.isEmpty()) {
271                 CLog.i("Mix %s in super image.", String.join(", ", filesNotInDeviceBuild.keySet()));
272 
273                 File miscInfoFile = mMiscInfoFile;
274                 if (miscInfoFile == null) {
275                     miscInfoFile = deviceBuildInfo.getFile(MISC_INFO_FILE_NAME);
276                 }
277                 if (miscInfoFile == null) {
278                     throw new BuildError(
279                             "Cannot get " + MISC_INFO_FILE_NAME + " from device build.",
280                             device.getDeviceDescriptor(),
281                             InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
282                 }
283 
284                 File otaToolsZip = mOtaToolsZip;
285                 if (otaToolsZip == null) {
286                     otaToolsZip = systemBuildInfo.getFile(OTATOOLS_ZIP_NAME);
287                 }
288                 if (otaToolsZip == null) {
289                     throw new BuildError(
290                             "Cannot get " + OTATOOLS_ZIP_NAME + " from system build.",
291                             systemNullDevice.getDeviceDescriptor(),
292                             InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
293                 }
294 
295                 File repackSuperImageFile = mRepackSuperImageFile;
296                 if (repackSuperImageFile == null) {
297                     repackSuperImageFile = systemBuildInfo.getFile(REPACK_SUPER_IMAGE_FILE_NAME);
298                 }
299                 if (repackSuperImageFile == null) {
300                     throw new BuildError(
301                             "Cannot get " + REPACK_SUPER_IMAGE_FILE_NAME + " from system build.",
302                             systemNullDevice.getDeviceDescriptor(),
303                             InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
304                 }
305 
306                 mixedSuperImage = FileUtil.createTempFile("super", ".img");
307                 repackSuperImage(
308                         repackSuperImageFile,
309                         otaToolsZip,
310                         miscInfoFile,
311                         files.get(SUPER_IMAGE_NAME),
312                         filesNotInDeviceBuild,
313                         mixedSuperImage);
314                 files.put(SUPER_IMAGE_NAME, new FileInputStreamFactory(mixedSuperImage));
315                 // The command ensures that all input images are used.
316                 filesNotInDeviceBuild.clear();
317             }
318 
319             if (!filesNotInDeviceBuild.isEmpty()) {
320                 throw new TargetSetupError(
321                         String.join(",", filesNotInDeviceBuild.keySet()) + " not in device build.",
322                         device.getDeviceDescriptor());
323             }
324 
325             CLog.d("Create mixed image zip.");
326             mixedImageZip = createZip(files, mCompressionLevel);
327         } catch (IOException e) {
328             throw new TargetSetupError(
329                     "Could not create mixed image zip", e, device.getDeviceDescriptor());
330         } finally {
331             FileUtil.deleteFile(mixedSuperImage);
332             ZipUtil.closeZip(deviceImageZip);
333             ZipUtil.closeZip(systemImageZip);
334         }
335 
336         IBuildInfo mixedBuildInfo =
337                 createBuildCopy(
338                         deviceBuildInfo,
339                         systemBuildInfo.getBuildFlavor(),
340                         systemBuildInfo.getBuildId(),
341                         mixedImageZip);
342         // Replace the build
343         context.addDeviceBuildInfo(mDeviceLabel, mixedBuildInfo);
344         // Clean up the original build
345         deviceBuildInfo.cleanUp();
346     }
347 
348     /**
349      * Get {@link InputStreamFactory} from entries in an image zip. The zip must not be closed when
350      * the returned {@link InputStreamFactory} are in use.
351      *
352      * @param zipFile image zip.
353      * @param predicate function that takes a file name as the argument and determines whether the
354      *     file name and the content should be added to the output map.
355      * @return map from file name to {@link InputStreamFactory}.
356      * @throws IOException if fails to create the temporary directory.
357      */
getInputStreamFactoriesFromImageZip( final ZipFile zipFile, Predicate<String> predicate)358     private static Map<String, InputStreamFactory> getInputStreamFactoriesFromImageZip(
359             final ZipFile zipFile, Predicate<String> predicate) throws IOException {
360         Map<String, InputStreamFactory> factories = new HashMap<String, InputStreamFactory>();
361         Enumeration<? extends ZipEntry> entries = zipFile.entries();
362         while (entries.hasMoreElements()) {
363             final ZipEntry entry = entries.nextElement();
364             if (entry.isDirectory()) {
365                 CLog.w("Image zip contains subdirectory %s.", entry.getName());
366                 continue;
367             }
368 
369             String name = new File(entry.getName()).getName();
370             if (!predicate.test(name)) {
371                 continue;
372             }
373 
374             if (entry.getSize() < 0) {
375                 throw new IllegalArgumentException("Invalid size.");
376             }
377             if (entry.getCrc() < 0) {
378                 throw new IllegalArgumentException("Invalid CRC value.");
379             }
380 
381             factories.put(
382                     name,
383                     new InputStreamFactory() {
384                         @Override
385                         public InputStream createInputStream() throws IOException {
386                             return zipFile.getInputStream(entry);
387                         }
388 
389                         @Override
390                         public long getSize() {
391                             return entry.getSize();
392                         }
393 
394                         @Override
395                         public long getCrc32() {
396                             return entry.getCrc();
397                         }
398                     });
399         }
400         return factories;
401     }
402 
createStubInputStreamFactories( Collection<String> stubFileNames)403     private static Map<String, InputStreamFactory> createStubInputStreamFactories(
404             Collection<String> stubFileNames) {
405         // The image size must be larger than zero. Otherwise fastboot cannot flash it.
406         byte[] data = new byte[] {0};
407         Map<String, InputStreamFactory> factories = new HashMap<>();
408         for (String stubFileName : stubFileNames) {
409             factories.put(
410                     stubFileName,
411                     new InputStreamFactory() {
412                         @Override
413                         public InputStream createInputStream() throws IOException {
414                             return new ByteArrayInputStream(data);
415                         }
416 
417                         @Override
418                         public long getSize() {
419                             return data.length;
420                         }
421 
422                         @Override
423                         public long getCrc32() throws IOException {
424                             // calculateCrc32 closes the stream.
425                             return StreamUtil.calculateCrc32(createInputStream());
426                         }
427                     });
428         }
429         return factories;
430     }
431 
432     /**
433      * Get {@link InputStreamFactory} from {@link IBuildInfo} by name.
434      *
435      * @param buildInfo {@link IBuildInfo} that contains files.
436      * @param buildFileNames collection of file names.
437      * @return map from file name to {@link InputStreamFactory}.
438      * @throws IOException if fails to get files from the build info.
439      */
getBuildFiles( IBuildInfo buildInfo, Collection<String> buildFileNames)440     private static Map<String, InputStreamFactory> getBuildFiles(
441             IBuildInfo buildInfo, Collection<String> buildFileNames) throws IOException {
442         Map<String, InputStreamFactory> factories = new HashMap<String, InputStreamFactory>();
443         for (String fileName : buildFileNames) {
444             final File file = buildInfo.getFile(fileName);
445             if (file == null) {
446                 throw new IOException(String.format("Could not get file with name: %s", fileName));
447             }
448             factories.put(fileName, new FileInputStreamFactory(file));
449         }
450         return factories;
451     }
452 
initStoredZipEntry(ZipEntry entry, InputStreamFactory factory)453     private static void initStoredZipEntry(ZipEntry entry, InputStreamFactory factory)
454             throws IOException {
455         entry.setMethod(ZipOutputStream.STORED);
456         entry.setCompressedSize(factory.getSize());
457         entry.setSize(factory.getSize());
458         entry.setCrc(factory.getCrc32());
459     }
460 
461     /**
462      * Create a zip file from {@link InputStreamFactory} instances.
463      *
464      * @param factories the map where the keys are the entry names and the values provide the data
465      *     to be compressed.
466      * @param compressionLevel an integer between 0 and 9. If the value is 0, this method creates
467      *     {@link ZipOutputStream#STORED} entries instead of default ones.
468      * @return the created zip file in temporary directory.
469      * @throws IOException if any file operation fails.
470      */
471     @VisibleForTesting
createZip(Map<String, ? extends InputStreamFactory> factories, int compressionLevel)472     static File createZip(Map<String, ? extends InputStreamFactory> factories, int compressionLevel)
473             throws IOException {
474         File zipFile = null;
475         OutputStream out = null;
476         try {
477             zipFile = FileUtil.createTempFile("MixedImg", ".zip");
478             out = new FileOutputStream(zipFile);
479             out = new BufferedOutputStream(out);
480             out = new ZipOutputStream(out);
481             ZipOutputStream zipOut = (ZipOutputStream) out;
482             zipOut.setLevel(compressionLevel);
483 
484             for (Map.Entry<String, ? extends InputStreamFactory> factory : factories.entrySet()) {
485                 ZipEntry entry = new ZipEntry(factory.getKey());
486                 // STORED is faster than the default DEFLATED in no compression mode.
487                 if (compressionLevel == Deflater.NO_COMPRESSION) {
488                     // STORED requires size and CRC-32 to be set before putNextEntry.
489                     // In some versions of Java, ZipEntry.getSize() returns (1L << 32) - 1
490                     // if the original size is larger than or equal to 4GB.
491                     // This condition avoids using the wrong size value.
492                     if (factory.getValue().getSize() != (1L << 32) - 1) {
493                         initStoredZipEntry(entry, factory.getValue());
494                     }
495                 }
496                 zipOut.putNextEntry(entry);
497                 try (InputStream in =
498                         new BufferedInputStream(factory.getValue().createInputStream())) {
499                     StreamUtil.copyStreams(in, zipOut);
500                 }
501                 zipOut.closeEntry();
502             }
503 
504             File returnValue = zipFile;
505             zipFile = null;
506             return returnValue;
507         } finally {
508             StreamUtil.close(out);
509             FileUtil.deleteFile(zipFile);
510         }
511     }
512 
513     /**
514      * Execute a script that unpacks a super image, replaces the unpacked images, and makes a new
515      * super image.
516      *
517      * @param repackSuperImageFile the script to be executed.
518      * @param otaToolsZip the OTA tools zip.
519      * @param miscInfoFile the misc info file.
520      * @param superImage the original super image.
521      * @param replacement the images that replace the ones in the super image.
522      * @param outputFile the output super image.
523      * @throws IOException if any file operation fails.
524      */
repackSuperImage( File repackSuperImageFile, File otaToolsZip, File miscInfoFile, InputStreamFactory superImage, Map<String, InputStreamFactory> replacement, File outputFile)525     private void repackSuperImage(
526             File repackSuperImageFile,
527             File otaToolsZip,
528             File miscInfoFile,
529             InputStreamFactory superImage,
530             Map<String, InputStreamFactory> replacement,
531             File outputFile)
532             throws IOException {
533         if (!repackSuperImageFile.canExecute()) {
534             if (!repackSuperImageFile.setExecutable(true, false)) {
535                 CLog.w("Fail to set %s to be executable.", repackSuperImageFile);
536             }
537         }
538 
539         try (InputStream imageStream = superImage.createInputStream()) {
540             FileUtil.writeToFile(imageStream, outputFile);
541         }
542 
543         CommandResult result = null;
544         List<File> tempFiles = new ArrayList<File>();
545         File tempDir = null;
546         try {
547             tempDir = FileUtil.createTempDir("RepackSuperImage");
548             List<String> command =
549                     new ArrayList<String>(
550                             Arrays.asList(
551                                     repackSuperImageFile.getAbsolutePath(),
552                                     "--temp-dir",
553                                     tempDir.getAbsolutePath(),
554                                     "--ota-tools",
555                                     otaToolsZip.getAbsolutePath(),
556                                     "--misc-info",
557                                     miscInfoFile.getAbsolutePath(),
558                                     outputFile.getAbsolutePath()));
559 
560             for (Map.Entry<String, InputStreamFactory> entry : replacement.entrySet()) {
561                 String partitionName = FileUtil.getBaseName(entry.getKey());
562                 File imageFile = FileUtil.createTempFile(partitionName, ".img");
563                 tempFiles.add(imageFile);
564                 try (InputStream imageStream = entry.getValue().createInputStream()) {
565                     FileUtil.writeToFile(imageStream, imageFile);
566                 }
567                 command.add(partitionName + "=" + imageFile.getAbsolutePath());
568             }
569             result = createRunUtil().runTimedCmd(4 * 60 * 1000, command.toArray(new String[0]));
570         } finally {
571             for (File tempFile : tempFiles) {
572                 FileUtil.deleteFile(tempFile);
573             }
574             FileUtil.recursiveDelete(tempDir);
575         }
576 
577         CLog.d("Repack super image stdout:\n%s", result.getStdout());
578         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
579             throw new IOException("Fail to repack super image. stderr:\n" + result.getStderr());
580         }
581         CLog.d("Repack super image stderr:\n%s", result.getStderr());
582     }
583 
createBuildCopy( IDeviceBuildInfo deviceBuildInfo, String buildFlavor, String buildId, File imageZip)584     private static IBuildInfo createBuildCopy(
585             IDeviceBuildInfo deviceBuildInfo, String buildFlavor, String buildId, File imageZip) {
586         deviceBuildInfo.setProperties(BuildInfoProperties.DO_NOT_COPY_IMAGE_FILE);
587         IDeviceBuildInfo newBuildInfo = (IDeviceBuildInfo) deviceBuildInfo.clone();
588         newBuildInfo.setBuildFlavor(buildFlavor);
589         newBuildInfo.setDeviceImageFile(imageZip, buildId);
590         return newBuildInfo;
591     }
592 
593     /**
594      * Replace the values if the keys exists in the map.
595      *
596      * @param replacement the map containing the entries to be added to the original map.
597      * @param original the map whose entries are replaced.
598      * @return the entries which are in the replacement map but not added to the original map.
599      */
replaceExistingEntries( Map<String, T> replacement, Map<String, T> original)600     private static <T> Map<String, T> replaceExistingEntries(
601             Map<String, T> replacement, Map<String, T> original) {
602         Map<String, T> remaining = new HashMap<String, T>();
603         for (Map.Entry<String, T> entry : replacement.entrySet()) {
604             String key = entry.getKey();
605             if (original.containsKey(key)) {
606                 original.put(key, entry.getValue());
607             } else {
608                 remaining.put(key, entry.getValue());
609             }
610         }
611         return remaining;
612     }
613 
614     @VisibleForTesting
addSystemFileName(String fileName)615     void addSystemFileName(String fileName) {
616         mSystemFileNames.add(fileName);
617     }
618 
619     @VisibleForTesting
addSystemFileNameMap(String fileNameInDeviceZip, String fileNameInSystemZip)620     void addSystemFileNameMap(String fileNameInDeviceZip, String fileNameInSystemZip) {
621         mSystemFileNameMaps.put(fileNameInDeviceZip, fileNameInSystemZip);
622     }
623 
624     @VisibleForTesting
addResourceFileName(String fileName)625     void addResourceFileName(String fileName) {
626         mExtraBuildResourceFiles.add(fileName);
627     }
628 
629     @VisibleForTesting
addStubFileName(String fileName)630     void addStubFileName(String fileName) {
631         mStubFileNames.add(fileName);
632     }
633 
634     @VisibleForTesting
setCompressionLevel(int compressionLevel)635     void setCompressionLevel(int compressionLevel) {
636         mCompressionLevel = compressionLevel;
637     }
638 
639     @VisibleForTesting
createRunUtil()640     IRunUtil createRunUtil() {
641         return new RunUtil();
642     }
643 }
644