blob: 650e6e7816cff065ba12a164025da684699b9454 [file] [log] [blame]
/*
* Copyright (C) 2011 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_NAME;
import static com.android.SdkConstants.ATTR_TEXT_SIZE;
import static com.android.SdkConstants.DIMEN_PREFIX;
import static com.android.SdkConstants.TAG_DIMEN;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_STYLE;
import static com.android.SdkConstants.UNIT_DIP;
import static com.android.SdkConstants.UNIT_DP;
import static com.android.SdkConstants.UNIT_IN;
import static com.android.SdkConstants.UNIT_MM;
import static com.android.SdkConstants.UNIT_PX;
import static com.android.SdkConstants.UNIT_SP;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceFolderType;
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.LayoutDetector;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
/**
* Check for px dimensions instead of dp dimensions.
* Also look for non-"sp" text sizes.
*/
public class PxUsageDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
PxUsageDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Using px instead of dp */
public static final Issue PX_ISSUE = Issue.create(
"PxUsage", //$NON-NLS-1$
"Using 'px' dimension",
// This description is from the below screen support document
"For performance reasons and to keep the code simpler, the Android system uses pixels " +
"as the standard unit for expressing dimension or coordinate values. That means that " +
"the dimensions of a view are always expressed in the code using pixels, but " +
"always based on the current screen density. For instance, if `myView.getWidth()` " +
"returns 10, the view is 10 pixels wide on the current screen, but on a device with " +
"a higher density screen, the value returned might be 15. If you use pixel values " +
"in your application code to work with bitmaps that are not pre-scaled for the " +
"current screen density, you might need to scale the pixel values that you use in " +
"your code to match the un-scaled bitmap source.",
Category.CORRECTNESS,
2,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/guide/practices/screens_support.html#screen-independence"); //$NON-NLS-1$
/** Using mm/in instead of dp */
public static final Issue IN_MM_ISSUE = Issue.create(
"InOrMmUsage", //$NON-NLS-1$
"Using `mm` or `in` dimensions",
"Avoid using `mm` (millimeters) or `in` (inches) as the unit for dimensions.\n" +
"\n" +
"While it should work in principle, unfortunately many devices do not report " +
"the correct true physical density, which means that the dimension calculations " +
"won't work correctly. You are better off using `dp` (and for font sizes, `sp`.)",
Category.CORRECTNESS,
4,
Severity.WARNING,
IMPLEMENTATION);
/** Using sp instead of dp */
public static final Issue DP_ISSUE = Issue.create(
"SpUsage", //$NON-NLS-1$
"Using `dp` instead of `sp` for text sizes",
"When setting text sizes, you should normally use `sp`, or \"scale-independent " +
"pixels\". This is like the `dp` unit, but it is also scaled " +
"by the user's font size preference. It is recommend you use this unit when " +
"specifying font sizes, so they will be adjusted for both the screen density " +
"and the user's preference.\n" +
"\n" +
"There *are* cases where you might need to use `dp`; typically this happens when " +
"the text is in a container with a specific dp-size. This will prevent the text " +
"from spilling outside the container. Note however that this means that the user's " +
"font size settings are not respected, so consider adjusting the layout itself " +
"to be more flexible.",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/training/multiscreen/screendensities.html"); //$NON-NLS-1$
/** Using text sizes that are too small */
public static final Issue SMALL_SP_ISSUE = Issue.create(
"SmallSp", //$NON-NLS-1$
"Text size is too small",
"Avoid using sizes smaller than 12sp.",
Category.USABILITY,
4,
Severity.WARNING,
IMPLEMENTATION);
private HashMap<String, Location.Handle> mTextSizeUsage;
/** Constructs a new {@link PxUsageDetector} */
public PxUsageDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
// Look in both layouts (at attribute values) and in value files (at style definitions)
return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
}
@Override
public Collection<String> getApplicableAttributes() {
return ALL;
}
@Override
@Nullable
public Collection<String> getApplicableElements() {
return Collections.singletonList(TAG_STYLE);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
if (context.getResourceFolderType() != ResourceFolderType.LAYOUT) {
assert context.getResourceFolderType() == ResourceFolderType.VALUES;
if (mTextSizeUsage != null
&& attribute.getOwnerElement().getTagName().equals(TAG_DIMEN)) {
Element element = attribute.getOwnerElement();
String name = element.getAttribute(ATTR_NAME);
if (name != null && mTextSizeUsage.containsKey(name)
&& context.isEnabled(DP_ISSUE)) {
NodeList children = element.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.TEXT_NODE &&
isDpUnit(child.getNodeValue())) {
String message = "This dimension is used as a text size: "
+ "Should use \"`sp`\" instead of \"`dp`\"";
Location location = context.getLocation(child);
Location secondary = mTextSizeUsage.get(name).resolve();
secondary.setMessage("Dimension used as a text size here");
location.setSecondary(secondary);
context.report(DP_ISSUE, attribute, location, message);
break;
}
}
}
}
return;
}
String value = attribute.getValue();
if (value.endsWith(UNIT_PX) && value.matches("\\d+px")) { //$NON-NLS-1$
if (value.charAt(0) == '0' || value.equals("1px")) { //$NON-NLS-1$
// 0px is fine. 0px is 0dp regardless of density...
// Similarly, 1px is typically used to create a single thin line (see issue 55722)
return;
}
if (context.isEnabled(PX_ISSUE)) {
context.report(PX_ISSUE, attribute, context.getLocation(attribute),
"Avoid using \"`px`\" as units; use \"`dp`\" instead");
}
} else if (value.endsWith(UNIT_MM) && value.matches("\\d+mm") //$NON-NLS-1$
|| value.endsWith(UNIT_IN) && value.matches("\\d+in")) { //$NON-NLS-1$
if (value.charAt(0) == '0') {
// 0mm == 0in == 0dp
return;
}
if (context.isEnabled(IN_MM_ISSUE)) {
String unit = value.substring(value.length() - 2);
context.report(IN_MM_ISSUE, attribute, context.getLocation(attribute),
String.format("Avoid using \"`%1$s`\" as units " +
"(it does not work accurately on all devices); use \"`dp`\" instead",
unit));
}
} else if (value.endsWith(UNIT_SP)
&& (ATTR_TEXT_SIZE.equals(attribute.getLocalName())
|| ATTR_LAYOUT_HEIGHT.equals(attribute.getLocalName()))
&& value.matches("\\d+sp")) { //$NON-NLS-1$
int size = getSize(value);
if (size > 0 && size < 12) {
context.report(SMALL_SP_ISSUE, attribute, context.getLocation(attribute),
String.format("Avoid using sizes smaller than `12sp`: `%1$s`", value));
}
} else if (ATTR_TEXT_SIZE.equals(attribute.getLocalName())) {
if (isDpUnit(value)) { //$NON-NLS-1$
if (context.isEnabled(DP_ISSUE)) {
context.report(DP_ISSUE, attribute, context.getLocation(attribute),
"Should use \"`sp`\" instead of \"`dp`\" for text sizes");
}
} else if (value.startsWith(DIMEN_PREFIX)) {
if (context.getClient().supportsProjectResources()) {
LintClient client = context.getClient();
Project project = context.getProject();
AbstractResourceRepository resources = client.getProjectResources(project,
true);
ResourceUrl url = ResourceUrl.parse(value);
if (resources != null && url != null) {
List<ResourceItem> items = resources.getResourceItem(url.type, url.name);
if (items != null) {
for (ResourceItem item : items) {
ResourceValue resourceValue = item.getResourceValue(false);
if (resourceValue != null) {
String dimenValue = resourceValue.getValue();
if (dimenValue != null && isDpUnit(dimenValue)
&& context.isEnabled(DP_ISSUE)) {
ResourceFile sourceFile = item.getSource();
assert sourceFile != null;
String message = String.format(
"Should use \"`sp`\" instead of \"`dp`\" for text sizes (`%1$s` is defined as `%2$s` in `%3$s`",
value, dimenValue, sourceFile.getFile());
context.report(DP_ISSUE, attribute,
context.getLocation(attribute),
message);
break;
}
}
}
}
}
} else {
ResourceUrl url = ResourceUrl.parse(value);
if (url != null) {
if (mTextSizeUsage == null) {
mTextSizeUsage = new HashMap<String, Location.Handle>();
}
Location.Handle handle = context.createLocationHandle(attribute);
mTextSizeUsage.put(url.name, handle);
}
}
}
}
}
private static boolean isDpUnit(String value) {
return (value.endsWith(UNIT_DP) || value.endsWith(UNIT_DIP))
&& (value.matches("\\d+di?p"));
}
private static int getSize(String text) {
assert text.matches("\\d+sp") : text; //$NON-NLS-1$
return Integer.parseInt(text.substring(0, text.length() - 2));
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (context.getResourceFolderType() != ResourceFolderType.VALUES) {
return;
}
assert element.getTagName().equals(TAG_STYLE);
NodeList itemNodes = element.getChildNodes();
for (int j = 0, nodeCount = itemNodes.getLength(); j < nodeCount; j++) {
Node item = itemNodes.item(j);
if (item.getNodeType() == Node.ELEMENT_NODE &&
TAG_ITEM.equals(item.getNodeName())) {
Element itemElement = (Element) item;
NodeList childNodes = item.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() != Node.TEXT_NODE) {
return;
}
checkStyleItem(context, itemElement, child);
}
}
}
}
private static void checkStyleItem(XmlContext context, Element item, Node textNode) {
String text = textNode.getNodeValue();
for (int j = text.length() - 1; j > 0; j--) {
char c = text.charAt(j);
if (!Character.isWhitespace(c)) {
if (c == 'x' && text.charAt(j - 1) == 'p') { // ends with px
text = text.trim();
if (text.matches("\\d+px") && text.charAt(0) != '0' && //$NON-NLS-1$
!text.equals("1px")) { //$NON-NLS-1$
if (context.isEnabled(PX_ISSUE)) {
context.report(PX_ISSUE, item, context.getLocation(textNode),
"Avoid using `\"px\"` as units; use `\"dp\"` instead");
}
}
} else if (c == 'm' && text.charAt(j - 1) == 'm' ||
c == 'n' && text.charAt(j - 1) == 'i') {
text = text.trim();
String unit = text.substring(text.length() - 2);
if (text.matches("\\d+" + unit) && text.charAt(0) != '0') { //$NON-NLS-1$
if (context.isEnabled(IN_MM_ISSUE)) {
context.report(IN_MM_ISSUE, item, context.getLocation(textNode),
String.format("Avoid using \"`%1$s`\" as units "
+ "(it does not work accurately on all devices); "
+ "use \"`dp`\" instead", unit));
}
}
} else if (c == 'p' && (text.charAt(j - 1) == 'd'
|| text.charAt(j - 1) == 'i')) { // ends with dp or di
text = text.trim();
String name = item.getAttribute(ATTR_NAME);
if ((name.equals(ATTR_TEXT_SIZE)
|| name.equals("android:textSize")) //$NON-NLS-1$
&& text.matches("\\d+di?p")) { //$NON-NLS-1$
if (context.isEnabled(DP_ISSUE)) {
context.report(DP_ISSUE, item, context.getLocation(textNode),
"Should use \"`sp`\" instead of \"`dp`\" for text sizes");
}
}
} else if (c == 'p' && text.charAt(j - 1) == 's') {
String name = item.getAttribute(ATTR_NAME);
if (ATTR_TEXT_SIZE.equals(name) || ATTR_LAYOUT_HEIGHT.equals(name)) {
text = text.trim();
String unit = text.substring(text.length() - 2);
if (text.matches("\\d+" + unit)) { //$NON-NLS-1$
if (context.isEnabled(SMALL_SP_ISSUE)) {
int size = getSize(text);
if (size > 0 && size < 12) {
context.report(SMALL_SP_ISSUE, item,
context.getLocation(textNode), String.format(
"Avoid using sizes smaller than `12sp`: `%1$s`",
text));
}
}
}
}
}
break;
}
}
}
}