/*
* 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.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
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.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.util.Projects;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.utils.XmlUtils;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.*;
import java.awt.*;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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");
private 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 color the color to resolve
* @return the corresponding {@link Color} color, or null
*/
@Nullable
public static Color resolveColor(@NotNull RenderResources resources, @Nullable ResourceValue color) {
if (color != null) {
color = resources.resolveResValue(color);
}
if (color == null) {
return null;
}
String value = color.getValue();
int depth = 0;
while (value != null && depth < MAX_RESOURCE_INDIRECTION) {
if (value.startsWith("#")) {
return parseColor(value);
}
if (value.startsWith(PREFIX_RESOURCE_REF)) {
boolean isFramework = color.isFramework();
color = resources.findResValue(value, isFramework);
if (color != null) {
value = color.getValue();
} else {
break;
}
} else {
File file = new File(value);
if (file.exists() && file.getName().endsWith(DOT_XML)) {
// Parse
try {
String xml = Files.toString(file, Charsets.UTF_8);
Document document = XmlUtils.parseDocumentSilently(xml, true);
if (document != null) {
NodeList items = document.getElementsByTagName(TAG_ITEM);
value = findInStateList(items, ATTR_COLOR);
continue;
}
} catch (Exception e) {
LOG.warn(String.format("Failed parsing color file %1$s", file.getName()), e);
}
}
return null;
}
depth++;
}
return null;
}
/**
* Searches a color XML file for the color definition element that does not
* have an associated state and returns its color
*/
@Nullable
private static String findInStateList(@NotNull NodeList items, String attributeName) {
for (int i = 0, n = items.getLength(); i < n; i++) {
// Find non-state color definition
Node item = items.item(i);
boolean hasState = false;
if (item.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) item;
if (element.hasAttributeNS(ANDROID_URI, attributeName)) {
NamedNodeMap attributes = element.getAttributes();
for (int j = 0, m = attributes.getLength(); j < m; j++) {
Attr attribute = (Attr) attributes.item(j);
if (attribute.getLocalName().startsWith(STATE_NAME_PREFIX)) {
hasState = true;
break;
}
}
if (!hasState) {
return element.getAttributeNS(ANDROID_URI, attributeName);
}
}
}
}
// If no match, go and look for the last mentioned item and use that one
for (int i = items.getLength() - 1; i >= 0; i--) {
Node item = items.item(i);
if (item.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) item;
if (element.hasAttributeNS(ANDROID_URI, attributeName)) {
return element.getAttributeNS(ANDROID_URI, attributeName);
}
}
}
return null;
}
/**
* Converts the supported color formats (#rgb, #argb, #rrggbb, #aarrggbb to a Color
* http://developer.android.com/guide/topics/resources/more-resources.html#Color
*/
@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;
}
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
* @return the corresponding {@link File}, or null
*/
@Nullable
public static File resolveDrawable(@NotNull RenderResources resources, @Nullable ResourceValue drawable) {
if (drawable != null) {
drawable = resources.resolveResValue(drawable);
}
if (drawable == null) {
return null;
}
String value = drawable.getValue();
int depth = 0;
while (value != null && depth < MAX_RESOURCE_INDIRECTION) {
if (value.startsWith(PREFIX_RESOURCE_REF)) {
boolean isFramework = drawable.isFramework();
drawable = resources.findResValue(value, isFramework);
if (drawable != null) {
value = drawable.getValue();
} else {
break;
}
} else {
File file = new File(value);
if (file.exists()) {
if (file.getName().endsWith(DOT_XML)) {
// Parse
try {
String xml = Files.toString(file, Charsets.UTF_8);
Document document = XmlUtils.parseDocumentSilently(xml, true);
if (document != null) {
NodeList items = document.getElementsByTagName(TAG_ITEM);
value = findInStateList(items, ATTR_DRAWABLE);
continue;
}
} catch (Exception e) {
LOG.warn(String.format("Failed parsing color file %1$s", file.getName()), e);
}
}
return file;
} else {
return null;
}
}
depth++;
}
return 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_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.
*
* @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 p = facet.getIdeaAndroidProject();
if (p != null) {
String resourcePrefix = LintUtils.computeResourcePrefix(p.getDelegate());
if (resourcePrefix != null) {
if (name != null) {
return LintUtils.computeResourceName(resourcePrefix, name);
} else {
return resourcePrefix;
}
}
}
}
}
return name;
}
}