diff options
author | Tor Norbye <tnorbye@google.com> | 2012-10-04 18:55:18 -0700 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2012-10-05 08:04:07 -0700 |
commit | 6cb5c7f7141a08fd355bb92c6a4153140400890b (patch) | |
tree | a2df993be817856f98eefd44c01f5ef56c8aec34 /lint | |
parent | c3e3de0a15a83422d3c43f536a45e53e8156bfe5 (diff) | |
download | sdk-6cb5c7f7141a08fd355bb92c6a4153140400890b.zip sdk-6cb5c7f7141a08fd355bb92c6a4153140400890b.tar.gz sdk-6cb5c7f7141a08fd355bb92c6a4153140400890b.tar.bz2 |
Check notification and action bar icons
These icons should not use color (and in the case of notification
icons, be white).
Change-Id: I32b9422735830a01bb069b90a5ad5a76d7aeb5de
Diffstat (limited to 'lint')
11 files changed, 739 insertions, 77 deletions
diff --git a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Context.java b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Context.java index 9ddd10a..40c9d1f 100644 --- a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Context.java +++ b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Context.java @@ -23,12 +23,15 @@ import com.android.tools.lint.client.api.LintClient; import com.android.tools.lint.client.api.LintDriver; import com.android.tools.lint.client.api.SdkInfo; import com.google.common.annotations.Beta; +import com.google.common.base.Splitter; import java.io.File; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Context passed to the detectors during an analysis run. It provides @@ -328,4 +331,47 @@ public class Context { public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet<Scope> scope) { mDriver.requestRepeat(detector, scope); } + + /** Pattern for version qualifiers */ + private final static Pattern VERSION_PATTERN = Pattern.compile("^v(\\d+)$"); //$NON-NLS-1$ + + private static File sCachedFolder = null; + private static int sCachedFolderVersion = -1; + + /** + * Returns the folder version. For example, for the file values-v14/foo.xml, + * it returns 14. + * + * @return the folder version, or -1 if no specific version was specified + */ + public int getFolderVersion() { + return getFolderVersion(file); + } + + /** + * Returns the folder version of the given file. For example, for the file values-v14/foo.xml, + * it returns 14. + * + * @param file the file to be checked + * @return the folder version, or -1 if no specific version was specified + */ + public static int getFolderVersion(File file) { + File parent = file.getParentFile(); + if (parent.equals(sCachedFolder)) { + return sCachedFolderVersion; + } + + sCachedFolder = parent; + sCachedFolderVersion = -1; + + for (String qualifier : Splitter.on('-').split(parent.getName())) { + Matcher matcher = VERSION_PATTERN.matcher(qualifier); + if (matcher.matches()) { + sCachedFolderVersion = Integer.parseInt(matcher.group(1)); + break; + } + } + + return sCachedFolderVersion; + } } diff --git a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/XmlContext.java b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/XmlContext.java index 14e0a91..34d6816 100644 --- a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/XmlContext.java +++ b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/XmlContext.java @@ -27,8 +27,6 @@ import org.w3c.dom.Document; import org.w3c.dom.Node; import java.io.File; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * A {@link Context} used when checking XML files. @@ -153,37 +151,4 @@ public class XmlContext extends Context { public ResourceFolderType getResourceFolderType() { return mFolderType; } - - - private final static Pattern sVersionPattern = Pattern.compile("^v(\\d+)$");//$NON-NLS-1$ - - private static File sCachedFolder = null; - private static int sCachedFolderVersion = -1; - - /** - * Returns the folder version. For example, for the file values-v14/foo.xml, - * it returns 14. - * - * @return the folder version, or -1 if no specific version was specified - */ - public int getFolderVersion() { - File parent = file.getParentFile(); - if (parent.equals(sCachedFolder)) { - return sCachedFolderVersion; - } - - sCachedFolder = parent; - sCachedFolderVersion = -1; - - String[] qualifiers = parent.getName().split("-"); //$NON-NLS-1$ - for (String qualifier : qualifiers) { - Matcher matcher = sVersionPattern.matcher(qualifier); - if (matcher.matches()) { - sCachedFolderVersion = Integer.parseInt(matcher.group(1)); - break; - } - } - - return sCachedFolderVersion; - } } diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java index 05765a4..7b18f81 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java @@ -55,7 +55,7 @@ public class BuiltinIssueRegistry extends IssueRegistry { private static final List<Issue> sIssues; static { - final int initialCapacity = 114; + final int initialCapacity = 115; List<Issue> issues = new ArrayList<Issue>(initialCapacity); issues.add(AccessibilityDetector.ISSUE); @@ -139,6 +139,7 @@ public class BuiltinIssueRegistry extends IssueRegistry { issues.add(IconDetector.DUPLICATES_CONFIGURATIONS); issues.add(IconDetector.ICON_NODPI); issues.add(IconDetector.ICON_EXTENSION); + issues.add(IconDetector.ICON_COLORS); issues.add(TypographyDetector.DASHES); issues.add(TypographyDetector.QUOTES); issues.add(TypographyDetector.FRACTIONS); diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/IconDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/IconDetector.java index cf87acd..5b2f632 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/IconDetector.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/IconDetector.java @@ -31,21 +31,36 @@ import static com.android.SdkConstants.DRAWABLE_LDPI; import static com.android.SdkConstants.DRAWABLE_MDPI; import static com.android.SdkConstants.DRAWABLE_PREFIX; import static com.android.SdkConstants.DRAWABLE_XHDPI; +import static com.android.SdkConstants.MENU_TYPE; import static com.android.SdkConstants.RES_FOLDER; +import static com.android.SdkConstants.R_CLASS; +import static com.android.SdkConstants.R_DRAWABLE_PREFIX; +import static com.android.SdkConstants.TAG_ACTIVITY; import static com.android.SdkConstants.TAG_APPLICATION; +import static com.android.SdkConstants.TAG_ITEM; +import static com.android.SdkConstants.TAG_PROVIDER; +import static com.android.SdkConstants.TAG_RECEIVER; +import static com.android.SdkConstants.TAG_SERVICE; import static com.android.tools.lint.detector.api.LintUtils.endsWith; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.resources.ResourceFolderType; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.Context; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.JavaContext; import com.android.tools.lint.detector.api.LintUtils; import com.android.tools.lint.detector.api.Location; +import com.android.tools.lint.detector.api.ResourceXmlDetector; 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 com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; import org.w3c.dom.Element; @@ -54,9 +69,11 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -65,18 +82,30 @@ import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; +import lombok.ast.AstVisitor; +import lombok.ast.ConstructorInvocation; +import lombok.ast.Expression; +import lombok.ast.ForwardingAstVisitor; +import lombok.ast.MethodDeclaration; +import lombok.ast.MethodInvocation; +import lombok.ast.Node; +import lombok.ast.Select; +import lombok.ast.StrictListAccessor; +import lombok.ast.TypeReference; +import lombok.ast.TypeReferencePart; +import lombok.ast.VariableReference; + /** * Checks for common icon problems, such as wrong icon sizes, placing icons in the * density independent drawable folder, etc. */ -public class IconDetector extends Detector implements Detector.XmlScanner { +public class IconDetector extends ResourceXmlDetector implements Detector.JavaScanner { private static final boolean INCLUDE_LDPI; static { @@ -94,9 +123,6 @@ public class IconDetector extends Detector implements Detector.XmlScanner { "^drawable-(nodpi|xhdpi|hdpi|mdpi" //$NON-NLS-1$ + (INCLUDE_LDPI ? "|ldpi" : "") + ")$"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - /** Pattern for version qualifiers */ - private final static Pattern VERSION_PATTERN = Pattern.compile("^v(\\d+)$");//$NON-NLS-1$ - private static final String[] REQUIRED_DENSITIES = INCLUDE_LDPI ? new String[] { DRAWABLE_LDPI, DRAWABLE_MDPI, DRAWABLE_HDPI, DRAWABLE_XHDPI } : new String[] { DRAWABLE_MDPI, DRAWABLE_HDPI, DRAWABLE_XHDPI }; @@ -109,6 +135,12 @@ public class IconDetector extends Detector implements Detector.XmlScanner { "-xhdpi" //$NON-NLS-1$ }; + /** Scope needed to detect the types of icons (which involves scanning .java files, + * the manifest, menu files etc to see how icons are used + */ + private static final EnumSet<Scope> ICON_TYPE_SCOPE = EnumSet.of(Scope.ALL_RESOURCE_FILES, + Scope.JAVA_FILE, Scope.MANIFEST); + /** Wrong icon size according to published conventions */ public static final Issue ICON_EXPECTED_SIZE = Issue.create( "IconExpectedSize", //$NON-NLS-1$ @@ -120,7 +152,7 @@ public class IconDetector extends Detector implements Detector.XmlScanner { 5, Severity.WARNING, IconDetector.class, - Scope.ALL_RESOURCES_SCOPE) + ICON_TYPE_SCOPE) // Still some potential false positives: .setEnabledByDefault(false) .setMoreInfo( @@ -266,8 +298,24 @@ public class IconDetector extends Detector implements Detector.XmlScanner { IconDetector.class, Scope.ALL_RESOURCES_SCOPE); - - private String mApplicationIcon; + /** Wrong filename according to the format */ + public static final Issue ICON_COLORS = Issue.create( + "IconColors", //$NON-NLS-1$ + "Checks that icons follow the recommended visual style", + + "Notification icons and Action Bar icons should only white and shades of gray. " + + "See the Android Design Guide for more details. " + + "Note that the way Lint decides whether an icon is an action bar icon or " + + "a notification icon is based on the filename prefix: `ic_menu_` for " + + "action bar icons, `ic_stat_` for notification icons etc. These correspond " + + "to the naming conventions documented in " + + "http://developer.android.com/guide/practices/ui_guidelines/icon_design.html", + Category.ICONS, + 6, + Severity.WARNING, + IconDetector.class, + ICON_TYPE_SCOPE).setMoreInfo( + "http://developer.android.com/design/style/iconography.html"); //$NON-NLS-1$ /** Constructs a new {@link IconDetector} check */ public IconDetector() { @@ -280,7 +328,9 @@ public class IconDetector extends Detector implements Detector.XmlScanner { @Override public void beforeCheckProject(@NonNull Context context) { - mApplicationIcon = null; + mLauncherIcons = null; + mActionBarIcons = null; + mNotificationIcons = null; } @Override @@ -996,6 +1046,27 @@ public class IconDetector extends Detector implements Detector.XmlScanner { } } + if (context.isEnabled(ICON_COLORS)) { + for (File file : files) { + String name = file.getName(); + + if (isDrawableFile(name) + && !endsWith(name, DOT_XML) + && !endsWith(name, DOT_9PNG)) { + String baseName = getBaseName(name); + boolean isActionBarIcon = isActionBarIcon(context, baseName, file); + if (isActionBarIcon || isNotificationIcon(baseName)) { + Dimension size = checkColor(context, file, isActionBarIcon); + + // Store dimension for size check if we went to the trouble of reading image + if (size != null && pixelSizes != null) { + pixelSizes.put(file, size); + } + } + } + } + } + // Check icon sizes if (context.isEnabled(ICON_EXPECTED_SIZE)) { checkExpectedSizes(context, folder, files); @@ -1011,7 +1082,8 @@ public class IconDetector extends Detector implements Detector.XmlScanner { || endsWith(fileName, DOT_JPEG)) { // Only scan .png files (except 9-patch png's) and jpg files for // dip sizes. Duplicate checks can also be performed on ninepatch files. - if (pixelSizes != null && !endsWith(fileName, DOT_9PNG)) { + if (pixelSizes != null && !endsWith(fileName, DOT_9PNG) + && !pixelSizes.containsKey(file)) { // already read by checkColor? Dimension size = getSize(file); pixelSizes.put(file, size); } @@ -1023,6 +1095,122 @@ public class IconDetector extends Detector implements Detector.XmlScanner { } } + /** + * Check whether the icons in the file are okay. Also return the image size + * if known (for use by other checks) + */ + private Dimension checkColor(Context context, File file, boolean isActionBarIcon) { + int folderVersion = Context.getFolderVersion(file); + if (isActionBarIcon) { + if (folderVersion != -1 && folderVersion < 11 + || !isAndroid30(context, folderVersion)) { + return null; + } + } else { + if (folderVersion != -1 && folderVersion < 9 + || !isAndroid23(context, folderVersion) + && !isAndroid30(context, folderVersion)) { + return null; + } + } + + // TODO: This only checks icons that are known to be using the Holo style. + // However, if the user has minSdk < 11 as well as targetSdk > 11, we should + // also check that they actually include a -v11 or -v14 folder with proper + // icons, since the below won't flag the older icons. + try { + BufferedImage image = ImageIO.read(file); + if (image != null) { + if (isActionBarIcon) { + checkPixels: + for (int y = 0, height = image.getHeight(); y < height; y++) { + for (int x = 0, width = image.getWidth(); x < width; x++) { + int rgb = image.getRGB(x, y); + if ((rgb & 0xFF000000) != 0) { // else: transparent + int r = (rgb & 0xFF0000) >>> 16; + int g = (rgb & 0x00FF00) >>> 8; + int b = (rgb & 0x0000FF); + if (r != g || r != b) { + String message = "Action Bar icons should use a single gray " + + "color (#333333 for light themes (with 60%/30% " + + "opacity for enabled/disabled), and #FFFFFF with " + + "opacity 80%/30% for dark themes"; + context.report(ICON_COLORS, Location.create(file), + message, null); + break checkPixels; + } + } + } + } + } else { + if (folderVersion >= 11 || isAndroid30(context, folderVersion)) { + // Notification icons. Should be white as of API 14 + checkPixels: + for (int y = 0, height = image.getHeight(); y < height; y++) { + for (int x = 0, width = image.getWidth(); x < width; x++) { + int rgb = image.getRGB(x, y); + if ((rgb & 0xFF000000) != 0 + && rgb != 0xFFFFFFFF) { + int r = (rgb & 0xFF0000) >>> 16; + int g = (rgb & 0x00FF00) >>> 8; + int b = (rgb & 0x0000FF); + if (r == g && r == b) { + // If the pixel is not white, it might be because of + // anti-aliasing. In that case, at least one neighbor + // should be of a different color + if (x < width - 1 && rgb != image.getRGB(x + 1, y)) { + continue; + } + if (x > 0 && rgb != image.getRGB(x - 1, y)) { + continue; + } + if (y < height - 1 && rgb != image.getRGB(x, y + 1)) { + continue; + } + if (y > 0 && rgb != image.getRGB(x, y - 1)) { + continue; + } + } + + String message = "Notification icons must be entirely white"; + context.report(ICON_COLORS, Location.create(file), + message, null); + break checkPixels; + } + } + } + } else { + // As of API 9, should be gray. + checkPixels: + for (int y = 0, height = image.getHeight(); y < height; y++) { + for (int x = 0, width = image.getWidth(); x < width; x++) { + int rgb = image.getRGB(x, y); + if ((rgb & 0xFF000000) != 0) { // else: transparent + int r = (rgb & 0xFF0000) >>> 16; + int g = (rgb & 0x00FF00) >>> 8; + int b = (rgb & 0x0000FF); + if (r != g || r != b) { + String message = "Notification icons should not use " + + "colors"; + context.report(ICON_COLORS, Location.create(file), + message, null); + break checkPixels; + } + } + } + } + } + } + + return new Dimension(image.getWidth(), image.getHeight()); + } + } catch (IOException e) { + // Pass: ignore files we can't read + } + + return null; + } + private void checkExtension(Context context, File file) { try { ImageInputStream input = ImageIO.createImageInputStream(file); @@ -1069,20 +1257,24 @@ public class IconDetector extends Detector implements Detector.XmlScanner { } } - private void checkExpectedSizes(Context context, File folder, File[] files) { - String folderName = folder.getName(); + private static String getBaseName(String name) { + String baseName = name; + int index = baseName.indexOf('.'); + if (index != -1) { + baseName = baseName.substring(0, index); + } - int folderVersion = -1; - String[] qualifiers = folderName.split("-"); //$NON-NLS-1$ - for (String qualifier : qualifiers) { - if (qualifier.startsWith("v")) { - Matcher matcher = VERSION_PATTERN.matcher(qualifier); - if (matcher.matches()) { - folderVersion = Integer.parseInt(matcher.group(1)); - } - } + return baseName; + } + + private void checkExpectedSizes(Context context, File folder, File[] files) { + if (files == null || files.length == 0) { + return; } + String folderName = folder.getName(); + int folderVersion = Context.getFolderVersion(files[0]); + for (File file : files) { String name = file.getName(); @@ -1091,17 +1283,12 @@ public class IconDetector extends Detector implements Detector.XmlScanner { // http://developer.android.com/guide/practices/ui_guidelines/icon_design.html#design-tips // See if we can figure out other types of icons from usage too. - String baseName = name; - int index = baseName.indexOf('.'); - if (index != -1) { - baseName = baseName.substring(0, index); - } + String baseName = getBaseName(name); - if (baseName.equals(mApplicationIcon) || name.startsWith("ic_launcher")) { //$NON-NLS-1$ + if (isLauncherIcon(baseName)) { // Launcher icons checkSize(context, folderName, file, 48, 48, true /*exact*/); - } else if (name.startsWith("ic_action_")) { //$NON-NLS-1$ - // Action Bar + } else if (isActionBarIcon(baseName)) { checkSize(context, folderName, file, 32, 32, true /*exact*/); } else if (name.startsWith("ic_dialog_")) { //$NON-NLS-1$ // Dialog @@ -1109,9 +1296,8 @@ public class IconDetector extends Detector implements Detector.XmlScanner { } else if (name.startsWith("ic_tab_")) { //$NON-NLS-1$ // Tab icons checkSize(context, folderName, file, 32, 32, true /*exact*/); - } else if (name.startsWith("ic_stat_")) { //$NON-NLS-1$ + } else if (isNotificationIcon(baseName)) { // Notification icons - if (isAndroid30(context, folderVersion)) { checkSize(context, folderName, file, 24, 24, true /*exact*/); } else if (isAndroid23(context, folderVersion)) { @@ -1278,7 +1464,62 @@ public class IconDetector extends Detector implements Detector.XmlScanner { } } - // XML detector: Skim manifest + + private Set<String> mActionBarIcons; + private Set<String> mNotificationIcons; + private Set<String> mLauncherIcons; + private Multimap<String, String> mMenuToIcons; + + private boolean isLauncherIcon(String name) { + assert name.indexOf('.') == -1; // Should supply base name + + // Naming convention + if (name.startsWith("ic_launcher")) { //$NON-NLS-1$ + return true; + } + return mLauncherIcons != null && mLauncherIcons.contains(name); + } + + private boolean isNotificationIcon(String name) { + assert name.indexOf('.') == -1; // Should supply base name + + // Naming convention + if (name.startsWith("ic_stat_")) { //$NON-NLS-1$ + return true; + } + + return mNotificationIcons != null && mNotificationIcons.contains(name); + } + + private boolean isActionBarIcon(String name) { + assert name.indexOf('.') == -1; // Should supply base name + + // Naming convention + if (name.startsWith("ic_action_")) { //$NON-NLS-1$ + return true; + } + + // Naming convention + + return mActionBarIcons != null && mActionBarIcons.contains(name); + } + + private boolean isActionBarIcon(Context context, String name, File file) { + if (isActionBarIcon(name)) { + return true; + } + + // As of Android 3.0 ic_menu_ are action icons + if (file != null && name.startsWith("ic_menu_") //$NON-NLS-1$ + && isAndroid30(context, Context.getFolderVersion(file))) { + // Naming convention + return true; + } + + return false; + } + + // XML detector: Skim manifest and menu files @Override public boolean appliesTo(@NonNull Context context, @NonNull File file) { @@ -1286,16 +1527,196 @@ public class IconDetector extends Detector implements Detector.XmlScanner { } @Override + public boolean appliesTo(@NonNull ResourceFolderType folderType) { + return folderType == ResourceFolderType.MENU; + } + + @Override public Collection<String> getApplicableElements() { - return Collections.singletonList(TAG_APPLICATION); + return Arrays.asList( + // Manifest + TAG_APPLICATION, + TAG_ACTIVITY, + TAG_SERVICE, + TAG_PROVIDER, + TAG_RECEIVER, + + // Menu + TAG_ITEM + ); } @Override public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - assert element.getTagName().equals(TAG_APPLICATION); - mApplicationIcon = element.getAttributeNS(ANDROID_URI, ATTR_ICON); - if (mApplicationIcon.startsWith(DRAWABLE_PREFIX)) { - mApplicationIcon = mApplicationIcon.substring(DRAWABLE_PREFIX.length()); + String icon = element.getAttributeNS(ANDROID_URI, ATTR_ICON); + if (icon != null && icon.startsWith(DRAWABLE_PREFIX)) { + icon = icon.substring(DRAWABLE_PREFIX.length()); + + String tagName = element.getTagName(); + if (tagName.equals(TAG_ITEM)) { + if (mMenuToIcons == null) { + mMenuToIcons = ArrayListMultimap.create(); + } + String menu = getBaseName(context.file.getName()); + mMenuToIcons.put(menu, icon); + } else { + // Manifest tags: launcher icons + if (mLauncherIcons == null) { + mLauncherIcons = Sets.newHashSet(); + } + mLauncherIcons.add(icon); + } + } + } + + + // ---- Implements JavaScanner ---- + + private static final String NOTIFICATION_CLASS = "Notification"; //$NON-NLS-1$ + private static final String NOTIFICATION_COMPAT_CLASS = "NotificationCompat"; //$NON-NLS-1$ + private static final String BUILDER_CLASS = "Builder"; //$NON-NLS-1$ + private static final String SET_SMALL_ICON = "setSmallIcon"; //$NON-NLS-1$ + private static final String ON_CREATE_OPTIONS_MENU = "onCreateOptionsMenu"; //$NON-NLS-1$ + + @Override + @Nullable + public AstVisitor createJavaVisitor(@NonNull JavaContext context) { + return new NotificationFinder(); + } + + @Override + @Nullable + public List<Class<? extends Node>> getApplicableNodeTypes() { + List<Class<? extends Node>> types = new ArrayList<Class<? extends Node>>(3); + types.add(MethodDeclaration.class); + types.add(ConstructorInvocation.class); + return types; + } + + private final class NotificationFinder extends ForwardingAstVisitor { + @Override + public boolean visitMethodDeclaration(MethodDeclaration node) { + if (ON_CREATE_OPTIONS_MENU.equals(node.astMethodName().astValue())) { + // Gather any R.menu references found in this method + node.accept(new MenuFinder()); + } + + return super.visitMethodDeclaration(node); + } + + @Override + public boolean visitConstructorInvocation(ConstructorInvocation node) { + TypeReference reference = node.astTypeReference(); + StrictListAccessor<TypeReferencePart, TypeReference> parts = reference.astParts(); + String typeName = parts.last().astIdentifier().astValue(); + if (NOTIFICATION_CLASS.equals(typeName)) { + StrictListAccessor<Expression, ConstructorInvocation> args = node.astArguments(); + if (args.size() == 3) { + if (args.first() instanceof Select && handleSelect((Select) args.first())) { + return super.visitConstructorInvocation(node); + } + + Node method = getParentMethod(node); + if (method != null) { + // Must track local types + String name = StringFormatDetector.getResourceForFirstArg(method, node); + if (name != null) { + if (mNotificationIcons == null) { + mNotificationIcons = Sets.newHashSet(); + } + mNotificationIcons.add(name); + } + } + } + } else if (BUILDER_CLASS.equals(typeName)) { + boolean isBuilder = false; + if (parts.size() == 1) { + isBuilder = true; + } else if (parts.size() == 2) { + String clz = parts.first().astIdentifier().astValue(); + if (NOTIFICATION_CLASS.equals(clz) || NOTIFICATION_COMPAT_CLASS.equals(clz)) { + isBuilder = true; + } + } + if (isBuilder) { + Node method = getParentMethod(node); + if (method != null) { + SetIconFinder finder = new SetIconFinder(); + method.accept(finder); + } + } + } + + return super.visitConstructorInvocation(node); + } + } + + @Nullable + private Node getParentMethod(@NonNull Node node) { + Node method = node; + while (method != null && !(method.getParent() instanceof MethodDeclaration)) { + method = method.getParent(); + } + + return method; + } + + private boolean handleSelect(Select select) { + if (select.toString().startsWith(R_DRAWABLE_PREFIX)) { + String name = select.astIdentifier().astValue(); + if (mNotificationIcons == null) { + mNotificationIcons = Sets.newHashSet(); + } + mNotificationIcons.add(name); + + return true; + } + + return false; + } + + private final class SetIconFinder extends ForwardingAstVisitor { + @Override + public boolean visitMethodInvocation(MethodInvocation node) { + if (SET_SMALL_ICON.equals(node.astName().astValue())) { + StrictListAccessor<Expression,MethodInvocation> arguments = node.astArguments(); + if (arguments.size() == 1 && arguments.first() instanceof Select) { + handleSelect((Select) arguments.first()); + } + } + return super.visitMethodInvocation(node); + } + } + + private final class MenuFinder extends ForwardingAstVisitor { + @Override + public boolean visitSelect(Select node) { + // R.type.name + if (node.astOperand() instanceof Select) { + Select select = (Select) node.astOperand(); + if (select.astOperand() instanceof VariableReference) { + VariableReference reference = (VariableReference) select.astOperand(); + if (reference.astIdentifier().astValue().equals(R_CLASS)) { + String type = select.astIdentifier().astValue(); + + if (type.equals(MENU_TYPE)) { + String name = node.astIdentifier().astValue(); + // Reclassify icons in the given menu as action bar icons + if (mMenuToIcons != null) { + Collection<String> icons = mMenuToIcons.get(name); + if (icons != null) { + if (mActionBarIcons == null) { + mActionBarIcons = Sets.newHashSet(); + } + mActionBarIcons.addAll(icons); + } + } + } + } + } + } + + return super.visitSelect(node); } } } diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/StringFormatDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/StringFormatDetector.java index f6f0f7e..bf16aeb 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/StringFormatDetector.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/StringFormatDetector.java @@ -65,6 +65,7 @@ import java.util.regex.Pattern; import lombok.ast.AstVisitor; import lombok.ast.CharLiteral; import lombok.ast.ConstructorDeclaration; +import lombok.ast.ConstructorInvocation; import lombok.ast.Expression; import lombok.ast.FloatingPointLiteral; import lombok.ast.ForwardingAstVisitor; @@ -1009,6 +1010,16 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto } } + /** Returns the resource name corresponding to the first argument in the given call */ + static String getResourceForFirstArg(lombok.ast.Node method, lombok.ast.Node call) { + assert call instanceof MethodInvocation || call instanceof ConstructorInvocation; + StringTracker tracker = new StringTracker(method, call); + method.accept(tracker); + String name = tracker.getFormatStringName(); + + return name; + } + /** * Given a variable reference, finds the original R.string value corresponding to it. * For example: @@ -1038,7 +1049,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto /** Map from variable name to corresponding type */ private final Map<String, Class<?>> mTypes = new HashMap<String, Class<?>>(); /** The AST node for the String.format we're interested in */ - private MethodInvocation mTargetNode; + private lombok.ast.Node mTargetNode; private boolean mDone; /** * Result: the name of the string resource being passed to the @@ -1046,7 +1057,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto */ private String mName; - public StringTracker(lombok.ast.Node top, MethodInvocation targetNode) { + public StringTracker(lombok.ast.Node top, lombok.ast.Node targetNode) { mTop = top; mTargetNode = targetNode; } @@ -1076,7 +1087,11 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto } public Expression getArgument(int argument) { - StrictListAccessor<Expression, MethodInvocation> args = mTargetNode.astArguments(); + if (!(mTargetNode instanceof MethodInvocation)) { + return null; + } + MethodInvocation call = (MethodInvocation) mTargetNode; + StrictListAccessor<Expression, MethodInvocation> args = call.astArguments(); if (argument >= args.size()) { return null; } @@ -1152,6 +1167,27 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto } @Override + public boolean visitConstructorInvocation(ConstructorInvocation node) { + if (node == mTargetNode) { + StrictListAccessor<Expression, ConstructorInvocation> args = node.astArguments(); + if (args.size() > 0) { + Expression first = args.first(); + if (first instanceof VariableReference) { + VariableReference reference = (VariableReference) first; + String variable = reference.astIdentifier().astValue(); + mName = mMap.get(variable); + mDone = true; + return true; + } + } + } + + // Is this a getString() call? On a resource object? If so, + // promote the resource argument up to the left hand side + return super.visitConstructorInvocation(node); + } + + @Override public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) { String name = node.astName().astValue(); Expression rhs = node.astInitializer(); diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/IconDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/IconDetectorTest.java index 1a84982..f382085 100644 --- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/IconDetectorTest.java +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/IconDetectorTest.java @@ -39,11 +39,11 @@ public class IconDetectorTest extends AbstractCheckTest { ALL.add(IconDetector.GIF_USAGE); ALL.add(IconDetector.ICON_DENSITIES); ALL.add(IconDetector.ICON_DIP_SIZE); - ALL.add(IconDetector.ICON_EXPECTED_SIZE); ALL.add(IconDetector.ICON_EXTENSION); ALL.add(IconDetector.ICON_LOCATION); ALL.add(IconDetector.ICON_MISSING_FOLDER); ALL.add(IconDetector.ICON_NODPI); + ALL.add(IconDetector.ICON_COLORS); } @Override @@ -205,4 +205,128 @@ public class IconDetectorTest extends AbstractCheckTest { "res/drawable-mdpi/sample_icon.gif=>res/drawable-mdpi/sample_icon.jpeg", "res/drawable-mdpi/sample_icon.gif=>res/drawable-mdpi/sample_icon.png")); } + + public void testColors() throws Exception { + mEnabled = Collections.singleton(IconDetector.ICON_COLORS); + assertEquals( + "res/drawable-mdpi/ic_menu_my_action.png: Warning: Action Bar icons should use a single gray color (#333333 for light themes (with 60%/30% opacity for enabled/disabled), and #FFFFFF with opacity 80%/30% for dark themes [IconColors]\n" + + "res/drawable-mdpi-v11/ic_stat_my_notification.png: Warning: Notification icons must be entirely white [IconColors]\n" + + "res/drawable-mdpi-v9/ic_stat_my_notification2.png: Warning: Notification icons must be entirely white [IconColors]\n" + + "0 errors, 3 warnings\n", + + lintProject( + "apicheck/minsdk14.xml=>AndroidManifest.xml", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_menu_my_action.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi-v11/ic_stat_my_notification.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi-v9/ic_stat_my_notification2.png", + "res/drawable-mdpi/ic_menu_add_clip_normal.png")); // OK + } + + public void testNotActionBarIcons() throws Exception { + mEnabled = Collections.singleton(IconDetector.ICON_COLORS); + assertEquals( + "No warnings.", + + // No Java code designates the menu as an action bar menu + lintProject( + "apicheck/minsdk14.xml=>AndroidManifest.xml", + "res/menu/menu.xml", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon1.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon2.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon3.png", // Not action bar + "res/drawable-mdpi/ic_menu_add_clip_normal.png")); // OK + } + + public void testActionBarIcons() throws Exception { + mEnabled = Collections.singleton(IconDetector.ICON_COLORS); + assertEquals( + "res/drawable-mdpi/icon1.png: Warning: Action Bar icons should use a single gray color (#333333 for light themes (with 60%/30% opacity for enabled/disabled), and #FFFFFF with opacity 80%/30% for dark themes [IconColors]\n" + + "res/drawable-mdpi/icon2.png: Warning: Action Bar icons should use a single gray color (#333333 for light themes (with 60%/30% opacity for enabled/disabled), and #FFFFFF with opacity 80%/30% for dark themes [IconColors]\n" + + "0 errors, 2 warnings\n", + + lintProject( + "apicheck/minsdk14.xml=>AndroidManifest.xml", + "res/menu/menu.xml", + "src/test/pkg/ActionBarTest.java.txt=>src/test/pkg/ActionBarTest.java", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon1.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon2.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon3.png", // Not action bar + "res/drawable-mdpi/ic_menu_add_clip_normal.png")); // OK + } + + public void testOkActionBarIcons() throws Exception { + mEnabled = Collections.singleton(IconDetector.ICON_COLORS); + assertEquals( + "No warnings.", + + lintProject( + "apicheck/minsdk14.xml=>AndroidManifest.xml", + "res/menu/menu.xml", + "res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon1.png", + "res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon2.png")); + } + + public void testNotificationIcons() throws Exception { + mEnabled = Collections.singleton(IconDetector.ICON_COLORS); + assertEquals( + "res/drawable-mdpi/icon1.png: Warning: Notification icons must be entirely white [IconColors]\n" + + "res/drawable-mdpi/icon2.png: Warning: Notification icons must be entirely white [IconColors]\n" + + "res/drawable-mdpi/icon3.png: Warning: Notification icons must be entirely white [IconColors]\n" + + "res/drawable-mdpi/icon4.png: Warning: Notification icons must be entirely white [IconColors]\n" + + "res/drawable-mdpi/icon5.png: Warning: Notification icons must be entirely white [IconColors]\n" + + "0 errors, 5 warnings\n", + + lintProject( + "apicheck/minsdk14.xml=>AndroidManifest.xml", + "src/test/pkg/NotificationTest.java.txt=>src/test/pkg/NotificationTest.java", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon1.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon2.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon3.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon4.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon5.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon6.png", // not a notification + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon7.png", // ditto + "res/drawable-mdpi/ic_menu_add_clip_normal.png")); // OK + } + + public void testOkNotificationIcons() throws Exception { + mEnabled = Collections.singleton(IconDetector.ICON_COLORS); + assertEquals( + "No warnings.", + + lintProject( + "apicheck/minsdk14.xml=>AndroidManifest.xml", + "src/test/pkg/NotificationTest.java.txt=>src/test/pkg/NotificationTest.java", + "res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon1.png", + "res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon2.png", + "res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon3.png", + "res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon4.png", + "res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon5.png")); + } + + public void testExpectedSize() throws Exception { + mEnabled = Collections.singleton(IconDetector.ICON_EXPECTED_SIZE); + assertEquals( + "res/drawable-mdpi/ic_launcher.png: Warning: Incorrect icon size for drawable-mdpi/ic_launcher.png: expected 48x48, but was 24x24 [IconExpectedSize]\n" + + "res/drawable-mdpi/icon1.png: Warning: Incorrect icon size for drawable-mdpi/icon1.png: expected 32x32, but was 48x48 [IconExpectedSize]\n" + + "res/drawable-mdpi/icon3.png: Warning: Incorrect icon size for drawable-mdpi/icon3.png: expected 24x24, but was 48x48 [IconExpectedSize]\n" + + "0 errors, 3 warnings\n", + + lintProject( + "apicheck/minsdk14.xml=>AndroidManifest.xml", + "src/test/pkg/NotificationTest.java.txt=>src/test/pkg/NotificationTest.java", + "res/menu/menu.xml", + "src/test/pkg/ActionBarTest.java.txt=>src/test/pkg/ActionBarTest.java", + + // 3 wrong-sized icons: + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon1.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/icon3.png", + "res/drawable-mdpi/stat_notify_alarm.png=>res/drawable-mdpi/ic_launcher.png", + + // OK sizes + "res/drawable-mdpi/ic_menu_add_clip_normal.png=>res/drawable-mdpi/icon2.png", + "res/drawable-mdpi/stat_notify_alarm.png=>res/drawable-mdpi/icon4.png", + "res/drawable/ic_launcher.png=>res/drawable-mdpi/ic_launcher2.png" + )); + } }
\ No newline at end of file diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/drawable-mdpi/ic_menu_add_clip_normal.png b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/drawable-mdpi/ic_menu_add_clip_normal.png Binary files differnew file mode 100644 index 0000000..26f5afe --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/drawable-mdpi/ic_menu_add_clip_normal.png diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/drawable-mdpi/stat_notify_alarm.png b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/drawable-mdpi/stat_notify_alarm.png Binary files differnew file mode 100644 index 0000000..c61626c --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/drawable-mdpi/stat_notify_alarm.png diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/menu/menu.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/menu/menu.xml new file mode 100644 index 0000000..4f0839b --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/menu/menu.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/item1" + android:icon="@drawable/icon1" + android:title="My title 1"> + </item> + <item + android:id="@+id/item2" + android:icon="@drawable/icon2" + android:showAsAction="ifRoom" + android:title="My title 2"> + </item> + +</menu>
\ No newline at end of file diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/src/test/pkg/ActionBarTest.java.txt b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/src/test/pkg/ActionBarTest.java.txt new file mode 100644 index 0000000..217f3b6 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/src/test/pkg/ActionBarTest.java.txt @@ -0,0 +1,14 @@ +package test.pkg; + +import android.app.Activity; +import android.view.Menu; +import android.view.MenuInflater; + +public class ActionBarTest extends Activity { + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu, menu); + return true; + } +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/src/test/pkg/NotificationTest.java.txt b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/src/test/pkg/NotificationTest.java.txt new file mode 100644 index 0000000..b03f7a8 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/src/test/pkg/NotificationTest.java.txt @@ -0,0 +1,39 @@ +package test.pkg; + +import android.app.Notification; +import android.app.Notification.Builder; +import android.content.Context; +import android.graphics.Bitmap; + +@SuppressWarnings({ "deprecation", "unused", "javadoc" }) +class NotificationTest { + public void test1() { + Notification notification = new Notification(R.drawable.icon1, "Test1", 0); + } + + public void test2() { + int resource = R.drawable.icon2; + Notification notification = new Notification(resource, "Test1", 0); + } + + public void test3() { + int icon = R.drawable.icon3; + CharSequence tickerText = "Hello"; + long when = System.currentTimeMillis(); + Notification notification = new Notification(icon, tickerText, when); + } + + public void test4(Context context, String sender, String subject, Bitmap bitmap) { + Notification notification = new Notification.Builder(context) + .setContentTitle("New mail from " + sender.toString()) + .setContentText(subject).setSmallIcon(R.drawable.icon4) + .setLargeIcon(bitmap).build(); + } + + public void test5(Context context, String sender, String subject, Bitmap bitmap) { + Notification notification = new Builder(context) + .setContentTitle("New mail from " + sender.toString()) + .setContentText(subject).setSmallIcon(R.drawable.icon5) + .setLargeIcon(bitmap).build(); + } +} |