com.android.tools.idea.model.MergedManifest.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.model.MergedManifest.java

Source

/*
 * Copyright (C) 2016 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.tools.idea.model;

import com.android.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.rendering.HardwareConfigHelper;
import com.android.manifmerger.Actions;
import com.android.manifmerger.MergingReport;
import com.android.manifmerger.XmlNode;
import com.android.resources.ScreenSize;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkVersionInfo;
import com.android.sdklib.devices.Device;
import com.android.tools.idea.rendering.multi.CompatibilityRenderTarget;
import com.android.tools.idea.run.activity.ActivityLocatorUtils;
import com.android.tools.lint.checks.PermissionHolder;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.android.dom.manifest.Manifest;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.util.*;

import static com.android.SdkConstants.*;
import static com.android.tools.lint.checks.PermissionRequirement.ATTR_PROTECTION_LEVEL;
import static com.android.tools.lint.checks.PermissionRequirement.VALUE_DANGEROUS;
import static com.android.xml.AndroidManifest.*;

/**
 * To get a {@linkplain MergedManifest} use {@link MergedManifest#get(AndroidFacet)} or {@link MergedManifest#get(Module)}
 */
public class MergedManifest {

    private final Module myModule;
    private String myPackage;
    private String myApplicationId;
    private Integer myVersionCode;
    private String myManifestTheme;
    private Map<String, ActivityAttributes> myActivityAttributesMap;
    private ManifestInfo.ManifestFile myManifestFile;
    private long myLastChecked;
    private AndroidVersion myMinSdk;
    private AndroidVersion myTargetSdk;
    private String myApplicationIcon;
    private String myApplicationLabel;
    private boolean myApplicationSupportsRtl;
    private Boolean myApplicationDebuggable;
    private @Nullable Map<String, XmlNode.NodeKey> myNodeKeys;
    private Document myDocument;
    private List<VirtualFile> myManifestFiles;
    private ModulePermissions myPermissionHolder;

    /**
     * Constructs a new MergedManifest
     * @param module the module containing the manifest
     */
    MergedManifest(@NotNull Module module) {
        myModule = module;
    }

    /**
     * Returns the {@link MergedManifest} for the given {@link Module}.
     *
     * @param module the android module
     * @return a {@link MergedManifest} for the given module, never null
     */
    @NotNull
    public static MergedManifest get(@NotNull Module module) {
        if (module.isDisposed()) {
            return new MergedManifest(module) {
                @Override
                protected void syncWithReadPermission() {
                }
            };
        }
        MergedManifest manifest = module.getComponent(MergedManifest.class);
        assert manifest != null;
        return manifest;
    }

    /**
     * Returns the {@link MergedManifest} for the given {@link AndroidFacet}.
     *
     * @param facet the Android facet associated with a module.
     * @return a {@link MergedManifest} for the given module
     */
    @NotNull
    public static MergedManifest get(@NotNull AndroidFacet facet) {
        return get(facet.getModule());
    }

    @Nullable
    public Document getDocument() {
        sync();
        return myDocument;
    }

    /**
     * Returns the manifest files relevant to this merge
     *
     * @return the list of files that participated in the merge
     */
    @Nullable
    public List<VirtualFile> getManifestFiles() {
        return myManifestFiles;
    }

    /**
     * Clears the cached manifest information. The next get call on one of the
     * properties will cause the information to be refreshed.
     */
    @VisibleForTesting
    public void clear() {
        myLastChecked = 0;
    }

    /**
     * Returns the default package registered in the Android manifest
     *
     * @return the default package registered in the manifest
     */
    @Nullable
    public String getPackage() {
        sync();
        return myPackage;
    }

    /**
     * Gets the merged manifest application ID.
     */
    @Nullable
    public String getApplicationId() {
        sync();
        return myApplicationId;
    }

    @Nullable
    public Integer getVersionCode() {
        sync();
        return myVersionCode;
    }

