com.android.tools.lint.checks.ConstraintLayoutDetector Maven / Gradle / Ivy
/*
* 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.ATTR_LAYOUT_EDITOR_ABSOLUTE_X;
import static com.android.SdkConstants.ATTR_LAYOUT_EDITOR_ABSOLUTE_Y;
import static com.android.SdkConstants.CLASS_CONSTRAINT_LAYOUT_GUIDELINE;
import static com.android.SdkConstants.CONSTRAINT_LAYOUT;
import static com.android.SdkConstants.CONSTRAINT_LAYOUT_LIB_ARTIFACT_ID;
import static com.android.SdkConstants.CONSTRAINT_LAYOUT_LIB_GROUP_ID;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.ide.common.repository.GradleCoordinate.*;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.Dependencies;
import com.android.builder.model.MavenCoordinates;
import com.android.builder.model.Variant;
import com.android.ide.common.repository.GradleCoordinate;
import com.android.ide.common.repository.GradleVersion;
import com.android.ide.common.repository.SdkMavenRepository;
import com.android.repository.api.ProgressIndicator;
import com.android.repository.api.RepoPackage;
import com.android.sdklib.repository.AndroidSdkHandler;
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.LayoutDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.tools.lint.detector.api.XmlContext;
import java.util.Collection;
import java.util.Collections;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
/**
* Check which looks for potential errors in declarations of ConstraintLayout, such as
* under specifying constraints
*/
public class ConstraintLayoutDetector extends LayoutDetector {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"MissingConstraints", //$NON-NLS-1$
"Missing Constraints in ConstraintLayout",
"The layout editor allows you to place widgets anywhere on the canvas, and it " +
"records the current position with designtime attributes (such as " +
"`layout_editor_absoluteX`.) These attributes are *not* applied at runtime, so if " +
"you push your layout on a device, the widgets may appear in a different location " +
"than shown in the editor. To fix this, make sure a widget has both horizontal and " +
"vertical constraints by dragging from the edge connections.",
Category.CORRECTNESS,
6,
Severity.ERROR,
new Implementation(
ConstraintLayoutDetector.class,
Scope.RESOURCE_FILE_SCOPE));
private static final String LAYOUT_CONSTRAINT_PREFIX = "layout_constraint";
/** Latest known version of the ConstraintLayout library (as a {@link GradleVersion} */
@SuppressWarnings("ConstantConditions")
@NonNull
public static final GradleCoordinate LATEST_KNOWN_VERSION =
new GradleCoordinate(
CONSTRAINT_LAYOUT_LIB_GROUP_ID,
CONSTRAINT_LAYOUT_LIB_ARTIFACT_ID,
SdkConstants.LATEST_CONSTRAINT_LAYOUT_VERSION);
/** Constructs a new {@link ConstraintLayoutDetector} check */
public ConstraintLayoutDetector() {
}
@Override
public Collection getApplicableElements() {
return Collections.singletonList(CONSTRAINT_LAYOUT);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element layout) {
// Make sure we're using the current version
Variant variant = context.getMainProject().getCurrentVariant();
GradleCoordinate latestAvailable = null;
if (variant != null) {
Dependencies dependencies = GradleDetector.getCompileDependencies(
variant.getMainArtifact(), context.getMainProject().getGradleModelVersion());
for (AndroidLibrary library : dependencies.getLibraries()) {
MavenCoordinates rc = library.getResolvedCoordinates();
if (CONSTRAINT_LAYOUT_LIB_GROUP_ID.equals(rc.getGroupId())
&& CONSTRAINT_LAYOUT_LIB_ARTIFACT_ID.equals(rc.getArtifactId())) {
if (latestAvailable == null) {
latestAvailable = getLatestVersion(context);
}
GradleCoordinate version = new GradleCoordinate(
CONSTRAINT_LAYOUT_LIB_GROUP_ID,
CONSTRAINT_LAYOUT_LIB_ARTIFACT_ID,
rc.getVersion());
if (COMPARE_PLUS_LOWER.compare(latestAvailable, version) > 0) {
// Keep in sync with #isUpgradeDependencyError below
String message = "Using version " + version.getRevision()
+ " of the constraint library, which is obsolete";
context.report(ISSUE, layout, context.getLocation(layout), message);
}
}
}
}
// Ensure that all the children have been constrained horizontally and vertically
for (Node child = layout.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element element = (Element)child;
if (element.getTagName().equals(CLASS_CONSTRAINT_LAYOUT_GUIDELINE)) {
continue;
}
boolean isConstrainedHorizontally = false;
boolean isConstrainedVertically = false;
// See if the layout doesn't use absoluteX/Y designtime positions for this
// child; if it doesn't, no need to complain
if (!element.hasAttributeNS(TOOLS_URI, ATTR_LAYOUT_EDITOR_ABSOLUTE_X)) {
// Not technically a constraint, but we'll use this to not complain
// about lacking constraints in this dimension
isConstrainedHorizontally = true;
}
if (!element.hasAttributeNS(TOOLS_URI, ATTR_LAYOUT_EDITOR_ABSOLUTE_Y)) {
isConstrainedVertically = true;
if (isConstrainedHorizontally) {
// Nothing to check
continue;
}
}
NamedNodeMap attributes = element.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attribute = attributes.item(i);
String name = attribute.getLocalName();
if (name == null) {
continue;
}
if (!name.startsWith(LAYOUT_CONSTRAINT_PREFIX) || name.endsWith("_creator")) {
continue;
}
if (name.endsWith("toLeftOf")
|| name.endsWith("toRightOf")
|| name.endsWith("toStartOf")
|| name.endsWith("toEndOf")
|| name.endsWith("toCenterX")) {
isConstrainedHorizontally = true;
if (isConstrainedVertically) {
break;
}
} else if (name.endsWith("toTopOf")
|| name.endsWith("toBottomOf")
|| name.endsWith("toCenterY")
|| name.endsWith("toBaselineOf")) {
isConstrainedVertically = true;
if (isConstrainedHorizontally) {
break;
}
}
}
if (!isConstrainedHorizontally || !isConstrainedVertically) {
// Don't complain if the element doesn't specify absolute x/y - that's
// when it gets confusing
String message;
if (isConstrainedVertically) {
message = "This view is not constrained horizontally: at runtime it will "
+ "jump to the left unless you add a horizontal constraint";
} else if (isConstrainedHorizontally) {
message = "This view is not constrained vertically: at runtime it will "
+ "jump to the left unless you add a vertical constraint";
} else {
message = "This view is not constrained, it only has designtime positions, "
+ "so it will jump to (0,0) unless you add constraints";
}
context.report(ISSUE, element, context.getLocation(element), message);
}
}
}
@NonNull
private static GradleCoordinate getLatestVersion(@NonNull XmlContext context) {
GradleCoordinate latestAvailable = LATEST_KNOWN_VERSION;
AndroidSdkHandler sdkHandler = context.getClient().getSdk();
if (sdkHandler != null) {
ProgressIndicator progress = context.getClient().getRepositoryLogger();
RepoPackage latestPackage = SdkMavenRepository
.findLatestVersion(LATEST_KNOWN_VERSION, sdkHandler, progress);
if (latestPackage != null) {
GradleCoordinate fromPackage = SdkMavenRepository
.getCoordinateFromSdkPath(latestPackage.getPath());
if (fromPackage != null &&
COMPARE_PLUS_LOWER.compare(latestAvailable, fromPackage) < 0) {
latestAvailable = fromPackage;
}
}
}
return latestAvailable;
}
/**
* Given an error message produced by this lint detector for the given issue type,
* returns whether it represents a suggestion update the library version.
*
* Intended for IDE quickfix implementations.
*
* @param errorMessage the error message associated with the error
* @param format the format of the error message
* @return true if this is a suggestion to update the version
*/
@SuppressWarnings("unused")
public static boolean isUpgradeDependencyError(
@NonNull String errorMessage,
@NonNull TextFormat format) {
return errorMessage.startsWith("Using version ");
}
}