blob: bf46f206765cd814817c29caa656c5af6464bd39 [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.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.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.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.
private final AtomicReference<XmlElement> mRootNode = new AtomicReference<XmlElement>(null);
private final PositionXmlParser mPositionXmlParser;
private final XmlLoader.SourceLocation mSourceLocation;
private final KeyResolver<String> mSelectors;
private final KeyBasedValueResolver<SystemProperty> mSystemPropertyResolver;
private final Type mType;
private final Optional<String> mMainManifestPackageName;
public XmlDocument(@NonNull PositionXmlParser positionXmlParser,
@NonNull XmlLoader.SourceLocation sourceLocation,
@NonNull KeyResolver<String> selectors,
@NonNull KeyBasedValueResolver<SystemProperty> systemPropertyResolver,
@NonNull Element element,
@NonNull Type type,
@NonNull Optional<String> mainManifestPackageName) {
this.mPositionXmlParser = Preconditions.checkNotNull(positionXmlParser);
this.mSourceLocation = Preconditions.checkNotNull(sourceLocation);
this.mRootElement = Preconditions.checkNotNull(element);
this.mSelectors = Preconditions.checkNotNull(selectors);
this.mSystemPropertyResolver = Preconditions.checkNotNull(systemPropertyResolver);
this.mType = type;
this.mMainManifestPackageName = mainManifestPackageName;
}
public Type getFileType() {
return mType;
}
/**
* Returns a pretty string representation of this document.
*/
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.
*/
public Optional<XmlDocument> merge(
XmlDocument lowerPriorityDocument,
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.<XmlDocument>absent()
: Optional.of(reparse());
}
/**
* Forces a re-parsing of the document
* @return a new {@link com.android.manifmerger.XmlDocument} with up to date information.
*/
public XmlDocument reparse() {
return new XmlDocument(mPositionXmlParser,
mSourceLocation,
mSelectors,
mSystemPropertyResolver,
mRootElement,
mType,
mMainManifestPackageName);
}
/**
* Returns a {@link com.android.manifmerger.KeyResolver} capable of resolving all selectors
* types
*/
public KeyResolver<String> getSelectors() {
return mSelectors;
}
/**
* Returns the {@link com.android.manifmerger.PlaceholderHandler.KeyBasedValueResolver} capable
* of resolving all injected {@link com.android.manifmerger.ManifestMerger2.SystemProperty}
*/
public KeyBasedValueResolver<SystemProperty> 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.
*/
public Optional<String> compareTo(XmlDocument other) {
return getRootNode().compareTo(other.getRootNode());
}
/**
* Returns a {@link XmlNode} position automatically offsetting the line and number
* columns by one (for PositionXmlParser, document starts at line 0, however for the common
* understanding, document should start at line 1).
*/
@NonNull
PositionXmlParser.Position getNodePosition(XmlNode node) {
return getNodePosition(node.getXml());
}
/**
* Returns a {@link org.w3c.dom.Node} position automatically offsetting the line and number
* columns by one (for PositionXmlParser, document starts at line 0, however for the common
* understanding, document should start at line 1).
*/
@NonNull
PositionXmlParser.Position getNodePosition(Node xml) {
final PositionXmlParser.Position position = mPositionXmlParser.getPosition(xml);
if (position == null) {
return PositionImpl.UNKNOWN;
}
return new PositionXmlParser.Position() {
@Nullable
@Override
public PositionXmlParser.Position getEnd() {
return position.getEnd();
}
@Override
public void setEnd(@NonNull PositionXmlParser.Position end) {
position.setEnd(end);
}
@Override
public int getLine() {
return position.getLine() + 1;
}
@Override
public int getOffset() {
return position.getOffset();
}
@Override
public int getColumn() {
return position.getColumn() +1;
}
};
}
@NonNull
public XmlLoader.SourceLocation getSourceLocation() {
return mSourceLocation;
}
public synchronized XmlElement getRootNode() {
if (mRootNode.get() == null) {
this.mRootNode.set(new XmlElement(mRootElement, this));
}
return mRootNode.get();
}
public Optional<XmlElement> 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<XmlAttribute> getPackage() {
Optional<XmlAttribute> 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.
*/
private String getRawMinSdkVersion() {
Optional<XmlElement> usesSdk = getByTypeAndKey(
ManifestModel.NodeTypes.USES_SDK, null);
if (usesSdk.isPresent()) {
Optional<XmlAttribute> 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.
*/
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.
*/
private String getRawTargetSdkVersion() {
Optional<XmlElement> usesSdk = getByTypeAndKey(
ManifestModel.NodeTypes.USES_SDK, null);
if (usesSdk.isPresent()) {
Optional<XmlAttribute> 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.
*/
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(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(XmlDocument lowerPriorityDocument,
MergingReport.Builder mergingReport) {
// if this document is an overlay, tolerate the absence of uses-sdk and do not
// assume implicit minimum versions.
Optional<XmlElement> 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(getSourceLocation(),
usesSdkElement.getLine(),
usesSdkElement.getColumn(),
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(getSourceLocation(), 0, 0, 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.getSourceLocation().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(getSourceLocation(), 0, 0, 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.getSourceLocation().print(false)
)
);
return;
}
}
if (!checkUsesSdkMinVersion(lowerPriorityDocument, mergingReport)) {
mergingReport.addMessage(getSourceLocation(),
usesSdk.isPresent() ? usesSdk.get().getLine() : 0,
usesSdk.isPresent() ? usesSdk.get().getColumn() : 0,
MergingReport.Record.Severity.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.getSourceLocation().print(false),
lowerPriorityDocument.getPackageName()
)
);
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",
Pair.of("maxSdkVersion", "18") // permission became implied at 19.
);
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",
// NOTE TO @xav, where can we find the list of implied permissions at versions X
Pair.of("maxSdkVersion", "18") // permission became implied at 19, DID IT ???
);
}
// 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(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<XmlElement> 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<Element> addIfAbsent(
@NonNull ActionRecorder actionRecorder,
@NonNull ManifestModel.NodeTypes nodeType,
@Nullable String keyValue,
@Nullable String reason,
@Nullable Pair<String, String>... attributes) {
Optional<XmlElement> xmlElementOptional = getByTypeAndKey(nodeType, keyValue);
if (xmlElementOptional.isPresent()) {
return Optional.absent();
}
Element elementNS = getXml()
.createElementNS(SdkConstants.ANDROID_URI, "android:" + nodeType.toXmlName());
ImmutableList<String> keyAttributesNames = nodeType.getNodeKeyResolver()
.getKeyAttributesNames();
if (keyAttributesNames.size() == 1) {
elementNS.setAttributeNS(
SdkConstants.ANDROID_URI, "android:" + keyAttributesNames.get(0), keyValue);
}
if (attributes != null) {
for (Pair<String, String> 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);
}
private static String permission(String permissionName) {
return "android.permission." + permissionName;
}
}