    /**
     * Returns a map from activity full class names to the corresponding {@link ActivityAttributes}
     *
     * @return a map from activity fqcn to ActivityAttributes
     */
    @NotNull
    public Map<String, ActivityAttributes> getActivityAttributesMap() {
        sync();
        if (myActivityAttributesMap == null) {
            return Collections.emptyMap();
        }
        return myActivityAttributesMap;
    }

    /**
     * Returns the attributes of an activity.
     */
    @Nullable
    public ActivityAttributes getActivityAttributes(@NotNull String activity) {
        int index = activity.indexOf('.');
        if (index <= 0 && myApplicationId != null && !myApplicationId.isEmpty()) {
            activity = myApplicationId + (index == -1 ? "." : "") + activity;
        }
        return getActivityAttributesMap().get(activity);
    }

    /**
     * Returns the manifest theme registered on the application, if any
     *
     * @return a manifest theme, or null if none was registered
     */
    @Nullable
    public String getManifestTheme() {
        sync();
        return myManifestTheme;
    }

    /**
     * Returns the default theme for this project, by looking at the manifest default
     * theme registration, target SDK, rendering target, etc.
     *
     * @param renderingTarget the rendering target use to render the theme, or null
     * @param screenSize      the screen size to obtain a default theme for, or null if unknown
     * @param device          the device to obtain a default theme for, or null if unknown
     * @return the theme to use for this project, never null
     */
    @NotNull
    public String getDefaultTheme(@Nullable IAndroidTarget renderingTarget, @Nullable ScreenSize screenSize,
            @Nullable Device device) {
        sync();

        if (myManifestTheme != null) {
            return myManifestTheme;
        }

        // For Android Wear and Android TV, the defaults differ
        if (device != null) {
            if (HardwareConfigHelper.isWear(device)) {
                return "@android:style/Theme.DeviceDefault.Light";
            } else if (HardwareConfigHelper.isTv(device)) {
                //noinspection SpellCheckingInspection
                return "@style/Theme.Leanback";
            }
        }

        // From manifest theme documentation:
        // "If that attribute is also not set, the default system theme is used."
        int targetSdk;
        AndroidFacet facet = AndroidFacet.getInstance(myModule);
        assert facet != null;
        AndroidModuleInfo info = facet.getAndroidModuleInfo();
        targetSdk = info.getTargetSdkVersion().getApiLevel();

        int renderingTargetSdk = targetSdk;
        if (renderingTarget instanceof CompatibilityRenderTarget) {
            renderingTargetSdk = renderingTarget.getVersion().getApiLevel();
            //targetSdk = SdkVersionInfo.HIGHEST_KNOWN_API
        } else if (renderingTarget != null) {
            renderingTargetSdk = renderingTarget.getVersion().getApiLevel();
        }

        int apiLevel = Math.min(targetSdk, renderingTargetSdk);
        if (apiLevel >= 21) {
            return ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Material.Light"; //$NON-NLS-1$
        } else if (apiLevel >= 14 || apiLevel >= 11 && screenSize == ScreenSize.XLARGE) {
            return ANDROID_STYLE_RESOURCE_PREFIX + "Theme.Holo"; //$NON-NLS-1$
        } else {
            return ANDROID_STYLE_RESOURCE_PREFIX + "Theme"; //$NON-NLS-1$
        }
    }

    /**
     * Returns the application icon, or null
     *
     * @return the application icon, or null
     */
    @Nullable
    public String getApplicationIcon() {
        sync();
        return myApplicationIcon;
    }

    /**
     * Returns the application label, or null
     *
     * @return the application label, or null
     */
    @Nullable
    public String getApplicationLabel() {
        sync();
        return myApplicationLabel;
    }

    /**
     * Returns true if the application has RTL support.
     *
     * @return true if the application has RTL support.
     */
    public boolean isRtlSupported() {
        sync();
        return myApplicationSupportsRtl;
    }

