/* * 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.javadoc; import com.android.SdkConstants; import com.android.builder.model.*; import com.android.ide.common.rendering.api.ArrayResourceValue; 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.res2.ResourceFile; import com.android.ide.common.res2.ResourceItem; import com.android.ide.common.resources.*; import com.android.ide.common.resources.configuration.DensityQualifier; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.resources.Density; import com.android.resources.ResourceType; import com.android.tools.idea.configurations.Configuration; import com.android.tools.idea.gradle.IdeaAndroidProject; import com.android.tools.idea.rendering.*; import com.android.utils.HtmlBuilder; import com.android.utils.SdkUtils; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.intellij.openapi.module.Module; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.ColorUtil; import org.jetbrains.android.AndroidColorAnnotator; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.sdk.AndroidPlatform; import org.jetbrains.android.sdk.AndroidTargetData; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import java.awt.*; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.List; import java.util.Locale; import static com.android.ide.common.resources.ResourceResolver.MAX_RESOURCE_INDIRECTION; import static org.jetbrains.android.util.AndroidUtils.hasImageExtension; public class AndroidJavaDocRenderer { /** Renders the Javadoc for a resource of given type and name. */ @Nullable public static String render(@NotNull Module module, @NotNull ResourceType type, @NotNull String name, boolean framework) { return render(module, ResourceUrl.create(type, name, framework, false)); } /** Renders the Javadoc for a resource of given type and name. */ @Nullable public static String render(@NotNull Module module, @NotNull ResourceUrl url) { ResourceValueRenderer renderer = ResourceValueRenderer.create(url.type, module); boolean framework = url.framework; if (renderer == null || framework && renderer.getFrameworkResources() == null || !framework && renderer.getAppResources() == null) { return null; } return renderer.render(url); } /** Combines external javadoc into the documentation rendered by the {@link #render} method */ @Nullable public static String injectExternalDocumentation(@Nullable String rendered, @Nullable String external) { if (rendered == null) { return external; } else if (external == null) { return rendered; } // Strip out HTML tags from the external documentation external = external.replace("<HTML>","").replace("</HTML>",""); // Strip out styles. int styleStart = external.indexOf("<style"); int styleEnd = external.indexOf("</style>"); if (styleStart != -1 && styleEnd != -1) { String style = external.substring(styleStart, styleEnd + "</style>".length()); external = external.substring(0, styleStart) + external.substring(styleEnd + "</style>".length()); // Insert into our own head int insert = rendered.indexOf("<body>"); if (insert != -1) { int headEnd = rendered.lastIndexOf("</head>", insert); if (headEnd != -1) { insert = headEnd; rendered = rendered.substring(0, insert) + style + rendered.substring(insert); } else { rendered = rendered.substring(0, insert) + "<head>" + style + "</head>" + rendered.substring(insert); } } } int bodyEnd = rendered.indexOf("</body>"); if (bodyEnd != -1) { rendered = rendered.substring(0, bodyEnd) + external + rendered.substring(bodyEnd); } return rendered; } private static abstract class ResourceValueRenderer implements ResourceItemResolver.ResourceProvider { protected final Module myModule; protected FrameworkResources myFrameworkResources; protected AppResourceRepository myAppResources; protected ResourceResolver myResourceResolver; protected boolean mySmall; protected ResourceValueRenderer(Module module) { myModule = module; } public void setSmall(boolean small) { mySmall = small; } public abstract void renderToHtml(@NotNull HtmlBuilder builder, @NotNull ItemInfo item, @NotNull ResourceUrl url, boolean showResolution, @Nullable ResourceValue resourceValue); /** Creates a renderer suitable for the given resource type */ @Nullable public static ResourceValueRenderer create(@NotNull ResourceType type, @NotNull Module module) { switch (type) { case ATTR: case STRING: case DIMEN: case INTEGER: case BOOL: return new TextValueRenderer(module); case ARRAY: return new ArrayRenderer(module); case DRAWABLE: return new DrawableValueRenderer(module); case COLOR: return new ColorValueRenderer(module); default: // Ignore return null; } } @Nullable private static FrameworkResources getFrameworkResources(Module module) { AndroidPlatform platform = AndroidPlatform.getPlatform(module); if (platform != null) { AndroidTargetData targetData = AndroidTargetData.getTargetData(platform.getTarget(), module); if (targetData != null) { try { return targetData.getFrameworkResources(); } catch (IOException e) { // Ignore docs } } } return null; } @Nullable public String render(@NotNull ResourceUrl url) { List<ItemInfo> items = gatherItems(url); if (items != null) { Collections.sort(items); return renderKeyValues(items, url); } return null; } @Nullable private List<ItemInfo> gatherItems(@NotNull ResourceUrl url) { ResourceType type = url.type; String resourceName = url.name; boolean framework = url.framework; if (framework) { List<ItemInfo> results = Lists.newArrayList(); addItemsFromFramework(null, MASK_NORMAL, 0, type, resourceName, results); return results; } AndroidFacet facet = AndroidFacet.getInstance(myModule); if (facet == null) { return null; } List<ItemInfo> results = Lists.newArrayList(); AppResourceRepository resources = getAppResources(); IdeaAndroidProject ideaAndroidProject = facet.getIdeaAndroidProject(); if (ideaAndroidProject != null) { assert facet.isGradleProject(); AndroidProject delegate = ideaAndroidProject.getDelegate(); Variant selectedVariant = ideaAndroidProject.getSelectedVariant(); Set<SourceProvider> selectedProviders = Sets.newHashSet(); BuildTypeContainer buildType = ideaAndroidProject.findBuildType(selectedVariant.getBuildType()); assert buildType != null; SourceProvider sourceProvider = buildType.getSourceProvider(); String buildTypeName = selectedVariant.getName(); int rank = 0; addItemsFromSourceSet(buildTypeName, MASK_FLAVOR_SELECTED, rank++, sourceProvider, type, resourceName, results, facet); selectedProviders.add(sourceProvider); List<String> productFlavors = selectedVariant.getProductFlavors(); // Iterate in *reverse* order for (int i = productFlavors.size() - 1; i >= 0; i--) { String flavorName = productFlavors.get(i); ProductFlavorContainer productFlavor = ideaAndroidProject.findProductFlavor(flavorName); assert productFlavor != null; SourceProvider provider = productFlavor.getSourceProvider(); addItemsFromSourceSet(flavorName, MASK_FLAVOR_SELECTED, rank++, provider, type, resourceName, results, facet); selectedProviders.add(provider); } SourceProvider main = delegate.getDefaultConfig().getSourceProvider(); addItemsFromSourceSet("main", MASK_FLAVOR_SELECTED, rank++, main, type, resourceName, results, facet); selectedProviders.add(main); // Next display any source sets that are *not* in the selected flavors or build types! Collection<BuildTypeContainer> buildTypes = delegate.getBuildTypes(); for (BuildTypeContainer container : buildTypes) { SourceProvider provider = container.getSourceProvider(); if (!selectedProviders.contains(provider)) { addItemsFromSourceSet(container.getBuildType().getName(), MASK_NORMAL, rank++, provider, type, resourceName, results, facet); selectedProviders.add(provider); } } Collection<ProductFlavorContainer> flavors = delegate.getProductFlavors(); for (ProductFlavorContainer container : flavors) { SourceProvider provider = container.getSourceProvider(); if (!selectedProviders.contains(provider)) { addItemsFromSourceSet(container.getProductFlavor().getName(), MASK_NORMAL, rank++, provider, type, resourceName, results, facet); selectedProviders.add(provider); } } // Also pull in items from libraries; this will include items from the current module as well, // so add them to a temporary list so we can only add the items that are missing if (resources != null) { for (LocalResourceRepository dependency : resources.getLibraries()) { addItemsFromRepository(dependency.getDisplayName(), MASK_NORMAL, rank++, dependency, type, resourceName, results); } } } else if (resources != null) { addItemsFromRepository(null, MASK_NORMAL, 0, resources, type, resourceName, results); } return results; } private static void addItemsFromSourceSet(@Nullable String flavor, int mask, int rank, @NotNull SourceProvider sourceProvider, @NotNull ResourceType type, @NotNull String name, @NotNull List<ItemInfo> results, @NotNull AndroidFacet facet) { Collection<File> resDirectories = sourceProvider.getResDirectories(); LocalFileSystem fileSystem = LocalFileSystem.getInstance(); for (File dir : resDirectories) { VirtualFile virtualFile = fileSystem.findFileByIoFile(dir); if (virtualFile != null) { ResourceFolderRepository resources = ResourceFolderRegistry.get(facet, virtualFile); addItemsFromRepository(flavor, mask, rank, resources, type, name, results); } } } private void addItemsFromFramework(@Nullable String flavor, int mask, int rank, @NotNull ResourceType type, @NotNull String name, @NotNull List<ItemInfo> results) { ResourceRepository frameworkResources = getFrameworkResources(); if (frameworkResources == null) { return; } if (frameworkResources.hasResourceItem(type, name)) { com.android.ide.common.resources.ResourceItem item = frameworkResources.getResourceItem(type, name); for (com.android.ide.common.resources.ResourceFile resourceFile : item.getSourceFileList()) { FolderConfiguration configuration = resourceFile.getConfiguration(); ResourceValue value = resourceFile.getValue(type, name); String folderName = resourceFile.getFolder().getFolder().getName(); String folder = renderFolderName(folderName); ItemInfo info = new ItemInfo(value, configuration, folder, flavor, rank, mask); results.add(info); } } } private static void addItemsFromRepository(@Nullable String flavor, int mask, int rank, @NotNull AbstractResourceRepository resources, @NotNull ResourceType type, @NotNull String name, @NotNull List<ItemInfo> results) { List<ResourceItem> items = resources.getResourceItem(type, name); if (items != null) { for (ResourceItem item : items) { String folderName = "?"; ResourceFile source = item.getSource(); if (source != null) { folderName = source.getFile().getParentFile().getName(); } String folder = renderFolderName(folderName); ResourceValue value = item.getResourceValue(resources.isFramework()); ItemInfo info = new ItemInfo(value, item.getConfiguration(), folder, flavor, rank, mask); results.add(info); } } } @Nullable private String renderKeyValues(@NotNull List<ItemInfo> items, @NotNull ResourceUrl url) { if (items.isEmpty()) { return null; } markHidden(items); HtmlBuilder builder = new HtmlBuilder(); builder.openHtmlBody(); if (items.size() == 1) { renderToHtml(builder, items.get(0), url, true, items.get(0).value); } else { //noinspection SpellCheckingInspection builder.beginTable("valign=\"top\""); boolean haveFlavors = haveFlavors(items); if (haveFlavors) { builder.addTableRow(true, "Flavor/Library", "Configuration", "Value"); } else { builder.addTableRow(true, "Configuration", "Value"); } String prevFlavor = null; boolean showResolution = true; for (ItemInfo info : items) { String folder = info.folder; String flavor = StringUtil.notNullize(info.flavor); if (flavor.equals(prevFlavor)) { flavor = ""; } else { prevFlavor = flavor; } builder.addHtml("<tr>"); if (haveFlavors) { // Bold selected flavors? String style = ( (info.displayMask & MASK_FLAVOR_SELECTED) != 0) ? "b" : null; addTableCell(builder, style, flavor, null, null, false); } addTableCell(builder, null, folder, null, null, false); String style = ( (info.displayMask & MASK_ITEM_HIDDEN) != 0) ? "s" : null; addTableCell(builder, style, null, info, url, showResolution); showResolution = false; // Only show for first item builder.addHtml("</tr>"); } builder.endTable(); } builder.closeHtmlBody(); return builder.getHtml(); } private void addTableCell(@NotNull HtmlBuilder builder, @Nullable String attribute, @Nullable String text, @Nullable ItemInfo info, @Nullable ResourceUrl url, boolean showResolution) { //noinspection SpellCheckingInspection builder.addHtml("<td valign=\"top\">"); if (attribute != null) { builder.addHtml("<").addHtml(attribute).addHtml(">"); } if (text != null) { builder.add(text); } else { assert info != null; assert url != null; renderToHtml(builder, info, url, showResolution, info.value); } if (attribute != null) { builder.addHtml("</").addHtml(attribute).addHtml(">"); } builder.addHtml("</td>"); } @NotNull protected ResourceItemResolver createResolver(@NotNull ItemInfo item) { ResourceItemResolver resolver = new ResourceItemResolver(item.configuration, this, null); List<ResourceValue> lookupChain = Lists.newArrayList(); lookupChain.add(item.value); resolver.setLookupChainList(lookupChain); return resolver; } @Nullable protected Object resolveValue(@NotNull ResourceItemResolver resolver, @Nullable ResourceValue itemValue, @NotNull ResourceUrl url) { assert resolver.getLookupChain() != null; resolver.setLookupChainList(Lists.<ResourceValue>newArrayList()); if (itemValue != null) { String value = itemValue.getValue(); if (value != null) { ResourceUrl parsed = ResourceUrl.parse(value); if (parsed != null) { ResourceValue v = new ResourceValue(url.type, url.name, url.framework); v.setValue(url.toString()); ResourceValue resourceValue = resolver.resolveResValue(v); if (resourceValue != null && resourceValue.getValue() != null) { return resourceValue.getValue(); } } return value; } else { ResourceValue v = new ResourceValue(url.type, url.name, url.framework); v.setValue(url.toString()); ResourceValue resourceValue = resolver.resolveResValue(v); if (resourceValue != null && resourceValue.getValue() != null) { return resourceValue.getValue(); } else if (resourceValue instanceof StyleResourceValue) { return ResourceUrl.create(resourceValue).toString(); } return url.toString(); } } return null; } protected void displayChain(@NotNull ResourceUrl url, @NotNull List<ResourceValue> lookupChain, @NotNull HtmlBuilder builder, boolean newlineBefore, boolean newlineAfter) { if (!lookupChain.isEmpty()) { if (newlineBefore) { builder.newline(); } String text = ResourceItemResolver.getDisplayString(url.toString(), lookupChain); builder.add(text); builder.newline(); if (newlineAfter) { builder.newline(); } } } // ---- Implements ResourceItemResolver.ResourceProvider ---- @Override @Nullable public ResourceRepository getFrameworkResources() { if (myFrameworkResources == null) { myFrameworkResources = getFrameworkResources(myModule); } return myFrameworkResources; } @Override @Nullable public AppResourceRepository getAppResources() { if (myAppResources == null) { myAppResources = AppResourceRepository.getAppResources(myModule, true); } return myAppResources; } @Override @Nullable public ResourceResolver getResolver(boolean createIfNecessary) { if (myResourceResolver == null && createIfNecessary) { AndroidFacet facet = AndroidFacet.getInstance(myModule); if (facet != null) { VirtualFile layout = AndroidColorAnnotator.pickLayoutFile(myModule, facet); if (layout != null) { Configuration configuration = facet.getConfigurationManager().getConfiguration(layout); myResourceResolver = configuration.getResourceResolver(); } } } return myResourceResolver; } } private static boolean haveFlavors(List<ItemInfo> items) { for (ItemInfo info : items) { if (info.flavor != null) { return true; } } return false; } private static void markHidden(List<ItemInfo> items) { Set<String> hiddenQualifiers = Sets.newHashSet(); for (ItemInfo info : items) { String folder = info.folder; if (hiddenQualifiers.contains(folder)) { info.displayMask |= MASK_ITEM_HIDDEN; } hiddenQualifiers.add(folder); } } private static String renderFolderName(String name) { String prefix = SdkConstants.FD_RES_VALUES; if (name.equals(prefix)) { return "Default"; } if (name.startsWith(prefix + '-')) { return name.substring(prefix.length() + 1); } else { return name; } } private static class TextValueRenderer extends ResourceValueRenderer { private TextValueRenderer(Module module) { super(module); } @Nullable @Override protected String resolveValue(@NotNull ResourceItemResolver resolver, @Nullable ResourceValue value, @NotNull ResourceUrl url) { return (String)super.resolveValue(resolver, value, url); } @Override public void renderToHtml(@NotNull HtmlBuilder builder, @NotNull ItemInfo item, @NotNull ResourceUrl url, boolean showResolution, @Nullable ResourceValue resourceValue) { ResourceItemResolver resolver = createResolver(item); String value = resolveValue(resolver, resourceValue, url); List<ResourceValue> lookupChain = resolver.getLookupChain(); if (value != null) { boolean found = false; if (url.theme) { // If it's a theme attribute such as ?foo, it might resolve to a value we can // preview in a better way, such as a drawable, color or array. In that case, // look at the resolution chain and figure out the type of the resolved value, // and if appropriate, append a customized rendering. if (value.startsWith("#")) { Color color = ResourceHelper.parseColor(value); if (color != null) { found = true; ResourceValueRenderer renderer = ResourceValueRenderer.create(ResourceType.COLOR, myModule); assert renderer != null; ResourceValue resolved = new ResourceValue(url.type, url.name, url.framework); resolved.setValue(value); renderer.renderToHtml(builder, item, url, false, resolved); builder.newline(); } } else if (value.endsWith(SdkConstants.DOT_PNG)) { File f = new File(value); if (f.exists()) { found = true; ResourceValueRenderer renderer = ResourceValueRenderer.create(ResourceType.DRAWABLE, myModule); assert renderer != null; ResourceValue resolved = new ResourceValue(url.type, url.name, url.framework); resolved.setValue(value); renderer.renderToHtml(builder, item, url, false, resolved); builder.newline(); } } if (!found) { assert lookupChain != null; for (int i = lookupChain.size() - 1; i >= 0; i--) { ResourceValue rv = lookupChain.get(i); if (rv != null) { String value2 = rv.getValue(); if (value2 != null) { ResourceUrl resourceUrl = ResourceUrl.parse(value2); if (resourceUrl != null && !resourceUrl.theme) { ResourceValueRenderer renderer = create(resourceUrl.type, myModule); if (renderer != null && renderer.getClass() != this.getClass()) { found = true; ResourceValue resolved = new ResourceValue(url.type, url.name, url.framework); resolved.setValue(value); renderer.renderToHtml(builder, item, resourceUrl, false, resolved); builder.newline(); break; } } } } } } } if (!found && (!showResolution || lookupChain == null || lookupChain.isEmpty())) { builder.add(value); } } else if (item.value != null && item.value.getValue() != null) { builder.add(item.value.getValue()); } if (showResolution) { assert lookupChain != null; displayChain(url, lookupChain, builder, true, true); if (!lookupChain.isEmpty()) { // See if we resolved to a style; if so, show its attributes ResourceValue rv = lookupChain.get(lookupChain.size() - 1); if (rv instanceof StyleResourceValue) { StyleResourceValue srv = (StyleResourceValue)rv; displayStyleValues(builder, item, resolver, srv); } } } } private void displayStyleValues(HtmlBuilder builder, ItemInfo item, ResourceItemResolver resolver, StyleResourceValue styleValue) { List<ResourceValue> lookupChain = resolver.getLookupChain(); builder.addHtml("<hr>"); builder.addBold(styleValue.getName()).add(":").newline(); Set<String> masked = Sets.newHashSet(); while (styleValue != null) { for (String name : styleValue.getNames()) { if (masked.contains(name)) { continue; } masked.add(name); ResourceValue v = styleValue.findValue(name, true); if (v == null) { v = styleValue.findValue(name, false); } String value = v != null ? v.getValue() : null; builder.addNbsps(4); builder.addBold(name).add(" = ").add(v != null ? v.getValue() : "null"); if (v != null && v.getValue() != null) { ResourceUrl url = ResourceUrl.parse(v.getValue()); if (url != null) { ResourceUrl resolvedUrl = url; int count = 0; while (resolvedUrl != null) { if (lookupChain != null) { lookupChain.clear(); } ResourceValue resourceValue; boolean framework = resolvedUrl.framework || styleValue.isFramework(); if (resolvedUrl.theme) { resourceValue = resolver.findItemInTheme(resolvedUrl.name, framework); } else { resourceValue = resolver.findResValue(resolvedUrl.toString(), framework); } if (resourceValue == null || resourceValue.getValue() == null) { break; } url = resolvedUrl; value = resourceValue.getValue(); resolvedUrl = ResourceUrl.parse(value); if (count++ == MAX_RESOURCE_INDIRECTION) { // prevent deep recursion (likely an invalid resource cycle) break; } } ResourceValueRenderer renderer = create(url.type, myModule); if (renderer != null && renderer.getClass() != this.getClass()) { builder.newline(); renderer.setSmall(true); ResourceValue resolved = new ResourceValue(url.type, url.name, url.framework); resolved.setValue(value); //noinspection ConstantConditions renderer.renderToHtml(builder, item, url, false, resolved); } else if (value != null) { builder.add(" => "); builder.add(value); builder.newline(); } } } else { builder.newline(); } } styleValue = resolver.getParent(styleValue); if (styleValue != null) { builder.newline(); builder.add("Inherits from: ").add(ResourceUrl.create(styleValue).toString()).add(":").newline(); } } } } private static class ArrayRenderer extends ResourceValueRenderer { private ArrayRenderer(Module module) { super(module); } @Nullable @Override protected Object resolveValue(@NotNull ResourceItemResolver resolver, @Nullable ResourceValue value, @NotNull ResourceUrl url) { if (value != null) { assert resolver.getLookupChain() != null; resolver.setLookupChainList(Lists.<ResourceValue>newArrayList()); return resolver.resolveResValue(value); } return null; } @Override public void renderToHtml(@NotNull HtmlBuilder builder, @NotNull ItemInfo item, @NotNull ResourceUrl url, boolean showResolution, @Nullable ResourceValue resourceValue) { ResourceItemResolver resolver = createResolver(item); Object value = resolveValue(resolver, resourceValue, url); if (value instanceof ArrayResourceValue) { ArrayResourceValue arv = (ArrayResourceValue)value; builder.add(Joiner.on(", ").skipNulls().join(arv)); } else if (value != null) { builder.add(value.toString()); } if (showResolution) { List<ResourceValue> lookupChain = resolver.getLookupChain(); assert lookupChain != null; // For arrays we end up pointing to the first element with PsiResourceItem.getValue, so only show the // resolution chain if it reveals something interesting (e.g. intermediate aliases) if (lookupChain.size() > 1) { displayChain(url, lookupChain, builder, true, false); } } } } private static class DrawableValueRenderer extends ResourceValueRenderer { private DrawableValueRenderer(Module module) { super(module); } @Nullable @Override protected File resolveValue(@NotNull ResourceItemResolver resolver, @Nullable ResourceValue value, @NotNull ResourceUrl url) { assert resolver.getLookupChain() != null; resolver.setLookupChainList(Lists.<ResourceValue>newArrayList()); return ResourceHelper.resolveDrawable(resolver, value); } @Override public void renderToHtml(@NotNull HtmlBuilder builder, @NotNull ItemInfo item, @NotNull ResourceUrl url, boolean showResolution, @Nullable ResourceValue resourceValue) { ResourceItemResolver resolver = createResolver(item); File bitmap = resolveValue(resolver, resourceValue, url); if (bitmap != null && bitmap.exists() && hasImageExtension(bitmap.getPath())) { URL fileUrl = null; try { fileUrl = SdkUtils.fileToUrl(bitmap); } catch (MalformedURLException e) { // pass } if (fileUrl != null) { builder.beginDiv("background-color:gray;padding:10px"); builder.addImage(fileUrl, bitmap.getPath()); builder.endDiv(); Dimension size = getSize(bitmap); if (size != null) { DensityQualifier densityQualifier = item.configuration.getDensityQualifier(); Density density = densityQualifier == null ? Density.MEDIUM : densityQualifier.getValue(); builder.addHtml(String.format(Locale.US, "%1$d×%2$d px (%3$d×%4$d dp @ %5$s)", size.width, size.height, px2dp(size.width, density), px2dp(size.height, density), density.getResourceValue())); } } } else if (bitmap != null) { builder.add(bitmap.getPath()); } if (showResolution) { List<ResourceValue> lookupChain = resolver.getLookupChain(); assert lookupChain != null; displayChain(url, lookupChain, builder, true, false); } } private static int px2dp(int px, Density density) { return (int)((float)px * Density.MEDIUM.getDpiValue() / density.getDpiValue()); } } private static class ColorValueRenderer extends ResourceValueRenderer { private ColorValueRenderer(Module module) { super(module); } @Nullable @Override protected Color resolveValue(@NotNull ResourceItemResolver resolver, @Nullable ResourceValue value, @NotNull ResourceUrl url) { assert resolver.getLookupChain() != null; resolver.setLookupChainList(Lists.<ResourceValue>newArrayList()); return ResourceHelper.resolveColor(resolver, value); } @Override public void renderToHtml(@NotNull HtmlBuilder builder, @NotNull ItemInfo item, @NotNull ResourceUrl url, boolean showResolution, @Nullable ResourceValue resourceValue) { ResourceItemResolver resolver = createResolver(item); Color color = resolveValue(resolver, resourceValue, url); if (color != null) { int width = 200; int height = 100; if (mySmall) { int divisor = 3; width /= divisor; height /= divisor; } String colorString = String.format(Locale.US, "rgb(%d,%d,%d)", color.getRed(), color.getGreen(), color.getBlue()); String foregroundColor = ColorUtil.isDark(color) ? "white" : "black"; String css = "background-color:" + colorString + ";color:" + foregroundColor + ";width:" + width + "px;text-align:center;vertical-align:middle;"; // Use <table> tag such that we can center the color text (Java's HTML renderer doesn't support // vertical-align:middle on divs) builder.addHtml("<table style=\"" + css + "\" border=\"0\"><tr height=\"" + height + "\">"); builder.addHtml("<td align=\"center\" valign=\"middle\" height=\"" + height + "\">"); builder.addHtml("#"); int alpha = color.getAlpha(); // If not opaque, include alpha if (alpha != 255) { String alphaString = Integer.toHexString(alpha); builder.addHtml((alphaString.length() < 2 ? "0" : "") + alphaString); } builder.addHtml(ColorUtil.toHex(color)); builder.addHtml("</td></tr></table>"); } else if (item.value != null && item.value.getValue() != null) { builder.add(item.value.getValue()); } if (showResolution) { List<ResourceValue> lookupChain = resolver.getLookupChain(); assert lookupChain != null; displayChain(url, lookupChain, builder, true, false); } } } /** * Returns the dimensions of an Image if it can be obtained without fully reading it into memory. * This is a copy of the method in {@link com.android.tools.lint.checks.IconDetector}. */ @Nullable private static Dimension getSize(File file) { try { ImageInputStream input = ImageIO.createImageInputStream(file); if (input != null) { try { Iterator<ImageReader> readers = ImageIO.getImageReaders(input); if (readers.hasNext()) { ImageReader reader = readers.next(); try { reader.setInput(input); return new Dimension(reader.getWidth(0), reader.getHeight(0)); } finally { reader.dispose(); } } } finally { input.close(); } } // Fallback: read the image using the normal means //BufferedImage image = ImageIO.read(file); //if (image != null) { // return new Dimension(image.getWidth(), image.getHeight()); //} else { // return null; //} return null; } catch (IOException e) { // Pass -- we can't handle all image types, warn about those we can return null; } } /** Normal display style */ private static final int MASK_NORMAL = 0; /** Display style for flavor folders that are selected */ private static final int MASK_FLAVOR_SELECTED = 1; /** Display style for items that are hidden by later resource folders */ private static final int MASK_ITEM_HIDDEN = 2; /** * Information about {@link ResourceItem} instances to be displayed; in addition to the item and the * folder name, we can also record the flavor or library name, as well as display attributes indicating * whether the item is from a selected flavor, or whether the item is masked by a higher priority repository */ private static class ItemInfo implements Comparable<ItemInfo> { @Nullable public final ResourceValue value; @NotNull public final FolderConfiguration configuration; @Nullable public final String flavor; @NotNull public final String folder; public final int rank; public int displayMask; private ItemInfo(@Nullable ResourceValue value, @NotNull FolderConfiguration configuration, @NotNull String folder, @Nullable String flavor, int rank, int initialMask) { this.value = value; this.configuration = configuration; this.flavor = flavor; this.folder = folder; this.displayMask = initialMask; this.rank = rank; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ItemInfo itemInfo = (ItemInfo)o; if (rank != itemInfo.rank) return false; if (!configuration.equals(itemInfo.configuration)) return false; if (flavor != null ? !flavor.equals(itemInfo.flavor) : itemInfo.flavor != null) return false; if (!folder.equals(itemInfo.folder)) return false; if (value != null ? !value.equals(itemInfo.value) : itemInfo.value != null) return false; return true; } @Override public int hashCode() { int result = value != null ? value.hashCode() : 0; result = 31 * result + configuration.hashCode(); result = 31 * result + (flavor != null ? flavor.hashCode() : 0); result = 31 * result + folder.hashCode(); result = 31 * result + rank; return result; } @Override public int compareTo(@NotNull ItemInfo other) { if (rank != other.rank) { return rank - other.rank; } // Special case density: when we're showing multiple drawables for different densities, // sort by density value, not alphabetical name. DensityQualifier density1 = configuration.getDensityQualifier(); DensityQualifier density2 = other.configuration.getDensityQualifier(); if (density1 != null && density2 != null) { // Start with the lowest densities to avoid case where you have a giant asset (say xxxhdpi) // and you only see the top left corner in the documentation window. int delta = density2.getValue().compareTo(density1.getValue()); if (delta != 0) { return delta; } } return configuration.compareTo(other.configuration); } } }