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

com.android.tools.lint.checks.ManifestResourceDetector Maven / Gradle / Ivy

There is a newer version: 25.3.0
Show newest version
/*
 * Copyright (C) 2015 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.lint.checks;

import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ICON;
import static com.android.SdkConstants.ATTR_LABEL;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_THEME;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.utils.SdkUtils.endsWithIgnoreCase;
import static com.android.xml.AndroidManifest.NODE_METADATA;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.ResourceUrl;
import com.android.ide.common.resources.configuration.VersionQualifier;
import com.android.resources.FolderTypeRelationship;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * Detects references to resources in the manifest that vary by configuration
 */
public class ManifestResourceDetector extends ResourceXmlDetector {
    /** Using resources in the manifest that vary by configuration */
    @SuppressWarnings("unchecked")
    public static final Issue ISSUE = Issue.create(
            "ManifestResource", //$NON-NLS-1$
            "Manifest Resource References",
            "Elements in the manifest can reference resources, but those resources cannot " +
            "vary across configurations (except as a special case, by version, and except " +
            "for a few specific package attributes such as the application title and icon.)",

            Category.CORRECTNESS,
            6,
            Severity.FATAL,
            new Implementation(
                    ManifestResourceDetector.class,
                    Scope.MANIFEST_AND_RESOURCE_SCOPE,
                    Scope.MANIFEST_SCOPE));

    /**
     * Map from resource name to resource type to manifest location; used
     * in batch mode to report errors when resource overrides are found
     */
    private Map> mManifestLocations;

    /** Constructs a new {@link ManifestResourceDetector} */
    public ManifestResourceDetector() {
    }

    @Override
    public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
        if (endsWithIgnoreCase(context.file.getPath(), ANDROID_MANIFEST_XML)) {
            checkManifest(context, document);
        } else {
            //noinspection VariableNotUsedInsideIf
            if (mManifestLocations != null) {
                checkResourceFile(context, document);
            }
        }
    }

    private void checkManifest(@NonNull XmlContext context, @NonNull Document document) {
        LintClient client = context.getClient();
        Project project = context.getProject();
        AbstractResourceRepository repository = null;
        if (client.supportsProjectResources()) {
            repository = client.getResourceRepository(project, true, false);
        }
        if (repository == null && !context.getScope().contains(Scope.RESOURCE_FILE)) {
            // Can't perform incremental analysis without a resource repository
            return;
        }

        Element root = document.getDocumentElement();
        if (root != null) {
            visit(context, root, repository);
        }
    }

    private void visit(@NonNull XmlContext context, @NonNull Element element,
            @Nullable AbstractResourceRepository repository) {
        if (NODE_METADATA.equals(element.getTagName())) {
            return;
        }

        NamedNodeMap attributes = element.getAttributes();
        for (int i = 0, n = attributes.getLength(); i < n; i++) {
            Node node = attributes.item(i);
            String value = node.getNodeValue();
            if (value.startsWith(PREFIX_RESOURCE_REF)) {
                Attr attribute = (Attr) node;
                if (!isAllowedToVary(attribute)) {
                    checkReference(context, attribute, value, repository);
                }
            }
        }

        NodeList children = element.getChildNodes();
        for (int i = 0, n = children.getLength(); i < n; i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                visit(context, ((Element)child), repository);
            }
        }
    }

    /**
     * Is the given attribute allowed to reference a resource that has different
     * values across configurations (other than with version qualifiers) ?
     * 

* When the manifest is read, it has a fixed configuration with only the API level set. * When strings are read, we can either read the actual string, or a resource reference. * For labels and icons, we only read the resource reference -- that is the package manager * doesn't need the actual string (like it would need for, say, the name of an activity), * but just gets the resource ID, and then clients if they need the actual resource value can * load it at that point using their current configuration. *

* To see which specific attributes in the manifest are processed this way, look at * android.content.pm.PackageItemInfo to see what pieces of data are kept as raw resource * IDs instead of loading their value. (For label resources we also keep the non localized * label resource to allow people to specify hardcoded strings instead of a resource reference.) * * @param attribute the attribute node to look up * @return true if this resource is allowed to have delayed configuration values */ private static boolean isAllowedToVary(@NonNull Attr attribute) { // This corresponds to the getResourceId() calls in PackageParser // where we store the actual resource id such that they can be // resolved later String name = attribute.getLocalName(); if (ATTR_LABEL.equals(name) || ATTR_ICON.equals(name) || ATTR_THEME.equals(name) || "description".equals(name) || "logo".equals(name) || "banner".equals(name) || "sharedUserLabel".equals(name)) { return ANDROID_URI.equals(attribute.getNamespaceURI()); } return false; } private void checkReference( @NonNull XmlContext context, @NonNull Attr attribute, @NonNull String value, @Nullable AbstractResourceRepository repository) { ResourceUrl url = ResourceUrl.parse(value); if (url != null && !url.framework) { if (repository != null) { List items = repository.getResourceItem(url.type, url.name); if (items != null && items.size() > 1) { List list = Lists.newArrayListWithExpectedSize(5); for (ResourceItem item : items) { String qualifiers = item.getQualifiers(); // Default folder is okay if (qualifiers.isEmpty()) { continue; } // Version qualifier is okay if (VersionQualifier.getQualifier(qualifiers) != null) { continue; } list.add(qualifiers); } if (!list.isEmpty()) { Collections.sort(list); String message = getErrorMessage(Joiner.on(", ").join(list)); context.report(ISSUE, attribute, context.getValueLocation(attribute), message); } } } else if (!context.getDriver().isSuppressed(context, ISSUE, attribute)) { // Don't have a resource repository; need to check resource files during batch // run if (mManifestLocations == null) { mManifestLocations = Maps.newHashMap(); } Multimap typeMap = mManifestLocations.get(url.name); if (typeMap == null) { typeMap = ArrayListMultimap.create(); mManifestLocations.put(url.name, typeMap); } typeMap.put(url.type, context.getValueLocation(attribute)); } } } private void checkResourceFile( @NonNull XmlContext context, @NonNull Document document) { File parentFile = context.file.getParentFile(); if (parentFile == null) { return; } String parentName = parentFile.getName(); // Base folders are okay int index = parentName.indexOf('-'); if (index == -1) { return; } // Version qualifier is okay String qualifiers = parentName.substring(index + 1); if (VersionQualifier.getQualifier(qualifiers) != null) { return; } ResourceFolderType folderType = context.getResourceFolderType(); if (folderType == ResourceFolderType.VALUES) { Element root = document.getDocumentElement(); if (root != null) { NodeList children = root.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { Element item = (Element)child; String name = item.getAttribute(ATTR_NAME); if (name != null && mManifestLocations.containsKey(name)) { String tag = item.getTagName(); String typeString = tag; if (tag.equals(TAG_ITEM)) { typeString = item.getAttribute(ATTR_TYPE); } ResourceType type = ResourceType.getEnum(typeString); if (type != null) { reportIfFound(context, qualifiers, name, type, item); } } } } } } else if (folderType != null) { String name = LintUtils.getBaseName(context.file.getName()); if (mManifestLocations.containsKey(name)) { List types = FolderTypeRelationship.getRelatedResourceTypes(folderType); for (ResourceType type : types) { reportIfFound(context, qualifiers, name, type, document.getDocumentElement()); } } } } private void reportIfFound(@NonNull XmlContext context, @NonNull String qualifiers, @NonNull String name, @NonNull ResourceType type, @Nullable Node secondary) { Multimap typeMap = mManifestLocations.get(name); if (typeMap != null) { Collection locations = typeMap.get(type); if (locations != null) { for (Location location : locations) { String message = getErrorMessage(qualifiers); if (secondary != null) { Location secondaryLocation = context.getLocation(secondary); secondaryLocation.setSecondary(location.getSecondary()); secondaryLocation.setMessage("This value will not be used"); location.setSecondary(secondaryLocation); } context.report(ISSUE, location, message); } } } } @NonNull private static String getErrorMessage(@NonNull String qualifiers) { return "Resources referenced from the manifest cannot vary by configuration " + "(except for version qualifiers, e.g. `-v21`.) Found variation in " + qualifiers; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy