| /* |
| * Copyright (C) 2013 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.PREFIX_RESOURCE_REF; |
| import static com.android.SdkConstants.PREFIX_THEME_REF; |
| import static com.android.ide.common.resources.ResourceResolver.MAX_RESOURCE_INDIRECTION; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| 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.ide.common.res2.AbstractResourceRepository; |
| import com.android.ide.common.resources.configuration.FolderConfiguration; |
| import com.android.resources.ResourceType; |
| |
| import java.util.List; |
| |
| /** |
| * Like {@link ResourceResolver} but for a single item, so it does not need the full resource maps |
| * to be resolved up front. Typically used for cases where we may not have fully configured |
| * resource maps and we need to look up a specific value, such as in Android Studio where |
| * a color reference is found in an XML style file, and we want to resolve it in order to |
| * display the final resolved color in the editor margin. |
| */ |
| public class ResourceItemResolver extends RenderResources { |
| private final FolderConfiguration mConfiguration; |
| private final LayoutLog mLogger; |
| private final ResourceProvider mResourceProvider; |
| private ResourceRepository mFrameworkResources; |
| private ResourceResolver mResolver; |
| private AbstractResourceRepository myAppResources; |
| @Nullable private List<ResourceValue> mLookupChain; |
| |
| public ResourceItemResolver( |
| @NonNull FolderConfiguration configuration, |
| @NonNull ResourceProvider resourceProvider, |
| @Nullable LayoutLog logger) { |
| mConfiguration = configuration; |
| mResourceProvider = resourceProvider; |
| mLogger = logger; |
| mResolver = resourceProvider.getResolver(false); |
| } |
| |
| public ResourceItemResolver( |
| @NonNull FolderConfiguration configuration, |
| @NonNull ResourceRepository frameworkResources, |
| @NonNull AbstractResourceRepository appResources, |
| @Nullable LayoutLog logger) { |
| mConfiguration = configuration; |
| mResourceProvider = null; |
| mLogger = logger; |
| mFrameworkResources = frameworkResources; |
| myAppResources = appResources; |
| } |
| |
| @Override |
| @Nullable |
| public ResourceValue resolveResValue(@Nullable ResourceValue resValue) { |
| if (mResolver != null) { |
| return mResolver.resolveResValue(resValue); |
| } |
| if (mLookupChain != null) { |
| mLookupChain.add(resValue); |
| } |
| return resolveResValue(resValue, 0); |
| } |
| |
| @Nullable |
| private ResourceValue resolveResValue(@Nullable ResourceValue resValue, int depth) { |
| if (resValue == null) { |
| return null; |
| } |
| |
| // if the resource value is null, we simply return it. |
| String value = resValue.getValue(); |
| if (value == null) { |
| 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); |
| } |
| |
| @Override |
| @Nullable |
| public ResourceValue findResValue(@Nullable String reference, boolean inFramework) { |
| if (mResolver != null) { |
| return mResolver.findResValue(reference, inFramework); |
| } |
| |
| if (reference == null) { |
| return null; |
| } |
| |
| if (mLookupChain != null && !mLookupChain.isEmpty() && |
| 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); |
| } |
| } |
| |
| ResourceUrl resource = ResourceUrl.parse(reference); |
| if (resource != null && resource.hasValidName()) { |
| if (resource.theme) { |
| // Do theme lookup? We can't do that here; requires full global analysis, so just use |
| // a real resource resolver |
| ResourceResolver resolver = getFullResolver(); |
| if (resolver != null) { |
| return resolver.findResValue(reference, inFramework); |
| } else { |
| return null; |
| } |
| } else if (reference.startsWith(PREFIX_RESOURCE_REF)) { |
| return findResValue(resource.type, resource.name, inFramework || resource.framework); |
| } |
| } |
| |
| // Looks like the value didn't reference anything. Return null. |
| return null; |
| } |
| |
| private ResourceValue findResValue(ResourceType resType, String resName, boolean framework) { |
| // map of ResourceValue for the given type |
| // if allowed, search in the project resources first. |
| if (!framework) { |
| if (myAppResources == null) { |
| assert mResourceProvider != null; |
| myAppResources = mResourceProvider.getAppResources(); |
| if (myAppResources == null) { |
| return null; |
| } |
| } |
| ResourceValue item = null; |
| item = myAppResources.getConfiguredValue(resType, resName, mConfiguration); |
| if (item != null) { |
| if (mLookupChain != null) { |
| mLookupChain.add(item); |
| } |
| return item; |
| } |
| } else { |
| if (mFrameworkResources == null) { |
| assert mResourceProvider != null; |
| mFrameworkResources = mResourceProvider.getFrameworkResources(); |
| if (mFrameworkResources == null) { |
| return null; |
| } |
| } |
| // now search in the framework resources. |
| if (mFrameworkResources.hasResourceItem(resType, resName)) { |
| ResourceItem item = mFrameworkResources.getResourceItem(resType, resName); |
| ResourceValue value = item.getResourceValue(resType, mConfiguration, true); |
| if (value != null && mLookupChain != null) { |
| mLookupChain.add(value); |
| } |
| return value; |
| } |
| } |
| |
| // didn't find the resource anywhere. |
| if (mLogger != null) { |
| mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE, |
| "Couldn't resolve resource @" + |
| (framework ? "android:" : "") + resType + "/" + resName, |
| new ResourceValue(resType, resName, framework)); |
| } |
| return null; |
| } |
| |
| @Override |
| public StyleResourceValue getCurrentTheme() { |
| ResourceResolver resolver = getFullResolver(); |
| if (resolver != null) { |
| return resolver.getCurrentTheme(); |
| } |
| |
| 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); |
| } |
| |
| // For theme lookup, we need to delegate to a full resource resolver |
| |
| @Override |
| public StyleResourceValue getTheme(String name, boolean frameworkTheme) { |
| assert false; // This method shouldn't be called on this resolver |
| return super.getTheme(name, frameworkTheme); |
| } |
| |
| @Override |
| public boolean themeIsParentOf(StyleResourceValue parentTheme, StyleResourceValue childTheme) { |
| assert false; // This method shouldn't be called on this resolver |
| return super.themeIsParentOf(parentTheme, childTheme); |
| } |
| |
| @SuppressWarnings("deprecation") |
| @Override |
| public ResourceValue findItemInTheme(String itemName) { |
| ResourceResolver resolver = getFullResolver(); |
| return resolver != null ? resolver.findItemInTheme(itemName) : null; |
| } |
| |
| @Override |
| public ResourceValue findItemInTheme(String attrName, boolean isFrameworkAttr) { |
| ResourceResolver resolver = getFullResolver(); |
| return resolver != null ? resolver.findItemInTheme(attrName, isFrameworkAttr) : null; |
| } |
| |
| @SuppressWarnings("deprecation") |
| @Override |
| public ResourceValue findItemInStyle(StyleResourceValue style, String attrName) { |
| ResourceResolver resolver = getFullResolver(); |
| return resolver != null ? resolver.findItemInStyle(style, attrName) : null; |
| } |
| |
| @Override |
| public ResourceValue findItemInStyle(StyleResourceValue style, String attrName, |
| boolean isFrameworkAttr) { |
| ResourceResolver resolver = getFullResolver(); |
| return resolver != null ? resolver.findItemInStyle(style, attrName, isFrameworkAttr) : null; |
| } |
| |
| @Override |
| public StyleResourceValue getParent(StyleResourceValue style) { |
| ResourceResolver resolver = getFullResolver(); |
| return resolver != null ? resolver.getParent(style) : null; |
| } |
| |
| private ResourceResolver getFullResolver() { |
| if (mResolver == null) { |
| if (mResourceProvider == null) { |
| return null; |
| } |
| mResolver = mResourceProvider.getResolver(true); |
| if (mResolver != null) { |
| if (mLookupChain != null) { |
| mResolver = mResolver.createRecorder(mLookupChain); |
| } |
| } |
| |
| } |
| return mResolver; |
| } |
| |
| /** |
| * Optional method to set a list the resolver should record all value resolutions |
| * into. Useful if you want to find out the resolution chain for a resource, |
| * e.g. {@code @color/buttonForeground => @color/foreground => @android:color/black }. |
| * <p> |
| * There is no getter. Clients setting this list should look it up themselves. |
| * Note also that if this resolver has to delegate to a full resource resolver, |
| * e.g. to follow theme attributes, those resolutions will not be recorded. |
| * |
| * @param lookupChain the list to set, or null |
| */ |
| public void setLookupChainList(@Nullable List<ResourceValue> lookupChain) { |
| mLookupChain = lookupChain; |
| } |
| |
| /** Returns the lookup chain being used by this resolver */ |
| @Nullable |
| public List<ResourceValue> getLookupChain() { |
| return mLookupChain; |
| } |
| |
| /** |
| * Returns a display string for a resource lookup |
| * |
| * @param type the resource type |
| * @param name the resource name |
| * @param isFramework whether the item is in the framework |
| * @param lookupChain the list of resolved items to display |
| * @return the display string |
| */ |
| public static String getDisplayString( |
| @NonNull ResourceType type, |
| @NonNull String name, |
| boolean isFramework, |
| @NonNull List<ResourceValue> lookupChain) { |
| String url = ResourceUrl.create(type, name, isFramework, false).toString(); |
| return getDisplayString(url, lookupChain); |
| } |
| |
| /** |
| * Returns a display string for a resource lookup |
| * @param url the resource url, such as {@code @string/foo} |
| * @param lookupChain the list of resolved items to display |
| * @return the display string |
| */ |
| @NonNull |
| public static String getDisplayString( |
| @NonNull String url, |
| @NonNull List<ResourceValue> lookupChain) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(url); |
| String prev = url; |
| for (ResourceValue element : lookupChain) { |
| if (element == null) { |
| continue; |
| } |
| String value = element.getValue(); |
| if (value == null) { |
| continue; |
| } |
| String text = value; |
| if (text.equals(prev)) { |
| continue; |
| } |
| |
| sb.append(" => "); |
| |
| // Strip paths |
| if (!(text.startsWith(PREFIX_THEME_REF) || text.startsWith(PREFIX_RESOURCE_REF))) { |
| int end = Math.max(text.lastIndexOf('/'), text.lastIndexOf('\\')); |
| if (end != -1) { |
| text = text.substring(end + 1); |
| } |
| } |
| sb.append(text); |
| |
| prev = value; |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Interface implemented by clients of the {@link ResourceItemResolver} which allows |
| * it to lazily look up the project resources, the framework resources and optionally |
| * to provide a fully configured resource resolver, if any |
| */ |
| public interface ResourceProvider { |
| @Nullable ResourceResolver getResolver(boolean createIfNecessary); |
| @Nullable ResourceRepository getFrameworkResources(); |
| @Nullable AbstractResourceRepository getAppResources(); |
| } |
| } |