blob: 69eafaeb9fe5ff0f8568c873f8ff0d95715280ab [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 com.android.SdkConstants.ANDROIDX_CONSTRAINT_LAYOUT_LIB_ARTIFACT
import com.android.SdkConstants.ATTR_CONSTRAINT_LAYOUT_DESCRIPTION
import com.android.SdkConstants.ATTR_ID
import com.android.SdkConstants.ATTR_LAYOUT_HEIGHT
import com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX
import com.android.SdkConstants.ATTR_LAYOUT_WIDTH
import com.android.SdkConstants.AUTO_URI
import com.android.SdkConstants.CLASS_CONSTRAINT_LAYOUT_BARRIER
import com.android.SdkConstants.CLASS_CONSTRAINT_LAYOUT_FLOW
import com.android.SdkConstants.CLASS_CONSTRAINT_LAYOUT_GROUP
import com.android.SdkConstants.CLASS_CONSTRAINT_LAYOUT_GUIDELINE
import com.android.SdkConstants.CONSTRAINT_LAYOUT
import com.android.SdkConstants.CONSTRAINT_LAYOUT_LIB_ARTIFACT
import com.android.SdkConstants.CONSTRAINT_LAYOUT_LIB_ARTIFACT_ID
import com.android.SdkConstants.CONSTRAINT_LAYOUT_LIB_GROUP_ID
import com.android.SdkConstants.LATEST_CONSTRAINT_LAYOUT_VERSION
import com.android.SdkConstants.MOTION_LAYOUT
import com.android.SdkConstants.TAG_INCLUDE
import com.android.SdkConstants.VALUE_MATCH_PARENT
import com.android.ide.common.repository.GradleCoordinate
import com.android.ide.common.repository.GradleCoordinate.COMPARE_PLUS_LOWER
import com.android.ide.common.repository.GradleVersion
import com.android.ide.common.repository.SdkMavenRepository
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.XmlContext
import com.android.tools.lint.detector.api.isLayoutMarkerTag
import org.w3c.dom.Element
import org.w3c.dom.Node
/**
* Check which looks for potential errors in declarations of ConstraintLayout, such as under
* specifying constraints
*/
class ConstraintLayoutDetector : LayoutDetector() {
override fun getApplicableElements(): Collection<String>? {
return setOf(
CONSTRAINT_LAYOUT.oldName(),
CONSTRAINT_LAYOUT.newName(),
MOTION_LAYOUT.oldName(),
MOTION_LAYOUT.newName()
)
}
override fun visitElement(
context: XmlContext,
element: Element
) {
// Make sure we're using the current version
val variant = context.mainProject.buildVariant
var latestAvailable: GradleCoordinate? = null
if (variant != null) {
val dependencies = variant.mainArtifact.dependencies.compileDependencies
val library = dependencies.findLibrary(CONSTRAINT_LAYOUT_LIB_ARTIFACT, true)
?: dependencies.findLibrary(ANDROIDX_CONSTRAINT_LAYOUT_LIB_ARTIFACT, true)
if (library != null) {
val rc = library.resolvedCoordinates
if (CONSTRAINT_LAYOUT_LIB_GROUP_ID == rc.groupId &&
CONSTRAINT_LAYOUT_LIB_ARTIFACT_ID == rc.artifactId
) {
if (latestAvailable == null) {
latestAvailable = getLatestVersion(context)
}
val version = GradleCoordinate(rc.groupId, rc.artifactId, rc.version)
if (COMPARE_PLUS_LOWER.compare(latestAvailable, version) > 0) {
val message = "Using version ${version.revision} of the constraint library, which is obsolete"
val fix = fix().data(ConstraintLayoutDetector::class.java)
context.report(
GradleDetector.DEPENDENCY,
element,
context.getLocation(element),
message,
fix
)
}
}
}
}
// In MotionLayouts you can specify the constraints elsewhere.
// Note that MotionLayoutDetector performs additional validation.
if (element.hasAttributeNS(AUTO_URI, ATTR_CONSTRAINT_LAYOUT_DESCRIPTION)) {
return
}
// Views that are constrained by Flow do not require additional constraint.
var child = element.firstChild
// List of views that are Flow-constrained.
val flowList = ArrayList<String>()
while (child != null) {
if (child.nodeType != Node.ELEMENT_NODE) {
child = child.nextSibling
continue
}
val elementTagName = (child as Element).tagName
if (CLASS_CONSTRAINT_LAYOUT_FLOW.isEquals(elementTagName)) {
val attributes = child.attributes
for (i in 0 until attributes.length) {
val attribute = attributes.item(i)
val name = attribute.localName ?: continue
val value = attribute.nodeValue
if (name.contains("constraint_referenced_ids")) {
flowList.addAll(value.split(","))
}
}
}
child = child.nextSibling
}
// Ensure that all the children have been constrained horizontally and vertically
child = element.firstChild
while (child != null) {
if (child.nodeType != Node.ELEMENT_NODE) {
child = child.nextSibling
continue
}
val layout = child as Element
val elementTagName = layout.tagName
if (CLASS_CONSTRAINT_LAYOUT_GUIDELINE.isEquals(elementTagName) ||
// Groups do not need to be constrained
CLASS_CONSTRAINT_LAYOUT_GROUP.isEquals(elementTagName) ||
// Don't flag includes; they might have the right constraints inside.
TAG_INCLUDE == elementTagName ||
// <requestFocus/>, <tag/>, etc should not have constraint attributes
isLayoutMarkerTag(elementTagName)
) {
child = child.getNextSibling()
continue
} else if (elementTagName.isNotBlank() &&
CLASS_CONSTRAINT_LAYOUT_BARRIER.isEquals(elementTagName) &&
scanForBarrierConstraint(layout)
) {
// The Barrier has the necessary layout constraints.
// This element is constrained correctly.
break
}
var isConstrainedHorizontally = false
var isConstrainedVertically = false
val attributes = layout.attributes
for (i in 0 until attributes.length) {
val attribute = attributes.item(i)
val name = attribute.localName ?: continue
// If the id is in the Flow, it's already constrained.
if (ATTR_ID == name) {
val value = attribute.nodeValue.split("/").last()
if (flowList.contains(value)) {
isConstrainedHorizontally = true
isConstrainedVertically = true
break
}
}
if (!name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) ||
name.endsWith("_creator")
) {
continue
}
if (ATTR_LAYOUT_WIDTH == name && VALUE_MATCH_PARENT == attribute.nodeValue ||
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 == name && VALUE_MATCH_PARENT == attribute.nodeValue ||
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
val message: String = when {
isConstrainedVertically ->
"This view is not constrained horizontally: at runtime it will jump to the left unless you add a horizontal constraint"
isConstrainedHorizontally ->
"This view is not constrained vertically: at runtime it will jump to the top unless you add a vertical constraint"
else ->
"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, layout, context.getNameLocation(layout), message)
}
child = child.getNextSibling()
}
}
companion object {
@JvmField
val ISSUE =
Issue.create(
id = "MissingConstraints",
briefDescription = "Missing Constraints in ConstraintLayout",
explanation = """
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 = Category.CORRECTNESS,
priority = 6,
severity = Severity.ERROR,
implementation = Implementation(
ConstraintLayoutDetector::class.java,
Scope.RESOURCE_FILE_SCOPE
),
androidSpecific = true
)
/** Latest known version of the ConstraintLayout library (as a [GradleVersion] */
@Suppress("MemberVisibilityCanBePrivate") // used in tools/adt/idea
@JvmField
val LATEST_KNOWN_VERSION = GradleCoordinate(
CONSTRAINT_LAYOUT_LIB_GROUP_ID,
CONSTRAINT_LAYOUT_LIB_ARTIFACT_ID,
LATEST_CONSTRAINT_LAYOUT_VERSION
)
/**
* @param element to scan
* @return true if barrier specific constraint is set. False otherwise.
*/
private fun scanForBarrierConstraint(element: Element): Boolean {
val attributes = element.attributes
for (i in 0 until attributes.length) {
val attribute = attributes.item(i)
val name = attribute.localName ?: continue
if (name.endsWith("barrierDirection")) {
return true
}
}
return false
}
private fun getLatestVersion(context: XmlContext): GradleCoordinate {
var latestAvailable = LATEST_KNOWN_VERSION
val sdkHandler = context.client.getSdk()
if (sdkHandler != null) {
val progress = context.client.getRepositoryLogger()
val latestPackage = SdkMavenRepository.findLatestVersion(
LATEST_KNOWN_VERSION,
sdkHandler,
null,
progress
)
if (latestPackage != null) {
val fromPackage =
SdkMavenRepository.getCoordinateFromSdkPath(latestPackage.path)
if (fromPackage != null &&
COMPARE_PLUS_LOWER.compare(latestAvailable, fromPackage) < 0
) {
latestAvailable = fromPackage
}
}
}
return latestAvailable
}
}
}