/* * 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.klint.checks; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_BACKGROUND; import static com.android.SdkConstants.ATTR_CONTEXT; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.ATTR_PARENT; import static com.android.SdkConstants.ATTR_THEME; import static com.android.SdkConstants.ATTR_TILE_MODE; import static com.android.SdkConstants.DOT_XML; import static com.android.SdkConstants.DRAWABLE_PREFIX; import static com.android.SdkConstants.NULL_RESOURCE; import static com.android.SdkConstants.R_CLASS; import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; import static com.android.SdkConstants.TAG_ACTIVITY; import static com.android.SdkConstants.TAG_APPLICATION; import static com.android.SdkConstants.TAG_BITMAP; import static com.android.SdkConstants.TAG_STYLE; import static com.android.SdkConstants.TOOLS_URI; import static com.android.SdkConstants.TRANSPARENT_COLOR; import static com.android.SdkConstants.VALUE_DISABLED; import static com.android.tools.klint.detector.api.LintUtils.endsWith; import static com.android.utils.SdkUtils.getResourceFieldName; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.tools.klint.client.api.AndroidReference; import com.android.tools.klint.client.api.UastLintUtils; import com.android.tools.klint.detector.api.Category; import com.android.tools.klint.detector.api.Context; import com.android.tools.klint.detector.api.Detector; import com.android.tools.klint.detector.api.Detector.JavaPsiScanner; import com.android.tools.klint.detector.api.Implementation; import com.android.tools.klint.detector.api.Issue; import com.android.tools.klint.detector.api.JavaContext; import com.android.tools.klint.detector.api.LayoutDetector; import com.android.tools.klint.detector.api.LintUtils; import com.android.tools.klint.detector.api.Location; import com.android.tools.klint.detector.api.Project; import com.android.tools.klint.detector.api.Scope; import com.android.tools.klint.detector.api.Severity; import com.android.tools.klint.detector.api.XmlContext; import com.android.utils.Pair; import com.intellij.psi.JavaRecursiveElementVisitor; import com.intellij.psi.PsiAnonymousClass; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiExpression; import com.intellij.psi.PsiMethodCallExpression; import com.intellij.psi.PsiReferenceExpression; import org.jetbrains.uast.UAnonymousClass; import org.jetbrains.uast.UCallExpression; import org.jetbrains.uast.UClass; import org.jetbrains.uast.UElement; import org.jetbrains.uast.UExpression; import org.jetbrains.uast.UQualifiedReferenceExpression; import org.jetbrains.uast.USimpleNameReferenceExpression; import org.jetbrains.uast.visitor.AbstractUastVisitor; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Check which looks for overdraw problems where view areas are painted and then * painted over, meaning that the bottom paint operation is a waste of time. */ public class OverdrawDetector extends LayoutDetector implements Detector.UastScanner { private static final String SET_THEME = "setTheme"; //$NON-NLS-1$ /** The main issue discovered by this detector */ public static final Issue ISSUE = Issue.create( "Overdraw", //$NON-NLS-1$ "Overdraw: Painting regions more than once", "If you set a background drawable on a root view, then you should use a " + "custom theme where the theme background is null. Otherwise, the theme background " + "will be painted first, only to have your custom background completely cover it; " + "this is called \"overdraw\".\n" + "\n" + "NOTE: This detector relies on figuring out which layouts are associated with " + "which activities based on scanning the Java code, and it's currently doing that " + "using an inexact pattern matching algorithm. Therefore, it can incorrectly " + "conclude which activity the layout is associated with and then wrongly complain " + "that a background-theme is hidden.\n" + "\n" + "If you want your custom background on multiple pages, then you should consider " + "making a custom theme with your custom background and just using that theme " + "instead of a root element background.\n" + "\n" + "Of course it's possible that your custom drawable is translucent and you want " + "it to be mixed with the background. However, you will get better performance " + "if you pre-mix the background with your drawable and use that resulting image or " + "color as a custom theme background instead.\n", Category.PERFORMANCE, 3, Severity.WARNING, new Implementation( OverdrawDetector.class, EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE, Scope.ALL_RESOURCE_FILES))); /** Mapping from FQN activity names to theme names registered in the manifest */ private Map<String, String> mActivityToTheme; /** The default theme declared in the manifest, or null */ private String mManifestTheme; /** Mapping from layout name (not including {@code @layout/} prefix) to activity FQN */ private Map<String, List<String>> mLayoutToActivity; /** List of theme names registered in the project which have blank backgrounds */ private List<String> mBlankThemes; /** List of drawable resources that are not flagged for overdraw (XML drawables * except for {@code <bitmap>} drawables without tiling) */ private List<String> mValidDrawables; /** * List of pairs of (location, background drawable) corresponding to root elements * in layouts that define a given background drawable. These should be checked to * see if they are painting on top of a non-transparent theme. */ private List<Pair<Location, String>> mRootAttributes; /** Constructs a new {@link OverdrawDetector} */ public OverdrawDetector() { } @Override public boolean appliesTo(@NonNull ResourceFolderType folderType) { // Look in layouts for drawable resources return super.appliesTo(folderType) // and in resource files for theme definitions || folderType == ResourceFolderType.VALUES // and in drawable files for bitmap tiling modes || folderType == ResourceFolderType.DRAWABLE; } /** Is the given theme a "blank" theme (one not painting its background) */ private boolean isBlankTheme(String name) { if (name.startsWith("@android:style/Theme_")) { //$NON-NLS-1$ if (name.contains("NoFrame") //$NON-NLS-1$ || name.contains("Theme_Wallpaper") //$NON-NLS-1$ || name.contains("Theme_Holo_Wallpaper") //$NON-NLS-1$ || name.contains("Theme_Translucent") //$NON-NLS-1$ || name.contains("Theme_Dialog_NoFrame") //$NON-NLS-1$ || name.contains("Theme_Holo_Dialog_Alert") //$NON-NLS-1$ || name.contains("Theme_Holo_Light_Dialog_Alert") //$NON-NLS-1$ || name.contains("Theme_Dialog_Alert") //$NON-NLS-1$ || name.contains("Theme_Panel") //$NON-NLS-1$ || name.contains("Theme_Light_Panel") //$NON-NLS-1$ || name.contains("Theme_Holo_Panel") //$NON-NLS-1$ || name.contains("Theme_Holo_Light_Panel")) { //$NON-NLS-1$ return true; } } return mBlankThemes != null && mBlankThemes.contains(name); } @Override public void afterCheckProject(@NonNull Context context) { if (mRootAttributes != null) { for (Pair<Location, String> pair : mRootAttributes) { Location location = pair.getFirst(); Object clientData = location.getClientData(); if (clientData instanceof Node) { if (context.getDriver().isSuppressed(null, ISSUE, (Node) clientData)) { return; } } String layoutName = location.getFile().getName(); if (endsWith(layoutName, DOT_XML)) { layoutName = layoutName.substring(0, layoutName.length() - DOT_XML.length()); } String theme = getTheme(context, layoutName); if (theme == null || !isBlankTheme(theme)) { String drawable = pair.getSecond(); String message = String.format( "Possible overdraw: Root element paints background `%1$s` with " + "a theme that also paints a background (inferred theme is `%2$s`)", drawable, theme); // TODO: Compute applicable scope node context.report(ISSUE, location, message); } } } } /** Return the theme to be used for the given layout */ private String getTheme(Context context, String layoutName) { if (mActivityToTheme != null && mLayoutToActivity != null) { List<String> activities = mLayoutToActivity.get(layoutName); if (activities != null) { for (String activity : activities) { String theme = mActivityToTheme.get(activity); if (theme != null) { return theme; } } } } if (mManifestTheme != null) { return mManifestTheme; } Project project = context.getMainProject(); int apiLevel = project.getTargetSdk(); if (apiLevel == -1) { apiLevel = project.getMinSdk(); } if (apiLevel >= 11) { return "@android:style/Theme.Holo"; //$NON-NLS-1$ } else { return "@android:style/Theme"; //$NON-NLS-1$ } } // ---- Implements XmlScanner ---- @Override public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { // Ignore tools:background and any other custom attribute that isn't actually the // android View background attribute if (!ANDROID_URI.equals(attribute.getNamespaceURI())) { return; } // Only consider the root element's background Element documentElement = attribute.getOwnerDocument().getDocumentElement(); if (documentElement == attribute.getOwnerElement()) { // If the drawable is a non-repeated pattern then the overdraw might be // intentional since the image isn't covering the whole screen String background = attribute.getValue(); if (mValidDrawables != null && mValidDrawables.contains(background)) { return; } if (background.equals(TRANSPARENT_COLOR) || background.equals(NULL_RESOURCE)) { return; } if (background.startsWith("@android:drawable/")) { //$NON-NLS-1$ // We haven't had a chance to study the builtin drawables the way we // check the project local ones in scanBitmap() and beforeCheckFile(), // but many of these are not bitmaps, so ignore these return; } String name = context.file.getName(); if (name.contains("list_") || name.contains("_item")) { //$NON-NLS-1$ //$NON-NLS-2$ // Canonical list_item layout name: don't warn about these, it's // pretty common to want to paint custom list item backgrounds return; } if (!context.getProject().getReportIssues()) { // If this is a library project not being analyzed, ignore it return; } Location location = context.getLocation(attribute); location.setClientData(attribute); if (mRootAttributes == null) { mRootAttributes = new ArrayList<Pair<Location,String>>(); } mRootAttributes.add(Pair.of(location, attribute.getValue())); String activity = documentElement.getAttributeNS(TOOLS_URI, ATTR_CONTEXT); if (activity != null && !activity.isEmpty()) { if (activity.startsWith(".")) { //$NON-NLS-1$ activity = context.getProject().getPackage() + activity; } registerLayoutActivity(LintUtils.getLayoutName(context.file), activity); } } } @Override public Collection<String> getApplicableAttributes() { return Collections.singletonList( // Layouts: Look for background attributes on root elements for possible overdraw ATTR_BACKGROUND ); } @Override public Collection<String> getApplicableElements() { return Arrays.asList( // Manifest: Look at theme registrations TAG_ACTIVITY, TAG_APPLICATION, // Resource files: Look at theme definitions TAG_STYLE, // Bitmaps TAG_BITMAP ); } @Override public void beforeCheckFile(@NonNull Context context) { if (endsWith(context.file.getName(), DOT_XML)) { // Drawable XML files should not be considered for overdraw, except for <bitmap>'s. // The bitmap elements are handled in the scanBitmap() method; it will clear // out anything added by this method. File parent = context.file.getParentFile(); ResourceFolderType type = ResourceFolderType.getFolderType(parent.getName()); if (type == ResourceFolderType.DRAWABLE) { if (mValidDrawables == null) { mValidDrawables = new ArrayList<String>(); } String resource = getDrawableResource(context.file); mValidDrawables.add(resource); } } } @Override public void visitElement(@NonNull XmlContext context, @NonNull Element element) { String tag = element.getTagName(); if (tag.equals(TAG_STYLE)) { scanTheme(element); } else if (tag.equals(TAG_ACTIVITY)) { scanActivity(context, element); } else if (tag.equals(TAG_APPLICATION)) { if (element.hasAttributeNS(ANDROID_URI, ATTR_THEME)) { mManifestTheme = element.getAttributeNS(ANDROID_URI, ATTR_THEME); } } else if (tag.equals(TAG_BITMAP)) { scanBitmap(context, element); } } private static String getDrawableResource(File drawableFile) { String resource = drawableFile.getName(); if (endsWith(resource, DOT_XML)) { resource = resource.substring(0, resource.length() - DOT_XML.length()); } return DRAWABLE_PREFIX + resource; } private void scanBitmap(Context context, Element element) { String tileMode = element.getAttributeNS(ANDROID_URI, ATTR_TILE_MODE); if (!(tileMode.equals(VALUE_DISABLED) || tileMode.isEmpty())) { if (mValidDrawables != null) { String resource = getDrawableResource(context.file); mValidDrawables.remove(resource); } } } private void scanActivity(Context context, Element element) { String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); if (name.indexOf('$') != -1) { name = name.replace('$', '.'); } if (name.startsWith(".")) { //$NON-NLS-1$ String pkg = context.getProject().getPackage(); if (pkg != null && !pkg.isEmpty()) { name = pkg + name; } } String theme = element.getAttributeNS(ANDROID_URI, ATTR_THEME); if (theme != null && !theme.isEmpty()) { if (mActivityToTheme == null) { mActivityToTheme = new HashMap<String, String>(); } mActivityToTheme.put(name, getResourceFieldName(theme)); } } private void scanTheme(Element element) { // Look for theme definitions, and record themes that provide a null background. String styleName = element.getAttribute(ATTR_NAME); String parent = element.getAttribute(ATTR_PARENT); if (parent == null) { // Eclipse DOM workaround parent = ""; } if (parent.isEmpty()) { int index = styleName.lastIndexOf('.'); if (index != -1) { parent = styleName.substring(0, index); } } parent = parent.replace('.', '_'); String resource = STYLE_RESOURCE_PREFIX + getResourceFieldName(styleName); NodeList items = element.getChildNodes(); for (int i = 0, n = items.getLength(); i < n; i++) { if (items.item(i).getNodeType() == Node.ELEMENT_NODE) { Element item = (Element) items.item(i); String name = item.getAttribute(ATTR_NAME); if (name.equals("android:windowBackground")) { //$NON-NLS-1$ NodeList textNodes = item.getChildNodes(); for (int j = 0, m = textNodes.getLength(); j < m; j++) { Node textNode = textNodes.item(j); if (textNode.getNodeType() == Node.TEXT_NODE) { String text = textNode.getNodeValue(); String trim = text.trim(); if (!trim.isEmpty()) { if (trim.equals(NULL_RESOURCE) || trim.equals(TRANSPARENT_COLOR) || mValidDrawables != null && mValidDrawables.contains(trim)) { if (mBlankThemes == null) { mBlankThemes = new ArrayList<String>(); } mBlankThemes.add(resource); } } } } return; } } } if (isBlankTheme(parent)) { if (mBlankThemes == null) { mBlankThemes = new ArrayList<String>(); } mBlankThemes.add(resource); } } private void registerLayoutActivity(String layout, String classFqn) { if (mLayoutToActivity == null) { mLayoutToActivity = new HashMap<String, List<String>>(); } List<String> list = mLayoutToActivity.get(layout); if (list == null) { list = new ArrayList<String>(); mLayoutToActivity.put(layout, list); } list.add(classFqn); } // ---- Implements UastScanner ---- @Nullable @Override public List<String> applicableSuperClasses() { return Collections.singletonList("android.app.Activity"); } @Nullable @Override public List<Class<? extends UElement>> getApplicableUastTypes() { return Collections.<Class<? extends UElement>>singletonList(UClass.class); } @Override public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) { if (!context.getProject().getReportIssues()) { return; } String name = declaration.getQualifiedName(); if (name != null) { declaration.accept(new OverdrawVisitor(name, declaration)); } } private class OverdrawVisitor extends AbstractUastVisitor { private final String mName; private final PsiClass mCls; public OverdrawVisitor(String name, PsiClass cls) { mName = name; mCls = cls; } @Override public boolean visitClass(UClass node) { // Don't go into inner classes if (mCls.equals(node.getPsi())) { return true; } return super.visitClass(node); } @Override public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) { AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(node); if (androidReference != null && androidReference.getType() == ResourceType.LAYOUT) { registerLayoutActivity(androidReference.getName(), mName); } return super.visitSimpleNameReferenceExpression(node); } @Override public boolean visitCallExpression(UCallExpression node) { if (SET_THEME.equals(node.getMethodName()) && node.getValueArgumentCount() == 1) { // Look at argument UExpression arg = node.getValueArguments().get(0); AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(arg); if (androidReference != null && androidReference.getType() == ResourceType.STYLE) { String style = androidReference.getName(); if (mActivityToTheme == null) { mActivityToTheme = new HashMap<String, String>(); } mActivityToTheme.put(mName, STYLE_RESOURCE_PREFIX + style); } } return super.visitCallExpression(node); } } }