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

com.android.ide.common.resources.ResourceResolver 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.ide.common.resources;

import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
import static com.android.SdkConstants.PREFIX_ANDROID;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.REFERENCE_STYLE;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ArrayResourceValue;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.resources.ResourceType;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ResourceResolver extends RenderResources {
    public static final String THEME_NAME = "Theme";
    public static final String THEME_NAME_DOT = "Theme.";
    public static final String XLIFF_NAMESPACE_PREFIX = "urn:oasis:names:tc:xliff:document:";
    public static final String XLIFF_G_TAG = "g";
    public static final String ATTR_EXAMPLE = "example";

    /**
     * Constant passed to {@link #setDeviceDefaults(String)} to indicate the DeviceDefault styles
     * should point to the default styles
     */
    public static final String LEGACY_THEME = "";

    public static final Pattern DEVICE_DEFAULT_PATTERN = Pattern
            .compile("(\\p{Alpha}+)?\\.?DeviceDefault\\.?(.+)?");

    /**
     * Number of indirections we'll follow for resource resolution before assuming there
     * is a cyclic dependency error in the input
     */
    public static final int MAX_RESOURCE_INDIRECTION = 50;

    private final Map> mProjectResources;
    private final Map> mFrameworkResources;
    private final Map mStyleInheritanceMap =
        new HashMap();
    private StyleResourceValue mDefaultTheme;
    // The resources should be searched in all the themes in the list in order.
    private final List mThemes;
    private FrameworkResourceIdProvider mFrameworkProvider;
    private LayoutLog mLogger;
    private String mThemeName;
    private boolean mIsProjectTheme;
    // AAPT flattens the names by converting '.', '-' and ':' to '_'. These maps undo the
    // flattening. We prefer lazy initialization of these maps since they are not used in many
    // applications.
    @Nullable
    private Map mReverseFrameworkStyles;
    @Nullable
    private Map mReverseProjectStyles;
    /** Contains the default parent for DeviceDefault styles (e.g. for API 18, "Holo") */
    private String mDeviceDefaultParent = null;

    private ResourceResolver(
            Map> projectResources,
            Map> frameworkResources,
            String themeName, boolean isProjectTheme) {
        mProjectResources = projectResources;
        mFrameworkResources = frameworkResources;
        mThemeName = themeName;
        mIsProjectTheme = isProjectTheme;
        mThemes = new LinkedList();
    }

    /**
     * Creates a new {@link ResourceResolver} object.
     *
     * @param projectResources the project resources.
     * @param frameworkResources the framework resources.
     * @param themeName the name of the current theme.
     * @param isProjectTheme Is this a project theme?
     * @return a new {@link ResourceResolver}
     */
    public static ResourceResolver create(
            Map> projectResources,
            Map> frameworkResources,
            String themeName, boolean isProjectTheme) {

        ResourceResolver resolver = new ResourceResolver(projectResources, frameworkResources,
                themeName, isProjectTheme);
        resolver.computeStyleMaps();

        return resolver;
    }

    /**
     * This will override the DeviceDefault styles so they point to the given parent styles (e.g. If
     * "Material" is passed, Theme.DeviceDefault parent will become Theme.Material). This patches
     * all the styles (not only themes) and takes care of the light and dark variants. If {@link
     * #LEGACY_THEME} is passed, parents will be directed to the default themes (i.e. Theme).
     */
    public void setDeviceDefaults(@NonNull String deviceDefaultParent) {
        if (deviceDefaultParent.equals(mDeviceDefaultParent)) {
            // No need to patch again with the same parent
            return;
        }

        mDeviceDefaultParent = deviceDefaultParent;
        // The joiner will ignore nulls so if the caller specified an empty name, we replace it with
        // a null so it gets ignored
        String parentName = Strings.emptyToNull(deviceDefaultParent);

        // The regexp gets the prefix and suffix if they exist (without the dots)
        for (ResourceValue value : mFrameworkResources.get(ResourceType.STYLE).values()) {
            Matcher matcher = DEVICE_DEFAULT_PATTERN.matcher(value.getName());
            if (!matcher.matches()) {
                continue;
            }

            String newParentStyle =
                    Joiner.on('.').skipNulls().join(matcher.group(1), parentName,
                            ((matcher.groupCount() > 1) ? matcher.group(2) : null));
            patchFrameworkStyleParent(value.getName(), newParentStyle);
        }
    }

    /**
     * Updates the parent of a given framework style. This method is used to patch DeviceDefault
     * styles when using a CompatibilityTarget
     */
    private void patchFrameworkStyleParent(String childStyleName, String parentName) {
        Map map = mFrameworkResources.get(ResourceType.STYLE);
        if (map != null) {
            StyleResourceValue from = (StyleResourceValue)map.get(childStyleName);
            StyleResourceValue to = (StyleResourceValue)map.get(parentName);

            if (from != null && to != null) {
                StyleResourceValue newStyle = new StyleResourceValue(ResourceType.STYLE,
                        from.getName(), parentName, from.isFramework());
                newStyle.replaceWith(from);
                mStyleInheritanceMap.put(newStyle, to);
            }
        }
    }

    // ---- Methods to help dealing with older LayoutLibs.

    public String getThemeName() {
        return mThemeName;
    }

    public boolean isProjectTheme() {
        return mIsProjectTheme;
    }

    public Map> getProjectResources() {
        return mProjectResources;
    }

    public Map> getFrameworkResources() {
        return mFrameworkResources;
    }

    // ---- RenderResources Methods

    @Override
    public void setFrameworkResourceIdProvider(FrameworkResourceIdProvider provider) {
        mFrameworkProvider = provider;
    }

    @Override
    public void setLogger(LayoutLog logger) {
        mLogger = logger;
    }

    @Override
    public StyleResourceValue getDefaultTheme() {
        return mDefaultTheme;
    }

    @Override
    public void applyStyle(StyleResourceValue theme, boolean useAsPrimary) {
        if (theme == null) {
            return;
        }
        if (useAsPrimary) {
            mThemes.add(0, theme);
        } else {
            mThemes.add(theme);
        }
    }

    @Override
    public void clearStyles() {
        mThemes.clear();
        mThemes.add(mDefaultTheme);
    }

    @Override
    public List getAllThemes() {
        return mThemes;
    }

  @Override
    public StyleResourceValue getTheme(String name, boolean frameworkTheme) {
        ResourceValue theme;

        if (frameworkTheme) {
            Map frameworkStyleMap = mFrameworkResources.get(
                    ResourceType.STYLE);
            theme = frameworkStyleMap.get(name);
        } else {
            Map projectStyleMap = mProjectResources.get(ResourceType.STYLE);
            theme = projectStyleMap.get(name);
        }

        if (theme instanceof StyleResourceValue) {
            return (StyleResourceValue) theme;
        }

        return null;
    }

    @Override
    public boolean themeIsParentOf(StyleResourceValue parentTheme, StyleResourceValue childTheme) {
        do {
            childTheme = mStyleInheritanceMap.get(childTheme);
            if (childTheme == null) {
                return false;
            } else if (childTheme == parentTheme) {
                return true;
            }
        } while (true);
    }

    @Override
    public ResourceValue getFrameworkResource(ResourceType resourceType, String resourceName) {
        return getResource(resourceType, resourceName, mFrameworkResources);
    }

    @Override
    public ResourceValue getProjectResource(ResourceType resourceType, String resourceName) {
        return getResource(resourceType, resourceName, mProjectResources);
    }

    @SuppressWarnings("deprecation") // Required to support older layoutlib clients
    @Override
    @Deprecated
    public ResourceValue findItemInStyle(StyleResourceValue style, String attrName) {
        // this method is deprecated because it doesn't know about the namespace of the
        // attribute so we search for the project namespace first and then in the
        // android namespace if needed.
        ResourceValue item = findItemInStyle(style, attrName, false);
        if (item == null) {
            item = findItemInStyle(style, attrName, true);
        }

        return item;
    }

    @Override
    public ResourceValue findItemInStyle(StyleResourceValue style, String itemName,
            boolean isFrameworkAttr) {
        return findItemInStyle(style, itemName, isFrameworkAttr, 0);
    }

    private ResourceValue findItemInStyle(StyleResourceValue style, String itemName,
                                          boolean isFrameworkAttr, int depth) {
        ResourceValue item = style.getItem(itemName, isFrameworkAttr);

        // if we didn't find it, we look in the parent style (if applicable)
        //noinspection VariableNotUsedInsideIf
        if (item == null) {
            StyleResourceValue parentStyle = mStyleInheritanceMap.get(style);
            if (parentStyle != null) {
                if (depth >= MAX_RESOURCE_INDIRECTION) {
                    if (mLogger != null) {
                        mLogger.error(LayoutLog.TAG_BROKEN,
                                String.format("Cyclic style parent definitions: %1$s",
                                        computeCyclicStyleChain(style)),
                                null);
                    }

                    return null;
                }

                return findItemInStyle(parentStyle, itemName, isFrameworkAttr, depth + 1);
            }
        }

        return item;
    }

    private String computeCyclicStyleChain(StyleResourceValue style) {
        StringBuilder sb = new StringBuilder(100);
        appendStyleParents(style, new HashSet(), 0, sb);
        return sb.toString();
    }

    private void appendStyleParents(StyleResourceValue style, Set seen,
            int depth, StringBuilder sb) {
        if (depth >= MAX_RESOURCE_INDIRECTION) {
            sb.append("...");
            return;
        }

        boolean haveSeen = seen.contains(style);
        seen.add(style);

        sb.append('"');
        if (style.isFramework()) {
            sb.append(PREFIX_ANDROID);
        }
        sb.append(style.getName());
        sb.append('"');

        if (haveSeen) {
            return;
        }

        StyleResourceValue parentStyle = mStyleInheritanceMap.get(style);
        if (parentStyle != null) {
            if (style.getParentStyle() != null) {
                sb.append(" specifies parent ");
            } else {
                sb.append(" implies parent ");
            }

            appendStyleParents(parentStyle, seen, depth + 1, sb);
        }
    }

    @Override
    public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) {
        if (reference == null) {
            return null;
        }

        ResourceUrl resource = ResourceUrl.parse(reference);
        if (resource != null && resource.hasValidName()) {
            if (resource.theme) {
                // no theme? no need to go further!
                if (mDefaultTheme == null) {
                    return null;
                }

                if (resource.type != ResourceType.ATTR) {
                    // At this time, no support for ?type/name where type is not "attr"
                    return null;
                }

                // Now look for the item in the theme, starting with the current one.
                return findItemInTheme(resource.name, forceFrameworkOnly || resource.framework);
            } else {
                return findResValue(resource, forceFrameworkOnly);
            }
        }

        // Looks like the value didn't reference anything. Return null.
        return null;
    }

    @Override
    public ResourceValue resolveValue(ResourceType type, String name, String value,
            boolean isFrameworkValue) {
        if (value == null) {
            return null;
        }

        // get the ResourceValue referenced by this value
        ResourceValue resValue = findResValue(value, isFrameworkValue);

        // if resValue is null, but value is not null, this means it was not a reference.
        // we return the name/value wrapper in a ResourceValue. the isFramework flag doesn't
        // matter.
        if (resValue == null) {
            return new ResourceValue(type, name, value, isFrameworkValue);
        }

        // we resolved a first reference, but we need to make sure this isn't a reference also.
        return resolveResValue(resValue);
    }

    @Override
    public ResourceValue resolveResValue(ResourceValue resValue) {
        return resolveResValue(resValue, 0);
    }

    private ResourceValue resolveResValue(ResourceValue resValue, int depth) {
        if (resValue == null) {
            return null;
        }

        String value = resValue.getValue();
        if (value == null || resValue instanceof ArrayResourceValue) {
            // If there's no value or this an array resource (eg. ), return.
            return resValue;
        }

        // else attempt to find another ResourceValue referenced by this one.
        ResourceValue resolvedResValue = findResValue(value, resValue.isFramework());

        // if the value did not reference anything, then we simply return the input value
        if (resolvedResValue == null) {
            return resValue;
        }

        // detect potential loop due to mishandled namespace in attributes
        if (resValue == resolvedResValue || depth >= MAX_RESOURCE_INDIRECTION) {
            if (mLogger != null) {
                mLogger.error(LayoutLog.TAG_BROKEN,
                        String.format("Potential stack overflow trying to resolve '%s': cyclic resource definitions? Render may not be accurate.", value),
                        null);
            }
            return resValue;
        }

        // otherwise, we attempt to resolve this new value as well
        return resolveResValue(resolvedResValue, depth + 1);
    }

    // ---- Private helper methods.

    /**
     * Searches for, and returns a {@link ResourceValue} by its parsed reference.
     * @param resource the parsed resource
     * @param forceFramework if true, the method does not search in the
     * project resources
     */
    private ResourceValue findResValue(ResourceUrl resource, boolean forceFramework) {
        // map of ResourceValue for the given type
        Map typeMap;
        ResourceType resType = resource.type;
        String resName = resource.name;
        boolean isFramework = forceFramework || resource.framework;

        if (!isFramework) {
            typeMap = mProjectResources.get(resType);
            ResourceValue item = typeMap.get(resName);
            if (item != null) {
                return item;
            }
        } else {
            typeMap = mFrameworkResources.get(resType);
            if (typeMap != null) {
                ResourceValue item = typeMap.get(resName);
                if (item != null) {
                    return item;
                }
            }

            // if it was not found and the type is an id, it is possible that the ID was
            // generated dynamically when compiling the framework resources.
            // Look for it in the R map.
            if (mFrameworkProvider != null && resType == ResourceType.ID) {
                if (mFrameworkProvider.getId(resType, resName) != null) {
                    return new ResourceValue(resType, resName, true);
                }
            }
        }

      // didn't find the resource anywhere.
        if (!resource.create && mLogger != null) {
            mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE,
                    "Couldn't resolve resource @" +
                    (isFramework ? "android:" : "") + resType + "/" + resName,
                    new ResourceValue(resType, resName, isFramework));
        }
        return null;
    }

    private ResourceValue getResource(ResourceType resourceType, String resourceName,
            Map> resourceRepository) {
        Map typeMap = resourceRepository.get(resourceType);
        if (typeMap != null) {
            ResourceValue item = typeMap.get(resourceName);
            if (item != null) {
                item = resolveResValue(item);
                return item;
            }
        }

        // didn't find the resource anywhere.
        return null;
    }

    /**
     * Compute style information from the given list of style for the project and framework.
     */
    private void computeStyleMaps() {
        Map projectStyleMap = mProjectResources.get(ResourceType.STYLE);
        Map frameworkStyleMap = mFrameworkResources.get(ResourceType.STYLE);

        // first, get the theme
        ResourceValue theme = null;

        // project theme names have been prepended with a *
        if (mIsProjectTheme) {
            if (projectStyleMap != null) {
                theme = projectStyleMap.get(mThemeName);
            }
        } else {
            if (frameworkStyleMap != null) {
                theme = frameworkStyleMap.get(mThemeName);
            }
        }

        if (theme instanceof StyleResourceValue) {
            // compute the inheritance map for both the project and framework styles
            computeStyleInheritance(projectStyleMap.values(), projectStyleMap,
                    frameworkStyleMap);

            // Compute the style inheritance for the framework styles/themes.
            // Since, for those, the style parent values do not contain 'android:'
            // we want to force looking in the framework style only to avoid using
            // similarly named styles from the project.
            // To do this, we pass null in lieu of the project style map.
            if (frameworkStyleMap != null) {
                computeStyleInheritance(frameworkStyleMap.values(), null /*inProjectStyleMap */,
                        frameworkStyleMap);
            }

            mDefaultTheme = (StyleResourceValue) theme;
            mThemes.clear();
            mThemes.add(mDefaultTheme);
        }
    }

    /**
     * Compute the parent style for all the styles in a given list.
     * @param styles the styles for which we compute the parent.
     * @param inProjectStyleMap the map of project styles.
     * @param inFrameworkStyleMap the map of framework styles.
     */
    private void computeStyleInheritance(Collection styles,
            Map inProjectStyleMap,
            Map inFrameworkStyleMap) {
        // This method will recalculate the inheritance map so any modifications done by
        // setDeviceDefault will be lost. Set mDeviceDefaultParent to null so when setDeviceDefault
        // is called again, it knows that it needs to modify the inheritance map again.
        mDeviceDefaultParent = null;
        for (ResourceValue value : styles) {
            if (value instanceof StyleResourceValue) {
                StyleResourceValue style = (StyleResourceValue)value;

                String parentName = getParentName(style);

                if (parentName != null) {
                    StyleResourceValue parentStyle = getStyle(parentName, inProjectStyleMap,
                            inFrameworkStyleMap);

                    if (parentStyle != null) {
                        mStyleInheritanceMap.put(style, parentStyle);
                    }
                }
            }
        }
    }

    /**
     * Computes the name of the parent style, or null if the style is a root style.
     */
    @Nullable
    public static String getParentName(StyleResourceValue style) {
        String parentName = style.getParentStyle();
        if (parentName != null) {
            return parentName;
        }

        String styleName = style.getName();
        int index = styleName.lastIndexOf('.');
        if (index != -1) {
            return styleName.substring(0, index);
        }
        return null;
    }

    @Override
    @Nullable
    public StyleResourceValue getParent(@NonNull StyleResourceValue style) {
        return mStyleInheritanceMap.get(style);
    }

    @Override
    @Nullable
    public StyleResourceValue getStyle(@NonNull String styleName, boolean isFramework) {
        ResourceValue res;
        Map styleMap;

        // First check if we can find the style directly.
        if (isFramework) {
            styleMap = mFrameworkResources.get(ResourceType.STYLE);
        } else {
            styleMap = mProjectResources.get(ResourceType.STYLE);
        }
        res = getStyleFromMap(styleMap, styleName);
        if (res != null) {
            // If the obtained resource is not StyleResourceValue, return null.
            return res instanceof StyleResourceValue ? (StyleResourceValue) res : null;
        }

        // We cannot find the style directly. The style name may have been flattened by AAPT for use
        // in the R class. Try and obtain the original name.
        String xmlStyleName = getReverseStyleMap(isFramework)
                .get(getNormalizedStyleName(styleName));
        if (!styleName.equals(xmlStyleName)) {
            res = getStyleFromMap(styleMap, xmlStyleName);
        }
        return res instanceof StyleResourceValue ? (StyleResourceValue) res : null;
    }

    @Override
    @Nullable
    public String getXmlName(@NonNull ResourceType type, @NonNull String name,
            boolean isFramework) {
        if (type != ResourceType.STYLE) {
            // The method is currently implemented for styles only.
            return null;
        }
        Map reverseStyles;
        reverseStyles = getReverseStyleMap(isFramework);
        return reverseStyles.get(name);
    }

    /**
     * Returns the reverse style map using the appropriate resources. It also initializes the map if
     * it hasn't been initialized yet.
     */
    private Map getReverseStyleMap(boolean isFramework) {
        if (isFramework) {
            // The reverse style map may need to be initialized.
            if (mReverseFrameworkStyles == null) {
                Map styleMap = mFrameworkResources.get(ResourceType.STYLE);
                mReverseFrameworkStyles = createReverseStyleMap(styleMap.keySet());
            }
            return mReverseFrameworkStyles;
        } else {
            if (mReverseProjectStyles == null) {
                Map styleMap = mProjectResources.get(ResourceType.STYLE);
                mReverseProjectStyles = createReverseStyleMap(styleMap.keySet());
            }
            return mReverseProjectStyles;
        }
    }

    /**
     * Create a map from the normalized form of the style names in {@code styles} to the original
     * style name.
     *
     * @see #getNormalizedStyleName(String)
     */
    private static Map createReverseStyleMap(@NonNull Set styles) {
        Map reverseStyles = Maps.newHashMapWithExpectedSize(styles.size());
        for (String style : styles) {
            reverseStyles.put(getNormalizedStyleName(style), style);
        }
        return reverseStyles;
    }

    /**
     * Flatten the styleName like AAPT by replacing dots, dashes and colons with underscores.
     */
    @NonNull
    private static String getNormalizedStyleName(@NonNull String styleName) {
        return styleName.replace('.', '_').replace('-', '_').replace(':', '_');
    }

    /**
     * Search for the style in the given map and log an error if the obtained resource is not
     * {@link StyleResourceValue}.
     *
     * @return The {@link ResourceValue} found in the map.
     */
    @Nullable
    private ResourceValue getStyleFromMap(@NonNull Map styleMap,
            @NonNull String styleName) {
        ResourceValue res;
        res = styleMap.get(styleName);
        if (res != null) {
            if (!(res instanceof StyleResourceValue) && mLogger != null) {
                mLogger.error(null, String.format(
                                "Style %1$s is not of type STYLE (instead %2$s)",
                                styleName, res.getResourceType().toString()),
                        null);
            }
        }
        return res;
    }

    /**
     * Searches for and returns the {@link StyleResourceValue} from a given name.
     * 

The format of the name can be: *

    *
  • [android:]<name>
  • *
  • [android:]style/<name>
  • *
  • @[android:]style/<name>
  • *
* @param parentName the name of the style. * @param inProjectStyleMap the project style map. Can be null * @param inFrameworkStyleMap the framework style map. * @return The matching {@link StyleResourceValue} object or null if not found. */ private StyleResourceValue getStyle(String parentName, Map inProjectStyleMap, Map inFrameworkStyleMap) { boolean frameworkOnly = false; String name = parentName; // remove the useless @ if it's there if (name.startsWith(PREFIX_RESOURCE_REF)) { name = name.substring(PREFIX_RESOURCE_REF.length()); } // check for framework identifier. if (name.startsWith(PREFIX_ANDROID)) { frameworkOnly = true; name = name.substring(PREFIX_ANDROID.length()); } // at this point we could have the format /. we want only the name as long as // the type is style. if (name.startsWith(REFERENCE_STYLE)) { name = name.substring(REFERENCE_STYLE.length()); } else if (name.indexOf('/') != -1) { return null; } ResourceValue parent = null; // if allowed, search in the project resources. if (!frameworkOnly && inProjectStyleMap != null) { parent = inProjectStyleMap.get(name); } // if not found, then look in the framework resources. if (parent == null) { if (inFrameworkStyleMap == null) { return null; } parent = inFrameworkStyleMap.get(name); } // make sure the result is the proper class type and return it. if (parent instanceof StyleResourceValue) { return (StyleResourceValue)parent; } if (mLogger != null) { mLogger.error(LayoutLog.TAG_RESOURCES_RESOLVE, String.format("Unable to resolve parent style name: %s", parentName), null /*data*/); } return null; } /** Returns true if the given {@link ResourceValue} represents a theme */ public boolean isTheme( @NonNull ResourceValue value, @Nullable Map cache) { return isTheme(value, cache, 0); } private boolean isTheme( @NonNull ResourceValue value, @Nullable Map cache, int depth) { if (cache != null) { Boolean known = cache.get(value); if (known != null) { return known; } } if (value instanceof StyleResourceValue) { StyleResourceValue srv = (StyleResourceValue) value; String name = srv.getName(); if (srv.isFramework() && (name.equals(THEME_NAME) || name.startsWith(THEME_NAME_DOT))) { if (cache != null) { cache.put(value, true); } return true; } StyleResourceValue parentStyle = mStyleInheritanceMap.get(srv); if (parentStyle != null) { if (depth >= MAX_RESOURCE_INDIRECTION) { if (mLogger != null) { mLogger.error(LayoutLog.TAG_BROKEN, String.format("Cyclic style parent definitions: %1$s", computeCyclicStyleChain(srv)), null); } return false; } boolean result = isTheme(parentStyle, cache, depth + 1); if (cache != null) { cache.put(value, result); } return result; } } return false; } /** * Returns true if the given {@code themeStyle} extends the theme given by * {@code parentStyle} */ public boolean themeExtends(@NonNull String parentStyle, @NonNull String themeStyle) { ResourceValue parentValue = findResValue(parentStyle, parentStyle.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)); if (parentValue instanceof StyleResourceValue) { ResourceValue themeValue = findResValue(themeStyle, themeStyle.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)); if (themeValue == parentValue) { return true; } if (themeValue instanceof StyleResourceValue) { return themeIsParentOf((StyleResourceValue) parentValue, (StyleResourceValue) themeValue); } } return false; } /** * Creates a new {@link ResourceResolver} which records all resource resolution * lookups into the given list. Note that it is the responsibility of the caller * to clear/reset the list between subsequent lookup operations. * * @param lookupChain the list to write resource lookups into * @return a new {@link ResourceResolver} */ public ResourceResolver createRecorder(List lookupChain) { ResourceResolver resolver = new RecordingResourceResolver( lookupChain, mProjectResources, mFrameworkResources, mThemeName, mIsProjectTheme); resolver.mFrameworkProvider = mFrameworkProvider; resolver.mLogger = mLogger; resolver.mDefaultTheme = mDefaultTheme; resolver.mStyleInheritanceMap.putAll(mStyleInheritanceMap); resolver.mThemes.addAll(mThemes); return resolver; } private static class RecordingResourceResolver extends ResourceResolver { @NonNull private List mLookupChain; private RecordingResourceResolver( @NonNull List lookupChain, @NonNull Map> projectResources, @NonNull Map> frameworkResources, @NonNull String themeName, boolean isProjectTheme) { super(projectResources, frameworkResources, themeName, isProjectTheme); mLookupChain = lookupChain; } @Override public ResourceValue resolveResValue(ResourceValue resValue) { if (resValue != null) { mLookupChain.add(resValue); } return super.resolveResValue(resValue); } @Override public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) { if (!mLookupChain.isEmpty() && reference != null && reference.startsWith(PREFIX_RESOURCE_REF)) { ResourceValue prev = mLookupChain.get(mLookupChain.size() - 1); if (!reference.equals(prev.getValue())) { ResourceValue next = new ResourceValue(prev.getResourceType(), prev.getName(), prev.isFramework()); next.setValue(reference); mLookupChain.add(next); } } ResourceValue resValue = super.findResValue(reference, forceFrameworkOnly); if (resValue != null) { mLookupChain.add(resValue); } return resValue; } @Override public ResourceValue findItemInStyle(StyleResourceValue style, String itemName, boolean isFrameworkAttr) { ResourceValue value = super.findItemInStyle(style, itemName, isFrameworkAttr); if (value != null) { mLookupChain.add(value); } return value; } @Override public ResourceValue findItemInTheme(String attrName, boolean isFrameworkAttr) { ResourceValue value = super.findItemInTheme(attrName, isFrameworkAttr); if (value != null) { mLookupChain.add(value); } return value; } @Override public ResourceValue resolveValue(ResourceType type, String name, String value, boolean isFrameworkValue) { ResourceValue resourceValue = super.resolveValue(type, name, value, isFrameworkValue); if (resourceValue != null) { mLookupChain.add(resourceValue); } return resourceValue; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy