Java tutorial
/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.build.gradle.tasks; import static com.google.common.base.Preconditions.checkNotNull; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.build.gradle.internal.annotations.PackageFile; import com.android.build.gradle.internal.dsl.AbiSplitOptions; import com.android.build.gradle.internal.dsl.CoreSigningConfig; import com.android.build.gradle.internal.dsl.PackagingOptions; import com.android.build.gradle.internal.incremental.DexPackagingPolicy; import com.android.build.gradle.internal.incremental.InstantRunBuildContext; import com.android.build.gradle.internal.incremental.InstantRunPatchingPolicy; import com.android.build.gradle.internal.packaging.ApkCreatorFactories; import com.android.build.gradle.internal.scope.ConventionMappingHelper; import com.android.build.gradle.internal.scope.PackagingScope; import com.android.build.gradle.internal.scope.TaskConfigAction; import com.android.build.gradle.internal.tasks.FileSupplier; import com.android.build.gradle.internal.tasks.IncrementalTask; import com.android.build.gradle.internal.transforms.InstantRunSlicer; import com.android.build.gradle.internal.variant.SplitHandlingPolicy; import com.android.builder.files.FileCacheByPath; import com.android.builder.files.IncrementalRelativeFileSets; import com.android.builder.files.RelativeFile; import com.android.builder.internal.packaging.IncrementalPackager; import com.android.builder.internal.utils.CachedFileContents; import com.android.builder.internal.utils.IOExceptionWrapper; import com.android.builder.model.AaptOptions; import com.android.builder.model.ApiVersion; import com.android.builder.packaging.ApkCreatorFactory; import com.android.builder.packaging.PackagerException; import com.android.builder.packaging.PackagingUtils; import com.android.ide.common.res2.FileStatus; import com.android.ide.common.signing.CertificateInfo; import com.android.ide.common.signing.KeystoreHelper; import com.android.ide.common.signing.KeytoolException; import com.android.utils.FileUtils; import com.google.common.base.Functions; import com.google.common.base.MoreObjects; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Verify; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.io.Closer; import com.google.common.io.Files; import org.gradle.api.Task; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputDirectory; import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputFile; import org.gradle.tooling.BuildException; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * Abstract task to package an Android artifact. */ public abstract class PackageAndroidArtifact extends IncrementalTask implements FileSupplier { public static final String INSTANT_RUN_PACKAGES_PREFIX = "instant-run"; // ----- PUBLIC TASK API ----- @InputFile public File getResourceFile() { return resourceFile; } public void setResourceFile(File resourceFile) { this.resourceFile = resourceFile; } @OutputFile public File getOutputFile() { return outputFile; } public void setOutputFile(File outputFile) { this.outputFile = outputFile; } @Input public Set<String> getAbiFilters() { return abiFilters; } public void setAbiFilters(Set<String> abiFilters) { this.abiFilters = abiFilters; } // ----- PRIVATE TASK API ----- @InputFiles @Optional public Collection<File> getJavaResourceFiles() { return javaResourceFiles; } @InputFiles @Optional public Collection<File> getJniFolders() { return jniFolders; } private File resourceFile; private Set<File> dexFolders; private File assets; @InputFiles @Optional public Set<File> getDexFolders() { return dexFolders; } public void setDexFolders(Set<File> dexFolders) { this.dexFolders = dexFolders; } @InputDirectory public File getAssets() { return assets; } public void setAssets(File assets) { this.assets = assets; } /** list of folders and/or jars that contain the merged java resources. */ private Set<File> javaResourceFiles; private Set<File> jniFolders; @PackageFile private File outputFile; private Set<String> abiFilters; private boolean debugBuild; private boolean jniDebugBuild; private CoreSigningConfig signingConfig; private PackagingOptions packagingOptions; private ApiVersion minSdkVersion; protected InstantRunBuildContext instantRunContext; protected File instantRunSupportDir; protected DexPackagingPolicy dexPackagingPolicy; protected File manifest; protected AaptOptions aaptOptions; protected InstantRunBuildContext.FileType instantRunFileType = InstantRunBuildContext.FileType.MAIN; /** * Name of directory, inside the intermediate directory, where zip caches are kept. */ private static final String ZIP_DIFF_CACHE_DIR = "zip-cache"; /** * Zip caches to allow incremental updates. */ protected FileCacheByPath cacheByPath; @Input public boolean getJniDebugBuild() { return jniDebugBuild; } public void setJniDebugBuild(boolean jniDebugBuild) { this.jniDebugBuild = jniDebugBuild; } @Input public boolean getDebugBuild() { return debugBuild; } public void setDebugBuild(boolean debugBuild) { this.debugBuild = debugBuild; } @Nested @Optional public CoreSigningConfig getSigningConfig() { return signingConfig; } public void setSigningConfig(CoreSigningConfig signingConfig) { this.signingConfig = signingConfig; } @Nested public PackagingOptions getPackagingOptions() { return packagingOptions; } public void setPackagingOptions(PackagingOptions packagingOptions) { this.packagingOptions = packagingOptions; } @Input public int getMinSdkVersion() { return this.minSdkVersion.getApiLevel(); } public void setMinSdkVersion(ApiVersion version) { this.minSdkVersion = version; } @Input String getDexPackagingPolicy() { return dexPackagingPolicy.toString(); } /* * We don't really use this. But this forces a full build if the native packaging mode changes. */ @Input public String getNativeLibrariesPackagingModeName() { return PackagingUtils.getNativeLibrariesLibrariesPackagingMode(manifest).toString(); } @Input public Collection<String> getNoCompressExtensions() { return MoreObjects.firstNonNull(aaptOptions.getNoCompress(), Collections.<String>emptyList()); } protected Predicate<String> getNoCompressPredicate() { return PackagingUtils.getNoCompressPredicate(aaptOptions, manifest); } @Override protected void doFullTaskAction() throws IOException { /* * Clear the cache to make sure we have do not do an incremental build. */ cacheByPath.clear(); /* * Also clear the intermediate build directory. We don't know if anything is in there and * since this is a full build, we don't want to get any interference from previous state. */ FileUtils.deleteDirectoryContents(getIncrementalFolder()); Set<File> androidResources = new HashSet<>(); File androidResourceFile = getResourceFile(); if (androidResourceFile != null) { androidResources.add(androidResourceFile); } /* * Additionally, make sure we have no previous package, if it exists. */ getOutputFile().delete(); ImmutableMap<RelativeFile, FileStatus> updatedDex = IncrementalRelativeFileSets .fromZipsAndDirectories(getDexFolders()); ImmutableMap<RelativeFile, FileStatus> updatedJavaResources = IncrementalRelativeFileSets .fromZipsAndDirectories(getJavaResourceFiles()); ImmutableMap<RelativeFile, FileStatus> updatedAssets = IncrementalRelativeFileSets .fromZipsAndDirectories(Collections.singleton(getAssets())); ImmutableMap<RelativeFile, FileStatus> updatedAndroidResources = IncrementalRelativeFileSets .fromZipsAndDirectories(androidResources); ImmutableMap<RelativeFile, FileStatus> updatedJniResources = IncrementalRelativeFileSets .fromZipsAndDirectories(getJniFolders()); doTask(updatedDex, updatedJavaResources, updatedAssets, updatedAndroidResources, updatedJniResources); /* * Update the known files. */ KnownFilesSaveData saveData = KnownFilesSaveData.make(getIncrementalFolder()); saveData.setInputSet(updatedDex.keySet(), InputSet.DEX); saveData.setInputSet(updatedJavaResources.keySet(), InputSet.JAVA_RESOURCE); saveData.setInputSet(updatedAssets.keySet(), InputSet.ASSET); saveData.setInputSet(updatedAndroidResources.keySet(), InputSet.ANDROID_RESOURCE); saveData.setInputSet(updatedJniResources.keySet(), InputSet.NATIVE_RESOURCE); saveData.saveCurrentData(); } /** * Packages the application incrementally. In case of instant run packaging, this is not a * perfectly incremental task as some files are always rewritten even if no change has * occurred. * * @param changedDex incremental dex packaging data * @param changedJavaResources incremental java resources * @param changedAssets incremental assets * @param changedAndroidResources incremental Android resource * @param changedNLibs incremental native libraries changed * @throws IOException failed to package the APK */ private void doTask(@NonNull ImmutableMap<RelativeFile, FileStatus> changedDex, @NonNull ImmutableMap<RelativeFile, FileStatus> changedJavaResources, @NonNull ImmutableMap<RelativeFile, FileStatus> changedAssets, @NonNull ImmutableMap<RelativeFile, FileStatus> changedAndroidResources, @NonNull ImmutableMap<RelativeFile, FileStatus> changedNLibs) throws IOException { ImmutableMap.Builder<RelativeFile, FileStatus> javaResourcesForApk = ImmutableMap.builder(); javaResourcesForApk.putAll(changedJavaResources); Collection<File> instantRunDexBaseFiles; switch (dexPackagingPolicy) { case INSTANT_RUN_SHARDS_IN_SINGLE_APK: /* * If we're doing instant run, then we don't want to treat all dex archives * as dex archives for packaging. We will package some of the dex files as * resources. * * All dex files in directories whose name contains INSTANT_RUN_PACKAGES_PREFIX * are kept in the apk as dex files. All other dex files are placed as * resources as defined by makeInstantRunResourcesFromDex. */ instantRunDexBaseFiles = getDexFolders().stream() .filter(input -> input.getName().contains(INSTANT_RUN_PACKAGES_PREFIX)) .collect(Collectors.toSet()); Iterable<File> nonInstantRunDexBaseFiles = getDexFolders().stream() .filter(f -> !instantRunDexBaseFiles.contains(f)).collect(Collectors.toSet()); ImmutableMap<RelativeFile, FileStatus> newInstantRunResources = makeInstantRunResourcesFromDex( nonInstantRunDexBaseFiles); @SuppressWarnings("unchecked") ImmutableMap<RelativeFile, FileStatus> updatedChangedResources = IncrementalRelativeFileSets .union(Sets.newHashSet(changedJavaResources, newInstantRunResources)); changedJavaResources = updatedChangedResources; changedDex = ImmutableMap.copyOf(Maps.filterKeys(changedDex, Predicates.compose(Predicates.in(instantRunDexBaseFiles), RelativeFile.EXTRACT_BASE))); break; case INSTANT_RUN_MULTI_APK: instantRunDexBaseFiles = getDexFolders().stream() .filter(input -> input.getName().contains(InstantRunSlicer.MAIN_SLICE_NAME)) .collect(Collectors.toSet()); changedDex = ImmutableMap.copyOf(Maps.filterKeys(changedDex, Predicates.compose(Predicates.in(instantRunDexBaseFiles), RelativeFile.EXTRACT_BASE))); case STANDARD: break; default: throw new RuntimeException("Unhandled DexPackagingPolicy : " + getDexPackagingPolicy()); } PrivateKey key; X509Certificate certificate; boolean v1SigningEnabled; boolean v2SigningEnabled; try { if (signingConfig != null && signingConfig.isSigningReady()) { CertificateInfo certificateInfo = KeystoreHelper.getCertificateInfo(signingConfig.getStoreType(), checkNotNull(signingConfig.getStoreFile()), checkNotNull(signingConfig.getStorePassword()), checkNotNull(signingConfig.getKeyPassword()), checkNotNull(signingConfig.getKeyAlias())); key = certificateInfo.getKey(); certificate = certificateInfo.getCertificate(); v1SigningEnabled = signingConfig.isV1SigningEnabled(); v2SigningEnabled = signingConfig.isV2SigningEnabled(); } else { key = null; certificate = null; v1SigningEnabled = false; v2SigningEnabled = false; } ApkCreatorFactory.CreationData creationData = new ApkCreatorFactory.CreationData(getOutputFile(), key, certificate, v1SigningEnabled, v2SigningEnabled, null, // BuiltBy getBuilder().getCreatedBy(), getMinSdkVersion(), PackagingUtils.getNativeLibrariesLibrariesPackagingMode(manifest), getNoCompressPredicate()::apply); try (IncrementalPackager packager = createPackager(creationData)) { packager.updateDex(changedDex); packager.updateJavaResources(changedJavaResources); packager.updateAssets(changedAssets); packager.updateAndroidResources(changedAndroidResources); packager.updateNativeLibraries(changedNLibs); } } catch (PackagerException | KeytoolException e) { throw new RuntimeException(e); } /* * Save all used zips in the cache. */ Stream.concat(changedDex.keySet().stream(), Stream.concat(changedJavaResources.keySet().stream(), Stream.concat(changedAndroidResources.keySet().stream(), changedNLibs.keySet().stream()))) .map(RelativeFile::getBase).filter(File::isFile).distinct().forEach((File f) -> { try { cacheByPath.add(f); } catch (IOException e) { throw new IOExceptionWrapper(e); } }); // Mark this APK production, this will eventually be saved when instant-run is enabled. // this might get overridden if the apk is signed/aligned. try { instantRunContext.addChangedFile(instantRunFileType, getOutputFile()); } catch (IOException e) { throw new BuildException(e.getMessage(), e); } } @NonNull private IncrementalPackager createPackager(ApkCreatorFactory.CreationData creationData) throws PackagerException, IOException { return new IncrementalPackager(creationData, getIncrementalFolder(), ApkCreatorFactories.fromProjectProperties(getProject(), getDebugBuild()), getAbiFilters(), getJniDebugBuild()); } @Override protected boolean isIncremental() { return true; } @Override protected void doIncrementalTaskAction(Map<File, FileStatus> changedInputs) throws IOException { checkNotNull(changedInputs, "changedInputs == null"); Set<File> androidResources = new HashSet<>(); File androidResourceFile = getResourceFile(); if (androidResourceFile != null) { androidResources.add(androidResourceFile); } KnownFilesSaveData saveData = KnownFilesSaveData.make(getIncrementalFolder()); ImmutableMap<RelativeFile, FileStatus> changedDexFiles = getChangedInputs(changedInputs, saveData, InputSet.DEX, getDexFolders(), cacheByPath); ImmutableMap<RelativeFile, FileStatus> changedJavaResources = getChangedInputs(changedInputs, saveData, InputSet.JAVA_RESOURCE, getJavaResourceFiles(), cacheByPath); ImmutableMap<RelativeFile, FileStatus> changedAssets = getChangedInputs(changedInputs, saveData, InputSet.ASSET, Collections.singleton(getAssets()), cacheByPath); ImmutableMap<RelativeFile, FileStatus> changedAndroidResources = getChangedInputs(changedInputs, saveData, InputSet.ANDROID_RESOURCE, androidResources, cacheByPath); ImmutableMap<RelativeFile, FileStatus> changedNLibs = getChangedInputs(changedInputs, saveData, InputSet.NATIVE_RESOURCE, getJniFolders(), cacheByPath); doTask(changedDexFiles, changedJavaResources, changedAssets, changedAndroidResources, changedNLibs); /* * Removed cached versions of deleted zip files because we no longer need to compute diffs. */ changedInputs.keySet().stream().filter(f -> !f.exists()).forEach(f -> { try { cacheByPath.remove(f); } catch (IOException e) { throw new IOExceptionWrapper(e); } }); /* * Update the save data keep files. */ ImmutableMap<RelativeFile, FileStatus> allDex = IncrementalRelativeFileSets .fromZipsAndDirectories(getDexFolders()); ImmutableMap<RelativeFile, FileStatus> allJavaResources = IncrementalRelativeFileSets .fromZipsAndDirectories(getJavaResourceFiles()); ImmutableMap<RelativeFile, FileStatus> allAndroidResources = IncrementalRelativeFileSets .fromZipsAndDirectories(androidResources); ImmutableMap<RelativeFile, FileStatus> allJniResources = IncrementalRelativeFileSets .fromZipsAndDirectories(getJniFolders()); saveData.setInputSet(allDex.keySet(), InputSet.DEX); saveData.setInputSet(allJavaResources.keySet(), InputSet.JAVA_RESOURCE); saveData.setInputSet(allAndroidResources.keySet(), InputSet.ANDROID_RESOURCE); saveData.setInputSet(allJniResources.keySet(), InputSet.NATIVE_RESOURCE); saveData.saveCurrentData(); } /** * Obtains all changed inputs of a given input set. Given a set of files mapped to their * changed status, this method returns a list of changes computed as follows: * * <ol> * <li>Changed inputs are split into deleted and non-deleted inputs. This separation is * needed because deleted inputs may no longer be mappable to any {@link InputSet} just * by looking at the file path, without using {@link KnownFilesSaveData}. * <li>Deleted inputs are filtered through {@link KnownFilesSaveData} to get only those * whose input set matches {@code inputSet}. * <li>Non-deleted inputs are processed through * {@link IncrementalRelativeFileSets#makeFromBaseFiles(Collection, Map, FileCacheByPath)} * to obtain the incremental file changes. * <li>The results of processed deleted and non-deleted are merged and returned. * </ol> * * @param changedInputs all changed inputs * @param saveData the save data with all input sets from last run * @param inputSet the input set to filter * @param baseFiles the base files of the input set * @param cacheByPath where to cache files * @return the status of all relative files in the input set */ @NonNull private ImmutableMap<RelativeFile, FileStatus> getChangedInputs(@NonNull Map<File, FileStatus> changedInputs, @NonNull KnownFilesSaveData saveData, @NonNull InputSet inputSet, @NonNull Collection<File> baseFiles, @NonNull FileCacheByPath cacheByPath) throws IOException { /* * Figure out changes to deleted files. */ Set<File> deletedFiles = Maps.filterValues(changedInputs, Predicates.equalTo(FileStatus.REMOVED)).keySet(); Set<RelativeFile> deletedRelativeFiles = saveData.find(deletedFiles, inputSet); /* * Figure out changes to non-deleted files. */ Map<File, FileStatus> nonDeletedFiles = Maps.filterValues(changedInputs, Predicates.not(Predicates.equalTo(FileStatus.REMOVED))); Map<RelativeFile, FileStatus> nonDeletedRelativeFiles = IncrementalRelativeFileSets .makeFromBaseFiles(baseFiles, nonDeletedFiles, cacheByPath); /* * Merge everything. */ return new ImmutableMap.Builder<RelativeFile, FileStatus>() .putAll(Maps.asMap(deletedRelativeFiles, Functions.constant(FileStatus.REMOVED))) .putAll(nonDeletedRelativeFiles).build(); } /** * Creates the new instant run resources from the dex files. This method is not * incremental. It will ignore updates and look at all dex files and always rebuild the * instant run resources. * * <p>The instant run resources are resources that package dex files. * * @param dexBaseFiles the base files to dex * @return the instant run resources * @throws IOException failed to create the instant run resources */ @NonNull private ImmutableMap<RelativeFile, FileStatus> makeInstantRunResourcesFromDex( @NonNull Iterable<File> dexBaseFiles) throws IOException { File tmpZipFile = new File(instantRunSupportDir, "instant-run.zip"); boolean existedBefore = tmpZipFile.exists(); Files.createParentDirs(tmpZipFile); ZipOutputStream zipFile = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmpZipFile))); // no need to compress a zip, the APK itself gets compressed. zipFile.setLevel(0); try { for (File dexFolder : dexBaseFiles) { for (File file : Files.fileTreeTraverser().breadthFirstTraversal(dexFolder)) { if (file.isFile() && file.getName().endsWith(SdkConstants.DOT_DEX)) { // There are several pieces of code in the runtime library that depend // on this exact pattern, so it should not be changed without thorough // testing (it's basically part of the contract). String entryName = file.getParentFile().getName() + "-" + file.getName(); zipFile.putNextEntry(new ZipEntry(entryName)); try { Files.copy(file, zipFile); } finally { zipFile.closeEntry(); } } } } } finally { zipFile.close(); } RelativeFile resourcesFile = new RelativeFile(instantRunSupportDir, tmpZipFile); return ImmutableMap.of(resourcesFile, existedBefore ? FileStatus.CHANGED : FileStatus.NEW); } // ----- FileSupplierTask ----- @Override public File get() { return getOutputFile(); } @NonNull @Override public Task getTask() { return this; } /** * Class that keeps track of which files are known in incremental builds. Gradle tells us * which files were modified, but doesn't tell us which inputs the files come from so when a * file is marked as deleted, we don't know which input set it was deleted from. This class * maintains the list of files and their source locations and can be saved to the intermediate * directory. * * <p>File data is loaded on creation and saved on close. * * <p><i>Implementation note:</i> the actual data is saved in a property file with the * file name mapped to the name of the {@link InputSet} enum defining its input set. */ private static class KnownFilesSaveData { /** * Name of the file with the save data. */ private static final String SAVE_DATA_FILE_NAME = "file-input-save-data.txt"; /** * Property with the number of files in the property file. */ private static final String COUNT_PROPERTY = "count"; /** * Suffix for property with the base file. */ private static final String BASE_SUFFIX = ".base"; /** * Suffix for property with the file. */ private static final String FILE_SUFFIX = ".file"; /** * Suffix for property with the input set. */ private static final String INPUT_SET_SUFFIX = ".set"; /** * Cache with all known cached files. */ private static final Map<File, CachedFileContents<KnownFilesSaveData>> mCache = Maps.newHashMap(); /** * File contents cache. */ @NonNull private final CachedFileContents<KnownFilesSaveData> mFileContentsCache; /** * Maps all files in the last build to their input set. */ @NonNull private final Map<RelativeFile, InputSet> mFiles; /** * Has the data been modified? */ private boolean mDirty; /** * Creates a new file save data and reads it one exists. To create new instances, the * factory method {@link #make(File)} should be used. * * @param cache the cache used * @throws IOException failed to read the file (not thrown if the file does not exist) */ private KnownFilesSaveData(@NonNull CachedFileContents<KnownFilesSaveData> cache) throws IOException { mFileContentsCache = cache; mFiles = Maps.newHashMap(); if (cache.getFile().isFile()) { readCurrentData(); } mDirty = false; } /** * Creates a new {@link KnownFilesSaveData}, or obtains one from cache if there already * exists a cached entry. * * @param intermediateDir the intermediate directory where the cache is stored * @return the save data * @throws IOException save data file exists but there was an error reading it (not thrown * if the file does not exist) */ @NonNull private static synchronized KnownFilesSaveData make(@NonNull File intermediateDir) throws IOException { File saveFile = computeSaveFile(intermediateDir); CachedFileContents<KnownFilesSaveData> cached = mCache.get(saveFile); if (cached == null) { cached = new CachedFileContents<>(saveFile); mCache.put(saveFile, cached); } KnownFilesSaveData saveData = cached.getCache(); if (saveData == null) { saveData = new KnownFilesSaveData(cached); cached.closed(saveData); } return saveData; } /** * Computes what is the save file for the provided intermediate directory. * * @param intermediateDir the intermediate directory * @return the file */ private static File computeSaveFile(@NonNull File intermediateDir) { return new File(intermediateDir, SAVE_DATA_FILE_NAME); } /** * Reads the save file data into the in-memory data structures. * * @throws IOException failed to read the file */ private void readCurrentData() throws IOException { Closer closer = Closer.create(); File saveFile = mFileContentsCache.getFile(); Properties properties = new Properties(); try { Reader saveDataReader = closer.register(new FileReader(saveFile)); properties.load(saveDataReader); } catch (Throwable t) { throw closer.rethrow(t); } finally { closer.close(); } String fileCountText = null; int fileCount; try { fileCountText = properties.getProperty(COUNT_PROPERTY); if (fileCountText == null) { throw new IOException("Invalid data stored in file '" + saveFile + "' (" + "property '" + COUNT_PROPERTY + "' has no value)."); } fileCount = Integer.parseInt(fileCountText); if (fileCount < 0) { throw new IOException("Invalid data stored in file '" + saveFile + "' (" + "property '" + COUNT_PROPERTY + "' has value " + fileCount + ")."); } } catch (NumberFormatException e) { throw new IOException("Invalid data stored in file '" + saveFile + "' (" + "property '" + COUNT_PROPERTY + "' has value '" + fileCountText + "').", e); } for (int i = 0; i < fileCount; i++) { String baseName = properties.getProperty(i + BASE_SUFFIX); if (baseName == null) { throw new IOException("Invalid data stored in file '" + saveFile + "' (" + "property '" + i + BASE_SUFFIX + "' has no value)."); } String fileName = properties.getProperty(i + FILE_SUFFIX); if (fileName == null) { throw new IOException("Invalid data stored in file '" + saveFile + "' (" + "property '" + i + FILE_SUFFIX + "' has no value)."); } String inputSetName = properties.getProperty(i + INPUT_SET_SUFFIX); if (inputSetName == null) { throw new IOException("Invalid data stored in file '" + saveFile + "' (" + "property '" + i + INPUT_SET_SUFFIX + "' has no value)."); } InputSet is; try { is = InputSet.valueOf(InputSet.class, inputSetName); } catch (IllegalArgumentException e) { throw new IOException("Invalid data stored in file '" + saveFile + "' (" + "property '" + i + INPUT_SET_SUFFIX + "' has invalid value '" + inputSetName + "')."); } mFiles.put(new RelativeFile(new File(baseName), new File(fileName)), is); } } /** * Saves current in-memory data structures to file. * * @throws IOException failed to save the data */ private void saveCurrentData() throws IOException { if (!mDirty) { return; } Closer closer = Closer.create(); Properties properties = new Properties(); properties.put(COUNT_PROPERTY, Integer.toString(mFiles.size())); int idx = 0; for (Map.Entry<RelativeFile, InputSet> e : mFiles.entrySet()) { RelativeFile rf = e.getKey(); String basePath = Verify.verifyNotNull(rf.getBase().getPath()); Verify.verify(!basePath.isEmpty()); String filePath = Verify.verifyNotNull(rf.getFile().getPath()); Verify.verify(!filePath.isEmpty()); properties.put(idx + BASE_SUFFIX, basePath); properties.put(idx + FILE_SUFFIX, filePath); properties.put(idx + INPUT_SET_SUFFIX, e.getValue().name()); idx++; } try { Writer saveDataWriter = closer.register(new FileWriter(mFileContentsCache.getFile())); properties.store(saveDataWriter, "Internal package file, do not edit."); mFileContentsCache.closed(this); } catch (Throwable t) { throw closer.rethrow(t); } finally { closer.close(); } } /** * Obtains all relative files stored in the save data that have the provided input set and * whose files are included in the provided set of files. This method allows retrieving * the original relative files from the files, while filtering for the desired input set. * * @param files the files to filter * @param inputSet the input set to filter * @return all saved relative files that have the given input set and whose files exist * in the provided set */ @NonNull private ImmutableSet<RelativeFile> find(@NonNull Set<File> files, @NonNull InputSet inputSet) { Set<RelativeFile> found = Sets.newHashSet(); for (RelativeFile rf : Maps.filterValues(mFiles, Predicates.equalTo(inputSet)).keySet()) { if (files.contains(rf.getFile())) { found.add(rf); } } return ImmutableSet.copyOf(found); } /** * Obtains a predicate that checks if a file is in an input set. * * @param inputSet the input set * @return the predicate */ @NonNull private Function<File, RelativeFile> inInputSet(@NonNull InputSet inputSet) { Map<File, RelativeFile> inverseFiltered = mFiles.entrySet().stream() .filter(e -> e.getValue() == inputSet).map(Map.Entry::getKey) .collect(HashMap::new, (m, rf) -> m.put(rf.getFile(), rf), Map::putAll); return inverseFiltered::get; } /** * Sets all files in an input set, replacing whatever existed previously. * * @param files the files * @param set the input set */ private void setInputSet(@NonNull Collection<RelativeFile> files, @NonNull InputSet set) { for (Iterator<Map.Entry<RelativeFile, InputSet>> it = mFiles.entrySet().iterator(); it.hasNext();) { Map.Entry<RelativeFile, InputSet> next = it.next(); if (next.getValue() == set && !files.contains(next.getKey())) { it.remove(); mDirty = true; } } files.forEach(f -> { if (!mFiles.containsKey(f)) { mFiles.put(f, set); mDirty = true; } }); } } /** * Input sets for files for save data (see {@link KnownFilesSaveData}). */ private enum InputSet { /** * File belongs to the dex file set. */ DEX, /** * File belongs to the java resources file set. */ JAVA_RESOURCE, /** * File belongs to the native resources file set. */ NATIVE_RESOURCE, /** * File belongs to the android resources file set. */ ANDROID_RESOURCE, /** * File belongs to the assets file set. */ ASSET } // ----- ConfigAction ----- public abstract static class ConfigAction<T extends PackageAndroidArtifact> implements TaskConfigAction<T> { protected final PackagingScope packagingScope; protected final DexPackagingPolicy dexPackagingPolicy; public ConfigAction(@NonNull PackagingScope packagingScope, @Nullable InstantRunPatchingPolicy patchingPolicy) { this.packagingScope = checkNotNull(packagingScope); dexPackagingPolicy = patchingPolicy == null ? DexPackagingPolicy.STANDARD : patchingPolicy.getDexPatchingPolicy(); } @Override public void execute(@NonNull final T packageAndroidArtifact) { packageAndroidArtifact.setAndroidBuilder(packagingScope.getAndroidBuilder()); packageAndroidArtifact.setVariantName(packagingScope.getFullVariantName()); packageAndroidArtifact.setMinSdkVersion(packagingScope.getMinSdkVersion()); packageAndroidArtifact.instantRunContext = packagingScope.getInstantRunBuildContext(); packageAndroidArtifact.dexPackagingPolicy = dexPackagingPolicy; packageAndroidArtifact.instantRunSupportDir = packagingScope.getInstantRunSupportDir(); packageAndroidArtifact .setIncrementalFolder(packagingScope.getIncrementalDir(packageAndroidArtifact.getName())); packageAndroidArtifact.aaptOptions = packagingScope.getAaptOptions(); packageAndroidArtifact.manifest = packagingScope.getManifestFile(); File cacheByPathDir = new File(packageAndroidArtifact.getIncrementalFolder(), ZIP_DIFF_CACHE_DIR); FileUtils.mkdirs(cacheByPathDir); packageAndroidArtifact.cacheByPath = new FileCacheByPath(cacheByPathDir); ConventionMappingHelper.map(packageAndroidArtifact, "resourceFile", packagingScope::getFinalResourcesFile); ConventionMappingHelper.map(packageAndroidArtifact, "dexFolders", packagingScope::getDexFolders); ConventionMappingHelper.map(packageAndroidArtifact, "javaResourceFiles", packagingScope::getJavaResources); packageAndroidArtifact.setAssets(packagingScope.getAssetsDir()); ConventionMappingHelper.map(packageAndroidArtifact, "jniFolders", () -> { if (packagingScope.getSplitHandlingPolicy() == SplitHandlingPolicy.PRE_21_POLICY) { return packagingScope.getJniFolders(); } Set<String> filters = AbiSplitOptions.getAbiFilters(packagingScope.getAbiFilters()); return filters.isEmpty() ? packagingScope.getJniFolders() : Collections.emptySet(); }); ConventionMappingHelper.map(packageAndroidArtifact, "abiFilters", () -> { String filter = packagingScope.getMainOutputFile().getFilter(com.android.build.OutputFile.ABI); if (filter != null) { return ImmutableSet.of(filter); } Set<String> supportedAbis = packagingScope.getSupportedAbis(); // TODO: nullability if (supportedAbis != null) { return supportedAbis; } return ImmutableSet.of(); }); ConventionMappingHelper.map(packageAndroidArtifact, "jniDebugBuild", packagingScope::isJniDebuggable); ConventionMappingHelper.map(packageAndroidArtifact, "debugBuild", packagingScope::isDebuggable); packageAndroidArtifact.setSigningConfig(packagingScope.getSigningConfig()); ConventionMappingHelper.map(packageAndroidArtifact, "packagingOptions", packagingScope::getPackagingOptions); } } }