blob: f218abed1443eb4a20cfd4f80e74a5c9dd4035a6 [file] [log] [blame]
/*
* 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();
}
}