blob: c75e715a634828702b374be4e7fd0708b8f1e5d7 [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.manifmerger.PlaceholderHandler.APPLICATION_ID;
import static com.android.manifmerger.PlaceholderHandler.KeyBasedValueResolver;
import static com.android.manifmerger.PlaceholderHandler.PACKAGE_NAME;
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.blame.SourceFilePosition;
import com.android.ide.common.blame.SourcePosition;
import com.android.utils.ILogger;
import com.android.utils.Pair;
import com.android.utils.SdkUtils;
import com.android.utils.StdLogger;
import com.android.utils.XmlUtils;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* merges android manifest files, idempotent.
*/
@Immutable
public class ManifestMerger2 {
@NonNull
private final File mManifestFile;
@NonNull
private final Map<String, Object> mPlaceHolderValues;
@NonNull
private final KeyBasedValueResolver<SystemProperty> mSystemPropertyResolver;
private final ILogger mLogger;
private final ImmutableList<Pair<String, File>> mLibraryFiles;
private final ImmutableList<File> mFlavorsAndBuildTypeFiles;
private final ImmutableList<Invoker.Feature> mOptionalFeatures;
private final MergeType mMergeType;
private final Optional<File> mReportFile;
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<SystemProperty> systemPropertiesResolver,
@NonNull MergeType mergeType,
@NonNull Optional<File> reportFile) {
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.mReportFile = reportFile;
}
/**
* Perform high level ordering of files merging and delegates actual merging to
* {@link XmlDocument#merge(XmlDocument, com.android.manifmerger.MergingReport.Builder)}
*
* @return the merging activity report.
* @throws MergeFailureException if the merging cannot be completed (for instance, if xml
* files cannot be loaded).
*/
private MergingReport merge() throws MergeFailureException {
// initiate a new merging report
MergingReport.Builder mergingReportBuilder = new MergingReport.Builder(mLogger);
SelectorResolver selectors = new SelectorResolver();
// load all the libraries xml files up front to have a list of all possible node:selector
// values.
List<LoadedManifestInfo> loadedLibraryDocuments =
loadLibraries(selectors, mergingReportBuilder);
// load the main manifest file to do some checking along the way.
LoadedManifestInfo loadedMainManifestInfo = load(
new ManifestInfo(
mManifestFile.getName(),
mManifestFile,
XmlDocument.Type.MAIN,
Optional.<String>absent() /* mainManifestPackageName */),
selectors,
mergingReportBuilder);
// first do we have a package declaration in the main manifest ?
Optional<XmlAttribute> mainPackageAttribute =
loadedMainManifestInfo.getXmlDocument().getPackage();
if (!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();
}
// 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.
Optional<XmlDocument> xmlDocumentOptional = Optional.absent();
for (File inputFile : mFlavorsAndBuildTypeFiles) {
mLogger.info("Merging flavors and build manifest %s \n", inputFile.getPath());
LoadedManifestInfo overlayDocument = load(
new ManifestInfo(null, inputFile, XmlDocument.Type.OVERLAY,
Optional.of(mainPackageAttribute.get().getValue())),
selectors,
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();
}
overlayDocument.getXmlDocument().getRootNode().getXml().setAttribute("package",
mainPackageAttribute.get().getValue());
xmlDocumentOptional = merge(xmlDocumentOptional, overlayDocument, mergingReportBuilder);
if (!xmlDocumentOptional.isPresent()) {
return mergingReportBuilder.build();
}
}
mLogger.info("Merging main manifest %s\n", mManifestFile.getPath());
xmlDocumentOptional =
merge(xmlDocumentOptional, loadedMainManifestInfo, mergingReportBuilder);
if (!xmlDocumentOptional.isPresent()) {
return mergingReportBuilder.build();
}
// 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.get().getRootNode().getXml()
.setAttribute("package", mainManifestPackageName);
}
}
for (LoadedManifestInfo libraryDocument : loadedLibraryDocuments) {
mLogger.info("Merging library manifest " + libraryDocument.getLocation());
xmlDocumentOptional = merge(
xmlDocumentOptional, libraryDocument, mergingReportBuilder);
if (!xmlDocumentOptional.isPresent()) {
return mergingReportBuilder.build();
}
}
// done with proper merging phase, now we need to trim unwanted elements, placeholder
// substitution and system properties injection.
ElementsTrimmer.trim(xmlDocumentOptional.get(), 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.
KeyBasedValueResolver<String> placeHolderValueResolver =
new MapBasedKeyBasedValueResolver<String>(mPlaceHolderValues);
PlaceholderHandler placeholderHandler = new PlaceholderHandler();
placeholderHandler.visit(
mMergeType,
xmlDocumentOptional.get(),
placeHolderValueResolver,
mergingReportBuilder);
if (mergingReportBuilder.hasErrors()) {
return mergingReportBuilder.build();
}
}
// perform system property injection.
performSystemPropertiesInjection(mergingReportBuilder, xmlDocumentOptional.get());
XmlDocument finalMergedDocument = xmlDocumentOptional.get();
PostValidator.validate(finalMergedDocument, mergingReportBuilder);
if (mergingReportBuilder.hasErrors()) {
finalMergedDocument.getRootNode().addMessage(mergingReportBuilder,
MergingReport.Record.Severity.WARNING,
"Post merge validation failed");
}
// only remove tools annotations if we are packaging an application.
if (mOptionalFeatures.contains(Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)) {
finalMergedDocument =
ToolsInstructionsCleaner.cleanToolsReferences(finalMergedDocument, mLogger);
}
if (mOptionalFeatures.contains(Invoker.Feature.EXTRACT_FQCNS)) {
extractFcqns(finalMergedDocument);
}
if (finalMergedDocument != null) {
mergingReportBuilder.setMergedDocument(finalMergedDocument);
}
MergingReport mergingReport = mergingReportBuilder.build();
StdLogger stdLogger = new StdLogger(StdLogger.Level.INFO);
mergingReport.log(stdLogger);
stdLogger.verbose(mergingReport.getMergedDocument().get().prettyPrint());
if (mReportFile.isPresent()) {
writeReport(mergingReport);
}
return mergingReport;
}
/**
* Creates the merging report file.
* @param mergingReport the merging activities report to serialize.
*/
private void writeReport(MergingReport mergingReport) {
FileWriter fileWriter = null;
try {
if (!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 void extractFcqns(XmlDocument finalMergedDocument) {
extractFcqns(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 void extractFcqns(String packageName, XmlElement xmlElement) {
for (XmlAttribute xmlAttribute : xmlElement.getAttributes()) {
if (xmlAttribute.getModel() !=null && xmlAttribute.getModel().isPackageDependent()) {
String value = xmlAttribute.getValue();
if (value != null && value.startsWith(packageName) &&
value.charAt(packageName.length()) == '.') {
xmlAttribute.getXml().setValue(value.substring(packageName.length()));
}
}
}
for (XmlElement child : xmlElement.getMergeableElements()) {
extractFcqns(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
*/
private LoadedManifestInfo load(
ManifestInfo manifestInfo,
KeyResolver<String> selectors,
MergingReport.Builder mergingReportBuilder) throws MergeFailureException {
XmlDocument xmlDocument;
try {
xmlDocument = XmlLoader.load(selectors,
mSystemPropertyResolver,
manifestInfo.mName,
manifestInfo.mLocation,
manifestInfo.getType(),
manifestInfo.getMainManifestPackageName());
} 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());
builder.getActionRecorder().recordDefaultNodeAction(
xmlDocument.getRootNode());
// perform place holder substitution, this is necessary to do so early in case placeholders
// are used in key attributes.
performPlaceHolderSubstitution(manifestInfo, xmlDocument, builder);
return new LoadedManifestInfo(manifestInfo,
Optional.fromNullable(originalPackageName), xmlDocument);
}
private void performPlaceHolderSubstitution(ManifestInfo manifestInfo,
XmlDocument xmlDocument,
MergingReport.Builder mergingReportBuilder) {
if (mOptionalFeatures.contains(Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT)) {
return;
}
// check for placeholders presence, switch first the packageName and application id if
// it is not explicitly set.
Map<String, Object> finalPlaceHolderValues = mPlaceHolderValues;
if (!mPlaceHolderValues.containsKey(PlaceholderHandler.APPLICATION_ID)) {
String packageName = manifestInfo.getMainManifestPackageName().isPresent()
? manifestInfo.getMainManifestPackageName().get()
: xmlDocument.getPackageName();
// add all existing placeholders except package name that will be swapped.
ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder();
for (Map.Entry<String, Object> entry : mPlaceHolderValues.entrySet()) {
if (!entry.getKey().equals(PlaceholderHandler.PACKAGE_NAME)) {
builder.put(entry);
}
}
builder.put(PlaceholderHandler.PACKAGE_NAME, packageName);
if (mMergeType != MergeType.LIBRARY) {
builder.put(PlaceholderHandler.APPLICATION_ID, packageName);
}
finalPlaceHolderValues = builder.build();
}
KeyBasedValueResolver<String> placeHolderValueResolver =
new MapBasedKeyBasedValueResolver<String>(finalPlaceHolderValues);
PlaceholderHandler placeholderHandler = new PlaceholderHandler();
placeholderHandler.visit(
mMergeType,
xmlDocument,
placeHolderValueResolver,
mergingReportBuilder);
}
// merge the optionally existing xmlDocument with a lower priority xml file.
private Optional<XmlDocument> merge(
Optional<XmlDocument> xmlDocument,
LoadedManifestInfo lowerPriorityDocument,
MergingReport.Builder mergingReportBuilder) throws MergeFailureException {
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.absent();
}
Optional<XmlDocument> result;
if (xmlDocument.isPresent()) {
result = xmlDocument.get().merge(
lowerPriorityDocument.getXmlDocument(), mergingReportBuilder);
} else {
mergingReportBuilder.getActionRecorder().recordDefaultNodeAction(
lowerPriorityDocument.getXmlDocument().getRootNode());
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(SelectorResolver selectors,
MergingReport.Builder mergingReportBuilder) throws MergeFailureException {
ImmutableList.Builder<LoadedManifestInfo> loadedLibraryDocuments = ImmutableList.builder();
for (Pair<String, File> libraryFile : mLibraryFiles) {
mLogger.info("Loading library manifest " + libraryFile.getSecond().getPath());
ManifestInfo manifestInfo = new ManifestInfo(libraryFile.getFirst(),
libraryFile.getSecond(),
XmlDocument.Type.LIBRARY, Optional.<String>absent());
XmlDocument libraryDocument;
try {
libraryDocument = XmlLoader.load(selectors,
mSystemPropertyResolver,
manifestInfo.mName, manifestInfo.mLocation,
XmlDocument.Type.LIBRARY,
Optional.<String>absent() /* mainManifestPackageName */);
} 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().recordDefaultNodeAction(libraryDocument.getRootNode());
performPlaceHolderSubstitution(manifestInfo, libraryDocument, builder);
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);
}
loadedLibraryDocuments.add(new LoadedManifestInfo(manifestInfo,
Optional.fromNullable(libraryDocument.getPackageName()),
libraryDocument));
}
return loadedLibraryDocuments.build();
}
/**
* Creates a new {@link com.android.manifmerger.ManifestMerger2.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 com.android.manifmerger.ManifestMerger2.Invoker} instance that will allow
* further customization and trigger the merging tool.
*/
public static Invoker newMerger(@NonNull File mainManifestFile,
@NonNull ILogger logger,
@NonNull MergeType mergeType) {
return new Invoker(mainManifestFile, logger, mergeType);
}
/**
* List of manifest files properties that can be directly overridden without using a
* placeholder.
*/
public enum SystemProperty implements AutoAddingProperty {
/**
* Allow setting the merged manifest file package name.
*/
PACKAGE {
@Override
public void addTo(@NonNull ActionRecorder actionRecorder,
@NonNull XmlDocument document,
@NonNull String value) {
addToElement(this, actionRecorder, value, document.getRootNode());
}
},
/**
* http://developer.android.com/guide/topics/manifest/manifest-element.html#vcode
*/
VERSION_CODE {
@Override
public void addTo(@NonNull ActionRecorder actionRecorder,
@NonNull XmlDocument document,
@NonNull String value) {
addToElementInAndroidNS(this, actionRecorder, value, document.getRootNode());
}
},
/**
* http://developer.android.com/guide/topics/manifest/manifest-element.html#vname
*/
VERSION_NAME {
@Override
public void addTo(@NonNull ActionRecorder actionRecorder,
@NonNull XmlDocument document,
@NonNull String value) {
addToElementInAndroidNS(this, actionRecorder, value, document.getRootNode());
}
},
/**
* http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#min
*/
MIN_SDK_VERSION {
@Override
public void addTo(@NonNull ActionRecorder actionRecorder,
@NonNull XmlDocument document,
@NonNull String value) {
addToElementInAndroidNS(this, actionRecorder, value,
createOrGetUseSdk(actionRecorder, document));
}
},
/**
* http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#target
*/
TARGET_SDK_VERSION {
@Override
public void addTo(@NonNull ActionRecorder actionRecorder,
@NonNull XmlDocument document,
@NonNull String value) {
addToElementInAndroidNS(this, actionRecorder, value,
createOrGetUseSdk(actionRecorder, document));
}
},
MAX_SDK_VERSION {
@Override
public void addTo(@NonNull ActionRecorder actionRecorder,
@NonNull XmlDocument document,
@NonNull String value) {
addToElementInAndroidNS(this, actionRecorder, value,
createOrGetUseSdk(actionRecorder, document));
}
};
public String toCamelCase() {
return SdkUtils.constantNameToCamelCase(name());
}
// utility method to add an attribute which name is derived from the enum name().
private static void addToElement(
SystemProperty systemProperty,
ActionRecorder actionRecorder,
String value,
XmlElement to) {
to.getXml().setAttribute(systemProperty.toCamelCase(), value);
XmlAttribute xmlAttribute = new XmlAttribute(to,
to.getXml().getAttributeNode(systemProperty.toCamelCase()), null);
actionRecorder.recordAttributeAction(xmlAttribute, new Actions.AttributeRecord(
Actions.ActionType.INJECTED,
new SourceFilePosition(to.getSourceFile(), SourcePosition.UNKNOWN),
xmlAttribute.getId(),
null, /* reason */
null /* attributeOperationType */));
}
// utility method to add an attribute in android namespace which local name is derived from
// the enum name().
private static void addToElementInAndroidNS(
SystemProperty systemProperty,
ActionRecorder actionRecorder,
String value,
XmlElement to) {
String toolsPrefix = getAndroidPrefix(to.getXml());
to.getXml().setAttributeNS(SdkConstants.ANDROID_URI,
toolsPrefix + XmlUtils.NS_SEPARATOR + systemProperty.toCamelCase(),
value);
Attr attr = to.getXml().getAttributeNodeNS(SdkConstants.ANDROID_URI,
systemProperty.toCamelCase());
XmlAttribute xmlAttribute = new XmlAttribute(to, attr, null);
actionRecorder.recordAttributeAction(xmlAttribute,
new Actions.AttributeRecord(
Actions.ActionType.INJECTED,
new SourceFilePosition(to.getSourceFile(), SourcePosition.UNKNOWN),
xmlAttribute.getId(),
null, /* reason */
null /* attributeOperationType */
)
);
}
// utility method to create or get an existing use-sdk xml element under manifest.
// this could be made more generic by adding more metadata to the enum but since there is
// only one case so far, keep it simple.
private static XmlElement createOrGetUseSdk(
ActionRecorder actionRecorder, XmlDocument document) {
Element manifest = document.getXml().getDocumentElement();
NodeList usesSdks = manifest
.getElementsByTagName(ManifestModel.NodeTypes.USES_SDK.toXmlName());
if (usesSdks.getLength() == 0) {
usesSdks = manifest
.getElementsByTagNameNS(
SdkConstants.ANDROID_URI,
ManifestModel.NodeTypes.USES_SDK.toXmlName());
}
if (usesSdks.getLength() == 0) {
// create it first.
Element useSdk = manifest.getOwnerDocument().createElement(
ManifestModel.NodeTypes.USES_SDK.toXmlName());
manifest.appendChild(useSdk);
XmlElement xmlElement = new XmlElement(useSdk, document);
Actions.NodeRecord nodeRecord = new Actions.NodeRecord(
Actions.ActionType.INJECTED,
new SourceFilePosition(xmlElement.getSourceFile(),
SourcePosition.UNKNOWN),
xmlElement.getId(),
"use-sdk injection requested",
NodeOperationType.STRICT);
actionRecorder.recordNodeAction(xmlElement, nodeRecord);
return xmlElement;
} else {
return new XmlElement((Element) usesSdks.item(0), document);
}
}
}
private static String getAndroidPrefix(Element xml) {
String toolsPrefix = XmlUtils.lookupNamespacePrefix(
xml, SdkConstants.ANDROID_URI, SdkConstants.ANDROID_NS_NAME, false);
if (!toolsPrefix.equals(SdkConstants.ANDROID_NS_NAME) && xml.getOwnerDocument()
.getDocumentElement().getAttribute("xmlns:" + toolsPrefix) == null) {
// this is weird, the document is using "android" prefix but it's not bound
// to our namespace. Add the proper xmlns declaration.
xml.setAttribute("xmlns:" + toolsPrefix, SdkConstants.ANDROID_URI);
}
return toolsPrefix;
}
/**
* 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 typee 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 com.android.manifmerger.ManifestMerger2.SystemProperty} injection.
* @param mergingReport to log actions and errors.
* @param xmlDocument the xml document to inject into.
*/
protected void performSystemPropertiesInjection(
MergingReport.Builder mergingReport,
XmlDocument xmlDocument) {
for (SystemProperty systemProperty : SystemProperty.values()) {
String propertyOverride = mSystemPropertyResolver.getValue(systemProperty);
if (propertyOverride != null) {
systemProperty.addTo(
mergingReport.getActionRecorder(), xmlDocument, propertyOverride);
}
}
}
/**
* This class will hold all invocation parameters for the manifest merging tool.
*
* There are broadly three types of input to the merging tool :
* <ul>
* <li>Build types and flavors overriding manifests</li>
* <li>Application main manifest</li>
* <li>Library manifest files</li></lib>
* </ul>
*
* Only the main manifest file is a mandatory parameter.
*
* 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>
* <li>Resulting document is merged with lower priority application main manifest file.</li>
* <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>
* <li>Resulting document is returned as results of the merging process.</li>
* </ol>
*
*/
public static class Invoker<T extends Invoker<T>>{
protected final File mMainManifestFile;
protected final ImmutableMap.Builder<SystemProperty, Object> mSystemProperties =
new ImmutableMap.Builder<SystemProperty, Object>();
protected final ILogger mLogger;
protected final ImmutableMap.Builder<String, Object> mPlaceholders =
new ImmutableMap.Builder<String, Object>();
private final ImmutableList.Builder<Pair<String, File>> mLibraryFilesBuilder =
new ImmutableList.Builder<Pair<String, File>>();
private final ImmutableList.Builder<File> mFlavorsAndBuildTypeFiles =
new ImmutableList.Builder<File>();
private final ImmutableList.Builder<Feature> mFeaturesBuilder =
new ImmutableList.Builder<Feature>();
private final MergeType mMergeType;
@Nullable private File mReportFile;
/**
* Sets a value for a {@link com.android.manifmerger.ManifestMerger2.SystemProperty}
* @param override the property to set
* @param value the value for the property
* @return itself.
*/
public Invoker setOverride(SystemProperty override, String value) {
mSystemProperties.put(override, value);
return thisAsT();
}
/**
* Adds placeholders names and associated values for substitution.
* @return itself.
*/
public Invoker setPlaceHolderValues(Map<String, String> keyValuePairs) {
mPlaceholders.putAll(keyValuePairs);
return thisAsT();
}
/**
* Adds a new placeholder name and value for substitution.
* @return itself.
*/
public Invoker setPlaceHolderValue(String placeHolderName, String value) {
mPlaceholders.put(placeHolderName, value);
return thisAsT();
}
/**
* 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 java.io.File#getName()} rather than
* {@link java.io.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
}
/**
* 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,
MergeType mergeType) {
this.mMainManifestFile = Preconditions.checkNotNull(mainManifestFile);
this.mLogger = logger;
this.mMergeType = mergeType;
}
/**
* 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.
*/
public Invoker setMergeReportFile(@NonNull 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.
*/
public Invoker addLibraryManifest(File file) {
if (mMergeType == MergeType.LIBRARY) {
throw new IllegalStateException(
"Cannot add library dependencies manifests when creating a library");
}
mLibraryFilesBuilder.add(Pair.of(file.getName(), file));
return thisAsT();
}
public Invoker addLibraryManifests(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 thisAsT();
}
/**
* 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.
*/
public Invoker addLibraryManifests(File... files) {
for (File file : files) {
addLibraryManifest(file);
}
return thisAsT();
}
/**
* Add a flavor or build type manifest file last in the list.
* @param file build type or flavor manifest file
* @return itself.
*/
public Invoker addFlavorAndBuildTypeManifest(File file) {
this.mFlavorsAndBuildTypeFiles.add(file);
return thisAsT();
}
/**
* 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.
*/
public Invoker addFlavorAndBuildTypeManifests(File... files) {
this.mFlavorsAndBuildTypeFiles.add(files);
return thisAsT();
}
/**
* Sets some optional features for the merge tool.
*
* @param features one to many features to set.
* @return itself.
*/
public Invoker withFeatures(Feature...features) {
mFeaturesBuilder.add(features);
return thisAsT();
}
/**
* Perform the merging and return the result.
*
* @return an instance of {@link com.android.manifmerger.MergingReport} that will give
* access to all the logging and merging records.
*
* This method can be invoked several time and will re-do the file merges.
*
* @throws com.android.manifmerger.ManifestMerger2.MergeFailureException if the merging
* cannot be completed successfully.
*/
public MergingReport merge() throws MergeFailureException {
// provide some free placeholders values.
ImmutableMap<SystemProperty, Object> systemProperties = mSystemProperties.build();
if (systemProperties.containsKey(SystemProperty.PACKAGE)) {
// if the package is provided, make it available for placeholder replacement.
mPlaceholders.put(PACKAGE_NAME, systemProperties.get(SystemProperty.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(SystemProperty.PACKAGE));
}
}
ManifestMerger2 manifestMerger =
new ManifestMerger2(
mLogger,
mMainManifestFile,
mLibraryFilesBuilder.build(),
mFlavorsAndBuildTypeFiles.build(),
mFeaturesBuilder.build(),
mPlaceholders.build(),
new MapBasedKeyBasedValueResolver<SystemProperty>(systemProperties),
mMergeType,
Optional.fromNullable(mReportFile));
return manifestMerger.merge();
}
@SuppressWarnings("unchecked")
private T thisAsT() {
return (T) this;
}
}
/**
* 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(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,
Optional<String> mainManifestPackageName) {
mName = name;
mLocation = location;
mType = type;
mMainManifestPackageName = mainManifestPackageName;
}
private final String mName;
private final File mLocation;
private final XmlDocument.Type mType;
private final Optional<String> mMainManifestPackageName;
File getLocation() {
return mLocation;
}
XmlDocument.Type getType() {
return mType;
}
Optional<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 com.android.manifmerger.KeyResolver} capable of resolving all
* selectors value in the context of the passed libraries to this merging activities.
*/
static class SelectorResolver implements KeyResolver<String> {
private final Map<String, String> mSelectors = new HashMap<String, String>();
protected void addSelector(String key, String value) {
mSelectors.put(key, value);
}
@Nullable
@Override
public String resolve(String key) {
return mSelectors.get(key);
}
@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);
}
}
}