com.android.manifmerger.XmlDocument Maven / Gradle / Ivy
Show all versions of manifest-merger Show documentation
/*
* 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.ManifestMerger2.SystemProperty;
import static com.android.manifmerger.ManifestModel.NodeTypes.USES_PERMISSION;
import static com.android.manifmerger.ManifestModel.NodeTypes.USES_SDK;
import static com.android.manifmerger.PlaceholderHandler.KeyBasedValueResolver;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.blame.SourceFile;
import com.android.ide.common.blame.SourceFilePosition;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.xml.XmlFormatPreferences;
import com.android.ide.common.xml.XmlFormatStyle;
import com.android.ide.common.xml.XmlPrettyPrinter;
import com.android.sdklib.SdkVersionInfo;
import com.android.utils.Pair;
import com.android.utils.PositionXmlParser;
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 org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
/**
* Represents a loaded xml document.
*
* Has pointers to the root {@link XmlElement} element and provides services to persist the document
* to an external format. Also provides abilities to be merged with other
* {@link com.android.manifmerger.XmlDocument} as well as access to the line numbers for all
* document's xml elements and attributes.
*
*/
public class XmlDocument {
private static final String DEFAULT_SDK_VERSION = "1";
/**
* The document type.
*/
enum Type {
/**
* A manifest overlay as found in the build types and variants.
*/
OVERLAY,
/**
* The main android manifest file.
*/
MAIN,
/**
* A library manifest that is imported in the application.
*/
LIBRARY
}
private final Element mRootElement;
// this is initialized lazily to avoid un-necessary early parsing.
@NonNull
private final AtomicReference mRootNode = new AtomicReference(null);
@NonNull
private final SourceFile mSourceFile;
@NonNull
private final KeyResolver mSelectors;
@NonNull
private final KeyBasedValueResolver mSystemPropertyResolver;
@NonNull
private final Type mType;
@NonNull
private final Optional mMainManifestPackageName;
public XmlDocument(
@NonNull SourceFile sourceLocation,
@NonNull KeyResolver selectors,
@NonNull KeyBasedValueResolver systemPropertyResolver,
@NonNull Element element,
@NonNull Type type,
@NonNull Optional mainManifestPackageName) {
this.mSourceFile = Preconditions.checkNotNull(sourceLocation);
this.mRootElement = Preconditions.checkNotNull(element);
this.mSelectors = Preconditions.checkNotNull(selectors);
this.mSystemPropertyResolver = Preconditions.checkNotNull(systemPropertyResolver);
this.mType = type;
this.mMainManifestPackageName = mainManifestPackageName;
}
@NonNull
public Type getFileType() {
return mType;
}
/**
* Returns a pretty string representation of this document.
*/
@NonNull
public String prettyPrint() {
return XmlPrettyPrinter.prettyPrint(
getXml(),
XmlFormatPreferences.defaults(),
XmlFormatStyle.get(getRootNode().getXml()),
null, /* endOfLineSeparator */
false /* endWithNewLine */);
}
/**
* merge this higher priority document with a higher priority document.
* @param lowerPriorityDocument the lower priority document to merge in.
* @param mergingReportBuilder the merging report to record errors and actions.
* @return a new merged {@link com.android.manifmerger.XmlDocument} or
* {@link Optional#absent()} if there were errors during the merging activities.
*/
@NonNull
public Optional merge(
@NonNull XmlDocument lowerPriorityDocument,
@NonNull MergingReport.Builder mergingReportBuilder) {
if (getFileType() == Type.MAIN) {
mergingReportBuilder.getActionRecorder().recordDefaultNodeAction(getRootNode());
}
getRootNode().mergeWithLowerPriorityNode(
lowerPriorityDocument.getRootNode(), mergingReportBuilder);
addImplicitElements(lowerPriorityDocument, mergingReportBuilder);
// force re-parsing as new nodes may have appeared.
return mergingReportBuilder.hasErrors()
? Optional.absent()
: Optional.of(reparse());
}
/**
* Forces a re-parsing of the document
* @return a new {@link com.android.manifmerger.XmlDocument} with up to date information.
*/
@NonNull
public XmlDocument reparse() {
return new XmlDocument(
mSourceFile,
mSelectors,
mSystemPropertyResolver,
mRootElement,
mType,
mMainManifestPackageName);
}
/**
* Returns a {@link com.android.manifmerger.KeyResolver} capable of resolving all selectors
* types
*/
@NonNull
public KeyResolver getSelectors() {
return mSelectors;
}
/**
* Returns the {@link com.android.manifmerger.PlaceholderHandler.KeyBasedValueResolver} capable
* of resolving all injected {@link com.android.manifmerger.ManifestMerger2.SystemProperty}
*/
@NonNull
public KeyBasedValueResolver getSystemPropertyResolver() {
return mSystemPropertyResolver;
}
/**
* Compares this document to another {@link com.android.manifmerger.XmlDocument} ignoring all
* attributes belonging to the {@link com.android.SdkConstants#TOOLS_URI} namespace.
*
* @param other the other document to compare against.
* @return a {@link String} describing the differences between the two XML elements or
* {@link Optional#absent()} if they are equals.
*/
@SuppressWarnings("CovariantCompareTo")
public Optional compareTo(@NonNull XmlDocument other) {
return getRootNode().compareTo(other.getRootNode());
}
/**
* Returns the position of the specified {@link XmlNode}.
*/
@NonNull
static SourcePosition getNodePosition(@NonNull XmlNode node) {
return getNodePosition(node.getXml());
}
/**
* Returns the position of the specified {@link org.w3c.dom.Node}.
*/
@NonNull
static SourcePosition getNodePosition(@NonNull Node xml) {
return PositionXmlParser.getPosition(xml);
}
/**
* Returns the {@link SourceFile} associated with this XML document.
*
* NOTE: You should not read the contents of the file directly; if you need to
* access the content, use {@link ManifestMerger2#getFileStreamProvider()} instead.
*
* @return the source file
*/
@NonNull
public SourceFile getSourceFile() {
return mSourceFile;
}
public synchronized XmlElement getRootNode() {
if (mRootNode.get() == null) {
this.mRootNode.set(new XmlElement(mRootElement, this));
}
return mRootNode.get();
}
public Optional getByTypeAndKey(
ManifestModel.NodeTypes type,
@Nullable String keyValue) {
return getRootNode().getNodeByTypeAndKey(type, keyValue);
}
/**
* Package name for this android manifest which will be used to resolve
* partial path. In the case of Overlays, this is absent and the main
* manifest packageName must be used.
* @return the package name to do partial class names resolution.
*/
public String getPackageName() {
return mMainManifestPackageName.or(mRootElement.getAttribute("package"));
}
/**
* Returns the package name to use to expand the attributes values with the
* document's package name
* @return the package name to use for attribute expansion.
*/
public String getPackageNameForAttributeExpansion() {
String aPackage = mRootElement.getAttribute("package");
if (aPackage != null) {
return aPackage;
}
if (mMainManifestPackageName.isPresent()) {
return mMainManifestPackageName.get();
}
throw new RuntimeException("No package present in overlay or main manifest file");
}
public Optional getPackage() {
Optional packageAttribute =
getRootNode().getAttribute(XmlNode.fromXmlName("package"));
return packageAttribute.isPresent()
? packageAttribute
: getRootNode().getAttribute(XmlNode.fromNSName(
SdkConstants.ANDROID_URI, "android", "package"));
}
public Document getXml() {
return mRootElement.getOwnerDocument();
}
/**
* Returns the minSdk version specified in the uses_sdk element if present or the
* default value.
*/
@NonNull
private String getRawMinSdkVersion() {
Optional usesSdk = getByTypeAndKey(
ManifestModel.NodeTypes.USES_SDK, null);
if (usesSdk.isPresent()) {
Optional minSdkVersion = usesSdk.get()
.getAttribute(XmlNode.fromXmlName("android:minSdkVersion"));
if (minSdkVersion.isPresent()) {
return minSdkVersion.get().getValue();
}
}
return DEFAULT_SDK_VERSION;
}
/**
* Returns the minSdk version for this manifest file. It can be injected from the outer
* build.gradle or can be expressed in the uses_sdk element.
*/
@NonNull
private String getMinSdkVersion() {
// check for system properties.
String injectedMinSdk = mSystemPropertyResolver.getValue(SystemProperty.MIN_SDK_VERSION);
if (injectedMinSdk != null) {
return injectedMinSdk;
}
return getRawMinSdkVersion();
}
/**
* Returns the targetSdk version specified in the uses_sdk element if present or the
* default value.
*/
@NonNull
private String getRawTargetSdkVersion() {
Optional usesSdk = getByTypeAndKey(
ManifestModel.NodeTypes.USES_SDK, null);
if (usesSdk.isPresent()) {
Optional targetSdkVersion = usesSdk.get()
.getAttribute(XmlNode.fromXmlName("android:targetSdkVersion"));
if (targetSdkVersion.isPresent()) {
return targetSdkVersion.get().getValue();
}
}
return getRawMinSdkVersion();
}
/**
* Returns the targetSdk version for this manifest file. It can be injected from the outer
* build.gradle or can be expressed in the uses_sdk element.
*/
@NonNull
private String getTargetSdkVersion() {
// check for system properties.
String injectedTargetVersion = mSystemPropertyResolver
.getValue(SystemProperty.TARGET_SDK_VERSION);
if (injectedTargetVersion != null) {
return injectedTargetVersion;
}
return getRawTargetSdkVersion();
}
/**
* Decodes a sdk version from either its decimal representation or from a platform code name.
* @param attributeVersion the sdk version attribute as specified by users.
* @return the integer representation of the platform level.
*/
private static int getApiLevelFromAttribute(@NonNull String attributeVersion) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(attributeVersion));
if (Character.isDigit(attributeVersion.charAt(0))) {
return Integer.parseInt(attributeVersion);
}
return SdkVersionInfo.getApiByPreviewName(attributeVersion, true);
}
/**
* Add all implicit elements from the passed lower priority document that are
* required in the target SDK.
*/
@SuppressWarnings("unchecked") // compiler confused about varargs and generics.
private void addImplicitElements(@NonNull XmlDocument lowerPriorityDocument,
@NonNull MergingReport.Builder mergingReport) {
// if this document is an overlay, tolerate the absence of uses-sdk and do not
// assume implicit minimum versions.
Optional usesSdk = getByTypeAndKey(
ManifestModel.NodeTypes.USES_SDK, null);
if (mType == Type.OVERLAY && !usesSdk.isPresent()) {
return;
}
// check that the uses-sdk element does not have any tools:node instruction.
if (usesSdk.isPresent()) {
XmlElement usesSdkElement = usesSdk.get();
if (usesSdkElement.getOperationType() != NodeOperationType.MERGE) {
mergingReport
.addMessage(
new SourceFilePosition(
getSourceFile(),
usesSdkElement.getPosition()),
MergingReport.Record.Severity.ERROR,
"uses-sdk element cannot have a \"tools:node\" attribute");
return;
}
}
int thisTargetSdk = getApiLevelFromAttribute(getTargetSdkVersion());
// when we are importing a library, we should never use the build.gradle injected
// values (only valid for overlay, main manifest) so use the raw versions coming from
// the AndroidManifest.xml
int libraryTargetSdk = getApiLevelFromAttribute(
lowerPriorityDocument.getFileType() == Type.LIBRARY
? lowerPriorityDocument.getRawTargetSdkVersion()
: lowerPriorityDocument.getTargetSdkVersion());
// if library is using a code name rather than an API level, make sure this document target
// sdk version is using the same code name.
String libraryTargetSdkVersion = lowerPriorityDocument.getTargetSdkVersion();
if (!Character.isDigit(libraryTargetSdkVersion.charAt(0))) {
// this is a code name, ensure this document uses the same code name.
if (!libraryTargetSdkVersion.equals(getTargetSdkVersion())) {
mergingReport.addMessage(getSourceFile(), MergingReport.Record.Severity.ERROR,
String.format(
"uses-sdk:targetSdkVersion %1$s cannot be different than version "
+ "%2$s declared in library %3$s",
getTargetSdkVersion(),
libraryTargetSdkVersion,
lowerPriorityDocument.getSourceFile().print(false)
)
);
return;
}
}
// same for minSdkVersion, if the library is using a code name, the application must
// also be using the same code name.
String libraryMinSdkVersion = lowerPriorityDocument.getRawMinSdkVersion();
if (!Character.isDigit(libraryMinSdkVersion.charAt(0))) {
// this is a code name, ensure this document uses the same code name.
if (!libraryMinSdkVersion.equals(getMinSdkVersion())) {
mergingReport.addMessage(getSourceFile(), MergingReport.Record.Severity.ERROR,
String.format(
"uses-sdk:minSdkVersion %1$s cannot be different than version "
+ "%2$s declared in library %3$s",
getMinSdkVersion(),
libraryMinSdkVersion,
lowerPriorityDocument.getSourceFile().print(false)
)
);
return;
}
}
if (!checkUsesSdkMinVersion(lowerPriorityDocument, mergingReport)) {
String error = String.format(
"uses-sdk:minSdkVersion %1$s cannot be smaller than version "
+ "%2$s declared in library %3$s\n"
+ "\tSuggestion: use tools:overrideLibrary=\"%4$s\" to force usage",
getMinSdkVersion(),
lowerPriorityDocument.getRawMinSdkVersion(),
lowerPriorityDocument.getSourceFile().print(false),
lowerPriorityDocument.getPackageName());
if (usesSdk.isPresent()) {
mergingReport.addMessage(
new SourceFilePosition(getSourceFile(), usesSdk.get().getPosition()),
MergingReport.Record.Severity.ERROR,
error);
} else {
mergingReport.addMessage(
getSourceFile(), MergingReport.Record.Severity.ERROR, error);
}
return;
}
// if the merged document target SDK is equal or smaller than the library's, nothing to do.
if (thisTargetSdk <= libraryTargetSdk) {
return;
}
// There is no need to add any implied permissions when targeting an old runtime.
if (thisTargetSdk < 4) {
return;
}
boolean hasWriteToExternalStoragePermission =
lowerPriorityDocument.getByTypeAndKey(
USES_PERMISSION, permission("WRITE_EXTERNAL_STORAGE")).isPresent();
if (libraryTargetSdk < 4) {
addIfAbsent(mergingReport.getActionRecorder(),
USES_PERMISSION,
permission("WRITE_EXTERNAL_STORAGE"),
lowerPriorityDocument.getPackageName() + " has a targetSdkVersion < 4");
hasWriteToExternalStoragePermission = true;
addIfAbsent(mergingReport.getActionRecorder(),
USES_PERMISSION,
permission("READ_PHONE_STATE"),
lowerPriorityDocument.getPackageName() + " has a targetSdkVersion < 4");
}
// If the application has requested WRITE_EXTERNAL_STORAGE, we will
// force them to always take READ_EXTERNAL_STORAGE as well. We always
// do this (regardless of target API version) because we can't have
// an app with write permission but not read permission.
if (hasWriteToExternalStoragePermission) {
addIfAbsent(mergingReport.getActionRecorder(),
USES_PERMISSION,
permission("READ_EXTERNAL_STORAGE"),
lowerPriorityDocument.getPackageName() + " requested WRITE_EXTERNAL_STORAGE");
}
// Pre-JellyBean call log permission compatibility.
if (thisTargetSdk >= 16 && libraryTargetSdk < 16) {
if (lowerPriorityDocument.getByTypeAndKey(
USES_PERMISSION, permission("READ_CONTACTS")).isPresent()) {
addIfAbsent(mergingReport.getActionRecorder(),
USES_PERMISSION, permission("READ_CALL_LOG"),
lowerPriorityDocument.getPackageName()
+ " has targetSdkVersion < 16 and requested READ_CONTACTS");
}
if (lowerPriorityDocument.getByTypeAndKey(
USES_PERMISSION, permission("WRITE_CONTACTS")).isPresent()) {
addIfAbsent(mergingReport.getActionRecorder(),
USES_PERMISSION, permission("WRITE_CALL_LOG"),
lowerPriorityDocument.getPackageName()
+ " has targetSdkVersion < 16 and requested WRITE_CONTACTS");
}
}
}
/**
* Returns true if the minSdkVersion of the application and the library are compatible, false
* otherwise.
*/
private boolean checkUsesSdkMinVersion(@NonNull XmlDocument lowerPriorityDocument,
MergingReport.Builder mergingReport) {
int thisMinSdk = getApiLevelFromAttribute(getMinSdkVersion());
int libraryMinSdk = getApiLevelFromAttribute(
lowerPriorityDocument.getRawMinSdkVersion());
// the merged document minSdk cannot be lower than a library
if (thisMinSdk < libraryMinSdk) {
// check if this higher priority document has any tools instructions for the node
Optional xmlElementOptional = getByTypeAndKey(USES_SDK, null);
if (!xmlElementOptional.isPresent()) {
return false;
}
XmlElement xmlElement = xmlElementOptional.get();
// if we find a selector that applies to this library. the users wants to explicitly
// allow this higher version library to be allowed.
for (Selector selector : xmlElement.getOverrideUsesSdkLibrarySelectors()) {
if (selector.appliesTo(lowerPriorityDocument.getRootNode())) {
return true;
}
}
return false;
}
return true;
}
/**
* Adds a new element of type nodeType with a specific keyValue if the element is absent in this
* document. Will also add attributes expressed through key value pairs.
*
* @param actionRecorder to records creation actions.
* @param nodeType the node type to crete
* @param keyValue the optional key for the element.
* @param attributes the optional array of key value pairs for extra element attribute.
* @return the Xml element whether it was created or existed or {@link Optional#absent()} if
* it does not exist in this document.
*/
private Optional addIfAbsent(
@NonNull ActionRecorder actionRecorder,
@NonNull ManifestModel.NodeTypes nodeType,
@Nullable String keyValue,
@Nullable String reason,
@Nullable Pair... attributes) {
Optional xmlElementOptional = getByTypeAndKey(nodeType, keyValue);
if (xmlElementOptional.isPresent()) {
return Optional.absent();
}
Element elementNS = getXml()
.createElementNS(SdkConstants.ANDROID_URI, "android:" + nodeType.toXmlName());
ImmutableList keyAttributesNames = nodeType.getNodeKeyResolver()
.getKeyAttributesNames();
if (keyAttributesNames.size() == 1) {
elementNS.setAttributeNS(
SdkConstants.ANDROID_URI, "android:" + keyAttributesNames.get(0), keyValue);
}
if (attributes != null) {
for (Pair attribute : attributes) {
elementNS.setAttributeNS(
SdkConstants.ANDROID_URI, "android:" + attribute.getFirst(),
attribute.getSecond());
}
}
// record creation.
XmlElement xmlElement = new XmlElement(elementNS, this);
actionRecorder.recordImpliedNodeAction(xmlElement, reason);
getRootNode().getXml().appendChild(elementNS);
return Optional.of(elementNS);
}
@NonNull
private static String permission(String permissionName) {
return "android.permission." + permissionName;
}
}