All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.manifmerger.XmlDocument Maven / Gradle / Ivy

There is a newer version: 25.3.0
Show newest version
/*
 * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy