blob: 6028a3c499c8c29b4bd3a3c202181f025177a8d7 [file] [log] [blame]
/*
* 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 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<String, Multimap<ResourceType, Location>> 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.getProjectResources(project, true);
}
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) {
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) ?
* <p>
* 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.
* <p>
* 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<ResourceItem> items = repository.getResourceItem(url.type, url.name);
if (items != null && items.size() > 1) {
List<String> 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<ResourceType, Location> 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<ResourceType> 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<ResourceType, Location> typeMap = mManifestLocations.get(name);
if (typeMap != null) {
Collection<Location> 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;
}
}