com.android.tools.idea.res.ResourceHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.res.ResourceHelper.java

Source

/*
 * Copyright (C) 2016 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.idea.res;

import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.repository.ResourceVisibilityLookup;
import com.android.ide.common.res2.DataFile;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.FrameworkResources;
import com.android.ide.common.resources.ResourceUrl;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.FolderTypeRelationship;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.sdklib.devices.Device;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.databinding.DataBindingUtil;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
import com.android.tools.lint.detector.api.LintUtils;
import com.google.common.base.Joiner;
import com.google.common.collect.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.xml.*;
import com.intellij.ui.ColorUtil;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidTargetData;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.android.SdkConstants.*;
import static com.android.ide.common.resources.ResourceResolver.*;

public class ResourceHelper {
    private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.res.ResourceHelper");
    public static final String STATE_NAME_PREFIX = "state_";
    public static final String ALPHA_FLOATING_ERROR_FORMAT = "The alpha attribute in %1$s/%2$s does not resolve to a floating point number";
    public static final String DIMENSION_ERROR_FORMAT = "The specified dimension %1$s does not have a unit";

    private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)");
    private final static float[] sFloatOut = new float[1];

    /**
     * Returns true if the given style represents a project theme
     *
     * @param styleResourceUrl a theme style resource url
     * @return true if the style string represents a project theme, as opposed
     *         to a framework theme
     */
    public static boolean isProjectStyle(@NotNull String styleResourceUrl) {
        return !styleResourceUrl.startsWith(ANDROID_STYLE_RESOURCE_PREFIX);
    }

    /**
     * Returns the theme name to be shown for theme styles, e.g. for "@style/Theme" it
     * returns "Theme"
     *
     * @param style a theme style string
     * @return the user visible theme name
     */
    @NotNull
    public static String styleToTheme(@NotNull String style) {
        if (style.startsWith(STYLE_RESOURCE_PREFIX)) {
            style = style.substring(STYLE_RESOURCE_PREFIX.length());
        } else if (style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) {
            style = style.substring(ANDROID_STYLE_RESOURCE_PREFIX.length());
        } else if (style.startsWith(PREFIX_RESOURCE_REF)) {
            // @package:style/foo
            int index = style.indexOf('/');
            if (index != -1) {
                style = style.substring(index + 1);
            }
        }
        return style;
    }

    /**
     * Is this a resource that can be defined in any file within the "values" folder?
     * <p/>
     * Some resource types can be defined <b>both</b> as a separate XML file as well
     * as defined within a value XML file. This method will return true for these types
     * as well. In other words, a ResourceType can return true for both
     * {@link #isValueBasedResourceType} and {@link #isFileBasedResourceType}.
     *
     * @param type the resource type to check
     * @return true if the given resource type can be represented as a value under the
     *         values/ folder
     */
    public static boolean isValueBasedResourceType(@NotNull ResourceType type) {
        List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type);
        for (ResourceFolderType folderType : folderTypes) {
            if (folderType == ResourceFolderType.VALUES) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the resource name of the given file.
     * <p>
     * For example, {@code getResourceName(</res/layout-land/foo.xml, false) = "foo"}.
     *
     * @param file the file to compute a resource name for
     * @return the resource name
     */
    @NotNull
    public static String getResourceName(@NotNull VirtualFile file) {
        // Note that we use getBaseName here rather than {@link VirtualFile#getNameWithoutExtension}
        // because that method uses lastIndexOf('.') rather than indexOf('.') -- which means that
        // for a nine patch drawable it would include ".9" in the resource name
        return LintUtils.getBaseName(file.getName());
    }

    /**
     * Returns the resource name of the given file.
     * <p>
     * For example, {@code getResourceName(</res/layout-land/foo.xml, false) = "foo"}.
     *
     * @param file the file to compute a resource name for
     * @return the resource name
     */
    @NotNull
    public static String getResourceName(@NotNull PsiFile file) {
        // See getResourceName(VirtualFile)
        // We're replicating that code here rather than just calling
        // getResourceName(file.getVirtualFile());
        // since file.getVirtualFile can return null
        return LintUtils.getBaseName(file.getName());
    }

    /**
     * Returns the resource URL of the given file. The file <b>must</b> be a valid resource
     * file, meaning that it is in a proper resource folder, and it <b>must</b> be a
     * file-based resource (e.g. layout, drawable, menu, etc) -- not a values file.
     * <p>
     * For example, {@code getResourceUrl(</res/layout-land/foo.xml, false) = "@layout/foo"}.
     *
     * @param file the file to compute a resource url for
     * @return the resource url
     */
    @NotNull
    public static String getResourceUrl(@NotNull VirtualFile file) {
        ResourceFolderType type = ResourceFolderType.getFolderType(file.getParent().getName());
        assert type != null && type != ResourceFolderType.VALUES;
        return PREFIX_RESOURCE_REF + type.getName() + '/' + getResourceName(file);
    }

    /**
     * Is this a resource that is defined in a file named by the resource plus the XML
     * extension?
     * <p/>
     * Some resource types can be defined <b>both</b> as a separate XML file as well as
     * defined within a value XML file along with other properties. This method will
     * return true for these resource types as well. In other words, a ResourceType can
     * return true for both {@link #isValueBasedResourceType} and
     * {@link #isFileBasedResourceType}.
     *
     * @param type the resource type to check
     * @return true if the given resource type is stored in a file named by the resource
     */
    public static boolean isFileBasedResourceType(@NotNull ResourceType type) {
        List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type);
        for (ResourceFolderType folderType : folderTypes) {
            if (folderType != ResourceFolderType.VALUES) {

                if (type == ResourceType.ID) {
                    // The folder types for ID is not only VALUES but also
                    // LAYOUT and MENU. However, unlike resources, they are only defined
                    // inline there so for the purposes of isFileBasedResourceType
                    // (where the intent is to figure out files that are uniquely identified
                    // by a resource's name) this method should return false anyway.
                    return false;
                }

                return true;
            }
        }

        return false;
    }

    @Nullable
    public static ResourceFolderType getFolderType(@Nullable final PsiFile file) {
        if (file != null) {
            if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
                return ApplicationManager.getApplication()
                        .runReadAction((Computable<ResourceFolderType>) () -> getFolderType(file));
            }
            if (!file.isValid()) {
                return getFolderType(file.getVirtualFile());
            }
            PsiDirectory parent = file.getParent();
            if (parent != null) {
                return ResourceFolderType.getFolderType(parent.getName());
            }
        }

        return null;
    }

    @Nullable
    public static ResourceFolderType getFolderType(@Nullable VirtualFile file) {
        if (file != null) {
            VirtualFile parent = file.getParent();
            if (parent != null) {
                return ResourceFolderType.getFolderType(parent.getName());
            }
        }

        return null;
    }

    @Nullable
    public static ResourceFolderType getFolderType(@NotNull ResourceFile file) {
        File parent = file.getFile().getParentFile();
        if (parent != null) {
            return ResourceFolderType.getFolderType(parent.getName());
        }
        return null;
    }

    @Nullable
    public static FolderConfiguration getFolderConfiguration(@Nullable final PsiFile file) {
        if (file != null) {
            if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
                return ApplicationManager.getApplication()
                        .runReadAction((Computable<FolderConfiguration>) () -> getFolderConfiguration(file));
            }
            if (!file.isValid()) {
                return getFolderConfiguration(file.getVirtualFile());
            }
            PsiDirectory parent = file.getParent();
            if (parent != null) {
                return FolderConfiguration.getConfigForFolder(parent.getName());
            }
        }

        return null;
    }

    @Nullable
    public static FolderConfiguration getFolderConfiguration(@Nullable VirtualFile file) {
        if (file != null) {
            VirtualFile parent = file.getParent();
            if (parent != null) {
                return FolderConfiguration.getConfigForFolder(parent.getName());
            }
        }

        return null;
    }

    /**
     * Returns all resource variations for the given file
     *
     * @param file resource file, which should be an XML file in one of the
     *            various resource folders, e.g. res/layout, res/values-xlarge, etc.
     * @param includeSelf if true, include the file itself in the list,
     *            otherwise exclude it
     * @return a list of all the resource variations
     */
    public static List<VirtualFile> getResourceVariations(@Nullable VirtualFile file, boolean includeSelf) {
        if (file == null) {
            return Collections.emptyList();
        }

        // Compute the set of layout files defining this layout resource
        List<VirtualFile> variations = new ArrayList<>();
        String name = file.getName();
        VirtualFile parent = file.getParent();
        if (parent != null) {
            VirtualFile resFolder = parent.getParent();
            if (resFolder != null) {
                String parentName = parent.getName();
                String prefix = parentName;
                int qualifiers = prefix.indexOf('-');

                if (qualifiers != -1) {
                    parentName = prefix.substring(0, qualifiers);
                    prefix = prefix.substring(0, qualifiers + 1);
                } else {
                    prefix += '-';
                }
                for (VirtualFile resource : resFolder.getChildren()) {
                    String n = resource.getName();
                    if ((n.startsWith(prefix) || n.equals(parentName)) && resource.isDirectory()) {
                        VirtualFile variation = resource.findChild(name);
                        if (variation != null) {
                            if (!includeSelf && file.equals(variation)) {
                                continue;
                            }
                            variations.add(variation);
                        }
                    }
                }
            }
        }

        return variations;
    }

    /**
     * Package prefixes used in {@link #isViewPackageNeeded(String, int)}
     */
    private static final String[] NO_PREFIX_PACKAGES = new String[] { ANDROID_WIDGET_PREFIX, ANDROID_VIEW_PKG,
            ANDROID_WEBKIT_PKG };

    /**
     * Returns true if views with the given fully qualified class name need to include
     * their package in the layout XML tag. Package prefixes that allow class name to be
     * unqualified are specified in {@link #NO_PREFIX_PACKAGES} and should reflect a list
     * of prefixes from framework's LayoutInflater and PhoneLayoutInflater.
     *
     * @param qualifiedName the fully qualified class name, such as android.widget.Button
     * @param apiLevel The API level for the calling context. This is the max of the
     *                 project's minSdkVersion and the layout file's version qualifier, if any.
     *                 You can pass -1 if this is not known, which will force fully qualified
     *                 names on some packages which recently no longer require it.
     * @return true if the full package path should be included in the layout XML element
     *         tag
     */
    public static boolean isViewPackageNeeded(@NotNull String qualifiedName, int apiLevel) {
        for (String noPrefixPackage : NO_PREFIX_PACKAGES) {
            // We need to check not only if prefix is a "whitelisted" package, but if the class
            // is stored in that package directly, as opposed to be stored in a subpackage.
            // For example, view with FQCN android.view.MyView can be abbreviated to "MyView",
            // but android.view.custom.MyView can not.
            if (isDirectlyInPackage(qualifiedName, noPrefixPackage)) {
                return false;
            }
        }

        if (apiLevel >= 20) {
            // Special case: starting from API level 20, classes from "android.app" also inflated
            // without fully qualified names
            return !isDirectlyInPackage(qualifiedName, ANDROID_APP_PKG);
        }
        return true;
    }

    /**
     * XML tags associated with classes usually can come either with fully-qualified names, which can be shortened
     * in case of common packages, which is handled by various inflaters in Android framework. This method checks
     * whether a class with given qualified name can be shortened to a simple name, or is required to have
     * a package qualifier.
     * <p/>
     * Accesses JavaPsiFacade, and thus should be run inside read action.
     *
     * @see #isViewPackageNeeded(String, int)
     */
    public static boolean isClassPackageNeeded(@NotNull String qualifiedName, @NotNull PsiClass baseClass,
            int apiLevel) {
        final PsiClass viewClass = JavaPsiFacade.getInstance(baseClass.getProject()).findClass(CLASS_VIEW,
                GlobalSearchScope.allScope(baseClass.getProject()));

        if (viewClass != null && baseClass.isInheritor(viewClass, true)) {
            return isViewPackageNeeded(qualifiedName, apiLevel);
        } else if (CLASS_PREFERENCE.equals(baseClass.getQualifiedName())) {
            // Handled by PreferenceInflater in Android framework
            return !isDirectlyInPackage(qualifiedName, "android.preference.");
        } else {
            // TODO: removing that makes some of unit tests fail, but leaving it as it is can introduce buggy XML validation
            // Issue with further information: http://b.android.com/186559
            return !qualifiedName.startsWith(ANDROID_PKG_PREFIX);
        }
    }

    /**
     * Returns whether a class with given qualified name resides directly in a package with
     * given prefix (as opposed to reside in a subpackage).
     * <p/>
     * For example,
     * <ul>
     *   <li>isDirectlyInPackage("android.view.View", "android.view.") -> true</li>
     *   <li>isDirectlyInPackage("android.view.internal.View", "android.view.") -> false</li>
     * </ul>
     */
    public static boolean isDirectlyInPackage(@NotNull String qualifiedName, @NotNull String packagePrefix) {
        return qualifiedName.startsWith(packagePrefix)
                && qualifiedName.indexOf('.', packagePrefix.length() + 1) == -1;
    }

    /**
     * Tries to resolve the given resource value to an actual RGB color. For state lists
     * it will pick the simplest/fallback color.
     *
     * @param resources the resource resolver to use to follow color references
     * @param colorValue the color to resolve
     * @param project the current project
     * @return the corresponding {@link Color} color, or null
     */
    @Nullable
    public static Color resolveColor(@NotNull RenderResources resources, @Nullable ResourceValue colorValue,
            @NotNull Project project) {
        return resolveColor(resources, colorValue, project, 0);
    }

    @Nullable
    private static Color resolveColor(@NotNull RenderResources resources, @Nullable ResourceValue colorValue,
            @NotNull Project project, int depth) {

        if (depth >= MAX_RESOURCE_INDIRECTION) {
            LOG.warn("too deep " + colorValue);
            return null;
        }

        if (colorValue != null) {
            colorValue = resources.resolveResValue(colorValue);
        }
        if (colorValue == null) {
            return null;
        }

        StateList stateList = resolveStateList(resources, colorValue, project);
        if (stateList != null) {
            List<StateListState> states = stateList.getStates();

            if (states.isEmpty()) {
                // In the case of an empty selector, we don't want to crash.
                return null;
            }

            // Getting the last color of the state list, because it's supposed to be the simplest / fallback one
            StateListState state = states.get(states.size() - 1);

            Color stateColor = parseColor(state.getValue());
            if (stateColor == null) {
                stateColor = resolveColor(resources, resources.findResValue(state.getValue(), false), project,
                        depth + 1);
            }
            if (stateColor == null) {
                return null;
            }
            try {
                return makeColorWithAlpha(resources, stateColor, state.getAlpha());
            } catch (NumberFormatException e) {
                // If the alpha value is not valid, Android uses 1.0
                LOG.warn(String.format("The alpha attribute in %s/%s does not resolve to a floating point number",
                        stateList.getDirName(), stateList.getFileName()));
                return stateColor;
            }
        }

        return parseColor(colorValue.getValue());
    }

    /**
     * Tries to resolve colors from given resource value. When state list is encountered all
     * possibilities are explored.
     */
    @NotNull
    public static List<Color> resolveMultipleColors(@NotNull RenderResources resources,
            @Nullable ResourceValue value, @NotNull Project project) {
        return resolveMultipleColors(resources, value, project, 0);
    }

    /**
     * Tries to resolve colors from given resource value. When state list is encountered all
     * possibilities are explored.
     */
    @NotNull
    private static List<Color> resolveMultipleColors(@NotNull RenderResources resources,
            @Nullable ResourceValue value, @NotNull Project project, int depth) {

        if (depth >= MAX_RESOURCE_INDIRECTION) {
            LOG.warn("too deep " + value);
            return Collections.emptyList();
        }

        if (value != null) {
            value = resources.resolveResValue(value);
        }
        if (value == null) {
            return Collections.emptyList();
        }

        final List<Color> result = new ArrayList<>();

        StateList stateList = resolveStateList(resources, value, project);
        if (stateList != null) {
            for (StateListState state : stateList.getStates()) {
                List<Color> stateColors;
                ResourceValue resolvedStateResource = resources.findResValue(state.getValue(), false);
                if (resolvedStateResource != null) {
                    stateColors = resolveMultipleColors(resources, resolvedStateResource, project, depth + 1);
                } else {
                    Color color = parseColor(state.getValue());
                    stateColors = color == null ? Collections.emptyList() : ImmutableList.of(color);
                }
                for (Color color : stateColors) {
                    try {
                        result.add(makeColorWithAlpha(resources, color, state.getAlpha()));
                    } catch (NumberFormatException e) {
                        // If the alpha value is not valid, Android uses 1.0 so nothing more needs to be done, we simply take color as it is
                        result.add(color);
                        LOG.warn(String.format(ALPHA_FLOATING_ERROR_FORMAT, stateList.getDirName(),
                                stateList.getFileName()));
                    }
                }
            }
        } else {
            Color color = parseColor(value.getValue());
            if (color != null) {
                result.add(color);
            }
        }
        return result;
    }

    @NotNull
    public static String resolveStringValue(@NotNull RenderResources resources, @NotNull String value) {
        ResourceValue resValue = resources.findResValue(value, false);
        if (resValue == null) {
            return value;
        }
        ResourceValue finalValue = resources.resolveResValue(resValue);
        if (finalValue == null || finalValue.getValue() == null) {
            return value;
        }
        return finalValue.getValue();
    }

    private static final class UnitEntry {
        String name;
        int type;
        int unit;
        float scale;

        UnitEntry(String name, int type, int unit, float scale) {
            this.name = name;
            this.type = type;
            this.unit = unit;
            this.scale = scale;
        }
    }

    private final static UnitEntry[] sUnitNames = new UnitEntry[] {
            new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f),
            new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
            new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
            new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f),
            new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f),
            new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f),
            new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f) };

    /**
     * Container for a dynamically typed data value. Used to hold resources values.
     */
    public static class TypedValue {
        static final int TYPE_FLOAT = 0x04;
        static final int TYPE_DIMENSION = 0x05;

        static final int COMPLEX_UNIT_SHIFT = 0;
        static final int COMPLEX_UNIT_MASK = 0xf;

        static final int COMPLEX_UNIT_PX = 0;
        static final int COMPLEX_UNIT_DIP = 1;
        static final int COMPLEX_UNIT_SP = 2;
        static final int COMPLEX_UNIT_PT = 3;
        static final int COMPLEX_UNIT_IN = 4;
        static final int COMPLEX_UNIT_MM = 5;

        static final int COMPLEX_RADIX_SHIFT = 4;
        static final int COMPLEX_RADIX_MASK = 0x3;

        static final int COMPLEX_RADIX_23p0 = 0;
        static final int COMPLEX_RADIX_16p7 = 1;
        static final int COMPLEX_RADIX_8p15 = 2;
        static final int COMPLEX_RADIX_0p23 = 3;

        static final int COMPLEX_MANTISSA_SHIFT = 8;
        static final int COMPLEX_MANTISSA_MASK = 0xffffff;

        private static final float MANTISSA_MULT = 1.0f / (1 << COMPLEX_MANTISSA_SHIFT);
        private static final float[] RADIX_MULTS = new float[] { 1.0f * MANTISSA_MULT,
                1.0f / (1 << 7) * MANTISSA_MULT, 1.0f / (1 << 15) * MANTISSA_MULT,
                1.0f / (1 << 23) * MANTISSA_MULT };

        public int type;
        public int data;

        /**
         * Converts a complex data value holding a dimension to its final value
         * as an integer pixel size. A size conversion involves rounding the base
         * value, and ensuring that a non-zero base value is at least one pixel
         * in size. The given <var>data</var> must be structured as a
         * {@link #TYPE_DIMENSION}.
         *
         * @param data   A complex data value holding a unit, magnitude, and mantissa.
         * @param config The device configuration
         * @return The number of pixels specified by the data and its desired
         * multiplier and units.
         */
        public static int complexToDimensionPixelSize(int data, Configuration config) {
            final float value = complexToFloat(data);
            //noinspection PointlessBitwiseExpression
            final float f = applyDimension((data >> COMPLEX_UNIT_SHIFT) & COMPLEX_UNIT_MASK, value, config);
            final int res = (int) (f + 0.5f);
            if (res != 0)
                return res;
            if (value == 0)
                return 0;
            if (value > 0)
                return 1;
            return -1;
        }

        /**
         * Retrieve the base value from a complex data integer.  This uses the
         * {@link #COMPLEX_MANTISSA_MASK} and {@link #COMPLEX_RADIX_MASK} fields of
         * the data to compute a floating point representation of the number they
         * describe.  The units are ignored.
         *
         * @param complex A complex data value.
         * @return A floating point value corresponding to the complex data.
         */
        static float complexToFloat(int complex) {
            return (complex & (COMPLEX_MANTISSA_MASK << COMPLEX_MANTISSA_SHIFT))
                    * RADIX_MULTS[(complex >> COMPLEX_RADIX_SHIFT) & COMPLEX_RADIX_MASK];
        }

        /**
         * Converts an unpacked complex data value holding a dimension to its final floating
         * point value. The two parameters <var>unit</var> and <var>value</var>
         * are as in {@link #TYPE_DIMENSION}.
         *
         * @param unit   The unit to convert from.
         * @param value  The value to apply the unit to.
         * @param config The device configuration
         * @return The complex floating point value multiplied by the appropriate
         * metrics depending on its unit.
         */
        static float applyDimension(int unit, float value, Configuration config) {
            Device device = config.getDevice();
            float xdpi = 493.0f; // assume Nexus 6 density
            if (device != null) {
                xdpi = (float) device.getDefaultHardware().getScreen().getXdpi();
            }

            switch (unit) {
            case COMPLEX_UNIT_PX:
                return value;
            case COMPLEX_UNIT_DIP:
                return value * config.getDensity().getDpiValue() / 160.0f;
            case COMPLEX_UNIT_SP:
                return value * config.getDensity().getDpiValue() / 160.0f;
            case COMPLEX_UNIT_PT:
                return value * xdpi * (1.0f / 72.0f);
            case COMPLEX_UNIT_IN:
                return value * xdpi * (1.0f / 72.0f);
            case COMPLEX_UNIT_MM:
                return value * xdpi * (1.0f / 72.0f);
            }
            return 0;
        }
    }

    /**
     * Parse a float attribute and return the parsed value into a given TypedValue.
     *
     * @param value       the string value of the attribute
     * @param outValue    the TypedValue to receive the parsed value
     * @param requireUnit whether the value is expected to contain a unit.
     * @return true if success.
     */
    public static boolean parseFloatAttribute(@NotNull String value, TypedValue outValue, boolean requireUnit) {
        // remove the space before and after
        value = value.trim();
        int len = value.length();

        if (len <= 0) {
            return false;
        }

        // check that there's no non ascii characters.
        char[] buf = value.toCharArray();
        for (int i = 0; i < len; i++) {
            if (buf[i] > 255) {
                return false;
            }
        }

        // check the first character
        if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') {
            return false;
        }

        // now look for the string that is after the float...
        Matcher m = sFloatPattern.matcher(value);
        if (m.matches()) {
            String f_str = m.group(1);
            String end = m.group(2);

            float f;
            try {
                f = Float.parseFloat(f_str);
            } catch (NumberFormatException e) {
                // this shouldn't happen with the regexp above.
                return false;
            }

            if (end.length() > 0 && end.charAt(0) != ' ') {
                // Might be a unit...
                if (parseUnit(end, outValue, sFloatOut)) {
                    computeTypedValue(outValue, f, sFloatOut[0]);
                    return true;
                }
                return false;
            }

            // make sure it's only spaces at the end.
            end = end.trim();

            if (end.length() == 0) {
                if (outValue != null) {
                    if (!requireUnit) {
                        outValue.type = TypedValue.TYPE_FLOAT;
                        outValue.data = Float.floatToIntBits(f);
                    } else {
                        // no unit when required? Use dp and out an error.
                        applyUnit(sUnitNames[1], outValue, sFloatOut);
                        computeTypedValue(outValue, f, sFloatOut[0]);

                        LOG.warn(String.format(DIMENSION_ERROR_FORMAT, value));
                    }
                    return true;
                }
            }
        }

        return false;
    }

    private static void computeTypedValue(TypedValue outValue, float value, float scale) {
        value *= scale;
        boolean neg = value < 0;
        if (neg) {
            value = -value;
        }
        long bits = (long) (value * (1 << 23) + .5f);
        int radix;
        int shift;
        if ((bits & 0x7fffff) == 0) {
            // Always use 23p0 if there is no fraction, just to make
            // things easier to read.
            radix = TypedValue.COMPLEX_RADIX_23p0;
            shift = 23;
        } else if ((bits & 0xffffffffff800000L) == 0) {
            // Magnitude is zero -- can fit in 0 bits of precision.
            radix = TypedValue.COMPLEX_RADIX_0p23;
            shift = 0;
        } else if ((bits & 0xffffffff80000000L) == 0) {
            // Magnitude can fit in 8 bits of precision.
            radix = TypedValue.COMPLEX_RADIX_8p15;
            shift = 8;
        } else if ((bits & 0xffffff8000000000L) == 0) {
            // Magnitude can fit in 16 bits of precision.
            radix = TypedValue.COMPLEX_RADIX_16p7;
            shift = 16;
        } else {
            // Magnitude needs entire range, so no fractional part.
            radix = TypedValue.COMPLEX_RADIX_23p0;
            shift = 23;
        }
        int mantissa = (int) ((bits >> shift) & TypedValue.COMPLEX_MANTISSA_MASK);
        if (neg) {
            mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK;
        }
        outValue.data |= (radix << TypedValue.COMPLEX_RADIX_SHIFT)
                | (mantissa << TypedValue.COMPLEX_MANTISSA_SHIFT);
    }

    private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) {
        str = str.trim();

        for (UnitEntry unit : sUnitNames) {
            if (unit.name.equals(str)) {
                applyUnit(unit, outValue, outScale);
                return true;
            }
        }

        return false;
    }

    private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) {
        outValue.type = unit.type;
        //noinspection PointlessBitwiseExpression
        outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT;
        outScale[0] = unit.scale;
    }

    @NotNull
    public static Color makeColorWithAlpha(@NotNull RenderResources resources, @NotNull Color color,
            @Nullable String alphaValue) throws NumberFormatException {
        float alpha = 1.0f;
        if (alphaValue != null) {
            alpha = Float.parseFloat(resolveStringValue(resources, alphaValue));
        }

        int combinedAlpha = (int) (color.getAlpha() * alpha);
        return ColorUtil.toAlpha(color, clamp(combinedAlpha, 0, 255));
    }

    /**
     * Returns a {@link StateList} description of the state list value, or null if value is not a state list.
     */
    @Nullable /*if there is no statelist with this name*/
    public static StateList resolveStateList(@NotNull RenderResources renderResources, @NotNull ResourceValue value,
            @NotNull Project project) {
        return resolveStateList(renderResources, value, project, 0);
    }

    @Nullable /*if there is no statelist with this name*/
    private static StateList resolveStateList(@NotNull RenderResources renderResources,
            @NotNull ResourceValue resourceValue, @NotNull Project project, int depth) {
        if (depth >= MAX_RESOURCE_INDIRECTION) {
            LOG.warn("too deep " + resourceValue);
            return null;
        }

        String value = resourceValue.getValue();
        if (value == null) {
            // Not all ResourceValue instances have values (e.g. StyleResourceValue)
            return null;
        }

        if (value.startsWith(PREFIX_RESOURCE_REF)) {
            final ResourceValue resValue = renderResources.findResValue(value, resourceValue.isFramework());
            if (resValue != null) {
                return resolveStateList(renderResources, resValue, project, depth + 1);
            }
        } else {
            VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(value);
            if (virtualFile != null) {
                PsiFile psiFile = AndroidPsiUtils.getPsiFileSafely(project, virtualFile);
                if (psiFile instanceof XmlFile) {
                    // Parse
                    XmlTag rootTag = ((XmlFile) psiFile).getRootTag();
                    if (rootTag != null && TAG_SELECTOR.equals(rootTag.getName())) {
                        StateList stateList = new StateList(psiFile.getName(),
                                psiFile.getContainingDirectory().getName());
                        for (XmlTag subTag : rootTag.findSubTags(TAG_ITEM)) {
                            final StateListState stateListState = createStateListState(subTag,
                                    resourceValue.isFramework());
                            if (stateListState == null) {
                                return null;
                            }
                            stateList.addState(stateListState);
                        }
                        return stateList;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Try to parse a state in the "item" tag. Only handles those items that have
     * either "android:color" or "android:drawable" attributes in "item" tag.
     *
     * @return {@link StateListState} representing the state in tag, null if parse is unsuccessful
     */
    @Nullable
    private static StateListState createStateListState(XmlTag tag, boolean isFramework) {
        String stateValue = null;
        String alphaValue = null;
        Map<String, Boolean> stateAttributes = new HashMap<>();
        XmlAttribute[] attributes = tag.getAttributes();
        for (XmlAttribute attr : attributes) {
            String name = attr.getLocalName();
            String value = attr.getValue();
            if (value == null) {
                continue;
            }
            if (ATTR_COLOR.equals(name) || ATTR_DRAWABLE.equals(name)) {
                ResourceUrl url = ResourceUrl.parse(value, isFramework);
                stateValue = url != null ? url.toString() : value;
            } else if ("alpha".equals(name)) {
                ResourceUrl url = ResourceUrl.parse(value, isFramework);
                alphaValue = url != null ? url.toString() : value;
            } else if (name.startsWith(STATE_NAME_PREFIX)) {
                stateAttributes.put(name, Boolean.valueOf(value));
            }
        }
        if (stateValue == null) {
            return null;
        }
        return new StateListState(stateValue, stateAttributes, alphaValue);
    }

    /**
     * Converts the supported color formats (#rgb, #argb, #rrggbb, #aarrggbb to a Color
     * http://developer.android.com/guide/topics/resources/more-resources.html#Color
     */
    @SuppressWarnings("UseJBColor")
    @Nullable
    public static Color parseColor(@Nullable String s) {
        s = StringUtil.trim(s);
        if (StringUtil.isEmpty(s)) {
            return null;
        }

        if (s.charAt(0) == '#') {
            long longColor;
            try {
                longColor = Long.parseLong(s.substring(1), 16);
            } catch (NumberFormatException e) {
                return null;
            }

            if (s.length() == 4 || s.length() == 5) {
                long a = s.length() == 4 ? 0xff : extend((longColor & 0xf000) >> 12);
                long r = extend((longColor & 0xf00) >> 8);
                long g = extend((longColor & 0x0f0) >> 4);
                long b = extend((longColor & 0x00f));
                longColor = (a << 24) | (r << 16) | (g << 8) | b;
                return new Color((int) longColor, true);
            }

            if (s.length() == 7) {
                longColor |= 0x00000000ff000000;
            } else if (s.length() != 9) {
                return null;
            }
            return new Color((int) longColor, true);
        }

        return null;
    }

    /**
     * Converts a color to hex-string representation, including alpha channel.
     * If alpha is FF then the output is #RRGGBB with no alpha component.
     */
    @NotNull
    public static String colorToString(@NotNull Color color) {
        long longColor = (color.getRed() << 16) | (color.getGreen() << 8) | color.getBlue();
        if (color.getAlpha() != 0xFF) {
            longColor |= (long) color.getAlpha() << 24;
            return String.format("#%08x", longColor);
        }
        return String.format("#%06x", longColor);
    }

    private static long extend(long nibble) {
        return nibble | nibble << 4;
    }

    /**
     * Tries to resolve the given resource value to an actual drawable bitmap file. For state lists
     * it will pick the simplest/fallback drawable.
     *
     * @param resources the resource resolver to use to follow drawable references
     * @param drawable the drawable to resolve
     * @param project the current project
     * @return the corresponding {@link File}, or null
     */
    @Nullable
    public static File resolveDrawable(@NotNull RenderResources resources, @Nullable ResourceValue drawable,
            @NotNull Project project) {
        if (drawable != null) {
            drawable = resources.resolveResValue(drawable);
        }
        if (drawable == null) {
            return null;
        }

        String result = drawable.getValue();

        StateList stateList = resolveStateList(resources, drawable, project);
        if (stateList != null) {
            List<StateListState> states = stateList.getStates();
            if (!states.isEmpty()) {
                StateListState state = states.get(states.size() - 1);
                result = state.getValue();
            }
        }

        if (result == null) {
            return null;
        }

        final File file = new File(result);
        return file.isFile() ? file : null;
    }

    /**
     * Tries to resolve the given resource value to an actual layout file.
     *
     * @param resources the resource resolver to use to follow layout references
     * @param layout the layout to resolve
     * @return the corresponding {@link File}, or null
     */
    @Nullable
    public static File resolveLayout(@NotNull RenderResources resources, @Nullable ResourceValue layout) {
        if (layout != null) {
            layout = resources.resolveResValue(layout);
        }
        if (layout == null) {
            return null;
        }
        String value = layout.getValue();

        int depth = 0;
        while (value != null && depth < MAX_RESOURCE_INDIRECTION) {
            if (DataBindingUtil.isBindingExpression(value)) {
                value = DataBindingUtil.getBindingExprDefault(value);
                if (value == null) {
                    return null;
                }
            }
            if (value.startsWith(PREFIX_RESOURCE_REF)) {
                boolean isFramework = layout.isFramework();
                layout = resources.findResValue(value, isFramework);
                if (layout != null) {
                    value = layout.getValue();
                } else {
                    break;
                }
            } else {
                File file = new File(value);
                if (file.exists()) {
                    return file;
                } else {
                    return null;
                }
            }

            depth++;
        }

        return null;
    }

    /**
     * Returns the given resource name, and possibly prepends a project-configured prefix to the name
     * if set on the Gradle module (but only if it does not already start with the prefix).
     *
     * @param module the corresponding module
     * @param name the resource name
     * @return the resource name, possibly with a new prefix at the beginning of it
     */
    @Contract("_, !null, _ -> !null")
    @Nullable
    public static String prependResourcePrefix(@Nullable Module module, @Nullable String name,
            @Nullable ResourceFolderType folderType) {
        if (module != null) {
            AndroidFacet facet = AndroidFacet.getInstance(module);
            if (facet != null) {
                // TODO: b/23032391
                AndroidModuleModel androidModel = AndroidModuleModel.get(facet);
                if (androidModel != null) {
                    String resourcePrefix = LintUtils.computeResourcePrefix(androidModel.getAndroidProject());
                    if (resourcePrefix != null) {
                        if (name != null) {
                            return name.startsWith(resourcePrefix) ? name
                                    : LintUtils.computeResourceName(resourcePrefix, name, folderType);
                        } else {
                            return resourcePrefix;
                        }
                    }
                }
            }
        }

        return name;
    }

    public static int clamp(int i, int min, int max) {
        return Math.max(min, Math.min(i, max));
    }

    /**
     * Returns the list of all resource names that can be used as a value for one of the {@link ResourceType} in completionTypes
     */
    @NotNull
    public static List<String> getCompletionFromTypes(@NotNull AndroidFacet facet,
            @NotNull EnumSet<ResourceType> completionTypes) {
        return getCompletionFromTypes(facet, completionTypes, true);
    }

    /**
     * Returns the list of all resource names that can be used as a value for one of the {@link ResourceType} in completionTypes,
     * optionally sorting/not sorting the results.
     */
    @NotNull
    public static List<String> getCompletionFromTypes(@NotNull AndroidFacet facet,
            @NotNull EnumSet<ResourceType> completionTypes, boolean sort) {
        EnumSet<ResourceType> types = Sets.newEnumSet(completionTypes, ResourceType.class);

        // Use drawables for mipmaps
        if (types.contains(ResourceType.MIPMAP)) {
            types.add(ResourceType.DRAWABLE);
        } else if (types.contains(ResourceType.DRAWABLE)) {
            types.add(ResourceType.MIPMAP);
        }

        boolean completionTypesContainsColor = types.contains(ResourceType.COLOR);
        if (types.contains(ResourceType.DRAWABLE)) {
            // The Drawable type accepts colors as value but not color state lists.
            types.add(ResourceType.COLOR);
        }

        AppResourceRepository repository = AppResourceRepository.getAppResources(facet, true);
        ResourceVisibilityLookup lookup = repository.getResourceVisibility(facet);
        AndroidPlatform androidPlatform = AndroidPlatform.getInstance(facet.getModule());
        FrameworkResources frameworkResources = null;
        if (androidPlatform != null) {
            AndroidTargetData targetData = androidPlatform.getSdkData().getTargetData(androidPlatform.getTarget());
            try {
                frameworkResources = targetData.getFrameworkResources(true);
            } catch (IOException ignore) {
            }
        }

        List<String> resources = Lists.newArrayListWithCapacity(500);
        for (ResourceType type : types) {
            // If type == ResourceType.COLOR, we want to include file resources (i.e. color state lists) only in the case where
            // color was present in completionTypes, and not if we added it because of the presence of ResourceType.DRAWABLES.
            // For any other ResourceType, we always include file resources.
            boolean includeFileResources = (type != ResourceType.COLOR) || completionTypesContainsColor;
            if (frameworkResources != null) {
                addFrameworkItems(resources, type, includeFileResources, frameworkResources);
            }
            addProjectItems(resources, type, includeFileResources, repository, lookup);
        }

        if (sort) {
            Collections.sort(resources, ResourceHelper::compareResourceReferences);
        }

        return resources;
    }

    /**
     * Comparator function for resource references (e.g. {@code @foo/bar}.
     * Sorts project resources higher than framework resources.
     */
    public static int compareResourceReferences(String resource1, String resource2) {
        int framework1 = resource1.startsWith(ANDROID_PREFIX) ? 1 : 0;
        int framework2 = resource2.startsWith(ANDROID_PREFIX) ? 1 : 0;
        int delta = framework1 - framework2;
        if (delta != 0) {
            return delta;
        }
        return resource1.compareToIgnoreCase(resource2);
    }

    private static void addFrameworkItems(@NotNull List<String> destination, @NotNull ResourceType type,
            boolean includeFileResources, @NotNull FrameworkResources frameworkResources) {
        List<com.android.ide.common.resources.ResourceItem> items;
        items = frameworkResources.getResourceItemsOfType(type);
        for (com.android.ide.common.resources.ResourceItem item : items) {
            if (!includeFileResources) {
                List<com.android.ide.common.resources.ResourceFile> sourceFileList = item.getSourceFileList();
                if (!sourceFileList.isEmpty()
                        && !sourceFileList.get(0).getFolder().getFolder().getName().startsWith(FD_RES_VALUES)) {
                    continue;
                }
            }

            destination.add(PREFIX_RESOURCE_REF + ANDROID_NS_NAME_PREFIX + type.getName() + '/' + item.getName());
        }
    }

    private static void addProjectItems(@NotNull List<String> destination, @NotNull ResourceType type,
            boolean includeFileResources, @NotNull AppResourceRepository repository,
            @Nullable ResourceVisibilityLookup lookup) {
        for (String resourceName : repository.getItemsOfType(type)) {
            if (lookup != null && lookup.isPrivate(type, resourceName)) {
                continue;
            }
            List<ResourceItem> items = repository.getResourceItem(type, resourceName);
            if (items == null) {
                continue;
            }
            if (!includeFileResources) {
                if (items.get(0).getSourceType() != DataFile.FileType.XML_VALUES) {
                    continue;
                }
            }

            destination.add(PREFIX_RESOURCE_REF + type.getName() + '/' + resourceName);
        }
    }

    /**
     * Returns the text content of a given tag
     */
    public static String getTextContent(@NotNull XmlTag tag) {
        // We can't just use tag.getValue().getTrimmedText() here because we need to remove
        // intermediate elements such as <xliff> text:
        // TODO: Make sure I correct handle HTML content for XML items in <string> nodes!
        // For example, for the following string we want to compute "Share with %s":
        // <string name="share">Share with <xliff:g id="application_name" example="Bluetooth">%s</xliff:g></string>
        XmlTag[] subTags = tag.getSubTags();
        XmlText[] textElements = tag.getValue().getTextElements();
        if (subTags.length == 0) {
            if (textElements.length == 1) {
                return getXmlTextValue(textElements[0]);
            } else if (textElements.length == 0) {
                return "";
            }
        }
        StringBuilder sb = new StringBuilder(40);
        appendText(sb, tag);
        return sb.toString();
    }

    private static String getXmlTextValue(XmlText element) {
        PsiElement current = element.getFirstChild();
        if (current != null) {
            if (current.getNextSibling() != null) {
                StringBuilder sb = new StringBuilder();
                for (; current != null; current = current.getNextSibling()) {
                    IElementType type = current.getNode().getElementType();
                    if (type == XmlElementType.XML_CDATA) {
                        PsiElement[] children = current.getChildren();
                        if (children.length == 3) { // XML_CDATA_START, XML_DATA_CHARACTERS, XML_CDATA_END
                            assert children[1].getNode().getElementType() == XmlTokenType.XML_DATA_CHARACTERS;
                            sb.append(children[1].getText());
                        }
                        continue;
                    }
                    sb.append(current.getText());
                }
                return sb.toString();
            } else if (current.getNode().getElementType() == XmlElementType.XML_CDATA) {
                PsiElement[] children = current.getChildren();
                if (children.length == 3) { // XML_CDATA_START, XML_DATA_CHARACTERS, XML_CDATA_END
                    assert children[1].getNode().getElementType() == XmlTokenType.XML_DATA_CHARACTERS;
                    return children[1].getText();
                }
            }
        }

        return element.getText();
    }

    private static void appendText(@NotNull StringBuilder sb, @NotNull XmlTag tag) {
        PsiElement[] children = tag.getChildren();
        for (PsiElement child : children) {
            if (child instanceof XmlText) {
                XmlText text = (XmlText) child;
                sb.append(getXmlTextValue(text));
            } else if (child instanceof XmlTag) {
                XmlTag childTag = (XmlTag) child;
                // xliff support
                if (XLIFF_G_TAG.equals(childTag.getLocalName())
                        && childTag.getNamespace().startsWith(XLIFF_NAMESPACE_PREFIX)) {
                    String example = childTag.getAttributeValue(ATTR_EXAMPLE);
                    if (example != null) {
                        // <xliff:g id="number" example="7">%d</xliff:g> minutes => "(7) minutes"
                        sb.append('(').append(example).append(')');
                        continue;
                    } else {
                        String id = childTag.getAttributeValue(ATTR_ID);
                        if (id != null) {
                            // Step <xliff:g id="step_number">%1$d</xliff:g> => Step ${step_number}
                            sb.append('$').append('{').append(id).append('}');
                            continue;
                        }
                    }
                }
                appendText(sb, childTag);
            }
        }
    }

    /**
     * Stores the information contained in a resource state list.
     */
    public static class StateList {
        private final String myFileName;
        private final String myDirName;
        private final List<StateListState> myStates;

        public StateList(@NotNull String fileName, @NotNull String dirName) {
            myFileName = fileName;
            myDirName = dirName;
            myStates = new ArrayList<>();
        }

        @NotNull
        public String getFileName() {
            return myFileName;
        }

        @NotNull
        public String getDirName() {
            return myDirName;
        }

        @NotNull
        public ResourceFolderType getFolderType() {
            return ResourceFolderType.getFolderType(myDirName);
        }

        /**
         * @return the type of statelist, can be {@link ResourceType#COLOR} or {@link ResourceType#DRAWABLE}
         */
        @NotNull
        public ResourceType getType() {
            final ResourceFolderType resFolderType = getFolderType();
            final ResourceType resType = ResourceType.getEnum(resFolderType.getName());
            assert resType != null;
            return resType;
        }

        @NotNull
        public List<StateListState> getStates() {
            return myStates;
        }

        public void addState(@NotNull StateListState state) {
            myStates.add(state);
        }

        /**
         * @return a list of all the states in this state list that have explicitly or implicitly state_enabled = false
         */
        @NotNull
        public ImmutableList<StateListState> getDisabledStates() {
            ImmutableList.Builder<StateListState> disabledStatesBuilder = ImmutableList.builder();
            ImmutableSet<ImmutableMap<String, Boolean>> remainingObjectStates = ImmutableSet.of(
                    ImmutableMap.of(StateListState.STATE_ENABLED, true),
                    ImmutableMap.of(StateListState.STATE_ENABLED, false));
            // An object state is a particular assignment of boolean values to all possible state list flags.
            // For example, in a world where there exists only three flags (a, b and c), there are 2^3 = 8 possible object state.
            // {a : true, b : false, c : true} is one such state.
            // {a : true} is not an object state, since it does not have values for b or c.
            // But we can use {a : true} as a representation for the set of all object states that have true assigned to a.
            // Since we do not know a priori how many different flags there are, that is how we are going to represent a set of object states.
            // We are using a set of maps, where each map represents a set of object states, and the overall set is their union.
            // For example, the set S = { {a : true} , {b : true, c : false} } is the union of two sets of object states.
            // The first one, described by {a : true} contains 4 object states, and the second one, described by {b : true, c : false} contains 2.
            // Overall this set S represents: {a : true, b : true, c : true}, {a : true, b : true, c : false}, {a : true, b : false, c : true}
            // {a : true, b : false, c : false} and {a : false, b : true, c : false}.
            // It is only 5 object states since {a : true, b : true, c : false} is described by both maps.

            // remainingObjects is going to represent all the object states that have not been matched in the state list until now.
            // So before we start we want to initialise it to represents all possible object states. One easy way to do so is to pick a flag
            // and make two representations {flag : true} and {flag : false} and take their union. We pick "state_enabled" as that flag but any
            // flag could have been used.

            // We now go through the state list state by state.
            for (StateListState state : myStates) {
                // For each state list state, we ask the question : does there exist an object state that could reach this state list state,
                // and match it, and have "state_enabled = true"? If that object state exists, it has to be represented in remainingObjectStates.
                if (!state.matchesWithEnabledObjectState(remainingObjectStates)) {
                    // if there is no such object state, then all the object states that would match this state list state would have
                    // "state_enabled = false", so this state list state is considered disabled.
                    disabledStatesBuilder.add(state);
                }

                // Before looking at the next state list state, we recompute remainingObjectStates so that it does not represent any more
                // the object states that match this state list state.
                remainingObjectStates = removeState(state, remainingObjectStates);
            }
            return disabledStatesBuilder.build();
        }

        /**
         * Returns a representation of all the object states that were in allowed states but do not match the state list state
         */
        @NotNull
        private static ImmutableSet<ImmutableMap<String, Boolean>> removeState(@NotNull StateListState state,
                @NotNull ImmutableSet<ImmutableMap<String, Boolean>> allowedStates) {
            ImmutableSet.Builder<ImmutableMap<String, Boolean>> remainingStates = ImmutableSet.builder();
            Map<String, Boolean> stateAttributes = state.getAttributes();
            for (String attribute : stateAttributes.keySet()) {
                for (ImmutableMap<String, Boolean> allowedState : allowedStates) {
                    if (!allowedState.containsKey(attribute)) {
                        // This allowed state does not have a constraint for attribute. So it represents object states that can take either value
                        // for it. We restrict this representation by adding to it explicitly the opposite constraint to the one in the state list state
                        // so that we remove from this representation all the object states that match the state list state while keeping all the ones
                        // that do not.
                        ImmutableMap.Builder<String, Boolean> newAllowedState = ImmutableMap.builder();
                        newAllowedState.putAll(allowedState).put(attribute, !stateAttributes.get(attribute));
                        remainingStates.add(newAllowedState.build());
                    } else if (allowedState.get(attribute) != stateAttributes.get(attribute)) {
                        // None of the object states represented by allowedState match the state list state. So we keep them all by keeping
                        // the same representation.
                        remainingStates.add(allowedState);
                    }
                }
            }
            return remainingStates.build();
        }
    }

    /**
     * Stores information about a particular state of a resource state list.
     */
    public static class StateListState {
        public static final String STATE_ENABLED = "state_enabled";
        private String myValue;
        private String myAlpha;
        private final Map<String, Boolean> myAttributes;

        public StateListState(@NotNull String value, @NotNull Map<String, Boolean> attributes,
                @Nullable String alpha) {
            myValue = value;
            myAttributes = attributes;
            myAlpha = alpha;
        }

        public void setValue(@NotNull String value) {
            myValue = value;
        }

        public void setAlpha(@Nullable String alpha) {
            myAlpha = alpha;
        }

        @NotNull
        public String getValue() {
            return myValue;
        }

        @Nullable
        public String getAlpha() {
            return myAlpha;
        }

        @NotNull
        public Map<String, Boolean> getAttributes() {
            return myAttributes;
        }

        /**
         * @return a list of all the attribute names. Names are capitalized is capitalize is true
         */
        @NotNull
        public ImmutableList<String> getAttributesNames(boolean capitalize) {
            Map<String, Boolean> attributes = getAttributes();

            if (attributes.isEmpty()) {
                return ImmutableList.of(capitalize ? "Default" : "default");
            }

            ImmutableList.Builder<String> attributeDescriptions = ImmutableList.builder();
            for (Map.Entry<String, Boolean> attribute : attributes.entrySet()) {
                String description = attribute.getKey().substring(STATE_NAME_PREFIX.length());
                if (!attribute.getValue()) {
                    description = "not " + description;
                }
                attributeDescriptions.add(capitalize ? StringUtil.capitalize(description) : description);
            }

            return attributeDescriptions.build();
        }

        /**
         * Checks if there exists an object state that matches this state list state, has state_enabled = true,
         * and is represented in allowedObjectStates.
         * @param allowedObjectStates
         */
        private boolean matchesWithEnabledObjectState(
                @NotNull ImmutableSet<ImmutableMap<String, Boolean>> allowedObjectStates) {
            if (myAttributes.containsKey(STATE_ENABLED) && !myAttributes.get(STATE_ENABLED)) {
                // This state list state has state_enabled = false, so no object state with state_enabled = true could match it
                return false;
            }
            for (Map<String, Boolean> allowedAttributes : allowedObjectStates) {
                if (allowedAttributes.containsKey(STATE_ENABLED) && !allowedAttributes.get(STATE_ENABLED)) {
                    // This allowed object state representation has explicitly state_enabled = false, so it does not represent any object state
                    // with state_enabled = true
                    continue;
                }
                boolean match = true;
                for (String attribute : myAttributes.keySet()) {
                    if (allowedAttributes.containsKey(attribute)
                            && myAttributes.get(attribute) != allowedAttributes.get(attribute)) {
                        // This state list state does not match any of the object states represented by allowedAttributes, since they explicitly
                        // disagree on one particular flag.
                        match = false;
                        break;
                    }
                }
                if (match) {
                    // There is one object state represented in allowedAttributes, that has state_enabled = true, and that matches this
                    // state list state.
                    return true;
                }
            }
            return false;
        }

        @NotNull
        public String getDescription() {
            return Joiner.on(", ").join(getAttributesNames(true));
        }
    }
}