/** * Copyright (c) 2014, Google Inc. * * 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.mail.utils; import android.graphics.Color; import android.graphics.Typeface; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.AbsoluteSizeSpan; import android.text.style.ForegroundColorSpan; import android.text.style.QuoteSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import com.android.mail.analytics.AnalyticsTimer; import com.google.android.mail.common.base.CharMatcher; import com.google.android.mail.common.html.parser.HTML; import com.google.android.mail.common.html.parser.HTML4; import com.google.android.mail.common.html.parser.HtmlDocument; import com.google.android.mail.common.html.parser.HtmlTree; import com.google.common.collect.Lists; import java.util.LinkedList; public class HtmlUtils { static final String LOG_TAG = LogTag.getLogTag(); /** * Use our custom SpannedConverter to process the HtmlNode results from HtmlTree. * @param html * @return processed HTML as a Spanned */ public static Spanned htmlToSpan(String html, HtmlTree.ConverterFactory factory) { AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.COMPOSE_HTML_TO_SPAN); // Get the html "tree" final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html); htmlTree.setConverterFactory(factory); final Spanned spanned = htmlTree.getSpanned(); AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.COMPOSE_HTML_TO_SPAN, true, "compose", "html_to_span", null); LogUtils.i(LOG_TAG, "htmlToSpan completed, input: %d, result: %d", html.length(), spanned.length()); return spanned; } /** * Class that handles converting the html into a Spanned. * This class will only handle a subset of the html tags. Below is the full list: * - bold * - italic * - underline * - font size * - font color * - font face * - a * - blockquote * - p * - div */ public static class SpannedConverter implements HtmlTree.Converter { // Pinto normal text size is 2 while normal for AbsoluteSizeSpan is 12. // So 6 seems to be the magic number here. Html.toHtml also uses 6 as divider. private static final int WEB_TO_ANDROID_SIZE_MULTIPLIER = 6; protected final SpannableStringBuilder mBuilder = new SpannableStringBuilder(); private final LinkedList mSeenTags = Lists.newLinkedList(); private final HtmlTree.DefaultPlainTextConverter mTextConverter = new HtmlTree.DefaultPlainTextConverter(); private int mTextConverterIndex = 0; @Override public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) { // Feed it into the plain text converter mTextConverter.addNode(n, nodeNum, endNum); if (n instanceof HtmlDocument.Tag) { handleStart((HtmlDocument.Tag) n); } else if (n instanceof HtmlDocument.EndTag) { handleEnd((HtmlDocument.EndTag) n); } appendPlainTextFromConverter(); } private void appendPlainTextFromConverter() { String textString = mTextConverter.getObject(); if (textString.length() > mTextConverterIndex) { mBuilder.append(textString.substring(mTextConverterIndex)); mTextConverterIndex = textString.length(); } } /** * Helper function to handle start tag */ protected void handleStart(HtmlDocument.Tag tag) { if (!tag.isSelfTerminating()) { // Add to the stack of tags needing closing tag mSeenTags.push(new TagWrapper(tag, mBuilder.length())); } } /** * Helper function to handle end tag */ protected void handleEnd(HtmlDocument.EndTag tag) { TagWrapper lastSeen; HTML.Element element = tag.getElement(); while ((lastSeen = mSeenTags.poll()) != null && lastSeen.tag.getElement() != null && !lastSeen.tag.getElement().equals(element)) { } // Misformatted html, just ignore this tag if (lastSeen == null) { return; } Object marker = null; if (HTML4.B_ELEMENT.equals(element)) { // BOLD marker = new StyleSpan(Typeface.BOLD); } else if (HTML4.I_ELEMENT.equals(element)) { // ITALIC marker = new StyleSpan(Typeface.ITALIC); } else if (HTML4.U_ELEMENT.equals(element)) { // UNDERLINE marker = new UnderlineSpan(); } else if (HTML4.A_ELEMENT.equals(element)) { // A HREF HtmlDocument.TagAttribute attr = lastSeen.tag.getAttribute(HTML4.HREF_ATTRIBUTE); // Ignore this tag if it doesn't have a link if (attr == null) { return; } marker = new URLSpan(attr.getValue()); } else if (HTML4.BLOCKQUOTE_ELEMENT.equals(element)) { // BLOCKQUOTE marker = new QuoteSpan(); } else if (HTML4.FONT_ELEMENT.equals(element)) { // FONT SIZE/COLOR/FACE, since this can insert more than one span // we special case it and return handleFont(lastSeen); } final int start = lastSeen.startIndex; final int end = mBuilder.length(); if (marker != null && start != end) { mBuilder.setSpan(marker, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } /** * Helper function to handle end font tags */ private void handleFont(TagWrapper wrapper) { final int start = wrapper.startIndex; final int end = mBuilder.length(); // check font color HtmlDocument.TagAttribute attr = wrapper.tag.getAttribute(HTML4.COLOR_ATTRIBUTE); if (attr != null) { int c = Color.parseColor(attr.getValue()); if (c != -1) { mBuilder.setSpan(new ForegroundColorSpan(c | 0xFF000000), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } // check font size attr = wrapper.tag.getAttribute(HTML4.SIZE_ATTRIBUTE); if (attr != null) { int i = Integer.parseInt(attr.getValue()); if (i != -1) { mBuilder.setSpan(new AbsoluteSizeSpan(i * WEB_TO_ANDROID_SIZE_MULTIPLIER, true /* use dip */), start, end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } // check font typeface attr = wrapper.tag.getAttribute(HTML4.FACE_ATTRIBUTE); if (attr != null) { String[] families = attr.getValue().split(","); for (String family : families) { mBuilder.setSpan(new TypefaceSpan(family.trim()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } @Override public int getPlainTextLength() { return mBuilder.length(); } @Override public Spanned getObject() { return mBuilder; } private static class TagWrapper { final HtmlDocument.Tag tag; final int startIndex; TagWrapper(HtmlDocument.Tag tag, int startIndex) { this.tag = tag; this.startIndex = startIndex; } } } }