    /**
     * Returns the value for the debuggable flag set in the manifest. Returns null if not set.
     */
    @Nullable
    public Boolean getApplicationDebuggable() {
        sync();
        return myApplicationDebuggable;
    }

    /**
     * Returns the target SDK version
     *
     * @return the target SDK version
     */
    @NotNull
    public AndroidVersion getTargetSdkVersion() {
        sync();
        return myTargetSdk != null ? myTargetSdk : getMinSdkVersion();
    }

    /**
     * Returns the minimum SDK version
     *
     * @return the minimum SDK version
     */
    @NotNull
    public AndroidVersion getMinSdkVersion() {
        sync();
        return myMinSdk != null ? myMinSdk : AndroidVersion.DEFAULT;
    }

    /**
     * @return instance of {@link PermissionHolder}
     */
    @NotNull
    public PermissionHolder getPermissionHolder() {
        sync();
        if (myPermissionHolder == null) {
            return new ModulePermissions(Collections.emptySet(), Collections.emptySet());
        }
        return myPermissionHolder;
    }

    /**
     * Ensure that the package, theme and activity maps are initialized and up to date
     * with respect to the manifest file
     */
    private void sync() {
        // Since each of the accessors call sync(), allow a bunch of immediate
        // accessors to all bypass the file stat() below
        long now = System.currentTimeMillis();
        if (now - myLastChecked < 50 && myManifestFile != null) {
            return;
        }
        myLastChecked = now;

        // TODO remove this time based checking

        // Ensure that two simultaneous sync requests from different threads don't interfere with each other.
        synchronized (this) {
            ApplicationManager.getApplication().runReadAction(new Runnable() {
                @Override
                public void run() {
                    syncWithReadPermission();
                }
            });
        }
    }

    static String getAttributeValue(@NotNull Element element, @Nullable String namespace,
            @NotNull String localName) {
        return Strings.emptyToNull(element.getAttributeNS(namespace, localName));
    }

