/* * 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.tools.lint; import static com.android.SdkConstants.DOT_JPG; import static com.android.SdkConstants.DOT_PNG; import static com.android.tools.lint.detector.api.LintUtils.endsWith; import static com.android.tools.lint.detector.api.TextFormat.HTML; import static com.android.tools.lint.detector.api.TextFormat.RAW; import com.android.tools.lint.checks.BuiltinIssueRegistry; import com.android.tools.lint.client.api.Configuration; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Position; import com.android.tools.lint.detector.api.Project; import com.android.tools.lint.detector.api.Severity; import com.android.tools.lint.detector.api.TextFormat; import com.android.utils.SdkUtils; import com.google.common.annotations.Beta; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.collect.Maps; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; import com.google.common.io.Files; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * A reporter which emits lint results into an HTML report. * <p> * <b>NOTE: This is not a public or final API; if you rely on this be prepared * to adjust your code for the next tools release.</b> */ @Beta public class HtmlReporter extends Reporter { private static final boolean USE_HOLO_STYLE = true; @SuppressWarnings("ConstantConditions") private static final String CSS = USE_HOLO_STYLE ? "hololike.css" : "default.css"; //$NON-NLS-1$ //$NON-NLS-2$ /** * Maximum number of warnings allowed for a single issue type before we * split up and hide all but the first {@link #SHOWN_COUNT} items. */ private static final int SPLIT_LIMIT = 8; /** * When a warning has at least {@link #SPLIT_LIMIT} items, then we show the * following number of items before the "Show more" button/link. */ private static final int SHOWN_COUNT = SPLIT_LIMIT - 3; protected final Writer mWriter; private String mStripPrefix; private String mFixUrl; /** * Creates a new {@link HtmlReporter} * * @param client the associated client * @param output the output file * @throws IOException if an error occurs */ public HtmlReporter(LintCliClient client, File output) throws IOException { super(client, output); mWriter = new BufferedWriter(Files.newWriter(output, Charsets.UTF_8)); } @Override public void write(int errorCount, int warningCount, List<Warning> issues) throws IOException { Map<Issue, String> missing = computeMissingIssues(issues); mWriter.write( "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n" + //$NON-NLS-1$ "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" + //$NON-NLS-1$ "<head>\n" + //$NON-NLS-1$ "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />" + //$NON-NLS-1$ "<title>" + mTitle + "</title>\n"); //$NON-NLS-1$//$NON-NLS-2$ writeStyleSheet(); if (!mSimpleFormat) { // JavaScript for collapsing/expanding long lists mWriter.write( "<script language=\"javascript\" type=\"text/javascript\"> \n" + //$NON-NLS-1$ "<!--\n" + //$NON-NLS-1$ "function reveal(id) {\n" + //$NON-NLS-1$ "if (document.getElementById) {\n" + //$NON-NLS-1$ "document.getElementById(id).style.display = 'block';\n" + //$NON-NLS-1$ "document.getElementById(id+'Link').style.display = 'none';\n" + //$NON-NLS-1$ "}\n" + //$NON-NLS-1$ "}\n" + //$NON-NLS-1$ "//--> \n" + //$NON-NLS-1$ "</script>\n"); //$NON-NLS-1$ } mWriter.write( "</head>\n" + //$NON-NLS-1$ "<body>\n" + //$NON-NLS-1$ "<h1>" + //$NON-NLS-1$ mTitle + "</h1>\n" + //$NON-NLS-1$ "<div class=\"titleSeparator\"></div>\n"); //$NON-NLS-1$ mWriter.write(String.format("Check performed at %1$s.", new Date().toString())); mWriter.write("<br/>\n"); //$NON-NLS-1$ mWriter.write(String.format("%1$d errors and %2$d warnings found:", errorCount, warningCount)); mWriter.write("<br/><br/>\n"); //$NON-NLS-1$ Issue previousIssue = null; if (!issues.isEmpty()) { List<List<Warning>> related = new ArrayList<List<Warning>>(); List<Warning> currentList = null; for (Warning warning : issues) { if (warning.issue != previousIssue) { previousIssue = warning.issue; currentList = new ArrayList<Warning>(); related.add(currentList); } assert currentList != null; currentList.add(warning); } writeOverview(related, missing.size()); Category previousCategory = null; for (List<Warning> warnings : related) { Warning first = warnings.get(0); Issue issue = first.issue; if (issue.getCategory() != previousCategory) { previousCategory = issue.getCategory(); mWriter.write("\n<a name=\""); //$NON-NLS-1$ mWriter.write(issue.getCategory().getFullName()); mWriter.write("\"></a>\n"); //$NON-NLS-1$ mWriter.write("<div class=\"category\"><a href=\"#\" title=\"Return to top\">"); //$NON-NLS-1$ mWriter.write(issue.getCategory().getFullName()); mWriter.write("</a><div class=\"categorySeparator\"></div>\n");//$NON-NLS-1$ mWriter.write("</div>\n"); //$NON-NLS-1$ } mWriter.write("<a name=\"" + issue.getId() + "\"></a>\n"); //$NON-NLS-1$ //$NON-NLS-2$ mWriter.write("<div class=\"issue\">\n"); //$NON-NLS-1$ // Explain this issue mWriter.write("<div class=\"id\"><a href=\"#\" title=\"Return to top\">"); //$NON-NLS-1$ mWriter.write(issue.getId()); mWriter.write(": "); //$NON-NLS-1$ mWriter.write(issue.getBriefDescription(HTML)); mWriter.write("</a><div class=\"issueSeparator\"></div>\n"); //$NON-NLS-1$ mWriter.write("</div>\n"); //$NON-NLS-1$ mWriter.write("<div class=\"warningslist\">\n"); //$NON-NLS-1$ boolean partialHide = !mSimpleFormat && warnings.size() > SPLIT_LIMIT; int count = 0; for (Warning warning : warnings) { if (partialHide && count == SHOWN_COUNT) { String id = warning.issue.getId() + "Div"; //$NON-NLS-1$ mWriter.write("<button id=\""); //$NON-NLS-1$ mWriter.write(id); mWriter.write("Link\" onclick=\"reveal('"); //$NON-NLS-1$ mWriter.write(id); mWriter.write("');\" />"); //$NON-NLS-1$ mWriter.write(String.format("+ %1$d More Occurrences...", warnings.size() - SHOWN_COUNT)); mWriter.write("</button>\n"); //$NON-NLS-1$ mWriter.write("<div id=\""); //$NON-NLS-1$ mWriter.write(id); mWriter.write("\" style=\"display: none\">\n"); //$NON-NLS-1$ } count++; String url = null; if (warning.path != null) { url = writeLocation(warning.file, warning.path, warning.line); mWriter.write(':'); mWriter.write(' '); } // Is the URL for a single image? If so, place it here near the top // of the error floating on the right. If there are multiple images, // they will instead be placed in a horizontal box below the error boolean addedImage = false; if (url != null && warning.location != null && warning.location.getSecondary() == null) { addedImage = addImage(url, warning.location); } mWriter.write("<span class=\"message\">"); //$NON-NLS-1$ mWriter.append(RAW.convertTo(warning.message, HTML)); mWriter.write("</span>"); //$NON-NLS-1$ if (addedImage) { mWriter.write("<br clear=\"right\"/>"); //$NON-NLS-1$ } else { mWriter.write("<br />"); //$NON-NLS-1$ } // Insert surrounding code block window if (warning.line >= 0 && warning.fileContents != null) { mWriter.write("<pre class=\"errorlines\">\n"); //$NON-NLS-1$ appendCodeBlock(warning.fileContents, warning.line, warning.offset); mWriter.write("\n</pre>"); //$NON-NLS-1$ } mWriter.write('\n'); if (warning.location != null && warning.location.getSecondary() != null) { mWriter.write("<ul>"); Location l = warning.location.getSecondary(); int otherLocations = 0; while (l != null) { String message = l.getMessage(); if (message != null && !message.isEmpty()) { Position start = l.getStart(); int line = start != null ? start.getLine() : -1; String path = mClient.getDisplayPath(warning.project, l.getFile()); writeLocation(l.getFile(), path, line); mWriter.write(':'); mWriter.write(' '); mWriter.write("<span class=\"message\">"); //$NON-NLS-1$ mWriter.append(RAW.convertTo(message, HTML)); mWriter.write("</span>"); //$NON-NLS-1$ mWriter.write("<br />"); //$NON-NLS-1$ String name = l.getFile().getName(); if (!(endsWith(name, DOT_PNG) || endsWith(name, DOT_JPG))) { String s = mClient.readFile(l.getFile()); if (s != null && !s.isEmpty()) { mWriter.write("<pre class=\"errorlines\">\n"); //$NON-NLS-1$ int offset = start != null ? start.getOffset() : -1; appendCodeBlock(s, line, offset); mWriter.write("\n</pre>"); //$NON-NLS-1$ } } } else { otherLocations++; } l = l.getSecondary(); } mWriter.write("</ul>"); if (otherLocations > 0) { String id = "Location" + count + "Div"; //$NON-NLS-1$ mWriter.write("<button id=\""); //$NON-NLS-1$ mWriter.write(id); mWriter.write("Link\" onclick=\"reveal('"); //$NON-NLS-1$ mWriter.write(id); mWriter.write("');\" />"); //$NON-NLS-1$ mWriter.write(String.format("+ %1$d Additional Locations...", otherLocations)); mWriter.write("</button>\n"); //$NON-NLS-1$ mWriter.write("<div id=\""); //$NON-NLS-1$ mWriter.write(id); mWriter.write("\" style=\"display: none\">\n"); //$NON-NLS-1$ mWriter.write("Additional locations: "); mWriter.write("<ul>\n"); //$NON-NLS-1$ l = warning.location.getSecondary(); while (l != null) { Position start = l.getStart(); int line = start != null ? start.getLine() : -1; String path = mClient.getDisplayPath(warning.project, l.getFile()); mWriter.write("<li> "); //$NON-NLS-1$ writeLocation(l.getFile(), path, line); mWriter.write("\n"); //$NON-NLS-1$ l = l.getSecondary(); } mWriter.write("</ul>\n"); //$NON-NLS-1$ mWriter.write("</div><br/><br/>\n"); //$NON-NLS-1$ } } // Place a block of images? if (!addedImage && url != null && warning.location != null && warning.location.getSecondary() != null) { addImage(url, warning.location); } if (warning.isVariantSpecific()) { mWriter.write("\n"); mWriter.write("Applies to variants: "); mWriter.write(Joiner.on(", ").join(warning.getIncludedVariantNames())); mWriter.write("<br/>\n"); mWriter.write("Does <b>not</b> apply to variants: "); mWriter.write(Joiner.on(", ").join(warning.getExcludedVariantNames())); mWriter.write("<br/>\n"); } } if (partialHide) { // Close up the extra div mWriter.write("</div>\n"); //$NON-NLS-1$ } mWriter.write("</div>\n"); //$NON-NLS-1$ writeIssueMetadata(issue, first.severity, null); mWriter.write("</div>\n"); //$NON-NLS-1$ } if (!mClient.isCheckingSpecificIssues()) { writeMissingIssues(missing); } writeSuppressInfo(); } else { mWriter.write("Congratulations!"); } mWriter.write("\n</body>\n</html>"); //$NON-NLS-1$ mWriter.close(); if (!mClient.getFlags().isQuiet() && (mDisplayEmpty || errorCount > 0 || warningCount > 0)) { String url = SdkUtils.fileToUrlString(mOutput.getAbsoluteFile()); System.out.println(String.format("Wrote HTML report to %1$s", url)); } } private void writeIssueMetadata(Issue issue, Severity severity, String disabledBy) throws IOException { mWriter.write("<div class=\"metadata\">"); //$NON-NLS-1$ if (mClient.getRegistry() instanceof BuiltinIssueRegistry) { boolean adtHasFix = QuickfixHandler.ADT.hasAutoFix(issue); boolean studioHasFix = QuickfixHandler.STUDIO.hasAutoFix(issue); if (adtHasFix || studioHasFix) { String adt = "Eclipse/ADT"; String studio = "Android Studio/IntelliJ"; String tools = adtHasFix && studioHasFix ? (adt + " & " + studio) : studioHasFix ? studio : adt; mWriter.write("Note: This issue has an associated quickfix operation in " + tools); if (mFixUrl != null) { mWriter.write(" <img alt=\"Fix\" border=\"0\" align=\"top\" src=\""); //$NON-NLS-1$ mWriter.write(mFixUrl); mWriter.write("\" />\n"); //$NON-NLS-1$ } mWriter.write("<br>\n"); } } if (disabledBy != null) { mWriter.write(String.format("Disabled By: %1$s<br/>\n", disabledBy)); } mWriter.write("Priority: "); mWriter.write(String.format("%1$d / 10", issue.getPriority())); mWriter.write("<br/>\n"); //$NON-NLS-1$ mWriter.write("Category: "); mWriter.write(issue.getCategory().getFullName()); mWriter.write("</div>\n"); //$NON-NLS-1$ mWriter.write("Severity: "); if (severity == Severity.ERROR || severity == Severity.FATAL) { mWriter.write("<span class=\"error\">"); //$NON-NLS-1$ } else if (severity == Severity.WARNING) { mWriter.write("<span class=\"warning\">"); //$NON-NLS-1$ } else { mWriter.write("<span>"); //$NON-NLS-1$ } appendEscapedText(severity.getDescription()); mWriter.write("</span>"); //$NON-NLS-1$ mWriter.write("<div class=\"summary\">\n"); //$NON-NLS-1$ mWriter.write("Explanation: "); String description = issue.getBriefDescription(HTML); mWriter.write(description); if (!description.isEmpty() && Character.isLetter(description.charAt(description.length() - 1))) { mWriter.write('.'); } mWriter.write("</div>\n"); //$NON-NLS-1$ mWriter.write("<div class=\"explanation\">\n"); //$NON-NLS-1$ String explanationHtml = issue.getExplanation(HTML); mWriter.write(explanationHtml); mWriter.write("\n</div>\n"); //$NON-NLS-1$; List<String> moreInfo = issue.getMoreInfo(); mWriter.write("<br/>"); //$NON-NLS-1$ mWriter.write("<div class=\"moreinfo\">"); //$NON-NLS-1$ mWriter.write("More info: "); int count = moreInfo.size(); if (count > 1) { mWriter.write("<ul>"); //$NON-NLS-1$ } for (String uri : moreInfo) { if (count > 1) { mWriter.write("<li>"); //$NON-NLS-1$ } mWriter.write("<a href=\""); //$NON-NLS-1$ mWriter.write(uri); mWriter.write("\">" ); //$NON-NLS-1$ mWriter.write(uri); mWriter.write("</a>\n"); //$NON-NLS-1$ } if (count > 1) { mWriter.write("</ul>"); //$NON-NLS-1$ } mWriter.write("</div>"); //$NON-NLS-1$ mWriter.write("<br/>"); //$NON-NLS-1$ mWriter.write(String.format( "To suppress this error, use the issue id \"%1$s\" as explained in the " + "%2$sSuppressing Warnings and Errors%3$s section.", issue.getId(), "<a href=\"#SuppressInfo\">", "</a>")); //$NON-NLS-1$ //$NON-NLS-2$ mWriter.write("<br/>\n"); } private void writeSuppressInfo() throws IOException { //getSuppressHelp mWriter.write("\n<a name=\"SuppressInfo\"></a>\n"); //$NON-NLS-1$ mWriter.write("<div class=\"category\">"); //$NON-NLS-1$ mWriter.write("Suppressing Warnings and Errors"); mWriter.write("<div class=\"categorySeparator\"></div>\n");//$NON-NLS-1$ mWriter.write("</div>\n"); //$NON-NLS-1$ mWriter.write(TextFormat.RAW.convertTo(Main.getSuppressHelp(), TextFormat.HTML)); mWriter.write('\n'); } protected Map<Issue, String> computeMissingIssues(List<Warning> warnings) { Set<Project> projects = new HashSet<Project>(); Set<Issue> seen = new HashSet<Issue>(); for (Warning warning : warnings) { projects.add(warning.project); seen.add(warning.issue); } Configuration cliConfiguration = mClient.getConfiguration(); Map<Issue, String> map = Maps.newHashMap(); for (Issue issue : mClient.getRegistry().getIssues()) { if (!seen.contains(issue)) { if (mClient.isSuppressed(issue)) { map.put(issue, "Command line flag"); continue; } if (!issue.isEnabledByDefault() && !mClient.isAllEnabled()) { map.put(issue, "Default"); continue; } if (cliConfiguration != null && !cliConfiguration.isEnabled(issue)) { map.put(issue, "Command line supplied --config lint.xml file"); continue; } // See if any projects disable this warning for (Project project : projects) { if (!project.getConfiguration(null).isEnabled(issue)) { map.put(issue, "Project lint.xml file"); break; } } } } return map; } private void writeMissingIssues(Map<Issue, String> missing) throws IOException { mWriter.write("\n<a name=\"MissingIssues\"></a>\n"); //$NON-NLS-1$ mWriter.write("<div class=\"category\">"); //$NON-NLS-1$ mWriter.write("Disabled Checks"); mWriter.write("<div class=\"categorySeparator\"></div>\n"); //$NON-NLS-1$ mWriter.write("</div>\n"); //$NON-NLS-1$ mWriter.write( "The following issues were not run by lint, either " + "because the check is not enabled by default, or because " + "it was disabled with a command line flag or via one or " + "more lint.xml configuration files in the project directories."); mWriter.write("\n<br/><br/>\n"); //$NON-NLS-1$ List<Issue> list = new ArrayList<Issue>(missing.keySet()); Collections.sort(list); for (Issue issue : list) { mWriter.write("<a name=\"" + issue.getId() + "\"></a>\n"); //$NON-NLS-1$ //$NON-NLS-2$ mWriter.write("<div class=\"issue\">\n"); //$NON-NLS-1$ // Explain this issue mWriter.write("<div class=\"id\">"); //$NON-NLS-1$ mWriter.write(issue.getId()); mWriter.write("<div class=\"issueSeparator\"></div>\n"); //$NON-NLS-1$ mWriter.write("</div>\n"); //$NON-NLS-1$ String disabledBy = missing.get(issue); writeIssueMetadata(issue, issue.getDefaultSeverity(), disabledBy); mWriter.write("</div>\n"); //$NON-NLS-1$ } } protected void writeStyleSheet() throws IOException { if (USE_HOLO_STYLE) { mWriter.write( "<link rel=\"stylesheet\" type=\"text/css\" " + //$NON-NLS-1$ "href=\"http://fonts.googleapis.com/css?family=Roboto\" />\n" );//$NON-NLS-1$ } URL cssUrl = HtmlReporter.class.getResource(CSS); if (mSimpleFormat) { // Inline the CSS mWriter.write("<style>\n"); //$NON-NLS-1$ InputStream input = cssUrl.openStream(); byte[] bytes = ByteStreams.toByteArray(input); try { Closeables.close(input, true /* swallowIOException */); } catch (IOException e) { // cannot happen } String css = new String(bytes, Charsets.UTF_8); mWriter.write(css); mWriter.write("</style>\n"); //$NON-NLS-1$ } else { String ref = addLocalResources(cssUrl); if (ref != null) { mWriter.write( "<link rel=\"stylesheet\" type=\"text/css\" href=\"" //$NON-NLS-1$ + ref + "\" />\n"); //$NON-NLS-1$ } } } private void writeOverview(List<List<Warning>> related, int missingCount) throws IOException { // Write issue id summary mWriter.write("<table class=\"overview\">\n"); //$NON-NLS-1$ String errorUrl = null; String warningUrl = null; if (!mSimpleFormat) { errorUrl = addLocalResources(getErrorIconUrl()); warningUrl = addLocalResources(getWarningIconUrl()); mFixUrl = addLocalResources(HtmlReporter.class.getResource("lint-run.png")); //$NON-NLS-1$) } Category previousCategory = null; for (List<Warning> warnings : related) { Issue issue = warnings.get(0).issue; boolean isError = false; for (Warning warning : warnings) { if (warning.severity == Severity.ERROR || warning.severity == Severity.FATAL) { isError = true; break; } } if (issue.getCategory() != previousCategory) { mWriter.write("<tr><td></td><td class=\"categoryColumn\">"); previousCategory = issue.getCategory(); String categoryName = issue.getCategory().getFullName(); mWriter.write("<a href=\"#"); //$NON-NLS-1$ mWriter.write(categoryName); mWriter.write("\">"); //$NON-NLS-1$ mWriter.write(categoryName); mWriter.write("</a>\n"); //$NON-NLS-1$ mWriter.write("</td></tr>"); //$NON-NLS-1$ mWriter.write("\n"); //$NON-NLS-1$ } mWriter.write("<tr>\n"); //$NON-NLS-1$ // Count column mWriter.write("<td class=\"countColumn\">"); //$NON-NLS-1$ mWriter.write(Integer.toString(warnings.size())); mWriter.write("</td>"); //$NON-NLS-1$ mWriter.write("<td class=\"issueColumn\">"); //$NON-NLS-1$ String imageUrl = isError ? errorUrl : warningUrl; if (imageUrl != null) { mWriter.write("<img border=\"0\" align=\"top\" src=\""); //$NON-NLS-1$ mWriter.write(imageUrl); mWriter.write("\" alt=\""); mWriter.write(isError ? "Error" : "Warning"); mWriter.write("\" />\n"); //$NON-NLS-1$ } mWriter.write("<a href=\"#"); //$NON-NLS-1$ mWriter.write(issue.getId()); mWriter.write("\">"); //$NON-NLS-1$ mWriter.write(issue.getId()); mWriter.write(": "); //$NON-NLS-1$ mWriter.write(issue.getBriefDescription(HTML)); mWriter.write("</a>\n"); //$NON-NLS-1$ mWriter.write("</td></tr>\n"); } if (missingCount > 0 && !mClient.isCheckingSpecificIssues()) { mWriter.write("<tr><td></td>"); //$NON-NLS-1$ mWriter.write("<td class=\"categoryColumn\">"); //$NON-NLS-1$ mWriter.write("<a href=\"#MissingIssues\">"); //$NON-NLS-1$ mWriter.write(String.format("Disabled Checks (%1$d)", missingCount)); mWriter.write("</a>\n"); //$NON-NLS-1$ mWriter.write("</td></tr>"); //$NON-NLS-1$ } mWriter.write("</table>\n"); //$NON-NLS-1$ mWriter.write("<br/>"); //$NON-NLS-1$ } private String writeLocation(File file, String path, int line) throws IOException { String url; mWriter.write("<span class=\"location\">"); //$NON-NLS-1$ url = getUrl(file); if (url != null) { mWriter.write("<a href=\""); //$NON-NLS-1$ mWriter.write(url); mWriter.write("\">"); //$NON-NLS-1$ } String displayPath = stripPath(path); if (url != null && url.startsWith("../") && new File(displayPath).isAbsolute()) { displayPath = url; } mWriter.write(displayPath); //noinspection VariableNotUsedInsideIf if (url != null) { mWriter.write("</a>"); //$NON-NLS-1$ } if (line >= 0) { // 0-based line numbers, but display 1-based mWriter.write(':'); mWriter.write(Integer.toString(line + 1)); } mWriter.write("</span>"); //$NON-NLS-1$ return url; } private boolean addImage(String url, Location location) throws IOException { if (url != null && endsWith(url, DOT_PNG)) { if (location.getSecondary() != null) { // Emit many images // Add in linked images as well List<String> urls = new ArrayList<String>(); while (location != null && location.getFile() != null) { String imageUrl = getUrl(location.getFile()); if (imageUrl != null && endsWith(imageUrl, DOT_PNG)) { urls.add(imageUrl); } location = location.getSecondary(); } if (!urls.isEmpty()) { // Sort in order Collections.sort(urls, new Comparator<String>() { @Override public int compare(String s1, String s2) { return getDpiRank(s1) - getDpiRank(s2); } }); mWriter.write("<table>"); //$NON-NLS-1$ mWriter.write("<tr>"); //$NON-NLS-1$ for (String linkedUrl : urls) { // Image series: align top mWriter.write("<td>"); //$NON-NLS-1$ mWriter.write("<a href=\""); //$NON-NLS-1$ mWriter.write(linkedUrl); mWriter.write("\">"); //$NON-NLS-1$ mWriter.write("<img border=\"0\" align=\"top\" src=\""); //$NON-NLS-1$ mWriter.write(linkedUrl); mWriter.write("\" /></a>\n"); //$NON-NLS-1$ mWriter.write("</td>"); //$NON-NLS-1$ } mWriter.write("</tr>"); //$NON-NLS-1$ mWriter.write("<tr>"); //$NON-NLS-1$ for (String linkedUrl : urls) { mWriter.write("<th>"); //$NON-NLS-1$ int index = linkedUrl.lastIndexOf("drawable-"); //$NON-NLS-1$ if (index != -1) { index += "drawable-".length(); //$NON-NLS-1$ int end = linkedUrl.indexOf('/', index); if (end != -1) { mWriter.write(linkedUrl.substring(index, end)); } } mWriter.write("</th>"); //$NON-NLS-1$ } mWriter.write("</tr>\n"); //$NON-NLS-1$ mWriter.write("</table>\n"); //$NON-NLS-1$ } } else { // Just this image: float to the right mWriter.write("<img class=\"embedimage\" align=\"right\" src=\""); //$NON-NLS-1$ mWriter.write(url); mWriter.write("\" />"); //$NON-NLS-1$ } return true; } return false; } /** Provide a sorting rank for a url */ private static int getDpiRank(String url) { if (url.contains("-xhdpi")) { //$NON-NLS-1$ return 0; } else if (url.contains("-hdpi")) { //$NON-NLS-1$ return 1; } else if (url.contains("-mdpi")) { //$NON-NLS-1$ return 2; } else if (url.contains("-ldpi")) { //$NON-NLS-1$ return 3; } else { return 4; } } private void appendCodeBlock(String contents, int lineno, int offset) throws IOException { int max = lineno + 3; int min = lineno - 3; for (int l = min; l < max; l++) { if (l >= 0) { int lineOffset = LintCliClient.getLineOffset(contents, l); if (lineOffset == -1) { break; } mWriter.write(String.format("<span class=\"lineno\">%1$4d</span> ", (l + 1))); //$NON-NLS-1$ String line = LintCliClient.getLineOfOffset(contents, lineOffset); if (offset != -1 && lineOffset <= offset && lineOffset+line.length() >= offset) { // Text nodes do not always have correct lines/offsets //assert l == lineno; // This line contains the beginning of the offset // First print everything before int delta = offset - lineOffset; appendEscapedText(line.substring(0, delta)); mWriter.write("<span class=\"errorspan\">"); //$NON-NLS-1$ appendEscapedText(line.substring(delta)); mWriter.write("</span>"); //$NON-NLS-1$ } else if (offset == -1 && l == lineno) { mWriter.write("<span class=\"errorline\">"); //$NON-NLS-1$ appendEscapedText(line); mWriter.write("</span>"); //$NON-NLS-1$ } else { appendEscapedText(line); } if (l < max - 1) { mWriter.write("\n"); //$NON-NLS-1$ } } } } protected void appendEscapedText(String textValue) throws IOException { for (int i = 0, n = textValue.length(); i < n; i++) { char c = textValue.charAt(i); if (c == '<') { mWriter.write("<"); //$NON-NLS-1$ } else if (c == '&') { mWriter.write("&"); //$NON-NLS-1$ } else if (c == '\n') { mWriter.write("<br/>\n"); } else { if (c > 255) { mWriter.write("&#"); //$NON-NLS-1$ mWriter.write(Integer.toString(c)); mWriter.write(';'); } else { mWriter.write(c); } } } } private String stripPath(String path) { if (mStripPrefix != null && path.startsWith(mStripPrefix) && path.length() > mStripPrefix.length()) { int index = mStripPrefix.length(); if (path.charAt(index) == File.separatorChar) { index++; } return path.substring(index); } return path; } /** Sets path prefix to strip from displayed file names */ void setStripPrefix(String prefix) { mStripPrefix = prefix; } static URL getWarningIconUrl() { return HtmlReporter.class.getResource("lint-warning.png"); //$NON-NLS-1$ } static URL getErrorIconUrl() { return HtmlReporter.class.getResource("lint-error.png"); //$NON-NLS-1$ } }