| /* |
| * 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.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.lint.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.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Context; |
| import com.android.tools.lint.detector.api.Detector.JavaPsiScanner; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.JavaContext; |
| import com.android.tools.lint.detector.api.LayoutDetector; |
| import com.android.tools.lint.detector.api.LintUtils; |
| 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.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.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 JavaPsiScanner { |
| 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 JavaScanner ---- |
| |
| |
| @Nullable |
| @Override |
| public List<String> applicableSuperClasses() { |
| return Collections.singletonList("android.app.Activity"); |
| } |
| |
| @Override |
| public List<Class<? extends PsiElement>> getApplicablePsiTypes() { |
| return Collections.<Class<? extends PsiElement>>singletonList(PsiClass.class); |
| } |
| |
| @Override |
| public void checkClass(@NonNull JavaContext context, @NonNull PsiClass declaration) { |
| if (!context.getProject().getReportIssues()) { |
| return; |
| } |
| String name = declaration.getQualifiedName(); |
| if (name != null) { |
| declaration.accept(new OverdrawVisitor(name, declaration)); |
| } |
| } |
| |
| private class OverdrawVisitor extends JavaRecursiveElementVisitor { |
| private final String mName; |
| private final PsiClass mCls; |
| |
| public OverdrawVisitor(String name, PsiClass cls) { |
| mName = name; |
| mCls = cls; |
| } |
| |
| @Override |
| public void visitAnonymousClass(PsiAnonymousClass aClass) { |
| // Don't go into inner classes |
| if (mCls == aClass) { |
| super.visitAnonymousClass(aClass); |
| } |
| } |
| |
| @Override |
| public void visitClass(PsiClass aClass) { |
| // Don't go into inner classes |
| if (mCls == aClass) { |
| super.visitClass(aClass); |
| } |
| } |
| |
| @Override |
| public void visitReferenceExpression(PsiReferenceExpression expression) { |
| if (expression.getQualifier() instanceof PsiReferenceExpression) { |
| PsiReferenceExpression type = (PsiReferenceExpression) expression.getQualifier(); |
| if (ResourceType.LAYOUT.getName().equals(type.getReferenceName())) { |
| if (type.getQualifier() instanceof PsiReferenceExpression) { |
| PsiReferenceExpression rClass = (PsiReferenceExpression) type.getQualifier(); |
| if (rClass.getQualifier() == null && R_CLASS.equals(rClass.getReferenceName())) { |
| String layout = expression.getReferenceName(); |
| if (layout != null) { |
| registerLayoutActivity(layout, mName); |
| } |
| } |
| } |
| } |
| } |
| super.visitReferenceExpression(expression); |
| } |
| |
| @Override |
| public void visitMethodCallExpression(PsiMethodCallExpression expression) { |
| if (SET_THEME.equals(expression.getMethodExpression().getReferenceName())) { |
| // Look at argument |
| PsiExpression[] args = expression.getArgumentList().getExpressions(); |
| if (args.length == 1) { |
| PsiExpression arg = args[0]; |
| if (arg instanceof PsiReferenceExpression) { |
| PsiReferenceExpression resource = (PsiReferenceExpression) arg; |
| if (resource.getQualifier() instanceof PsiReferenceExpression) { |
| PsiReferenceExpression type = (PsiReferenceExpression) resource.getQualifier(); |
| if (ResourceType.STYLE.getName().equals(type.getReferenceName())) { |
| if (type.getQualifier() instanceof PsiReferenceExpression) { |
| PsiReferenceExpression rClass = (PsiReferenceExpression) type.getQualifier(); |
| if (rClass.getQualifier() == null && R_CLASS.equals(rClass.getReferenceName())) { |
| String style = resource.getReferenceName(); |
| if (style != null) { |
| if (mActivityToTheme == null) { |
| mActivityToTheme = new HashMap<String, String>(); |
| } |
| mActivityToTheme.put(mName, STYLE_RESOURCE_PREFIX + |
| style); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| super.visitMethodCallExpression(expression); |
| } |
| } |
| } |