blob: 5e974f71faa78fd11639277351023e21b21561aa [file] [log] [blame]
/*
* 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.lint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_SRC;
import static com.android.SdkConstants.ATTR_SRC_COMPAT;
import static com.android.SdkConstants.AUTO_URI;
import static com.android.SdkConstants.TAG_ANIMATED_VECTOR;
import static com.android.SdkConstants.TAG_VECTOR;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.Variant;
import com.android.ide.common.gradle.model.IdeAndroidProject;
import com.android.ide.common.rendering.api.ResourceNamespace;
import com.android.ide.common.repository.GradleVersion;
import com.android.ide.common.resources.ResourceItem;
import com.android.ide.common.resources.ResourceRepository;
import com.android.ide.common.util.PathString;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.resources.ResourceUrl;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Lint;
import com.android.tools.lint.detector.api.Location;
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.android.utils.XmlUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
/**
* Finds all the vector drawables and checks references to them in layouts.
*
* <p>This detector looks for common mistakes related to AppCompat support for vector drawables,
* that is:
*
* <ul>
* <li>Using app:srcCompat without useSupportLibrary in build.gradle
* <li>Using android:src with useSupportLibrary in build.gradle
* </ul>
*/
public class VectorDrawableCompatDetector extends ResourceXmlDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE =
Issue.create(
"VectorDrawableCompat",
"Using VectorDrawableCompat",
"To use VectorDrawableCompat, you need to make two modifications to your project. "
+ "First, set `android.defaultConfig.vectorDrawables.useSupportLibrary = true` "
+ "in your `build.gradle` file, "
+ "and second, use `app:srcCompat` instead of `android:src` to refer to vector "
+ "drawables.",
Category.CORRECTNESS,
5,
Severity.ERROR,
new Implementation(
VectorDrawableCompatDetector.class,
Scope.ALL_RESOURCES_SCOPE,
Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo(
"http://chris.banes.me/2016/02/25/appcompat-vector/#enabling-the-flag");
/** Whether to skip the checks altogether. */
private boolean mSkipChecks;
/** All vector drawables in the project. */
private final Set<String> mVectors = Sets.newHashSet();
/** Whether the project uses AppCompat for vectors. */
private boolean mUseSupportLibrary;
@Override
public void beforeCheckRootProject(@NonNull Context context) {
IdeAndroidProject model = context.getProject().getGradleProjectModel();
if (model == null) {
mSkipChecks = true;
return;
}
if (context.getProject().getMinSdk() >= 21) {
mSkipChecks = true;
return;
}
GradleVersion version = context.getProject().getGradleModelVersion();
if (version == null || version.getMajor() < 2) {
mSkipChecks = true;
return;
}
Variant currentVariant = context.getProject().getCurrentVariant();
if (currentVariant == null) {
mSkipChecks = true;
return;
}
if (Boolean.TRUE.equals(
currentVariant.getMergedFlavor().getVectorDrawables().getUseSupportLibrary())) {
mUseSupportLibrary = true;
}
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
//noinspection SimplifiableIfStatement
if (mSkipChecks) {
return false;
}
return folderType == ResourceFolderType.DRAWABLE || folderType == ResourceFolderType.LAYOUT;
}
/**
* Saves names of all vector resources encountered. Because "drawable" is before "layout" in
* alphabetical order, Lint will first call this on every vector, before calling {@link
* #visitAttribute(XmlContext, Attr)} on every attribute.
*/
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (mSkipChecks) {
return;
}
String resourceName = Lint.getBaseName(context.file.getName());
mVectors.add(resourceName);
}
@Nullable
@Override
public Collection<String> getApplicableElements() {
return mSkipChecks ? null : Arrays.asList(TAG_VECTOR, TAG_ANIMATED_VECTOR);
}
@Nullable
@Override
public Collection<String> getApplicableAttributes() {
return mSkipChecks ? null : ImmutableList.of(ATTR_SRC, ATTR_SRC_COMPAT);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (mSkipChecks) {
return;
}
boolean incrementalMode =
!context.getDriver().getScope().contains(Scope.ALL_RESOURCE_FILES);
if (!incrementalMode && mVectors.isEmpty()) {
return;
}
Predicate<String> isVector;
if (!incrementalMode) {
// TODO: Always use resources, once command-line client supports it.
isVector = mVectors::contains;
} else {
LintClient client = context.getClient();
ResourceRepository resources =
client.getResourceRepository(context.getMainProject(), true, false);
if (resources == null) {
// We only run on a single layout file, but have no access to the resources
// database, there's no way we can perform the check.
return;
}
isVector = name -> checkResourceRepository(resources, name);
}
String name = attribute.getLocalName();
String namespace = attribute.getNamespaceURI();
if ((ATTR_SRC.equals(name) && !ANDROID_URI.equals(namespace)
|| (ATTR_SRC_COMPAT.equals(name) && !AUTO_URI.equals(namespace)))) {
// Not the attribute we are looking for.
return;
}
ResourceUrl resourceUrl = ResourceUrl.parse(attribute.getValue());
if (resourceUrl == null) {
return;
}
if (mUseSupportLibrary && ATTR_SRC.equals(name) && isVector.test(resourceUrl.name)) {
Location location = context.getNameLocation(attribute);
String message = "When using VectorDrawableCompat, you need to use `app:srcCompat`.";
context.report(ISSUE, attribute, location, message);
}
if (!mUseSupportLibrary
&& ATTR_SRC_COMPAT.equals(name)
&& isVector.test(resourceUrl.name)) {
Location location = context.getNameLocation(attribute);
String message =
"To use VectorDrawableCompat, you need to set "
+ "`android.defaultConfig.vectorDrawables.useSupportLibrary = true`.";
context.report(ISSUE, attribute, location, message);
}
}
private static boolean checkResourceRepository(
@NonNull ResourceRepository resources, @NonNull String name) {
List<ResourceItem> items =
resources.getResources(ResourceNamespace.TODO(), ResourceType.DRAWABLE, name);
// Check if at least one drawable with this name is a vector.
for (ResourceItem item : items) {
PathString source = item.getSource();
if (source == null) {
return false;
}
File file = source.toFile();
if (file == null) {
return false;
}
if (!source.getFileName().endsWith(SdkConstants.DOT_XML)) {
continue;
}
String rootTagName = XmlUtils.getRootTagName(file);
return TAG_VECTOR.equals(rootTagName) || TAG_ANIMATED_VECTOR.equals(rootTagName);
}
return false;
}
}