blob: 397966063fad4e9c1616129f800d1521175cca44 [file] [log] [blame]
/*
* Copyright (C) 2016 - 2018 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_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.CLASS_CONSTRAINT_LAYOUT_BARRIER;
import static com.android.SdkConstants.CLASS_CONSTRAINT_LAYOUT_GROUP;
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.REQUEST_FOCUS;
import static com.android.SdkConstants.TAG_INCLUDE;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import static com.android.ide.common.repository.GradleCoordinate.COMPARE_PLUS_LOWER;
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.LintFix;
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.Strings;
import com.google.common.collect.ImmutableSet;
import java.util.Collection;
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",
"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));
/** 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<String> getApplicableElements() {
return ImmutableSet.of(CONSTRAINT_LAYOUT.oldName(), CONSTRAINT_LAYOUT.newName());
}
@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 = variant.getMainArtifact().getDependencies();
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) {
String message =
"Using version "
+ version.getRevision()
+ " of the constraint library, which is obsolete";
LintFix fix = fix().data(ConstraintLayoutDetector.class);
context.report(
GradleDetector.DEPENDENCY,
layout,
context.getLocation(layout),
message,
fix);
}
}
}
}
// 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;
String elementTagName = element.getTagName();
if (CLASS_CONSTRAINT_LAYOUT_GUIDELINE.isEquals(elementTagName)) {
continue;
}
if (CLASS_CONSTRAINT_LAYOUT_GROUP.isEquals(elementTagName)) {
// Groups do not need to be constrained
continue;
}
if (TAG_INCLUDE.equals(elementTagName)) {
// Don't flag includes; they might have the right constraints inside.
continue;
}
if (REQUEST_FOCUS.equals(elementTagName)) {
// <requestFocus/> is just a marker tag and should not have attributes
continue;
}
if (!Strings.isNullOrEmpty(elementTagName)
&& CLASS_CONSTRAINT_LAYOUT_BARRIER.isEquals(elementTagName)
&& scanForBarrierConstraint(element)) {
// The Barrier has the necessary layout constraints. This element is constrained correctly.
break;
}
boolean isConstrainedHorizontally = false;
boolean isConstrainedVertically = false;
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(ATTR_LAYOUT_RESOURCE_PREFIX) || name.endsWith("_creator")) {
continue;
}
if ((ATTR_LAYOUT_WIDTH.equals(name)
&& VALUE_MATCH_PARENT.equals(attribute.getNodeValue()))
|| name.endsWith("toLeftOf")
|| name.endsWith("toRightOf")
|| name.endsWith("toStartOf")
|| name.endsWith("toEndOf")
|| name.endsWith("toCenterX")) {
isConstrainedHorizontally = true;
if (isConstrainedVertically) {
break;
}
} else if ((ATTR_LAYOUT_HEIGHT.equals(name)
&& VALUE_MATCH_PARENT.equals(attribute.getNodeValue()))
|| 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 top 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) at runtime unless you add the constraints";
}
context.report(ISSUE, element, context.getNameLocation(element), message);
}
}
}
/**
* @param element to scan
* @return true if barrier specific constraint is set. False otherwise.
*/
private static boolean scanForBarrierConstraint(Element element) {
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.endsWith("barrierDirection")) {
return true;
}
}
return false;
}
@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, null, progress);
if (latestPackage != null) {
GradleCoordinate fromPackage =
SdkMavenRepository.getCoordinateFromSdkPath(latestPackage.getPath());
if (fromPackage != null
&& COMPARE_PLUS_LOWER.compare(latestAvailable, fromPackage) < 0) {
latestAvailable = fromPackage;
}
}
}
return latestAvailable;
}
}