| /* |
| * 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.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 SourceFile mSourceFile; |
| private final KeyResolver<String> mSelectors; |
| private final KeyBasedValueResolver<SystemProperty> mSystemPropertyResolver; |
| private final Type mType; |
| private final Optional<String> mMainManifestPackageName; |
| |
| public XmlDocument( |
| @NonNull SourceFile sourceLocation, |
| @NonNull KeyResolver<String> selectors, |
| @NonNull KeyBasedValueResolver<SystemProperty> systemPropertyResolver, |
| @NonNull Element element, |
| @NonNull Type type, |
| @NonNull Optional<String> 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; |
| } |
| |
| 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( |
| mSourceFile, |
| 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 the position of the specified {@link XmlNode}. |
| */ |
| @NonNull |
| static SourcePosition getNodePosition(XmlNode node) { |
| return getNodePosition(node.getXml()); |
| } |
| |
| /** |
| * Returns the position of the specified {@link org.w3c.dom.Node}. |
| */ |
| @NonNull |
| static SourcePosition getNodePosition(Node xml) { |
| return PositionXmlParser.getPosition(xml); |
| } |
| |
| @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<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( |
| 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(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; |
| } |
| } |