1 /** 2 * Copyright (c) 2014, Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.mail.utils; 18 19 import android.graphics.Color; 20 import android.graphics.Typeface; 21 import android.text.SpannableStringBuilder; 22 import android.text.Spanned; 23 import android.text.style.AbsoluteSizeSpan; 24 import android.text.style.ForegroundColorSpan; 25 import android.text.style.QuoteSpan; 26 import android.text.style.StyleSpan; 27 import android.text.style.TypefaceSpan; 28 import android.text.style.URLSpan; 29 import android.text.style.UnderlineSpan; 30 31 import com.android.mail.analytics.AnalyticsTimer; 32 import com.google.android.mail.common.base.CharMatcher; 33 import com.google.android.mail.common.html.parser.HTML; 34 import com.google.android.mail.common.html.parser.HTML4; 35 import com.google.android.mail.common.html.parser.HtmlDocument; 36 import com.google.android.mail.common.html.parser.HtmlTree; 37 import com.google.common.collect.Lists; 38 39 import java.util.LinkedList; 40 41 public class HtmlUtils { 42 43 static final String LOG_TAG = LogTag.getLogTag(); 44 45 /** 46 * Use our custom SpannedConverter to process the HtmlNode results from HtmlTree. 47 * @param html 48 * @return processed HTML as a Spanned 49 */ 50 public static Spanned htmlToSpan(String html, HtmlTree.ConverterFactory factory) { 51 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.COMPOSE_HTML_TO_SPAN); 52 // Get the html "tree" 53 final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html); 54 htmlTree.setConverterFactory(factory); 55 final Spanned spanned = htmlTree.getSpanned(); 56 AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.COMPOSE_HTML_TO_SPAN, true, 57 "compose", "html_to_span", null); 58 LogUtils.i(LOG_TAG, "htmlToSpan completed, input: %d, result: %d", html.length(), 59 spanned.length()); 60 return spanned; 61 } 62 63 /** 64 * Class that handles converting the html into a Spanned. 65 * This class will only handle a subset of the html tags. Below is the full list: 66 * - bold 67 * - italic 68 * - underline 69 * - font size 70 * - font color 71 * - font face 72 * - a 73 * - blockquote 74 * - p 75 * - div 76 */ 77 public static class SpannedConverter implements HtmlTree.Converter<Spanned> { 78 // Pinto normal text size is 2 while normal for AbsoluteSizeSpan is 12. 79 // So 6 seems to be the magic number here. Html.toHtml also uses 6 as divider. 80 private static final int WEB_TO_ANDROID_SIZE_MULTIPLIER = 6; 81 82 protected final SpannableStringBuilder mBuilder = new SpannableStringBuilder(); 83 private final LinkedList<TagWrapper> mSeenTags = Lists.newLinkedList(); 84 85 private final HtmlTree.DefaultPlainTextConverter mTextConverter = 86 new HtmlTree.DefaultPlainTextConverter(); 87 private int mTextConverterIndex = 0; 88 89 @Override 90 public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) { 91 // Feed it into the plain text converter 92 mTextConverter.addNode(n, nodeNum, endNum); 93 if (n instanceof HtmlDocument.Tag) { 94 handleStart((HtmlDocument.Tag) n); 95 } else if (n instanceof HtmlDocument.EndTag) { 96 handleEnd((HtmlDocument.EndTag) n); 97 } 98 appendPlainTextFromConverter(); 99 } 100 101 private void appendPlainTextFromConverter() { 102 String textString = mTextConverter.getObject(); 103 if (textString.length() > mTextConverterIndex) { 104 mBuilder.append(textString.substring(mTextConverterIndex)); 105 mTextConverterIndex = textString.length(); 106 } 107 } 108 109 /** 110 * Helper function to handle start tag 111 */ 112 protected void handleStart(HtmlDocument.Tag tag) { 113 if (!tag.isSelfTerminating()) { 114 // Add to the stack of tags needing closing tag 115 mSeenTags.push(new TagWrapper(tag, mBuilder.length())); 116 } 117 } 118 119 /** 120 * Helper function to handle end tag 121 */ 122 protected void handleEnd(HtmlDocument.EndTag tag) { 123 TagWrapper lastSeen; 124 HTML.Element element = tag.getElement(); 125 while ((lastSeen = mSeenTags.poll()) != null && lastSeen.tag.getElement() != null && 126 !lastSeen.tag.getElement().equals(element)) { } 127 128 // Misformatted html, just ignore this tag 129 if (lastSeen == null) { 130 return; 131 } 132 133 Object marker = null; 134 if (HTML4.B_ELEMENT.equals(element)) { 135 // BOLD 136 marker = new StyleSpan(Typeface.BOLD); 137 } else if (HTML4.I_ELEMENT.equals(element)) { 138 // ITALIC 139 marker = new StyleSpan(Typeface.ITALIC); 140 } else if (HTML4.U_ELEMENT.equals(element)) { 141 // UNDERLINE 142 marker = new UnderlineSpan(); 143 } else if (HTML4.A_ELEMENT.equals(element)) { 144 // A HREF 145 HtmlDocument.TagAttribute attr = lastSeen.tag.getAttribute(HTML4.HREF_ATTRIBUTE); 146 // Ignore this tag if it doesn't have a link 147 if (attr == null) { 148 return; 149 } 150 marker = new URLSpan(attr.getValue()); 151 } else if (HTML4.BLOCKQUOTE_ELEMENT.equals(element)) { 152 // BLOCKQUOTE 153 marker = new QuoteSpan(); 154 } else if (HTML4.FONT_ELEMENT.equals(element)) { 155 // FONT SIZE/COLOR/FACE, since this can insert more than one span 156 // we special case it and return 157 handleFont(lastSeen); 158 } 159 160 final int start = lastSeen.startIndex; 161 final int end = mBuilder.length(); 162 if (marker != null && start != end) { 163 mBuilder.setSpan(marker, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 164 } 165 } 166 167 /** 168 * Helper function to handle end font tags 169 */ 170 private void handleFont(TagWrapper wrapper) { 171 final int start = wrapper.startIndex; 172 final int end = mBuilder.length(); 173 174 // check font color 175 HtmlDocument.TagAttribute attr = wrapper.tag.getAttribute(HTML4.COLOR_ATTRIBUTE); 176 if (attr != null) { 177 int c = Color.parseColor(attr.getValue()); 178 if (c != -1) { 179 mBuilder.setSpan(new ForegroundColorSpan(c | 0xFF000000), start, end, 180 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 181 } 182 } 183 184 // check font size 185 attr = wrapper.tag.getAttribute(HTML4.SIZE_ATTRIBUTE); 186 if (attr != null) { 187 int i = Integer.parseInt(attr.getValue()); 188 if (i != -1) { 189 mBuilder.setSpan(new AbsoluteSizeSpan(i * WEB_TO_ANDROID_SIZE_MULTIPLIER, 190 true /* use dip */), start, end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 191 } 192 } 193 194 // check font typeface 195 attr = wrapper.tag.getAttribute(HTML4.FACE_ATTRIBUTE); 196 if (attr != null) { 197 String[] families = attr.getValue().split(","); 198 for (String family : families) { 199 mBuilder.setSpan(new TypefaceSpan(family.trim()), start, end, 200 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 201 } 202 } 203 } 204 205 @Override 206 public int getPlainTextLength() { 207 return mBuilder.length(); 208 } 209 210 @Override 211 public Spanned getObject() { 212 return mBuilder; 213 } 214 215 private static class TagWrapper { 216 final HtmlDocument.Tag tag; 217 final int startIndex; 218 219 TagWrapper(HtmlDocument.Tag tag, int startIndex) { 220 this.tag = tag; 221 this.startIndex = startIndex; 222 } 223 } 224 } 225 } 226