blob: 36cd9e62b25a2ff0e0e2ae9b771e1ef023c5e9cf [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.tools.idea.rendering;
import com.android.SdkConstants;
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
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.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.lang.databinding.DbUtil;
import com.android.tools.lint.detector.api.LintUtils;
import com.google.common.collect.ImmutableList;
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.PsiDirectory;
import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.ui.ColorUtil;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.Color;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.android.SdkConstants.*;
import static com.android.ide.common.resources.ResourceResolver.MAX_RESOURCE_INDIRECTION;
public class ResourceHelper {
private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.rendering.ResourceHelper");
public static final String STATE_NAME_PREFIX = "state_";
/**
* Returns true if the given style represents a project theme
*
* @param style a theme style string
* @return true if the style string represents a project theme, as opposed
* to a framework theme
*/
public static boolean isProjectStyle(@NotNull String style) {
assert style.startsWith(STYLE_RESOURCE_PREFIX) || style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : style;
return style.startsWith(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(new Computable<ResourceFolderType>() {
@Nullable
@Override
public ResourceFolderType compute() {
return 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 FolderConfiguration getFolderConfiguration(@Nullable final PsiFile file) {
if (file != null) {
if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
return ApplicationManager.getApplication().runReadAction(new Computable<FolderConfiguration>() {
@Nullable
@Override
public FolderConfiguration compute() {
return 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<VirtualFile>();
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;
}
/**
* Returns true if views with the given fully qualified class name need to include
* their package in the layout XML tag
*
* @param fqcn the fully qualified class name, such as android.widget.Button
* @return true if the full package path should be included in the layout XML element
* tag
*/
public static boolean viewNeedsPackage(String fqcn) {
return !(fqcn.startsWith(ANDROID_WIDGET_PREFIX) || fqcn.startsWith(ANDROID_VIEW_PKG) || fqcn.startsWith(ANDROID_WEBKIT_PKG));
}
/**
* 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) {
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();
// 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);
}
if (stateColor == null) {
return null;
}
try {
return makeColorWithAlpha(resources, stateColor, state.getAlpha());
}
catch (NumberFormatException e) {
LOG.error(String.format("The alpha attribute in %s/%s does not resolve to a floating point number", stateList.getDirName(),
stateList.getFileName()));
}
}
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) {
if (value != null) {
value = resources.resolveResValue(value);
}
if (value == null) {
return Collections.emptyList();
}
final List<Color> result = new ArrayList<Color>();
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);
}
else {
Color color = parseColor(state.getValue());
stateColors = color == null ? Collections.<Color>emptyList() : ImmutableList.of(color);
}
for (Color color : stateColors) {
try {
result.add(makeColorWithAlpha(resources, color, state.getAlpha()));
}
catch (NumberFormatException e) {
LOG.error(String.format("The alpha attribute in %s/%s does not resolve to a floating point number", 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();
}
@NotNull
private 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);
if (combinedAlpha < 0) {
combinedAlpha = 0;
}
if (combinedAlpha > 255) {
combinedAlpha = 255;
}
return ColorUtil.toAlpha(color, combinedAlpha);
}
/**
* Returns a {@link StateList} description of the state list value, or null if value is not a state list.
*/
@Nullable
public static StateList resolveStateList(@NotNull RenderResources renderResources,
@NotNull ResourceValue value,
@NotNull Project project) {
if (value.getValue().startsWith(PREFIX_RESOURCE_REF)) {
final ResourceValue resValue = renderResources.findResValue(value.getValue(), value.isFramework());
if (resValue != null) {
return resolveStateList(renderResources, resValue, project);
}
}
else {
VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(value.getValue());
if (virtualFile != null) {
PsiFile psiFile = AndroidPsiUtils.getPsiFileSafely(project, virtualFile);
if (psiFile instanceof XmlFile) {
// Parse
try {
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)) {
stateList.addState(createStateListState(subTag, value.isFramework()));
}
return stateList;
}
}
catch (IllegalArgumentException e) {
LOG.error(String.format("%1$s is not a valid state list file", virtualFile.getName()));
}
}
}
}
return null;
}
/**
* Returns a {@link StateListState} representing the state in tag.
*/
@NotNull
private static StateListState createStateListState(XmlTag tag, boolean isFramework) {
String stateValue = null;
String alphaValue = null;
Map<String, Boolean> stateAttributes = new HashMap<String, Boolean>();
XmlAttribute[] attributes = tag.getAttributes();
for (XmlAttribute attr : attributes) {
String name = attr.getLocalName();
String value = attr.getValue();
if (value != null && (SdkConstants.ATTR_COLOR.equals(name) || SdkConstants.ATTR_DRAWABLE.equals(name))) {
ResourceUrl url = ResourceUrl.parse(value, isFramework);
stateValue = url != null ? url.toString() : value;
}
else if (value != null && "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) {
throw new IllegalArgumentException("Not a valid item");
}
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(String 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.
*/
public static String colorToString(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);
}
else {
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();
StateListState state = states.get(states.size() - 1);
result = state.getValue();
}
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 (value.startsWith(PREFIX_BINDING_EXPR)) {
value = DbUtil.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) {
if (module != null) {
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet != null) {
IdeaAndroidProject androidModel = facet.getAndroidModel();
if (androidModel != null) {
String resourcePrefix = LintUtils.computeResourcePrefix(androidModel.getAndroidProject());
if (resourcePrefix != null) {
if (name != null) {
return name.startsWith(resourcePrefix) ? name : LintUtils.computeResourceName(resourcePrefix, name);
} else {
return resourcePrefix;
}
}
}
}
}
return name;
}
/**
* 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<StateListState>();
}
@NotNull
public String getFileName() {
return myFileName;
}
@NotNull
public String getDirName() {
return myDirName;
}
@NotNull
public ResourceFolderType getType() {
return ResourceFolderType.getFolderType(myDirName);
}
@NotNull
public List<StateListState> getStates() {
return myStates;
}
public void addState(@NotNull StateListState state) {
myStates.add(state);
}
}
/**
* Stores information about a particular state of a resource state list.
*/
public static class StateListState {
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(String alpha) {
myAlpha = alpha;
}
@NotNull
public String getValue() {
return myValue;
}
@Nullable
public String getAlpha() {
return myAlpha;
}
@NotNull
public Map<String, Boolean> getAttributes() {
return myAttributes;
}
}
}