blob: 219c93f01fba236ce15407cddb3a2ce08bcef02c [file] [log] [blame]
/*
* 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_PREFIX;
import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
import static com.android.SdkConstants.PREFIX_ANDROID;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.PREFIX_THEME_REF;
import static com.android.SdkConstants.REFERENCE_STYLE;
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 java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class ResourceResolver extends RenderResources {
private final Map<ResourceType, Map<String, ResourceValue>> mProjectResources;
private final Map<ResourceType, Map<String, ResourceValue>> mFrameworkResources;
private final Map<StyleResourceValue, StyleResourceValue> mStyleInheritanceMap =
new HashMap<StyleResourceValue, StyleResourceValue>();
private StyleResourceValue mTheme;
private FrameworkResourceIdProvider mFrameworkProvider;
private LayoutLog mLogger;
private String mThemeName;
private boolean mIsProjectTheme;
private ResourceResolver(
Map<ResourceType, Map<String, ResourceValue>> projectResources,
Map<ResourceType, Map<String, ResourceValue>> frameworkResources) {
mProjectResources = projectResources;
mFrameworkResources = frameworkResources;
}
/**
* 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<ResourceType, Map<String, ResourceValue>> projectResources,
Map<ResourceType, Map<String, ResourceValue>> frameworkResources,
String themeName, boolean isProjectTheme) {
ResourceResolver resolver = new ResourceResolver(
projectResources, frameworkResources);
resolver.computeStyleMaps(themeName, isProjectTheme);
return resolver;
}
// ---- Methods to help dealing with older LayoutLibs.
public String getThemeName() {
return mThemeName;
}
public boolean isProjectTheme() {
return mIsProjectTheme;
}
public Map<ResourceType, Map<String, ResourceValue>> getProjectResources() {
return mProjectResources;
}
public Map<ResourceType, Map<String, ResourceValue>> getFrameworkResources() {
return mFrameworkResources;
}
// ---- RenderResources Methods
@Override
public void setFrameworkResourceIdProvider(FrameworkResourceIdProvider provider) {
mFrameworkProvider = provider;
}
@Override
public void setLogger(LayoutLog logger) {
mLogger = logger;
}
@Override
public StyleResourceValue getCurrentTheme() {
return mTheme;
}
@Override
public StyleResourceValue getTheme(String name, boolean frameworkTheme) {
ResourceValue theme = null;
if (frameworkTheme) {
Map<String, ResourceValue> frameworkStyleMap = mFrameworkResources.get(
ResourceType.STYLE);
theme = frameworkStyleMap.get(name);
} else {
Map<String, ResourceValue> 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);
}
@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 /*isFrameworkAttr*/);
if (item == null) {
item = findItemInStyle(style, attrName, true /*isFrameworkAttr*/);
}
return item;
}
@Override
public ResourceValue findItemInStyle(StyleResourceValue style, String itemName,
boolean isFrameworkAttr) {
ResourceValue item = style.findValue(itemName, isFrameworkAttr);
// if we didn't find it, we look in the parent style (if applicable)
if (item == null && mStyleInheritanceMap != null) {
StyleResourceValue parentStyle = mStyleInheritanceMap.get(style);
if (parentStyle != null) {
return findItemInStyle(parentStyle, itemName, isFrameworkAttr);
}
}
return item;
}
@Override
public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) {
if (reference == null) {
return null;
}
if (reference.startsWith(PREFIX_THEME_REF)
&& reference.length() > PREFIX_THEME_REF.length()) {
// no theme? no need to go further!
if (mTheme == null) {
return null;
}
boolean frameworkOnly = false;
// eliminate the prefix from the string
String originalReference = reference;
if (reference.startsWith(ANDROID_THEME_PREFIX)) {
frameworkOnly = true;
reference = reference.substring(ANDROID_THEME_PREFIX.length());
} else {
reference = reference.substring(PREFIX_THEME_REF.length());
}
// at this point, value can contain type/name (drawable/foo for instance).
// split it to make sure.
String[] segments = reference.split("/");
// we look for the referenced item name.
String referenceName = null;
if (segments.length == 2) {
// there was a resType in the reference. If it's attr, we ignore it
// else, we assert for now.
if (ResourceType.ATTR.getName().equals(segments[0])) {
referenceName = segments[1];
} else {
// At this time, no support for ?type/name where type is not "attr"
return null;
}
} else {
// it's just an item name.
referenceName = segments[0];
// Make sure it looks like a resource name; if not, it could just be a string
// which starts with a ?
if (!Character.isJavaIdentifierStart(referenceName.charAt(0))) {
return null;
}
for (int i = 1, n = referenceName.length(); i < n; i++) {
char c = referenceName.charAt(i);
if (!Character.isJavaIdentifierPart(c) && c != '.') {
return null;
}
}
}
// now we look for android: in the referenceName in order to support format
// such as: ?attr/android:name
if (referenceName.startsWith(PREFIX_ANDROID)) {
frameworkOnly = true;
referenceName = referenceName.substring(PREFIX_ANDROID.length());
}
// Now look for the item in the theme, starting with the current one.
ResourceValue item = findItemInStyle(mTheme, referenceName,
forceFrameworkOnly || frameworkOnly);
if (item == null && mLogger != null) {
mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR,
String.format("Couldn't find theme resource %1$s for the current theme",
reference),
new ResourceValue(ResourceType.ATTR, originalReference, frameworkOnly));
}
return item;
} else if (reference.startsWith(PREFIX_RESOURCE_REF)) {
boolean frameworkOnly = false;
// check for the specific null reference value.
if (REFERENCE_NULL.equals(reference)) {
return null;
}
// Eliminate the prefix from the string.
if (reference.startsWith(ANDROID_PREFIX)) {
frameworkOnly = true;
reference = reference.substring(ANDROID_PREFIX.length());
} else {
reference = reference.substring(PREFIX_RESOURCE_REF.length());
}
// at this point, value contains type/[android:]name (drawable/foo for instance)
String[] segments = reference.split("/");
if (segments.length != 2) {
return null;
}
// now we look for android: in the resource name in order to support format
// such as: @drawable/android:name
String referenceName = segments[1];
if (referenceName.startsWith(PREFIX_ANDROID)) {
frameworkOnly = true;
referenceName = referenceName.substring(PREFIX_ANDROID.length());
}
ResourceType type = ResourceType.getEnum(segments[0]);
// unknown type?
if (type == null) {
return null;
}
// Make sure it looks like a resource name; if not, it could just be a string
// which starts with a ?
if (!Character.isJavaIdentifierStart(referenceName.charAt(0))) {
return null;
}
for (int i = 1, n = referenceName.length(); i < n; i++) {
char c = referenceName.charAt(i);
if (!Character.isJavaIdentifierPart(c) && c != '.') {
return null;
}
}
return findResValue(type, referenceName,
forceFrameworkOnly ? true :frameworkOnly);
}
// 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) {
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) {
if (mLogger != null) {
mLogger.error(LayoutLog.TAG_BROKEN,
String.format("Potential stackoverflow trying to resolve '%s'. Render may not be accurate.", value),
null);
}
return resValue;
}
// otherwise, we attempt to resolve this new value as well
return resolveResValue(resolvedResValue);
}
// ---- Private helper methods.
/**
* Searches for, and returns a {@link ResourceValue} by its name, and type.
* @param resType the type of the resource
* @param resName the name of the resource
* @param frameworkOnly if <code>true</code>, the method does not search in the
* project resources
*/
private ResourceValue findResValue(ResourceType resType, String resName,
boolean frameworkOnly) {
// map of ResouceValue for the given type
Map<String, ResourceValue> typeMap;
// if allowed, search in the project resources first.
if (frameworkOnly == false) {
typeMap = mProjectResources.get(resType);
ResourceValue item = typeMap.get(resName);
if (item != null) {
return item;
}
}
// now search in the framework resources.
typeMap = mFrameworkResources.get(resType);
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 (mLogger != null) {
mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE,
"Couldn't resolve resource @" +
(frameworkOnly ? "android:" : "") + resType + "/" + resName,
new ResourceValue(resType, resName, frameworkOnly));
}
return null;
}
private ResourceValue getResource(ResourceType resourceType, String resourceName,
Map<ResourceType, Map<String, ResourceValue>> resourceRepository) {
Map<String, ResourceValue> 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.
* @param themeName the name of the current theme.
* @param isProjectTheme Is this a project theme?
*/
private void computeStyleMaps(String themeName, boolean isProjectTheme) {
mThemeName = themeName;
mIsProjectTheme = isProjectTheme;
Map<String, ResourceValue> projectStyleMap = mProjectResources.get(ResourceType.STYLE);
Map<String, ResourceValue> frameworkStyleMap = mFrameworkResources.get(ResourceType.STYLE);
// first, get the theme
ResourceValue theme = null;
// project theme names have been prepended with a *
if (isProjectTheme) {
theme = projectStyleMap.get(themeName);
} else {
theme = frameworkStyleMap.get(themeName);
}
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.
computeStyleInheritance(frameworkStyleMap.values(), null /*inProjectStyleMap */,
frameworkStyleMap);
mTheme = (StyleResourceValue) theme;
}
}
/**
* 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.
* @param outInheritanceMap the map of style inheritance. This is filled by the method.
*/
private void computeStyleInheritance(Collection<ResourceValue> styles,
Map<String, ResourceValue> inProjectStyleMap,
Map<String, ResourceValue> inFrameworkStyleMap) {
for (ResourceValue value : styles) {
if (value instanceof StyleResourceValue) {
StyleResourceValue style = (StyleResourceValue)value;
StyleResourceValue parentStyle = null;
// first look for a specified parent.
String parentName = style.getParentStyle();
// no specified parent? try to infer it from the name of the style.
if (parentName == null) {
parentName = getParentName(value.getName());
}
if (parentName != null) {
parentStyle = getStyle(parentName, inProjectStyleMap, inFrameworkStyleMap);
if (parentStyle != null) {
mStyleInheritanceMap.put(style, parentStyle);
}
}
}
}
}
/**
* Computes the name of the parent style, or <code>null</code> if the style is a root style.
*/
private String getParentName(String styleName) {
int index = styleName.lastIndexOf('.');
if (index != -1) {
return styleName.substring(0, index);
}
return null;
}
/**
* Searches for and returns the {@link StyleResourceValue} from a given name.
* <p/>The format of the name can be:
* <ul>
* <li>[android:]&lt;name&gt;</li>
* <li>[android:]style/&lt;name&gt;</li>
* <li>@[android:]style/&lt;name&gt;</li>
* </ul>
* @param parentName the name of the style.
* @param inProjectStyleMap the project style map. Can be <code>null</code>
* @param inFrameworkStyleMap the framework style map.
* @return The matching {@link StyleResourceValue} object or <code>null</code> if not found.
*/
private StyleResourceValue getStyle(String parentName,
Map<String, ResourceValue> inProjectStyleMap,
Map<String, ResourceValue> 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 <type>/<name>. 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 == false && inProjectStyleMap != null) {
parent = inProjectStyleMap.get(name);
}
// if not found, then look in the framework resources.
if (parent == 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;
}
}