    protected void syncWithReadPermission() {
        AndroidFacet facet = AndroidFacet.getInstance(myModule);
        assert facet != null : "Attempt to obtain manifest info from a non Android module: " + myModule.getName();

        if (myManifestFile == null) {
            myManifestFile = ManifestInfo.ManifestFile.create(facet);
        }

        // Check to see if our data is up to date
        boolean refresh = myManifestFile.refresh();
        if (!refresh) {
            // Already have up to date data
            return;
        }

        myActivityAttributesMap = new HashMap<String, ActivityAttributes>();
        myManifestTheme = null;
        myTargetSdk = AndroidVersion.DEFAULT;
        myMinSdk = AndroidVersion.DEFAULT;
        myPackage = ""; //$NON-NLS-1$
        myApplicationId = ""; //$NON-NLS-1$
        myVersionCode = null;
        myApplicationIcon = null;
        myApplicationLabel = null;
        myApplicationSupportsRtl = false;
        myNodeKeys = null;
        myActivities = Lists.newArrayList();
        myActivityAliases = Lists.newArrayListWithExpectedSize(4);
        myServices = Lists.newArrayListWithExpectedSize(4);
        Set<String> permissions = Sets.newHashSetWithExpectedSize(30);
        Set<String> revocable = Sets.newHashSetWithExpectedSize(2);

        try {
            Document document = myManifestFile.getXmlDocument();
            if (document == null) {
                return;
            }
            myDocument = document;
            myManifestFiles = myManifestFile.getManifestFiles();

            Element root = document.getDocumentElement();
            if (root == null) {
                return;
            }

            myApplicationId = getAttributeValue(root, null, ATTRIBUTE_PACKAGE);

            // The package comes from the main manifest, NOT from the merged manifest.
            Manifest manifest = facet.getManifest();
            myPackage = manifest == null ? myApplicationId : manifest.getPackage().getValue();

            String versionCode = getAttributeValue(root, ANDROID_URI, SdkConstants.ATTR_VERSION_CODE);
            try {
                myVersionCode = Integer.valueOf(versionCode);
            } catch (NumberFormatException ignored) {
            }

            Node node = root.getFirstChild();
            while (node != null) {
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    String nodeName = node.getNodeName();
                    if (NODE_APPLICATION.equals(nodeName)) {
                        Element application = (Element) node;
                        myApplicationIcon = getAttributeValue(application, ANDROID_URI, ATTRIBUTE_ICON);
                        myApplicationLabel = getAttributeValue(application, ANDROID_URI, ATTRIBUTE_LABEL);
                        myManifestTheme = getAttributeValue(application, ANDROID_URI, ATTRIBUTE_THEME);
                        myApplicationSupportsRtl = VALUE_TRUE
                                .equals(getAttributeValue(application, ANDROID_URI, ATTRIBUTE_SUPPORTS_RTL));

                        String debuggable = getAttributeValue(application, ANDROID_URI, ATTRIBUTE_DEBUGGABLE);
                        myApplicationDebuggable = debuggable == null ? null : VALUE_TRUE.equals(debuggable);

                        Node child = node.getFirstChild();
                        while (child != null) {
                            if (child.getNodeType() == Node.ELEMENT_NODE) {
                                String childNodeName = child.getNodeName();
                                if (NODE_ACTIVITY.equals(childNodeName)) {
                                    Element element = (Element) child;
                                    ActivityAttributes attributes = new ActivityAttributes(element,
                                            myApplicationId);
                                    myActivityAttributesMap.put(attributes.getName(), attributes);
                                    myActivities.add(element);
                                } else if (NODE_ACTIVITY_ALIAS.equals(childNodeName)) {
                                    myActivityAliases.add((Element) child);
                                } else if (NODE_SERVICE.equals(childNodeName)) {
                                    myServices.add((Element) child);
                                }
                            }
                            child = child.getNextSibling();
                        }
                    } else if (NODE_USES_SDK.equals(nodeName)) {
                        // Look up target SDK
                        Element usesSdk = (Element) node;
                        myMinSdk = getApiVersion(usesSdk, ATTRIBUTE_MIN_SDK_VERSION, AndroidVersion.DEFAULT);
                        myTargetSdk = getApiVersion(usesSdk, ATTRIBUTE_TARGET_SDK_VERSION, myMinSdk);
                    } else if (TAG_USES_PERMISSION.equals(nodeName) || TAG_USES_PERMISSION_SDK_23.equals(nodeName)
                            || TAG_USES_PERMISSION_SDK_M.equals(nodeName)) {
                        Element element = (Element) node;
                        String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
                        if (!name.isEmpty()) {
                            permissions.add(name);
                        }
                    } else if (nodeName.equals(TAG_PERMISSION)) {
                        Element element = (Element) node;
                        String protectionLevel = element.getAttributeNS(ANDROID_URI, ATTR_PROTECTION_LEVEL);
                        if (VALUE_DANGEROUS.equals(protectionLevel)) {
                            String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
                            if (!name.isEmpty()) {
                                revocable.add(name);
                            }
                        }
                    }
                }

                node = node.getNextSibling();
            }
            myPermissionHolder = new ModulePermissions(ImmutableSet.copyOf(permissions),
                    ImmutableSet.copyOf(revocable));
        } catch (ProcessCanceledException e) {
            myManifestFile = null; // clear the file, to make sure we reload everything on next call to this method
            myDocument = null;
            throw e;
        } catch (Exception e) {
            Logger.getInstance(MergedManifest.class).warn("Could not read Manifest data", e);
        }
    }

    private static AndroidVersion getApiVersion(Element usesSdk, String attribute, AndroidVersion defaultApiLevel) {
        String valueString = getAttributeValue(usesSdk, ANDROID_URI, attribute);
        if (valueString != null) {
            // TODO: Pass in platforms if we have them
            AndroidVersion version = SdkVersionInfo.getVersion(valueString, null);
            if (version != null) {
                return version;
            }
        }
        return defaultApiLevel;
    }

    @NotNull
    public List<Element> getActivities() {
        sync();
        return myActivities;
    }

    private List<Element> myActivities = Collections.emptyList();
    private List<Element> myActivityAliases = Collections.emptyList();
    private List<Element> myServices = Collections.emptyList();

    /**
     * @return the list of activity aliases defined in the manifest.
     */
    @NotNull
    public List<Element> getActivityAliases() {
        sync();
        return myActivityAliases;
    }

    /**
     * @return the list of services defined in the manifest.
     */
    @NotNull
    public List<Element> getServices() {
        return myServices;
    }

    @Nullable
    public Element findUsedFeature(@NotNull String name) {
        sync();
        if (myDocument == null) {
            return null;
        }
        Node node = myDocument.getDocumentElement().getFirstChild();
        while (node != null) {
            if (node.getNodeType() == Node.ELEMENT_NODE && NODE_USES_FEATURE.equals(node.getNodeName())) {
                Element element = (Element) node;
                if (name.equals(element.getAttributeNS(ANDROID_URI, ATTR_NAME))) {
                    return element;
                }
            }
            node = node.getNextSibling();
        }

        return null;
    }

    @NotNull
    public ImmutableList<MergingReport.Record> getLoggingRecords() {
        sync();
        return myManifestFile == null ? ImmutableList.<MergingReport.Record>of()
                : myManifestFile.getLoggingRecords();
    }

    @Nullable
    public Actions getActions() {
        sync();
        return myManifestFile == null ? null : myManifestFile.getActions();
    }

    @Nullable
    public XmlNode.NodeKey getNodeKey(String name) {
        sync();
        if (myNodeKeys == null) {
            HashMap<String, XmlNode.NodeKey> nodeKeys = new HashMap<>();
            Actions actions = getActions();
            if (actions != null) {
                Set<XmlNode.NodeKey> keys = actions.getNodeKeys();
                for (XmlNode.NodeKey key : keys) {
                    nodeKeys.put(key.toString(), key);
                }
            }
            myNodeKeys = nodeKeys;
        }
        return myNodeKeys.get(name);
    }

    @Nullable
    public Element findActivity(@Nullable String qualifiedName) {
        sync();
        if (qualifiedName == null || myActivities == null) {
            return null;
        }
        return getActivityOrAliasByName(qualifiedName, myActivities);
    }

    @Nullable
    public Element findActivityAlias(@Nullable String qualifiedName) {
        sync();
        if (qualifiedName == null || myActivityAliases == null) {
            return null;
        }
        return getActivityOrAliasByName(qualifiedName, myActivityAliases);
    }

    @Nullable
    private static Element getActivityOrAliasByName(@NotNull String qualifiedName,
            @NotNull List<Element> activityOrAliasElements) {
        for (Element activity : activityOrAliasElements) {
            if (qualifiedName.equals(ActivityLocatorUtils.getQualifiedName(activity))) {
                return activity;
            }
        }

        return null;
    }

    public static class ActivityAttributes {
        @NotNull
        private final Element myElement;
        @Nullable
        private final String myIcon;
        @Nullable
        private final String myLabel;
        @NotNull
        private final String myName;
        @Nullable
        private final String myParentActivity;
        @Nullable
        private final String myTheme;
        @Nullable
        private final String myUiOptions;

        public ActivityAttributes(@NotNull Element activity, @Nullable String packageName) {
            myElement = activity;

            // Get activity name.
            String name = getAttributeValue(activity, ANDROID_URI, ATTRIBUTE_NAME);
            if (name == null || name.length() == 0) {
                throw new RuntimeException("Activity name cannot be empty.");
            }
            int index = name.indexOf('.');
            if (index <= 0 && packageName != null && !packageName.isEmpty()) {
                name = packageName + (index == -1 ? "." : "") + name;
            }
            myName = name;

            // Get activity icon.
            String value = getAttributeValue(activity, ANDROID_URI, ATTRIBUTE_ICON);
            if (value != null && value.length() > 0) {
                myIcon = value;
            } else {
                myIcon = null;
            }

            // Get activity label.
            value = getAttributeValue(activity, ANDROID_URI, ATTRIBUTE_LABEL);
            if (value != null && value.length() > 0) {
                myLabel = value;
            } else {
                myLabel = null;
            }

            // Get activity parent. Also search the meta-data for parent info.
            value = getAttributeValue(activity, ANDROID_URI, ATTRIBUTE_PARENT_ACTIVITY_NAME);
            if (value == null || value.length() == 0) {
                Node child = activity.getFirstChild();
                // TODO: Not sure if meta data can be used for API Level > 16
                while (child != null) {
                    if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(NODE_METADATA)) {
                        String metaDataName = getAttributeValue((Element) child, ANDROID_URI, ATTRIBUTE_NAME);
                        if (VALUE_PARENT_ACTIVITY.equals(metaDataName)) {
                            value = getAttributeValue(activity, ANDROID_URI, ATTRIBUTE_VALUE);
                            if (value != null) {
                                index = value.indexOf('.');
                                if (index <= 0 && packageName != null && !packageName.isEmpty()) {
                                    value = packageName + (index == -1 ? "." : "") + value;
                                    break;
                                }
                            }
                        }

                    }
                    child = child.getNextSibling();
                }
            }
            if (value != null && value.length() > 0) {
                myParentActivity = value;
            } else {
                myParentActivity = null;
            }

            // Get activity theme.
            value = getAttributeValue(activity, ANDROID_URI, ATTRIBUTE_THEME);
            if (value != null && value.length() > 0) {
                myTheme = value;
            } else {
                myTheme = null;
            }

            // Get UI options.
            value = getAttributeValue(activity, ANDROID_URI, ATTRIBUTE_UI_OPTIONS);
            if (value != null && value.length() > 0) {
                myUiOptions = value;
            } else {
                myUiOptions = null;
            }
        }

        @NotNull
        public Element getElement() {
            return myElement;
        }

        @Nullable
        public String getIcon() {
            return myIcon;
        }

        @Nullable
        public String getLabel() {
            return myLabel;
        }

        @NotNull
        public String getName() {
            return myName;
        }

        @Nullable
        public String getParentActivity() {
            return myParentActivity;
        }

        @Nullable
        public String getTheme() {
            return myTheme;
        }

        @Nullable
        public String getUiOptions() {
            return myUiOptions;
        }
    }

    private class ModulePermissions implements PermissionHolder {

        @NotNull
        private final Set<String> myPermissions;
        @NotNull
        private final Set<String> myRevocable;

        private ModulePermissions(@NotNull Set<String> permissions, @NotNull Set<String> revocable) {
            myPermissions = permissions;
            myRevocable = revocable;
        }

        @Override
        public boolean hasPermission(@NotNull String permission) {
            return myPermissions.contains(permission);
        }

        @Override
        public boolean isRevocable(@NotNull String permission) {
            return myRevocable.contains(permission);
        }

        @NotNull
        @Override
        public AndroidVersion getMinSdkVersion() {
            AndroidModuleInfo androidModuleInfo = AndroidModuleInfo.get(myModule);
            return androidModuleInfo != null ? androidModuleInfo.getMinSdkVersion()
                    : MergedManifest.this.getMinSdkVersion();
        }

        @NotNull
        @Override
        public AndroidVersion getTargetSdkVersion() {
            AndroidModuleInfo androidModuleInfo = AndroidModuleInfo.get(myModule);
            return androidModuleInfo != null ? androidModuleInfo.getTargetSdkVersion()
                    : MergedManifest.this.getTargetSdkVersion();
        }
    }
}