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