/* * 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; } } } }