blob: a876001582b5c9ee764a47f6e59c474f3e90a010 [file] [log] [blame]
/*
* Copyright (C) 2014 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.manifmerger;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_SPLIT;
import static com.android.manifmerger.PlaceholderHandler.APPLICATION_ID;
import static com.android.manifmerger.PlaceholderHandler.KeyBasedValueResolver;
import static com.android.manifmerger.PlaceholderHandler.PACKAGE_NAME;
import static com.android.sdklib.AndroidVersion.VersionCodes.M;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.concurrency.Immutable;
import com.android.ide.common.xml.XmlFormatPreferences;
import com.android.ide.common.xml.XmlFormatStyle;
import com.android.ide.common.xml.XmlPrettyPrinter;
import com.android.utils.FileUtils;
import com.android.utils.ILogger;
import com.android.utils.Pair;
import com.android.utils.XmlUtils;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* merges android manifest files, idempotent.
*/
@Immutable
public class ManifestMerger2 {
public static final String COMPATIBLE_SCREENS_SUB_MANIFEST = "Compatible-Screens sub-manifest";
public static final String WEAR_APP_SUB_MANIFEST = "Wear App sub-manifest";
private static final String SPLIT_IN_DYNAMIC_FEATURE =
"https://d.android.com/r/studio-ui/dynamic-delivery/dynamic-feature-manifest";
private static final List<String> WHITELISTED_NON_UNIQUE_PACKAGE_NAMES =
ImmutableList.of(
"androidx.test" // TODO(b/151171905)
);
@NonNull
private final File mManifestFile;
@NonNull
private final Map<String, Object> mPlaceHolderValues;
@NonNull
private final KeyBasedValueResolver<ManifestSystemProperty> mSystemPropertyResolver;
@NonNull
private final ILogger mLogger;
@NonNull
private final ImmutableList<Pair<String, File>> mLibraryFiles;
@NonNull
private final ImmutableList<File> mFlavorsAndBuildTypeFiles;
@NonNull
private final ImmutableList<Invoker.Feature> mOptionalFeatures;
@NonNull
private final MergeType mMergeType;
@NonNull
private final XmlDocument.Type mDocumentType;
@NonNull
private final Optional<File> mReportFile;
@NonNull private final String mFeatureName;
@NonNull private final FileStreamProvider mFileStreamProvider;
@NonNull private final ImmutableList<File> mNavigationFiles;
@NonNull private final ImmutableList<File> mNavigationJsons;
@NonNull private final DocumentModel<ManifestModel.NodeTypes> mModel;
@NonNull private final ImmutableList<String> mDependencyFeatureNames;
private ManifestMerger2(
@NonNull ILogger logger,
@NonNull File mainManifestFile,
@NonNull ImmutableList<Pair<String, File>> libraryFiles,
@NonNull ImmutableList<File> flavorsAndBuildTypeFiles,
@NonNull ImmutableList<Invoker.Feature> optionalFeatures,
@NonNull Map<String, Object> placeHolderValues,
@NonNull KeyBasedValueResolver<ManifestSystemProperty> systemPropertiesResolver,
@NonNull MergeType mergeType,
@NonNull XmlDocument.Type documentType,
@NonNull Optional<File> reportFile,
@NonNull String featureName,
@NonNull FileStreamProvider fileStreamProvider,
@NonNull ImmutableList<File> navigationFiles,
@NonNull ImmutableList<File> navigationJsons,
@NonNull ImmutableList<String> dependencyFeatureNames) {
this.mSystemPropertyResolver = systemPropertiesResolver;
this.mPlaceHolderValues = placeHolderValues;
this.mManifestFile = mainManifestFile;
this.mLogger = logger;
this.mLibraryFiles = libraryFiles;
this.mFlavorsAndBuildTypeFiles = flavorsAndBuildTypeFiles;
this.mOptionalFeatures = optionalFeatures;
this.mMergeType = mergeType;
this.mDocumentType = documentType;
this.mReportFile = reportFile;
this.mFeatureName = featureName;
this.mFileStreamProvider = fileStreamProvider;
this.mNavigationFiles = navigationFiles;
this.mNavigationJsons = navigationJsons;
this.mDependencyFeatureNames = dependencyFeatureNames;
this.mModel =
new ManifestModel(
mOptionalFeatures.contains(
Invoker.Feature.HANDLE_VALUE_CONFLICTS_AUTOMATICALLY));
}
/**
* Perform high level ordering of files merging and delegates actual merging to {@link
* XmlDocument#merge(XmlDocument, MergingReport.Builder)}
*
* @return the merging activity report.
* @throws MergeFailureException if the merging cannot be completed (for instance, if xml files
* cannot be loaded).
*/
@NonNull
private MergingReport merge() throws MergeFailureException {
// initiate a new merging report
MergingReport.Builder mergingReportBuilder = new MergingReport.Builder(mLogger);
SelectorResolver selectors = new SelectorResolver();
// load the main manifest file to do some checking along the way.
LoadedManifestInfo loadedMainManifestInfo =
load(
new ManifestInfo(
mManifestFile.getName(),
mManifestFile,
mDocumentType,
null /* mainManifestPackageName */),
selectors,
mergingReportBuilder);
// first do we have a package declaration in the main manifest ?
Optional<XmlAttribute> mainPackageAttribute =
loadedMainManifestInfo.getXmlDocument().getPackage();
if (mDocumentType != XmlDocument.Type.OVERLAY && !mainPackageAttribute.isPresent()) {
mergingReportBuilder.addMessage(
loadedMainManifestInfo.getXmlDocument().getSourceFile(),
MergingReport.Record.Severity.ERROR,
String.format(
"Main AndroidManifest.xml at %1$s manifest:package attribute "
+ "is not declared",
loadedMainManifestInfo.getXmlDocument().getSourceFile()
.print(true)));
return mergingReportBuilder.build();
}
if (!mFeatureName.isEmpty()) {
loadedMainManifestInfo =
removeDynamicFeatureManifestSplitAttributeIfSpecified(
loadedMainManifestInfo, mergingReportBuilder);
}
// load all the libraries xml files early to have a list of all possible node:selector
// values.
List<LoadedManifestInfo> loadedLibraryDocuments =
loadLibraries(
selectors,
mergingReportBuilder,
mainPackageAttribute.map(XmlAttribute::getValue).orElse(null));
// make sure each module/library has a unique package name
checkUniquePackageName(
loadedMainManifestInfo,
loadedLibraryDocuments,
mergingReportBuilder,
mOptionalFeatures.contains(Invoker.Feature.ENFORCE_UNIQUE_PACKAGE_NAME));
// perform system property injection
performSystemPropertiesInjection(mergingReportBuilder,
loadedMainManifestInfo.getXmlDocument());
// force the re-parsing of the xml as elements may have been added through system
// property injection.
loadedMainManifestInfo = new LoadedManifestInfo(loadedMainManifestInfo,
loadedMainManifestInfo.getOriginalPackageName(),
loadedMainManifestInfo.getXmlDocument().reparse());
// invariant : xmlDocumentOptional holds the higher priority document and we try to
// merge in lower priority documents.
@Nullable XmlDocument xmlDocumentOptional = null;
for (File inputFile : mFlavorsAndBuildTypeFiles) {
mLogger.verbose("Merging flavors and build manifest %s \n", inputFile.getPath());
LoadedManifestInfo overlayDocument =
load(
new ManifestInfo(
null,
inputFile,
XmlDocument.Type.OVERLAY,
mainPackageAttribute.map(XmlAttribute::getValue).orElse(null)),
selectors,
mergingReportBuilder);
if (!mFeatureName.isEmpty()) {
overlayDocument =
removeDynamicFeatureManifestSplitAttributeIfSpecified(
overlayDocument, mergingReportBuilder);
}
// check package declaration.
Optional<XmlAttribute> packageAttribute =
overlayDocument.getXmlDocument().getPackage();
// if both files declare a package name, it should be the same.
if (loadedMainManifestInfo.getOriginalPackageName().isPresent() &&
packageAttribute.isPresent()
&& !loadedMainManifestInfo.getOriginalPackageName().get().equals(
packageAttribute.get().getValue())) {
// no suggestion for library since this is actually forbidden to change the
// the package name per flavor.
String message = mMergeType == MergeType.APPLICATION
? String.format(
"Overlay manifest:package attribute declared at %1$s value=(%2$s)\n"
+ "\thas a different value=(%3$s) "
+ "declared in main manifest at %4$s\n"
+ "\tSuggestion: remove the overlay declaration at %5$s "
+ "\tand place it in the build.gradle:\n"
+ "\t\tflavorName {\n"
+ "\t\t\tapplicationId = \"%2$s\"\n"
+ "\t\t}",
packageAttribute.get().printPosition(),
packageAttribute.get().getValue(),
mainPackageAttribute.get().getValue(),
mainPackageAttribute.get().printPosition(),
packageAttribute.get().getSourceFile().print(true))
: String.format(
"Overlay manifest:package attribute declared at %1$s value=(%2$s)\n"
+ "\thas a different value=(%3$s) "
+ "declared in main manifest at %4$s",
packageAttribute.get().printPosition(),
packageAttribute.get().getValue(),
mainPackageAttribute.get().getValue(),
mainPackageAttribute.get().printPosition());
mergingReportBuilder.addMessage(
overlayDocument.getXmlDocument().getSourceFile(),
MergingReport.Record.Severity.ERROR,
message);
return mergingReportBuilder.build();
}
if (mainPackageAttribute.isPresent()) {
overlayDocument
.getXmlDocument()
.getRootNode()
.getXml()
.setAttribute("package", mainPackageAttribute.get().getValue());
}
Optional<XmlDocument> newMergedDocument =
merge(xmlDocumentOptional, overlayDocument, mergingReportBuilder);
xmlDocumentOptional = newMergedDocument.orElse(null);
if (!newMergedDocument.isPresent()) {
return mergingReportBuilder.build();
}
}
mLogger.verbose("Merging main manifest %s\n", mManifestFile.getPath());
Optional<XmlDocument> newMergedDocument =
merge(xmlDocumentOptional, loadedMainManifestInfo, mergingReportBuilder);
if (!newMergedDocument.isPresent()) {
return mergingReportBuilder.build();
}
xmlDocumentOptional = newMergedDocument.get();
// force main manifest package into resulting merged file when creating a library manifest.
if (mMergeType == MergeType.LIBRARY) {
// extract the package name...
String mainManifestPackageName = loadedMainManifestInfo.getXmlDocument().getRootNode()
.getXml().getAttribute("package");
// save it in the selector instance.
if (!Strings.isNullOrEmpty(mainManifestPackageName)) {
xmlDocumentOptional
.getRootNode()
.getXml()
.setAttribute("package", mainManifestPackageName);
}
}
for (LoadedManifestInfo libraryDocument : loadedLibraryDocuments) {
mLogger.verbose("Merging library manifest " + libraryDocument.getLocation());
newMergedDocument = merge(xmlDocumentOptional, libraryDocument, mergingReportBuilder);
if (!newMergedDocument.isPresent()) {
return mergingReportBuilder.build();
}
xmlDocumentOptional = newMergedDocument.get();
}
// done with proper merging phase, now we need to expand <nav-graph> elements, trim unwanted
// elements, perform placeholder substitution and system properties injection.
if (mMergeType == MergeType.APPLICATION) {
Map<String, NavigationXmlDocument> loadedNavigationMap = createNavigationMap();
xmlDocumentOptional =
NavGraphExpander.INSTANCE.expandNavGraphs(
xmlDocumentOptional, loadedNavigationMap, mergingReportBuilder);
}
if (mergingReportBuilder.hasErrors()) {
return mergingReportBuilder.build();
}
ElementsTrimmer.trim(xmlDocumentOptional, mergingReportBuilder);
if (mergingReportBuilder.hasErrors()) {
return mergingReportBuilder.build();
}
if (!mOptionalFeatures.contains(Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT)) {
// do one last placeholder substitution, this is useful as we don't stop the build
// when a library failed a placeholder substitution, but the element might have
// been overridden so the problem was transient. However, with the final document
// ready, all placeholders values must have been provided.
MergingReport.Record.Severity severity =
mMergeType == MergeType.LIBRARY
? MergingReport.Record.Severity.INFO
: MergingReport.Record.Severity.ERROR;
performPlaceHolderSubstitution(
loadedMainManifestInfo, xmlDocumentOptional, mergingReportBuilder, severity);
if (mergingReportBuilder.hasErrors()) {
return mergingReportBuilder.build();
}
}
// perform system property injection.
performSystemPropertiesInjection(mergingReportBuilder, xmlDocumentOptional);
XmlDocument finalMergedDocument = xmlDocumentOptional;
Optional<XmlAttribute> packageAttr = finalMergedDocument.getPackage();
// We allow single word package name for library... so far...
if (mMergeType != MergeType.LIBRARY && packageAttr.isPresent()) {
XmlAttribute packageNameAttribute = packageAttr.get();
String packageName = packageNameAttribute.getValue();
// We accept absence of dot only if NO_PLACEHOLDER_REPLACEMENT is true and packageName
// is a placeholder
if (!(mOptionalFeatures.contains(Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT)
&& PlaceholderHandler.isPlaceHolder(packageName))
&& !packageName.contains(".")) {
mergingReportBuilder.addMessage(
loadedMainManifestInfo.getXmlDocument().getSourceFile(),
MergingReport.Record.Severity.ERROR,
String.format(
"Package name '%1$s' at position %2$s should contain at "
+ "least one '.' (dot) character",
packageName, packageNameAttribute.printPosition()));
return mergingReportBuilder.build();
}
}
if (!mOptionalFeatures.contains(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)) {
PostValidator.enforceToolsNamespaceDeclaration(finalMergedDocument);
}
PostValidator.validate(finalMergedDocument, mergingReportBuilder);
if (mergingReportBuilder.hasErrors()) {
mergingReportBuilder.addMessage(
finalMergedDocument.getRootNode(),
MergingReport.Record.Severity.WARNING,
"Post merge validation failed");
}
finalMergedDocument.clearNodeNamespaces();
// extract fully qualified class names before handling other optional features.
if (mOptionalFeatures.contains(Invoker.Feature.EXTRACT_FQCNS)) {
extractFqcns(finalMergedDocument);
}
String minSdkVersion = finalMergedDocument.getMinSdkVersion();
if (mMergeType == MergeType.APPLICATION && parseMinSdkVersion(minSdkVersion) >= M) {
maybeAddExtractNativeLibAttribute(finalMergedDocument.getXml());
}
// handle optional features which don't need access to XmlDocument layer.
processOptionalFeatures(finalMergedDocument.getXml(), mergingReportBuilder);
// call blame after other optional features handled.
if (!mOptionalFeatures.contains(Invoker.Feature.SKIP_BLAME)) {
try {
mergingReportBuilder.setMergedDocument(
MergingReport.MergedManifestKind.BLAME,
mergingReportBuilder.blame(finalMergedDocument));
} catch (Exception e) {
mLogger.error(e, "Error while saving blame file, build will continue");
}
}
mergingReportBuilder.setFinalPackageName(finalMergedDocument.getPackageName());
mergingReportBuilder.setMergedXmlDocument(finalMergedDocument);
MergingReport mergingReport = mergingReportBuilder.build();
if (mReportFile.isPresent()) {
writeReport(mergingReport);
}
return mergingReport;
}
private Map<String, NavigationXmlDocument> createNavigationMap() throws MergeFailureException {
Map<String, NavigationXmlDocument> loadedNavigationMap = new HashMap<>();
for (File navigationFile : mNavigationFiles) {
String navigationId = navigationFile.getName().replaceAll("\\.xml$", "");
if (loadedNavigationMap.get(navigationId) != null) {
continue;
}
try (InputStream inputStream = mFileStreamProvider.getInputStream(navigationFile)) {
loadedNavigationMap.put(
navigationId,
NavigationXmlLoader.INSTANCE.load(
navigationId, navigationFile, inputStream));
} catch (Exception e) {
throw new MergeFailureException(e);
}
}
Gson gson = new GsonBuilder().create();
for (File navigationJson : mNavigationJsons) {
try {
String jsonText = FileUtils.loadFileWithUnixLineSeparators(navigationJson);
NavigationXmlDocumentData[] navDatas =
gson.fromJson(jsonText, NavigationXmlDocumentData[].class);
for (NavigationXmlDocumentData navData : navDatas) {
String navigationId = navData.getName();
if (loadedNavigationMap.get(navigationId) != null) {
mLogger.info(
"Navigation file %s from %s is ignored (skipped).",
navigationId, navigationJson);
continue;
}
loadedNavigationMap.put(navigationId, new NavigationXmlDocument(navData));
}
} catch (IOException e) {
throw new MergeFailureException(e);
}
}
return loadedNavigationMap;
}
private static LoadedManifestInfo removeDynamicFeatureManifestSplitAttributeIfSpecified(
@NonNull LoadedManifestInfo dynamicFeatureManifest,
@NonNull MergingReport.Builder mergingReportBuilder) {
Optional<XmlAttribute> splitAttribute =
dynamicFeatureManifest
.getXmlDocument()
.getRootNode()
.getAttribute(XmlNode.fromXmlName(ATTR_SPLIT));
if (splitAttribute.isPresent()) {
String message =
String.format(
"Attribute '%1$s' was removed from %2$s.\n"
+ "The Android Gradle plugin includes it for you "
+ "when building your project.\n"
+ "See %3$s for details.",
ATTR_SPLIT,
splitAttribute.get().printPosition(),
SPLIT_IN_DYNAMIC_FEATURE);
mergingReportBuilder.addMessage(
dynamicFeatureManifest.getXmlDocument().getSourceFile(),
MergingReport.Record.Severity.WARNING,
message);
dynamicFeatureManifest
.getXmlDocument()
.getXml()
.getDocumentElement()
.removeAttribute(ATTR_SPLIT);
return new LoadedManifestInfo(
dynamicFeatureManifest,
dynamicFeatureManifest.getOriginalPackageName(),
dynamicFeatureManifest.getXmlDocument().reparse());
}
return dynamicFeatureManifest;
}
private static int parseMinSdkVersion(@NonNull String minSdkVersion) {
try {
return Integer.parseInt(minSdkVersion);
} catch (NumberFormatException ex) {
return 1;
}
}
/**
* Processes optional features which are not already handled in merge()
*
* @param document the resulting document after merging
* @param mergingReport the merging report builder
*/
private void processOptionalFeatures(
@Nullable Document document, @NonNull MergingReport.Builder mergingReport)
throws MergeFailureException {
if (document == null) {
return;
}
// perform tools: annotations removal if requested.
if (mOptionalFeatures.contains(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)) {
ToolsInstructionsCleaner.cleanToolsReferences(mMergeType, document, mLogger);
}
if (mOptionalFeatures.contains(Invoker.Feature.ADVANCED_PROFILING)) {
addInternetPermission(document);
}
if (mOptionalFeatures.contains(Invoker.Feature.TEST_ONLY)) {
addTestOnlyAttribute(document);
}
if (mOptionalFeatures.contains(Invoker.Feature.DEBUGGABLE)) {
addDebuggableAttribute(document);
}
if (mMergeType == MergeType.APPLICATION) {
optionalAddApplicationTagIfMissing(document);
}
if (mOptionalFeatures.contains(
Invoker.Feature.ADD_ANDROIDX_MULTIDEX_APPLICATION_IF_NO_NAME)) {
addMultiDexApplicationIfNoName(document, SdkConstants.MULTI_DEX_APPLICATION.newName());
} else if (mOptionalFeatures.contains(
Invoker.Feature.ADD_SUPPORT_MULTIDEX_APPLICATION_IF_NO_NAME)) {
addMultiDexApplicationIfNoName(document, SdkConstants.MULTI_DEX_APPLICATION.oldName());
}
if (mOptionalFeatures.contains(Invoker.Feature.ADD_DYNAMIC_FEATURE_ATTRIBUTES)) {
addFeatureSplitAttribute(document, mFeatureName);
adjustInstantAppFeatureSplitInfo(document, mFeatureName);
addUsesSplitTagsForDependencies(document, mDependencyFeatureNames);
}
mergingReport.setMergedDocument(
MergingReport.MergedManifestKind.MERGED, prettyPrint(document));
if (mOptionalFeatures.contains(Invoker.Feature.MAKE_AAPT_SAFE)) {
createAaptSafeManifest(document, mergingReport);
}
}
/**
* Creates a manifest suitable for use with AAPT by (1) substituting placeholders to an AAPT
* friendly encoding and (2) removing any <nav-graph> tags. Saves the modified manifest as part
* of the merging report. Does not mutate the passed in document.
*/
private static void createAaptSafeManifest(
@NonNull Document document, @NonNull MergingReport.Builder mergingReport)
throws MergeFailureException {
Document clonedDocument = cloneDocument(document);
PlaceholderEncoder.visit(clonedDocument);
removeNavGraphs(clonedDocument);
mergingReport.setMergedDocument(
MergingReport.MergedManifestKind.AAPT_SAFE, prettyPrint(clonedDocument));
}
/**
* Set android:testOnly="true" to ensure APK will be rejected by the Play store.
*
* @param document the document for which the testOnly attribute should be set to true.
*/
private static void addTestOnlyAttribute(@NonNull Document document) {
Element manifest = document.getDocumentElement();
ImmutableList<Element> applicationElements =
getChildElementsByName(manifest, SdkConstants.TAG_APPLICATION);
if (!applicationElements.isEmpty()) {
// assumes just 1 application element among manifest's immediate children.
Element application = applicationElements.get(0);
setAndroidAttribute(application, SdkConstants.ATTR_TEST_ONLY, SdkConstants.VALUE_TRUE);
}
}
/**
* Set android:debuggable="true"
*
* @param document the document for which the debuggable attribute should be set to true.
*/
private static void addDebuggableAttribute(@NonNull Document document) {
Element manifest = document.getDocumentElement();
ImmutableList<Element> applicationElements =
getChildElementsByName(manifest, SdkConstants.TAG_APPLICATION);
if (!applicationElements.isEmpty()) {
// assumes just 1 application element among manifest's immediate children.
Element application = applicationElements.get(0);
setAndroidAttribute(application, SdkConstants.ATTR_DEBUGGABLE, SdkConstants.VALUE_TRUE);
}
}
/**
* Adds android:name="{multiDexApplicationName}" if there is no value specified for that field.
*
* @param document the document for which the name attribute might be set.
* @param multiDexApplicationName the FQCN of MultiDexApplication
*/
private static void addMultiDexApplicationIfNoName(
@NonNull Document document, @NonNull String multiDexApplicationName) {
Element manifest = document.getDocumentElement();
ImmutableList<Element> applicationElements =
getChildElementsByName(manifest, SdkConstants.TAG_APPLICATION);
if (!applicationElements.isEmpty()) {
Element application = applicationElements.get(0);
setAndroidAttributeIfMissing(application, ATTR_NAME, multiDexApplicationName);
}
}
/**
* Set android:extractNativeLibs="false" by default is minSdkVersion is M+
*
* @param document the document for which the extractNativeLibs attribute should be set to
* false.
*/
private static void maybeAddExtractNativeLibAttribute(@NonNull Document document) {
Element manifest = document.getDocumentElement();
ImmutableList<Element> applicationElements =
getChildElementsByName(manifest, SdkConstants.TAG_APPLICATION);
if (!applicationElements.isEmpty()) {
Element application = applicationElements.get(0);
setAndroidAttributeIfMissing(
application, SdkConstants.ATTR_EXTRACT_NATIVE_LIBS, SdkConstants.VALUE_FALSE);
}
}
/**
* Set the {@code featureSplit} attribute to {@code featureName} for the manifest element.
*
* @param document the document whose attributes are changed
* @param featureName the feature name of this feature subproject.
*/
private static void addFeatureSplitAttribute(
@NonNull Document document, @NonNull String featureName) {
Element manifest = document.getDocumentElement();
if (manifest == null) {
return;
}
String attributeName = SdkConstants.ATTR_FEATURE_SPLIT;
manifest.setAttribute(attributeName, featureName);
}
/**
* Set the "android:splitName" attribute to {@code featureName} for every {@code activity},
* {@code service} and {@code provider} element.
*
* @param document the document whose attributes are changed
* @param featureName the value all of the changed attributes are set to
*/
private static void adjustInstantAppFeatureSplitInfo(
@NonNull Document document, @NonNull String featureName) {
Element manifest = document.getDocumentElement();
if (manifest == null) {
return;
}
// then update attributes in the application element's child elements
ImmutableList<Element> applicationElements =
getChildElementsByName(manifest, SdkConstants.TAG_APPLICATION);
if (applicationElements.isEmpty()) {
return;
}
// assumes just 1 application element among manifest's immediate children.
Element application = applicationElements.get(0);
List<String> elementNamesToUpdate =
Arrays.asList(
SdkConstants.TAG_ACTIVITY,
SdkConstants.TAG_SERVICE,
SdkConstants.TAG_PROVIDER);
for (String elementName : elementNamesToUpdate) {
for (Element elementToUpdate : getChildElementsByName(application, elementName)) {
setAndroidAttribute(elementToUpdate, SdkConstants.ATTR_SPLIT_NAME, featureName);
}
}
}
/**
* Set an android namespaced attribute for the manifest element.
*
* @param document the document whose attributes will be modified
* @param attribute the new attribute to be set
* @param value the new value of the attribute
* @return the previous value of the attribute or null if the attribute was not set.
*/
public static String setManifestAndroidAttribute(
@NonNull Document document, @NonNull String attribute, @NonNull String value) {
Element manifest = document.getDocumentElement();
if (manifest == null) {
return null;
}
String previousValue =
manifest.hasAttributeNS(SdkConstants.ANDROID_URI, attribute)
? manifest.getAttributeNS(SdkConstants.ANDROID_URI, attribute)
: null;
setAndroidAttribute(manifest, attribute, value);
return previousValue;
}
/**
* Adds internet permission to document if not already present.
*
* @param document the document which gets edited if necessary.
*/
private static void addInternetPermission(@NonNull Document document) {
String permission = "android.permission.INTERNET";
Element manifest = document.getDocumentElement();
ImmutableList<Element> usesPermissions =
getChildElementsByName(manifest, SdkConstants.TAG_USES_PERMISSION);
for (Element usesPermission : usesPermissions) {
if (permission.equals(
usesPermission.getAttributeNS(SdkConstants.ANDROID_URI, ATTR_NAME))) {
return;
}
}
Element uses = document.createElement(SdkConstants.TAG_USES_PERMISSION);
// Add the node to the document before setting the attribute to make sure
// the namespace prefix is found correctly.
document.getDocumentElement().appendChild(uses);
setAndroidAttribute(uses, ATTR_NAME, permission);
}
/**
* Adds <uses-split> tags for feature-on-feature dependencies.
*
* @param dependencyFeatureNames the names of feature modules on which this depends, if any.
*/
private static void addUsesSplitTagsForDependencies(
@NonNull Document document, ImmutableList<String> dependencyFeatureNames) {
Element manifest = document.getDocumentElement();
for (String usedSplitName : dependencyFeatureNames) {
Element usesSplit = document.createElement(SdkConstants.TAG_USES_SPLIT);
setAndroidAttribute(usesSplit, ATTR_NAME, usedSplitName);
manifest.appendChild(usesSplit);
}
}
/**
* Adds <application> tag if missing as it required by package manager in R and above..
*
* @param document the loaded manifest file
*/
private static void optionalAddApplicationTagIfMissing(@NonNull Document document) {
Element manifest = document.getDocumentElement();
if (manifest.getElementsByTagName(SdkConstants.TAG_APPLICATION).getLength() > 0) return;
Element application = document.createElement(SdkConstants.TAG_APPLICATION);
manifest.appendChild(application);
}
/**
* Remove an Android-namespaced XML attribute on the given node.
*
* @param node Node in which to remove the attribute; must be part of a document
* @param localName Non-prefixed attribute name
*/
private static void removeAndroidAttribute(Element node, String localName) {
// removeAttributeNS calculates the prefix.
// Setting it with localName will actually prevent it from working properly.
node.removeAttributeNS(SdkConstants.ANDROID_URI, localName);
}
/**
* Set an Android-namespaced XML attribute on the given node.
*
* @param node Node in which to set the attribute; must be part of a document
* @param localName Non-prefixed attribute name
* @param value value of the attribute
*/
public static void setAndroidAttribute(Element node, String localName, String value) {
String prefix =
XmlUtils.lookupNamespacePrefix(
node, SdkConstants.ANDROID_URI, SdkConstants.ANDROID_NS_NAME, true);
node.setAttributeNS(SdkConstants.ANDROID_URI, prefix + ":" + localName, value);
}
/**
* Set an Android-namespaced XML attribute on the given node, if that attribute is missing.
*
* @param node Node in which to set the attribute; must be part of a document
* @param localName Non-prefixed attribute name
* @param value value of the attribute
*/
private static void setAndroidAttributeIfMissing(Element node, String localName, String value) {
if (!node.hasAttributeNS(SdkConstants.ANDROID_URI, localName)) {
setAndroidAttribute(node, localName, value);
}
}
/**
* Returns a list of elements which are the immediate children of the given element and have the
* given name.
*
* @param element the immediate parent of any elements in the returned list
* @param name the name of any elements in the returned list
* @return the list (possibly empty) of children elements with the given name
*/
@NonNull
public static ImmutableList<Element> getChildElementsByName(
@NonNull Element element, @NonNull String name) {
ImmutableList.Builder<Element> childListBuilder = ImmutableList.builder();
NodeList childNodes = element.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node childNode = childNodes.item(i);
if (childNode instanceof Element && name.equals(childNode.getNodeName())) {
childListBuilder.add((Element) childNode);
}
}
return childListBuilder.build();
}
/** Returns a pretty string representation of the document. */
@NonNull
private static String prettyPrint(Document document) {
return XmlPrettyPrinter.prettyPrint(
document,
XmlFormatPreferences.defaults(),
XmlFormatStyle.get(document.getDocumentElement()),
null, /* endOfLineSeparator */
false /* endWithNewLine */);
}
/** Clones an XML document. */
@NonNull
private static Document cloneDocument(Document document) throws MergeFailureException {
try {
DOMResult domResult = new DOMResult();
TransformerFactory.newInstance()
.newTransformer()
.transform(new DOMSource(document), domResult);
return (Document) domResult.getNode();
} catch (Exception e) {
throw new MergeFailureException(e);
}
}
/**
* Removes all {@link SdkConstants#TAG_NAV_GRAPH} elements from the document. Useful when
* creating an aapt friendly manifest.
*
* @param document the document to clean
*/
public static void removeNavGraphs(@NonNull Document document) {
removeNavGraphs(document.getDocumentElement());
}
/**
* Recursively removes all {@link SdkConstants#TAG_NAV_GRAPH} elements.
*
* @param element the element to recursively clean
*/
private static void removeNavGraphs(@NonNull Element element) {
if (SdkConstants.TAG_NAV_GRAPH.equals(element.getTagName())) {
// Delete the entire node
element.getParentNode().removeChild(element);
return;
}
// make a copy of the element children since we will be removing some during
// this process, we don't want side effects.
NodeList childNodes = element.getChildNodes();
ImmutableList.Builder<Element> childElements = ImmutableList.builder();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
childElements.add((Element) node);
}
}
for (Element childElement : childElements.build()) {
removeNavGraphs(childElement);
}
}
/**
* Returns the {@link FileStreamProvider} used by this manifest merger. Use this to read files
* if you need to access the content of a {@link XmlDocument}.
*/
@SuppressWarnings("unused") // Allow future library usage, if necessary
@NonNull
public FileStreamProvider getFileStreamProvider() {
return mFileStreamProvider;
}
/**
* Creates the merging report file.
* @param mergingReport the merging activities report to serialize.
*/
private void writeReport(@NonNull MergingReport mergingReport) {
FileWriter fileWriter = null;
try {
if (!mReportFile.isPresent()
|| !mReportFile.get().getParentFile().exists()
&& !mReportFile.get().getParentFile().mkdirs()) {
mLogger.warning(String.format(
"Cannot create %1$s manifest merger report file,"
+ "build will continue but merging activities "
+ "will not be documented",
mReportFile.get().getAbsolutePath()));
} else {
fileWriter = new FileWriter(mReportFile.get());
mergingReport.getActions().log(fileWriter);
}
} catch (IOException e) {
mLogger.warning(String.format(
"Error '%1$s' while writing the merger report file, "
+ "build can continue but merging activities "
+ "will not be documented ",
e.getMessage()));
} finally {
if (fileWriter != null) {
try {
fileWriter.close();
} catch (IOException e) {
mLogger.warning(String.format(
"Error '%1$s' while closing the merger report file, "
+ "build can continue but merging activities "
+ "will not be documented ",
e.getMessage()));
}
}
}
}
/**
* shorten all fully qualified class name that belong to the same package as the manifest's
* package attribute value.
*
* @param finalMergedDocument the AndroidManifest.xml document.
*/
private static void extractFqcns(@NonNull XmlDocument finalMergedDocument) {
extractFqcns(finalMergedDocument.getPackageName(), finalMergedDocument.getRootNode());
}
/**
* shorten recursively all attributes that are package dependent of the passed nodes and all its
* child nodes.
*
* @param packageName the manifest package name.
* @param xmlElement the xml element to process recursively.
*/
private static void extractFqcns(@NonNull String packageName, @NonNull XmlElement xmlElement) {
String packagePrefix = packageName + ".";
for (XmlAttribute xmlAttribute : xmlElement.getAttributes()) {
if (xmlAttribute.getModel() != null && xmlAttribute.getModel().isPackageDependent()) {
String value = xmlAttribute.getValue();
if (value.startsWith(packagePrefix)) {
xmlAttribute.getXml().setValue(value.substring(packageName.length()));
}
}
}
for (XmlElement child : xmlElement.getMergeableElements()) {
extractFqcns(packageName, child);
}
}
/**
* Load an xml file and perform placeholder substitution
*
* @param manifestInfo the android manifest information like if it is a library, an overlay or a
* main manifest file.
* @param selectors all the libraries selectors
* @param mergingReportBuilder the merging report to store events and errors.
* @return a loaded manifest info.
* @throws MergeFailureException if the merging cannot be completed successfully.
*/
@NonNull
private LoadedManifestInfo load(
@NonNull ManifestInfo manifestInfo,
@NonNull KeyResolver<String> selectors,
@NonNull MergingReport.Builder mergingReportBuilder)
throws MergeFailureException {
boolean rewriteNamespaces =
mOptionalFeatures.contains(Invoker.Feature.FULLY_NAMESPACE_LOCAL_RESOURCES);
File xmlFile = manifestInfo.mLocation;
XmlDocument xmlDocument;
try {
InputStream inputStream = mFileStreamProvider.getInputStream(xmlFile);
xmlDocument =
XmlLoader.load(
selectors,
mSystemPropertyResolver,
manifestInfo.mName,
xmlFile,
inputStream,
manifestInfo.getType(),
manifestInfo.getMainManifestPackageName(),
mModel,
rewriteNamespaces);
} catch (Exception e) {
throw new MergeFailureException(e);
}
String originalPackageName = xmlDocument.getPackageName();
MergingReport.Builder builder =
manifestInfo.getType() == XmlDocument.Type.MAIN
? mergingReportBuilder
: new MergingReport.Builder(mergingReportBuilder.getLogger());
// create updatedManifestInfo to have access to the packageName for
// placeholder substitution if this is the MAIN manifest
ManifestInfo updatedManifestInfo =
manifestInfo.getType() == XmlDocument.Type.MAIN
? new ManifestInfo(
manifestInfo.getName(),
manifestInfo.getLocation(),
manifestInfo.getType(),
originalPackageName)
: manifestInfo;
// perform place holder substitution, this is necessary to do so early in case placeholders
// are used in key attributes.
MergingReport.Record.Severity severity =
mMergeType == MergeType.LIBRARY
? MergingReport.Record.Severity.INFO
: MergingReport.Record.Severity.ERROR;
performPlaceHolderSubstitution(updatedManifestInfo, xmlDocument, builder, severity);
builder.getActionRecorder().recordAddedNodeAction(xmlDocument.getRootNode(), false);
return new LoadedManifestInfo(
updatedManifestInfo, Optional.ofNullable(originalPackageName), xmlDocument);
}
private void performPlaceHolderSubstitution(
@NonNull ManifestInfo manifestInfo,
@NonNull XmlDocument xmlDocument,
@NonNull MergingReport.Builder mergingReportBuilder,
@NonNull MergingReport.Record.Severity severity) {
if (mOptionalFeatures.contains(Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT)) {
return;
}
// check for placeholders presence, switch first the packageName and applicationId if
// it is not explicitly set, unless dealing with a LIBRARY MergeType.
// In case of a LIBRARY MergeType, we don't replace packageName or applicationId,
// unless they're already specified in mPlaceHolderValues.
Map<String, Object> finalPlaceHolderValues = mPlaceHolderValues;
if (!mPlaceHolderValues.containsKey(APPLICATION_ID)
&& mMergeType != MergeType.LIBRARY
&& manifestInfo.getMainManifestPackageName() != null) {
String packageName = manifestInfo.getMainManifestPackageName();
// add all existing placeholders except package name that will be swapped.
ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
for (Map.Entry<String, Object> entry : mPlaceHolderValues.entrySet()) {
if (!entry.getKey().equals(PACKAGE_NAME)) {
builder.put(entry);
}
}
builder.put(PACKAGE_NAME, packageName);
builder.put(APPLICATION_ID, packageName);
finalPlaceHolderValues = builder.build();
}
KeyBasedValueResolver<String> placeHolderValueResolver =
new MapBasedKeyBasedValueResolver<>(finalPlaceHolderValues);
PlaceholderHandler.visit(
severity, xmlDocument, placeHolderValueResolver, mergingReportBuilder);
}
// merge the optionally existing xmlDocument with a lower priority xml file.
private Optional<XmlDocument> merge(
@Nullable XmlDocument xmlDocument,
@NonNull LoadedManifestInfo lowerPriorityDocument,
@NonNull MergingReport.Builder mergingReportBuilder) {
MergingReport.Result validationResult = PreValidator
.validate(mergingReportBuilder, lowerPriorityDocument.getXmlDocument());
if (validationResult == MergingReport.Result.ERROR) {
mergingReportBuilder.addMessage(
lowerPriorityDocument.getXmlDocument().getSourceFile(),
MergingReport.Record.Severity.ERROR,
"Validation failed, exiting");
return Optional.empty();
}
Optional<XmlDocument> result;
if (xmlDocument != null) {
result =
xmlDocument.merge(
lowerPriorityDocument.getXmlDocument(),
mergingReportBuilder,
!mOptionalFeatures.contains(
Invoker.Feature.NO_IMPLICIT_PERMISSION_ADDITION));
} else {
// exhaustiveSearch is true in recordAddedNodeAction() below because some of this
// manifest's nodes might have already been recorded from the loading of
// the main manifest, but we want to record any unrecorded descendants.
// e.g., if the main manifest did not contain any meta-data nodes below its
// application node, we still want to record the addition of any such
// meta-data nodes this manifest contains.
mergingReportBuilder
.getActionRecorder()
.recordAddedNodeAction(
lowerPriorityDocument.getXmlDocument().getRootNode(), true);
result = Optional.of(lowerPriorityDocument.getXmlDocument());
}
// if requested, dump each intermediary merging stage into the report.
if (mOptionalFeatures.contains(Invoker.Feature.KEEP_INTERMEDIARY_STAGES)
&& result.isPresent()) {
mergingReportBuilder.addMergingStage(result.get().prettyPrint());
}
return result;
}
private List<LoadedManifestInfo> loadLibraries(
@NonNull SelectorResolver selectors,
@NonNull MergingReport.Builder mergingReportBuilder,
@Nullable String mainManifestPackageName)
throws MergeFailureException {
ImmutableList.Builder<LoadedManifestInfo> loadedLibraryDocuments = ImmutableList.builder();
for (Pair<String, File> libraryFile : Sets.newLinkedHashSet(mLibraryFiles)) {
mLogger.verbose("Loading library manifest " + libraryFile.getSecond().getPath());
ManifestInfo manifestInfo =
new ManifestInfo(
libraryFile.getFirst(),
libraryFile.getSecond(),
XmlDocument.Type.LIBRARY,
mainManifestPackageName);
File xmlFile = manifestInfo.mLocation;
XmlDocument libraryDocument;
try {
InputStream inputStream = mFileStreamProvider.getInputStream(xmlFile);
libraryDocument =
XmlLoader.load(
selectors,
mSystemPropertyResolver,
manifestInfo.mName,
xmlFile,
inputStream,
XmlDocument.Type.LIBRARY,
null, /* mainManifestPackageName */
mModel,
false);
} catch (Exception e) {
throw new MergeFailureException(e);
}
// extract the package name...
String libraryPackage = libraryDocument.getRootNode().getXml().getAttribute("package");
// save it in the selector instance.
if (!Strings.isNullOrEmpty(libraryPackage)) {
selectors.addSelector(libraryPackage, libraryFile.getFirst());
}
// perform placeholder substitution, this is useful when the library is using
// a placeholder in a key element, we however do not need to record these
// substitutions so feed it with a fake merging report.
MergingReport.Builder builder =
new MergingReport.Builder(mergingReportBuilder.getLogger());
builder.getActionRecorder().recordAddedNodeAction(libraryDocument.getRootNode(), false);
performPlaceHolderSubstitution(
manifestInfo, libraryDocument, builder, MergingReport.Record.Severity.INFO);
if (builder.hasErrors()) {
// we log the errors but continue, in case the error is of no consequence
// to the application consuming the library.
builder.build().log(mLogger);
}
LoadedManifestInfo info =
new LoadedManifestInfo(
manifestInfo,
Optional.ofNullable(libraryDocument.getPackageName()),
libraryDocument);
loadedLibraryDocuments.add(info);
}
return loadedLibraryDocuments.build();
}
/**
* Checks whether all manifests have unique package names. If the strict mode is enabled it will
* result in an error for name collisions, otherwise it will result in a warning.
*/
private static void checkUniquePackageName(
@NonNull LoadedManifestInfo mainPackage,
@NonNull List<LoadedManifestInfo> libraries,
@NonNull MergingReport.Builder mergingReportBuilder,
boolean strictUniquePackageNameCheck) {
Multimap<String, LoadedManifestInfo> uniquePackageNameMap = ArrayListMultimap.create();
// Is main manifest is a Overlay we need to fallback.
if (mainPackage.getOriginalPackageName().isPresent()) {
uniquePackageNameMap.put(mainPackage.getOriginalPackageName().get(), mainPackage);
} else if (mainPackage.getMainManifestPackageName() != null) {
uniquePackageNameMap.put(mainPackage.getMainManifestPackageName(), mainPackage);
}
libraries
.stream()
.filter(l -> l.getOriginalPackageName().isPresent())
.forEach(l -> uniquePackageNameMap.put(l.getOriginalPackageName().get(), l));
uniquePackageNameMap.asMap().entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.forEach(
e -> {
Collection<String> offendingTargets =
e.getValue().stream()
.map(ManifestInfo::getName)
.collect(Collectors.toList());
String repeatedPackageErrors =
"Package name '"
+ e.getKey()
+ "' used in: "
+ Joiner.on(", ").join(offendingTargets)
+ ".";
// We know that there is at least one because of the filter check.
LoadedManifestInfo info = e.getValue().stream().findFirst().get();
// Report only once per error, since the error message contain the path
// to all manifests with the repeated package name.
mergingReportBuilder.addMessage(
info.getXmlDocument().getSourceFile(),
getNonUniquePackageSeverity(
e.getKey(), strictUniquePackageNameCheck),
repeatedPackageErrors);
});
}
/** Returns the correct logging severity for a clashing package name. */
private static MergingReport.Record.Severity getNonUniquePackageSeverity(
String packageName, boolean strictMode) {
// If we've whitelisted a library package only report in info.
if (WHITELISTED_NON_UNIQUE_PACKAGE_NAMES.contains(packageName))
return MergingReport.Record.Severity.INFO;
return strictMode
? MergingReport.Record.Severity.ERROR
: MergingReport.Record.Severity.WARNING;
}
/**
* Creates a new {@link Invoker} instance to invoke the merging tool to merge manifest files for
* an application.
*
* @param mainManifestFile application main manifest file.
* @param logger the logger interface to use.
* @return an {@link Invoker} instance that will allow further customization and trigger the
* merging tool.
*/
@NonNull
public static Invoker newMerger(
@NonNull File mainManifestFile, @NonNull ILogger logger, @NonNull MergeType mergeType) {
return new Invoker(mainManifestFile, logger, mergeType, XmlDocument.Type.MAIN);
}
/**
* Defines the merging type expected from the tool.
*/
public enum MergeType {
/**
* Application merging type is used when packaging an application with a set of imported
* libraries. The resulting merged android manifest is final and is not expected to be
* imported in another application.
*/
APPLICATION,
/**
* Library merging type is used when packaging a library. The resulting android manifest
* file will not merge in all the imported libraries this library depends on. Also the tools
* annotations will not be removed as they can be useful when later importing the resulting
* merged android manifest into an application.
*/
LIBRARY
}
/**
* Defines a property that can add or override itself into an XML document.
*/
public interface AutoAddingProperty {
/**
* Add itself (possibly just override the current value) with the passed value
* @param actionRecorder to record actions.
* @param document the xml document to add itself to.
* @param value the value to set of this property.
*/
void addTo(@NonNull ActionRecorder actionRecorder,
@NonNull XmlDocument document,
@NonNull String value);
}
/**
* Perform {@link ManifestSystemProperty} injection.
* @param mergingReport to log actions and errors.
* @param xmlDocument the xml document to inject into.
*/
protected void performSystemPropertiesInjection(
@NonNull MergingReport.Builder mergingReport,
@NonNull XmlDocument xmlDocument) {
for (ManifestSystemProperty manifestSystemProperty : ManifestSystemProperty.values()) {
String propertyOverride = mSystemPropertyResolver.getValue(manifestSystemProperty);
if (propertyOverride != null) {
manifestSystemProperty.addTo(
mergingReport.getActionRecorder(), xmlDocument, propertyOverride);
}
}
}
/**
* A {@linkplain FileStreamProvider} provides (buffered, if necessary) {@link InputStream}
* instances for a given {@link File} handle.
*/
public static class FileStreamProvider {
/**
* Creates a reader for the given file -- which may not necessarily read the contents of the
* file on disk. For example, in the IDE, the client will map the file handle to a document in
* the editor, and read the current contents of that editor whether or not it has been saved.
* <p>
* This method is responsible for providing its own buffering, if necessary (e.g. when
* reading from disk, make sure you wrap the file stream in a buffering input stream.)
*
* @param file the file handle
* @return the contents of the file
* @throws FileNotFoundException if the file handle is invalid
*/
protected InputStream getInputStream(@NonNull File file) throws IOException {
return new BufferedInputStream(new FileInputStream(file));
}
}
/**
* This class will hold all invocation parameters for the manifest merging tool.
*
* <p>There are broadly three types of input to the merging tool :
*
* <ul>
* <li>Build types and flavors overriding manifests
* <li>Application main manifest
* <li>Library manifest files
* </ul>
*
* Only the main manifest file is a mandatory parameter.
*
* <p>High level description of the merging will be as follow :
*
* <ol>
* <li>Build type and flavors will be merged first in the order they were added. Highest
* priority file added first, lowest added last.
* <li>Resulting document is merged with lower priority application main manifest file.
* <li>Resulting document is merged with each library file manifest file in the order they
* were added. Highest priority added first, lowest added last.
* <li>Resulting document is returned as results of the merging process.
* </ol>
*/
public static class Invoker {
protected final File mMainManifestFile;
protected final ImmutableMap.Builder<ManifestSystemProperty, Object> mSystemProperties =
new ImmutableMap.Builder<>();
@NonNull
protected final ILogger mLogger;
@NonNull
protected final ImmutableMap.Builder<String, Object> mPlaceholders =
new ImmutableMap.Builder<>();
@NonNull
private final ImmutableList.Builder<Pair<String, File>> mLibraryFilesBuilder =
new ImmutableList.Builder<>();
@NonNull
private final ImmutableList.Builder<File> mFlavorsAndBuildTypeFiles =
new ImmutableList.Builder<>();
@NonNull
private final ImmutableList.Builder<Feature> mFeaturesBuilder =
new ImmutableList.Builder<>();
@NonNull
private final MergeType mMergeType;
@NonNull private XmlDocument.Type mDocumentType;
@Nullable private File mReportFile;
@Nullable
private FileStreamProvider mFileStreamProvider;
@NonNull private String mFeatureName;
@NonNull
private final ImmutableList.Builder<File> mNavigationFilesBuilder =
new ImmutableList.Builder<>();
@NonNull
private final ImmutableList.Builder<File> mNavigationJsonsBuilder =
new ImmutableList.Builder<>();
@NonNull
private final ImmutableList.Builder<String> mDependencyFetureNamesBuilder =
new ImmutableList.Builder<>();
/**
* Sets a value for a {@link ManifestSystemProperty}
* @param override the property to set
* @param value the value for the property
* @return itself.
*/
@NonNull
public Invoker setOverride(@NonNull ManifestSystemProperty override, @NonNull String value) {
mSystemProperties.put(override, value);
return this;
}
/**
* Adds placeholders names and associated values for substitution.
* @return itself.
*/
@NonNull
public Invoker setPlaceHolderValues(@NonNull Map<String, Object> keyValuePairs) {
mPlaceholders.putAll(keyValuePairs);
return this;
}
/**
* Adds a new placeholder name and value for substitution.
* @return itself.
*/
@NonNull
public Invoker setPlaceHolderValue(@NonNull String placeHolderName, @NonNull String value) {
mPlaceholders.put(placeHolderName, value);
return this;
}
/**
* Optional behavior of the merging tool can be turned on by setting these Feature.
*/
public enum Feature {
/**
* Keep all intermediary merged files during the merging process. This is particularly
* useful for debugging/tracing purposes.
*/
KEEP_INTERMEDIARY_STAGES,
/**
* When logging file names, use {@link File#getName()} rather than {@link
* File#getPath()}
*/
PRINT_SIMPLE_FILENAMES,
/**
* Perform a sweep after all merging activities to remove all fully qualified class
* names and replace them with the equivalent short version.
*/
EXTRACT_FQCNS,
/**
* Perform a sweep after all merging activities to remove all tools: decorations.
*/
REMOVE_TOOLS_DECLARATIONS,
/**
* Do no perform placeholders replacement.
*/
NO_PLACEHOLDER_REPLACEMENT,
/**
* Encode unresolved placeholders to be AAPT friendly.
*/
MAKE_AAPT_SAFE,
/**
* Clients will not request the blame history
*/
SKIP_BLAME,
/**
* Clients will only request the merged XML documents, not XML pretty printed documents
*/
SKIP_XML_STRING,
/**
* Add android:testOnly="true" attribute to prevent APK from being uploaded to Play
* store.
*/
TEST_ONLY,
/**
* Do not perform implicit permission addition.
*/
NO_IMPLICIT_PERMISSION_ADDITION,
/** Perform Studio advanced profiling manifest modifications */
ADVANCED_PROFILING,
/** Mark this application as a feature split */
ADD_DYNAMIC_FEATURE_ATTRIBUTES,
/** Set the android:debuggable flag to the application. */
DEBUGGABLE,
/**
* When there are attribute value conflicts, automatically pick the higher priority
* value.
*
* <p>This is for example used in the IDE when we need to merge a new manifest template
* into an existing one and we don't want to abort the merge.
*
* <p>(This will log a warning.)
*/
HANDLE_VALUE_CONFLICTS_AUTOMATICALLY,
/**
* Adds the AndroidX name of {@link SdkConstants#MULTI_DEX_APPLICATION} as application
* name if none is specified. Used for legacy multidex.
*/
ADD_ANDROIDX_MULTIDEX_APPLICATION_IF_NO_NAME,
/**
* Adds the pre-AndroidX name of {@link SdkConstants#MULTI_DEX_APPLICATION} as
* application name if none is specified. Used for legacy multidex.
*/
ADD_SUPPORT_MULTIDEX_APPLICATION_IF_NO_NAME,
/** Rewrite local resource references with fully qualified namespace */
FULLY_NAMESPACE_LOCAL_RESOURCES,
/** Enforce that dependencies manifests don't have duplicated package names. */
ENFORCE_UNIQUE_PACKAGE_NAME,
}
/**
* Creates a new builder with the mandatory main manifest file.
* @param mainManifestFile application main manifest file.
* @param logger the logger interface to use.
*/
private Invoker(
@NonNull File mainManifestFile,
@NonNull ILogger logger,
@NonNull MergeType mergeType,
@NonNull XmlDocument.Type documentType) {
this.mMainManifestFile = Preconditions.checkNotNull(mainManifestFile);
this.mLogger = logger;
this.mMergeType = mergeType;
this.mDocumentType = documentType;
this.mFeatureName = "";
}
/**
* Sets the file to use to write the merging report. If not called,
* the merging process will not write a report.
* @param mergeReport the file to write the report in.
* @return itself.
*/
@NonNull
public Invoker setMergeReportFile(@Nullable File mergeReport) {
mReportFile = mergeReport;
return this;
}
/**
* Add one library file manifest, will be added last in the list of library files which will
* make the parameter the lowest priority library manifest file.
* @param file the library manifest file to add.
* @return itself.
*/
@NonNull
public Invoker addLibraryManifest(@NonNull File file) {
addLibraryManifest(file.getName(), file);
return this;
}
/**
* Add one library file manifest, will be added last in the list of library files which will
* make the parameter the lowest priority library manifest file.
* @param file the library manifest file to add.
* @param name the library name.
* @return itself.
*/
@NonNull
public Invoker addLibraryManifest(@NonNull String name, @NonNull File file) {
if (mMergeType == MergeType.LIBRARY) {
throw new IllegalStateException(
"Cannot add library dependencies manifests when creating a library");
}
mLibraryFilesBuilder.add(Pair.of(name, file));
return this;
}
/**
* Sets library dependencies for this merging activity.
* @param namesAndFiles the list of library dependencies.
* @return itself.
*
* @deprecated use addLibraryManifest or addAndroidBundleManifests
*/
@NonNull
@Deprecated
public Invoker addBundleManifests(@NonNull List<Pair<String, File>> namesAndFiles) {
if (mMergeType == MergeType.LIBRARY && !namesAndFiles.isEmpty()) {
throw new IllegalStateException(
"Cannot add library dependencies manifests when creating a library");
}
mLibraryFilesBuilder.addAll(namesAndFiles);
return this;
}
/**
* Sets manifest providers for this merging activity.
* @param providers the list of manifest providers.
* @return itself.
*/
@NonNull
public Invoker addManifestProviders(@NonNull Iterable<? extends ManifestProvider> providers) {
for (ManifestProvider provider : providers) {
mLibraryFilesBuilder.add(Pair.of(provider.getName(), provider.getManifest()));
}
return this;
}
/**
* Add several library file manifests at then end of the list which will make them the
* lowest priority manifest files. The relative priority between all the files passed as
* parameters will be respected.
* @param files library manifest files to add last.
* @return itself.
*/
@NonNull
public Invoker addLibraryManifests(@NonNull File... files) {
for (File file : files) {
addLibraryManifest(file);
}
return this;
}
/**
* Add a flavor or build type manifest file last in the list.
* @param file build type or flavor manifest file
* @return itself.
*/
@NonNull
public Invoker addFlavorAndBuildTypeManifest(@NonNull File file) {
this.mFlavorsAndBuildTypeFiles.add(file);
return this;
}
/**
* Add several flavor or build type manifest files last in the list. Relative priorities
* between the passed files as parameters will be respected.
* @param files build type of flavor manifest files to add.
* @return itself.
*/
@NonNull
public Invoker addFlavorAndBuildTypeManifests(File... files) {
this.mFlavorsAndBuildTypeFiles.add(files);
return this;
}
/**
* Sets some optional features for the merge tool.
*
* @param features one to many features to set.
* @return itself.
*/
@NonNull
public Invoker withFeatures(Feature...features) {
mFeaturesBuilder.add(features);
return this;
}
/**
* Sets a file stream provider which allows the client of the manifest merger to provide
* arbitrary content lookup for files. <p> NOTE: There should only be one.
*
* @param provider the provider to use
* @return itself.
*/
@NonNull
public Invoker withFileStreamProvider(@Nullable FileStreamProvider provider) {
assert mFileStreamProvider == null || provider == null;
mFileStreamProvider = provider;
return this;
}
/** Regular expression defining legal feature split name. */
private static final Pattern FEATURE_NAME_PATTERN =
Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_]*");
/**
* Specify the feature name for feature merging.
*
* @param featureName the feature name to use.
* @return itself.
*/
@NonNull
public Invoker setFeatureName(@Nullable String featureName) {
if (featureName != null) {
mFeatureName = featureName;
if (!FEATURE_NAME_PATTERN.matcher(mFeatureName).matches()) {
throw new IllegalArgumentException(
"FeatureName must follow "
+ FEATURE_NAME_PATTERN.pattern()
+ " regex, found "
+ featureName);
}
}
return this;
}
/**
* Add several navigation files last in the list. Relative priorities between the passed
* files as parameters will be respected.
*
* @param files the navigation files to add.
* @return itself.
*/
@NonNull
public Invoker addNavigationFiles(@NonNull Iterable<File> files) {
this.mNavigationFilesBuilder.addAll(files);
return this;
}
/**
* Add several navigation.json files in the list.
*
* @param files the navigation.json files to add.
* @return itself.
*/
@NonNull
public Invoker addNavigationJsons(@NonNull Iterable<File> files) {
this.mNavigationJsonsBuilder.addAll(files);
return this;
}
/**
* Specify if the file being merged is an overlay (flavor). If not called, the merging
* process will assume a master manifest merge. The master manifest needs to have a package
* and some other mandatory fields like "uses-sdk", etc.
*
* @return itself.
*/
@NonNull
public Invoker asType(XmlDocument.Type type) {
mDocumentType = type;
return this;
}
/**
* Specifies a list of feature modules on which this module will depend. This is only valid
* for feature manifests.
*
* @param names the names of the dynamic features.
* @return itself.
*/
public Invoker addDependencyFeatureNames(@NonNull Iterable<String> names) {
this.mDependencyFetureNamesBuilder.addAll(names);
return this;
}
/**
* Perform the merging and return the result.
*
* @return an instance of {@link MergingReport} that will give access to all the logging and
* merging records.
* <p>This method can be invoked several time and will re-do the file merges.
* @throws MergeFailureException if the merging cannot be completed successfully.
*/
@NonNull
public MergingReport merge() throws MergeFailureException {
// provide some free placeholders values.
ImmutableMap<ManifestSystemProperty, Object> systemProperties = mSystemProperties.build();
if (systemProperties.containsKey(ManifestSystemProperty.PACKAGE)) {
// if the package is provided, make it available for placeholder replacement.
mPlaceholders.put(PACKAGE_NAME, systemProperties.get(ManifestSystemProperty.PACKAGE));
// as well as applicationId since package system property overrides everything
// but not when output is a library since only the final (application)
// application Id should be used to replace libraries "applicationId" placeholders.
if (mMergeType != MergeType.LIBRARY) {
mPlaceholders.put(APPLICATION_ID, systemProperties.get(ManifestSystemProperty.PACKAGE));
}
}
FileStreamProvider fileStreamProvider = mFileStreamProvider != null
? mFileStreamProvider : new FileStreamProvider();
ManifestMerger2 manifestMerger =
new ManifestMerger2(
mLogger,
mMainManifestFile,
mLibraryFilesBuilder.build(),
mFlavorsAndBuildTypeFiles.build(),
mFeaturesBuilder.build(),
mPlaceholders.build(),
new MapBasedKeyBasedValueResolver<>(systemProperties),
mMergeType,
mDocumentType,
Optional.ofNullable(mReportFile),
mFeatureName,
fileStreamProvider,
mNavigationFilesBuilder.build(),
mNavigationJsonsBuilder.build(),
mDependencyFetureNamesBuilder.build());
return manifestMerger.merge();
}
}
/**
* Helper class for map based placeholders key value pairs.
*/
public static class MapBasedKeyBasedValueResolver<T> implements KeyBasedValueResolver<T> {
private final ImmutableMap<T, Object> keyValues;
public MapBasedKeyBasedValueResolver(@NonNull Map<T, Object> keyValues) {
this.keyValues = ImmutableMap.copyOf(keyValues);
}
@Nullable
@Override
public String getValue(@NonNull T key) {
Object value = keyValues.get(key);
return value == null ? null : value.toString();
}
}
private static class ManifestInfo {
private ManifestInfo(
String name,
File location,
XmlDocument.Type type,
@Nullable String mainManifestPackageName) {
mName = name;
mLocation = location;
mType = type;
mMainManifestPackageName = mainManifestPackageName;
}
private final String mName;
private final File mLocation;
private final XmlDocument.Type mType;
@Nullable private final String mMainManifestPackageName;
String getName() {
return mName;
}
File getLocation() {
return mLocation;
}
XmlDocument.Type getType() {
return mType;
}
@Nullable
String getMainManifestPackageName() {
return mMainManifestPackageName;
}
}
private static class LoadedManifestInfo extends ManifestInfo {
@NonNull private final XmlDocument mXmlDocument;
@NonNull private final Optional<String> mOriginalPackageName;
private LoadedManifestInfo(@NonNull ManifestInfo manifestInfo,
@NonNull Optional<String> originalPackageName,
@NonNull XmlDocument xmlDocument) {
super(manifestInfo.mName,
manifestInfo.mLocation,
manifestInfo.mType,
manifestInfo.getMainManifestPackageName());
mXmlDocument = xmlDocument;
mOriginalPackageName = originalPackageName;
}
@NonNull
public XmlDocument getXmlDocument() {
return mXmlDocument;
}
@NonNull
public Optional<String> getOriginalPackageName() {
return mOriginalPackageName;
}
}
/**
* Implementation a {@link KeyResolver} capable of resolving all selectors value in the context
* of the passed libraries to this merging activities.
*/
public static class SelectorResolver implements KeyResolver<String> {
private final Map<String, String> mSelectors = new HashMap<>();
protected void addSelector(String key, String value) {
mSelectors.put(key, value);
}
@Nullable
@Override
public String resolve(String key) {
return mSelectors.get(key);
}
@NonNull
@Override
public Iterable<String> getKeys() {
return mSelectors.keySet();
}
}
// a wrapper exception to all sorts of failure exceptions that can be thrown during merging.
public static class MergeFailureException extends Exception {
protected MergeFailureException(Exception cause) {
super(cause);
}
}
}