All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.checks.OverdrawDetector Maven / Gradle / Ivy

/*
 * 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_JAVA;
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.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 com.android.annotations.NonNull;
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.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.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.Pair;

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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lombok.ast.AstVisitor;
import lombok.ast.ClassDeclaration;
import lombok.ast.CompilationUnit;
import lombok.ast.Expression;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodInvocation;
import lombok.ast.PackageDeclaration;
import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
import lombok.ast.VariableReference;

/**
 * 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.JavaScanner {
    private static final String R_STYLE_PREFIX = "R.style.";    //$NON-NLS-1$
    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 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> mLayoutToActivity;

    /** List of theme names registered in the project which have blank backgrounds */
    private List mBlankThemes;

    /** Set of activities registered in the manifest. We will limit the Java analysis to
     * these. */
    private Set mActivities;

    /** List of drawable resources that are not flagged for overdraw (XML drawables
     * except for {@code } drawables without tiling) */
    private List 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> 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;
    }

    @Override
    public boolean appliesTo(@NonNull Context context, @NonNull File file) {
        return LintUtils.isXmlFile(file) || LintUtils.endsWith(file.getName(), DOT_JAVA);
    }

    @NonNull
    @Override
    public Speed getSpeed() {
        return Speed.FAST;
    }

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

        if (mBlankThemes != null && mBlankThemes.contains(name)) {
            return true;
        }

        return false;
    }

    @Override
    public void afterCheckProject(@NonNull Context context) {
        if (mRootAttributes != null) {
            for (Pair 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 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>();
            }
            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 getApplicableAttributes() {
        return Collections.singletonList(
                // Layouts: Look for background attributes on root elements for possible overdraw
                ATTR_BACKGROUND
        );
    }

    @Override
    public Collection 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 '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 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;
            }
        }

        if (mActivities == null) {
            mActivities = new HashSet();
        }
        mActivities.add(name);

        String theme = element.getAttributeNS(ANDROID_URI, ATTR_THEME);
        if (theme != null && !theme.isEmpty()) {
            if (mActivityToTheme == null) {
                mActivityToTheme = new HashMap();
            }
            mActivityToTheme.put(name, theme.replace('.', '_'));
        }
    }

    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 + styleName.replace('.', '_');

        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();
                                    }
                                    mBlankThemes.add(resource);
                                }
                            }
                        }
                    }

                    return;
                }
            }
        }

        if (isBlankTheme(parent)) {
            if (mBlankThemes == null) {
                mBlankThemes = new ArrayList();
            }
            mBlankThemes.add(resource);
        }
    }

    private void registerLayoutActivity(String layout, String classFqn) {
        if (mLayoutToActivity == null) {
            mLayoutToActivity = new HashMap>();
        }
        List list = mLayoutToActivity.get(layout);
        if (list == null) {
            list = new ArrayList();
            mLayoutToActivity.put(layout, list);
        }
        list.add(classFqn);
    }

    // ---- Implements JavaScanner ----

    @Override
    public List> getApplicableNodeTypes() {
        // This detector does not specify specific node types; this means
        // that the infrastructure will run the full visitor on the compilation
        // unit rather than on individual nodes. This is important since this
        // detector relies on pruning (if it gets to a class declaration that is
        // not an activity, it skips everything inside).
        return null;
    }

    @Override
    public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
        if (!context.getProject().getReportIssues()) {
            return null;
        }
        return new OverdrawVisitor();
    }

    private class OverdrawVisitor extends ForwardingAstVisitor {
        private static final String ACTIVITY = "Activity"; //$NON-NLS-1$
        private String mClassFqn;

        @Override
        public boolean visitClassDeclaration(ClassDeclaration node) {
            String name = node.astName().astValue();
            if (mActivities != null && mActivities.contains(mClassFqn) || name.endsWith(ACTIVITY)
                    || node.astExtending() != null &&
                        node.astExtending().getTypeName().endsWith(ACTIVITY)) {
                String packageName = "";
                if (node.getParent() instanceof CompilationUnit) {
                    CompilationUnit compilationUnit = (CompilationUnit) node.getParent();
                    PackageDeclaration packageDeclaration = compilationUnit.astPackageDeclaration();
                    if (packageDeclaration == null) {
                        // No package declaration: ignore this one
                        return true;
                    }
                    packageName = packageDeclaration.getPackageName();
                }
                mClassFqn = (!packageName.isEmpty() ? (packageName + '.') : "") + name;

                return false;
            }

            return true; // Done: No need to look inside this class
        }

        // Store R.layout references in activity classes in a map mapping back layouts
        // to activities
        @Override
        public boolean visitSelect(Select node) {
            if (node.astIdentifier().astValue().equals("layout") //$NON-NLS-1$
                    && node.astOperand() instanceof VariableReference
                    && ((VariableReference) node.astOperand()).astIdentifier().astValue()
                        .equals("R")                             //$NON-NLS-1$
                    && node.getParent() instanceof Select) {
                String layout = ((Select) node.getParent()).astIdentifier().astValue();
                registerLayoutActivity(layout, mClassFqn);
            }

            return false;
        }


        // Look for setTheme(R.style.whatever) and register as a theme registration
        // for the current activity
        @Override
        public boolean visitMethodInvocation(MethodInvocation node) {
            if (node.astName().astValue().equals(SET_THEME)) {
                // Look at argument
                StrictListAccessor args = node.astArguments();
                if (args.size() == 1) {
                    Expression arg = args.first();
                    if (arg instanceof Select) {
                        String resource = arg.toString();
                        if (resource.startsWith(R_STYLE_PREFIX)) {
                            if (mActivityToTheme == null) {
                                mActivityToTheme = new HashMap();
                            }
                            String name = ((Select) arg).astIdentifier().astValue();
                            mActivityToTheme.put(mClassFqn, STYLE_RESOURCE_PREFIX + name);
                        }
                    }
                }
            }

            return false;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy