• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
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 android.text;
18 
19 import org.ccil.cowan.tagsoup.HTMLSchema;
20 import org.ccil.cowan.tagsoup.Parser;
21 import org.xml.sax.Attributes;
22 import org.xml.sax.ContentHandler;
23 import org.xml.sax.InputSource;
24 import org.xml.sax.Locator;
25 import org.xml.sax.SAXException;
26 import org.xml.sax.XMLReader;
27 
28 import android.content.res.ColorStateList;
29 import android.content.res.Resources;
30 import android.graphics.Typeface;
31 import android.graphics.drawable.Drawable;
32 import android.text.style.AbsoluteSizeSpan;
33 import android.text.style.AlignmentSpan;
34 import android.text.style.CharacterStyle;
35 import android.text.style.ForegroundColorSpan;
36 import android.text.style.ImageSpan;
37 import android.text.style.ParagraphStyle;
38 import android.text.style.QuoteSpan;
39 import android.text.style.RelativeSizeSpan;
40 import android.text.style.StrikethroughSpan;
41 import android.text.style.StyleSpan;
42 import android.text.style.SubscriptSpan;
43 import android.text.style.SuperscriptSpan;
44 import android.text.style.TextAppearanceSpan;
45 import android.text.style.TypefaceSpan;
46 import android.text.style.URLSpan;
47 import android.text.style.UnderlineSpan;
48 import android.util.Log;
49 
50 import com.android.internal.util.XmlUtils;
51 
52 import java.io.IOException;
53 import java.io.StringReader;
54 import java.nio.CharBuffer;
55 import java.util.HashMap;
56 
57 /**
58  * This class processes HTML strings into displayable styled text.
59  * Not all HTML tags are supported.
60  */
61 public class Html {
62     /**
63      * Retrieves images for HTML <img> tags.
64      */
65     public static interface ImageGetter {
66         /**
67          * This methos is called when the HTML parser encounters an
68          * &lt;img&gt; tag.  The <code>source</code> argument is the
69          * string from the "src" attribute; the return value should be
70          * a Drawable representation of the image or <code>null</code>
71          * for a generic replacement image.  Make sure you call
72          * setBounds() on your Drawable if it doesn't already have
73          * its bounds set.
74          */
getDrawable(String source)75         public Drawable getDrawable(String source);
76     }
77 
78     /**
79      * Is notified when HTML tags are encountered that the parser does
80      * not know how to interpret.
81      */
82     public static interface TagHandler {
83         /**
84          * This method will be called whenn the HTML parser encounters
85          * a tag that it does not know how to interpret.
86          */
handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader)87         public void handleTag(boolean opening, String tag,
88                                  Editable output, XMLReader xmlReader);
89     }
90 
Html()91     private Html() { }
92 
93     /**
94      * Returns displayable styled text from the provided HTML string.
95      * Any &lt;img&gt; tags in the HTML will display as a generic
96      * replacement image which your program can then go through and
97      * replace with real images.
98      *
99      * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
100      */
fromHtml(String source)101     public static Spanned fromHtml(String source) {
102         return fromHtml(source, null, null);
103     }
104 
105     /**
106      * Lazy initialization holder for HTML parser. This class will
107      * a) be preloaded by the zygote, or b) not loaded until absolutely
108      * necessary.
109      */
110     private static class HtmlParser {
111         private static final HTMLSchema schema = new HTMLSchema();
112     }
113 
114     /**
115      * Returns displayable styled text from the provided HTML string.
116      * Any &lt;img&gt; tags in the HTML will use the specified ImageGetter
117      * to request a representation of the image (use null if you don't
118      * want this) and the specified TagHandler to handle unknown tags
119      * (specify null if you don't want this).
120      *
121      * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
122      */
fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler)123     public static Spanned fromHtml(String source, ImageGetter imageGetter,
124                                    TagHandler tagHandler) {
125         Parser parser = new Parser();
126         try {
127             parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
128         } catch (org.xml.sax.SAXNotRecognizedException e) {
129             // Should not happen.
130             throw new RuntimeException(e);
131         } catch (org.xml.sax.SAXNotSupportedException e) {
132             // Should not happen.
133             throw new RuntimeException(e);
134         }
135 
136         HtmlToSpannedConverter converter =
137                 new HtmlToSpannedConverter(source, imageGetter, tagHandler,
138                         parser);
139         return converter.convert();
140     }
141 
142     /**
143      * Returns an HTML representation of the provided Spanned text.
144      */
toHtml(Spanned text)145     public static String toHtml(Spanned text) {
146         StringBuilder out = new StringBuilder();
147         withinHtml(out, text);
148         return out.toString();
149     }
150 
withinHtml(StringBuilder out, Spanned text)151     private static void withinHtml(StringBuilder out, Spanned text) {
152         int len = text.length();
153 
154         int next;
155         for (int i = 0; i < text.length(); i = next) {
156             next = text.nextSpanTransition(i, len, ParagraphStyle.class);
157             ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
158             String elements = " ";
159             boolean needDiv = false;
160 
161             for(int j = 0; j < style.length; j++) {
162                 if (style[j] instanceof AlignmentSpan) {
163                     Layout.Alignment align =
164                         ((AlignmentSpan) style[j]).getAlignment();
165                     needDiv = true;
166                     if (align == Layout.Alignment.ALIGN_CENTER) {
167                         elements = "align=\"center\" " + elements;
168                     } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
169                         elements = "align=\"right\" " + elements;
170                     } else {
171                         elements = "align=\"left\" " + elements;
172                     }
173                 }
174             }
175             if (needDiv) {
176                 out.append("<div " + elements + ">");
177             }
178 
179             withinDiv(out, text, i, next);
180 
181             if (needDiv) {
182                 out.append("</div>");
183             }
184         }
185     }
186 
withinDiv(StringBuilder out, Spanned text, int start, int end)187     private static void withinDiv(StringBuilder out, Spanned text,
188             int start, int end) {
189         int next;
190         for (int i = start; i < end; i = next) {
191             next = text.nextSpanTransition(i, end, QuoteSpan.class);
192             QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
193 
194             for (QuoteSpan quote: quotes) {
195                 out.append("<blockquote>");
196             }
197 
198             withinBlockquote(out, text, i, next);
199 
200             for (QuoteSpan quote: quotes) {
201                 out.append("</blockquote>\n");
202             }
203         }
204     }
205 
withinBlockquote(StringBuilder out, Spanned text, int start, int end)206     private static void withinBlockquote(StringBuilder out, Spanned text,
207                                          int start, int end) {
208         out.append("<p>");
209 
210         int next;
211         for (int i = start; i < end; i = next) {
212             next = TextUtils.indexOf(text, '\n', i, end);
213             if (next < 0) {
214                 next = end;
215             }
216 
217             int nl = 0;
218 
219             while (next < end && text.charAt(next) == '\n') {
220                 nl++;
221                 next++;
222             }
223 
224             withinParagraph(out, text, i, next - nl, nl, next == end);
225         }
226 
227         out.append("</p>\n");
228     }
229 
withinParagraph(StringBuilder out, Spanned text, int start, int end, int nl, boolean last)230     private static void withinParagraph(StringBuilder out, Spanned text,
231                                         int start, int end, int nl,
232                                         boolean last) {
233         int next;
234         for (int i = start; i < end; i = next) {
235             next = text.nextSpanTransition(i, end, CharacterStyle.class);
236             CharacterStyle[] style = text.getSpans(i, next,
237                                                    CharacterStyle.class);
238 
239             for (int j = 0; j < style.length; j++) {
240                 if (style[j] instanceof StyleSpan) {
241                     int s = ((StyleSpan) style[j]).getStyle();
242 
243                     if ((s & Typeface.BOLD) != 0) {
244                         out.append("<b>");
245                     }
246                     if ((s & Typeface.ITALIC) != 0) {
247                         out.append("<i>");
248                     }
249                 }
250                 if (style[j] instanceof TypefaceSpan) {
251                     String s = ((TypefaceSpan) style[j]).getFamily();
252 
253                     if (s.equals("monospace")) {
254                         out.append("<tt>");
255                     }
256                 }
257                 if (style[j] instanceof SuperscriptSpan) {
258                     out.append("<sup>");
259                 }
260                 if (style[j] instanceof SubscriptSpan) {
261                     out.append("<sub>");
262                 }
263                 if (style[j] instanceof UnderlineSpan) {
264                     out.append("<u>");
265                 }
266                 if (style[j] instanceof StrikethroughSpan) {
267                     out.append("<strike>");
268                 }
269                 if (style[j] instanceof URLSpan) {
270                     out.append("<a href=\"");
271                     out.append(((URLSpan) style[j]).getURL());
272                     out.append("\">");
273                 }
274                 if (style[j] instanceof ImageSpan) {
275                     out.append("<img src=\"");
276                     out.append(((ImageSpan) style[j]).getSource());
277                     out.append("\">");
278 
279                     // Don't output the dummy character underlying the image.
280                     i = next;
281                 }
282                 if (style[j] instanceof AbsoluteSizeSpan) {
283                     out.append("<font size =\"");
284                     out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
285                     out.append("\">");
286                 }
287                 if (style[j] instanceof ForegroundColorSpan) {
288                     out.append("<font color =\"#");
289                     String color = Integer.toHexString(((ForegroundColorSpan)
290                             style[j]).getForegroundColor() + 0x01000000);
291                     while (color.length() < 6) {
292                         color = "0" + color;
293                     }
294                     out.append(color);
295                     out.append("\">");
296                 }
297             }
298 
299             withinStyle(out, text, i, next);
300 
301             for (int j = style.length - 1; j >= 0; j--) {
302                 if (style[j] instanceof ForegroundColorSpan) {
303                     out.append("</font>");
304                 }
305                 if (style[j] instanceof AbsoluteSizeSpan) {
306                     out.append("</font>");
307                 }
308                 if (style[j] instanceof URLSpan) {
309                     out.append("</a>");
310                 }
311                 if (style[j] instanceof StrikethroughSpan) {
312                     out.append("</strike>");
313                 }
314                 if (style[j] instanceof UnderlineSpan) {
315                     out.append("</u>");
316                 }
317                 if (style[j] instanceof SubscriptSpan) {
318                     out.append("</sub>");
319                 }
320                 if (style[j] instanceof SuperscriptSpan) {
321                     out.append("</sup>");
322                 }
323                 if (style[j] instanceof TypefaceSpan) {
324                     String s = ((TypefaceSpan) style[j]).getFamily();
325 
326                     if (s.equals("monospace")) {
327                         out.append("</tt>");
328                     }
329                 }
330                 if (style[j] instanceof StyleSpan) {
331                     int s = ((StyleSpan) style[j]).getStyle();
332 
333                     if ((s & Typeface.BOLD) != 0) {
334                         out.append("</b>");
335                     }
336                     if ((s & Typeface.ITALIC) != 0) {
337                         out.append("</i>");
338                     }
339                 }
340             }
341         }
342 
343         String p = last ? "" : "</p>\n<p>";
344 
345         if (nl == 1) {
346             out.append("<br>\n");
347         } else if (nl == 2) {
348             out.append(p);
349         } else {
350             for (int i = 2; i < nl; i++) {
351                 out.append("<br>");
352             }
353 
354             out.append(p);
355         }
356     }
357 
withinStyle(StringBuilder out, Spanned text, int start, int end)358     private static void withinStyle(StringBuilder out, Spanned text,
359                                     int start, int end) {
360         for (int i = start; i < end; i++) {
361             char c = text.charAt(i);
362 
363             if (c == '<') {
364                 out.append("&lt;");
365             } else if (c == '>') {
366                 out.append("&gt;");
367             } else if (c == '&') {
368                 out.append("&amp;");
369             } else if (c > 0x7E || c < ' ') {
370                 out.append("&#" + ((int) c) + ";");
371             } else if (c == ' ') {
372                 while (i + 1 < end && text.charAt(i + 1) == ' ') {
373                     out.append("&nbsp;");
374                     i++;
375                 }
376 
377                 out.append(' ');
378             } else {
379                 out.append(c);
380             }
381         }
382     }
383 }
384 
385 class HtmlToSpannedConverter implements ContentHandler {
386 
387     private static final float[] HEADER_SIZES = {
388         1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
389     };
390 
391     private String mSource;
392     private XMLReader mReader;
393     private SpannableStringBuilder mSpannableStringBuilder;
394     private Html.ImageGetter mImageGetter;
395     private Html.TagHandler mTagHandler;
396 
HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser)397     public HtmlToSpannedConverter(
398             String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
399             Parser parser) {
400         mSource = source;
401         mSpannableStringBuilder = new SpannableStringBuilder();
402         mImageGetter = imageGetter;
403         mTagHandler = tagHandler;
404         mReader = parser;
405     }
406 
convert()407     public Spanned convert() {
408 
409         mReader.setContentHandler(this);
410         try {
411             mReader.parse(new InputSource(new StringReader(mSource)));
412         } catch (IOException e) {
413             // We are reading from a string. There should not be IO problems.
414             throw new RuntimeException(e);
415         } catch (SAXException e) {
416             // TagSoup doesn't throw parse exceptions.
417             throw new RuntimeException(e);
418         }
419 
420         // Fix flags and range for paragraph-type markup.
421         Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
422         for (int i = 0; i < obj.length; i++) {
423             int start = mSpannableStringBuilder.getSpanStart(obj[i]);
424             int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
425 
426             // If the last line of the range is blank, back off by one.
427             if (end - 2 >= 0) {
428                 if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
429                     mSpannableStringBuilder.charAt(end - 2) == '\n') {
430                     end--;
431                 }
432             }
433 
434             if (end == start) {
435                 mSpannableStringBuilder.removeSpan(obj[i]);
436             } else {
437                 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
438             }
439         }
440 
441         return mSpannableStringBuilder;
442     }
443 
handleStartTag(String tag, Attributes attributes)444     private void handleStartTag(String tag, Attributes attributes) {
445         if (tag.equalsIgnoreCase("br")) {
446             // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
447             // so we can safely emite the linebreaks when we handle the close tag.
448         } else if (tag.equalsIgnoreCase("p")) {
449             handleP(mSpannableStringBuilder);
450         } else if (tag.equalsIgnoreCase("div")) {
451             handleP(mSpannableStringBuilder);
452         } else if (tag.equalsIgnoreCase("strong")) {
453             start(mSpannableStringBuilder, new Bold());
454         } else if (tag.equalsIgnoreCase("b")) {
455             start(mSpannableStringBuilder, new Bold());
456         } else if (tag.equalsIgnoreCase("em")) {
457             start(mSpannableStringBuilder, new Italic());
458         } else if (tag.equalsIgnoreCase("cite")) {
459             start(mSpannableStringBuilder, new Italic());
460         } else if (tag.equalsIgnoreCase("dfn")) {
461             start(mSpannableStringBuilder, new Italic());
462         } else if (tag.equalsIgnoreCase("i")) {
463             start(mSpannableStringBuilder, new Italic());
464         } else if (tag.equalsIgnoreCase("big")) {
465             start(mSpannableStringBuilder, new Big());
466         } else if (tag.equalsIgnoreCase("small")) {
467             start(mSpannableStringBuilder, new Small());
468         } else if (tag.equalsIgnoreCase("font")) {
469             startFont(mSpannableStringBuilder, attributes);
470         } else if (tag.equalsIgnoreCase("blockquote")) {
471             handleP(mSpannableStringBuilder);
472             start(mSpannableStringBuilder, new Blockquote());
473         } else if (tag.equalsIgnoreCase("tt")) {
474             start(mSpannableStringBuilder, new Monospace());
475         } else if (tag.equalsIgnoreCase("a")) {
476             startA(mSpannableStringBuilder, attributes);
477         } else if (tag.equalsIgnoreCase("u")) {
478             start(mSpannableStringBuilder, new Underline());
479         } else if (tag.equalsIgnoreCase("sup")) {
480             start(mSpannableStringBuilder, new Super());
481         } else if (tag.equalsIgnoreCase("sub")) {
482             start(mSpannableStringBuilder, new Sub());
483         } else if (tag.length() == 2 &&
484                    Character.toLowerCase(tag.charAt(0)) == 'h' &&
485                    tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
486             handleP(mSpannableStringBuilder);
487             start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
488         } else if (tag.equalsIgnoreCase("img")) {
489             startImg(mSpannableStringBuilder, attributes, mImageGetter);
490         } else if (mTagHandler != null) {
491             mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
492         }
493     }
494 
handleEndTag(String tag)495     private void handleEndTag(String tag) {
496         if (tag.equalsIgnoreCase("br")) {
497             handleBr(mSpannableStringBuilder);
498         } else if (tag.equalsIgnoreCase("p")) {
499             handleP(mSpannableStringBuilder);
500         } else if (tag.equalsIgnoreCase("div")) {
501             handleP(mSpannableStringBuilder);
502         } else if (tag.equalsIgnoreCase("strong")) {
503             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
504         } else if (tag.equalsIgnoreCase("b")) {
505             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
506         } else if (tag.equalsIgnoreCase("em")) {
507             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
508         } else if (tag.equalsIgnoreCase("cite")) {
509             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
510         } else if (tag.equalsIgnoreCase("dfn")) {
511             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
512         } else if (tag.equalsIgnoreCase("i")) {
513             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
514         } else if (tag.equalsIgnoreCase("big")) {
515             end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
516         } else if (tag.equalsIgnoreCase("small")) {
517             end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
518         } else if (tag.equalsIgnoreCase("font")) {
519             endFont(mSpannableStringBuilder);
520         } else if (tag.equalsIgnoreCase("blockquote")) {
521             handleP(mSpannableStringBuilder);
522             end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
523         } else if (tag.equalsIgnoreCase("tt")) {
524             end(mSpannableStringBuilder, Monospace.class,
525                     new TypefaceSpan("monospace"));
526         } else if (tag.equalsIgnoreCase("a")) {
527             endA(mSpannableStringBuilder);
528         } else if (tag.equalsIgnoreCase("u")) {
529             end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
530         } else if (tag.equalsIgnoreCase("sup")) {
531             end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
532         } else if (tag.equalsIgnoreCase("sub")) {
533             end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
534         } else if (tag.length() == 2 &&
535                 Character.toLowerCase(tag.charAt(0)) == 'h' &&
536                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
537             handleP(mSpannableStringBuilder);
538             endHeader(mSpannableStringBuilder);
539         } else if (mTagHandler != null) {
540             mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
541         }
542     }
543 
handleP(SpannableStringBuilder text)544     private static void handleP(SpannableStringBuilder text) {
545         int len = text.length();
546 
547         if (len >= 1 && text.charAt(len - 1) == '\n') {
548             if (len >= 2 && text.charAt(len - 2) == '\n') {
549                 return;
550             }
551 
552             text.append("\n");
553             return;
554         }
555 
556         if (len != 0) {
557             text.append("\n\n");
558         }
559     }
560 
handleBr(SpannableStringBuilder text)561     private static void handleBr(SpannableStringBuilder text) {
562         text.append("\n");
563     }
564 
getLast(Spanned text, Class kind)565     private static Object getLast(Spanned text, Class kind) {
566         /*
567          * This knows that the last returned object from getSpans()
568          * will be the most recently added.
569          */
570         Object[] objs = text.getSpans(0, text.length(), kind);
571 
572         if (objs.length == 0) {
573             return null;
574         } else {
575             return objs[objs.length - 1];
576         }
577     }
578 
start(SpannableStringBuilder text, Object mark)579     private static void start(SpannableStringBuilder text, Object mark) {
580         int len = text.length();
581         text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
582     }
583 
end(SpannableStringBuilder text, Class kind, Object repl)584     private static void end(SpannableStringBuilder text, Class kind,
585                             Object repl) {
586         int len = text.length();
587         Object obj = getLast(text, kind);
588         int where = text.getSpanStart(obj);
589 
590         text.removeSpan(obj);
591 
592         if (where != len) {
593             text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
594         }
595 
596         return;
597     }
598 
startImg(SpannableStringBuilder text, Attributes attributes, Html.ImageGetter img)599     private static void startImg(SpannableStringBuilder text,
600                                  Attributes attributes, Html.ImageGetter img) {
601         String src = attributes.getValue("", "src");
602         Drawable d = null;
603 
604         if (img != null) {
605             d = img.getDrawable(src);
606         }
607 
608         if (d == null) {
609             d = Resources.getSystem().
610                     getDrawable(com.android.internal.R.drawable.unknown_image);
611             d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
612         }
613 
614         int len = text.length();
615         text.append("\uFFFC");
616 
617         text.setSpan(new ImageSpan(d, src), len, text.length(),
618                      Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
619     }
620 
startFont(SpannableStringBuilder text, Attributes attributes)621     private static void startFont(SpannableStringBuilder text,
622                                   Attributes attributes) {
623         String color = attributes.getValue("", "color");
624         String face = attributes.getValue("", "face");
625 
626         int len = text.length();
627         text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
628     }
629 
endFont(SpannableStringBuilder text)630     private static void endFont(SpannableStringBuilder text) {
631         int len = text.length();
632         Object obj = getLast(text, Font.class);
633         int where = text.getSpanStart(obj);
634 
635         text.removeSpan(obj);
636 
637         if (where != len) {
638             Font f = (Font) obj;
639 
640             if (!TextUtils.isEmpty(f.mColor)) {
641                 if (f.mColor.startsWith("@")) {
642                     Resources res = Resources.getSystem();
643                     String name = f.mColor.substring(1);
644                     int colorRes = res.getIdentifier(name, "color", "android");
645                     if (colorRes != 0) {
646                         ColorStateList colors = res.getColorStateList(colorRes);
647                         text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
648                                 where, len,
649                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
650                     }
651                 } else {
652                     int c = getHtmlColor(f.mColor);
653                     if (c != -1) {
654                         text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
655                                 where, len,
656                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
657                     }
658                 }
659             }
660 
661             if (f.mFace != null) {
662                 text.setSpan(new TypefaceSpan(f.mFace), where, len,
663                              Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
664             }
665         }
666     }
667 
startA(SpannableStringBuilder text, Attributes attributes)668     private static void startA(SpannableStringBuilder text, Attributes attributes) {
669         String href = attributes.getValue("", "href");
670 
671         int len = text.length();
672         text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
673     }
674 
endA(SpannableStringBuilder text)675     private static void endA(SpannableStringBuilder text) {
676         int len = text.length();
677         Object obj = getLast(text, Href.class);
678         int where = text.getSpanStart(obj);
679 
680         text.removeSpan(obj);
681 
682         if (where != len) {
683             Href h = (Href) obj;
684 
685             if (h.mHref != null) {
686                 text.setSpan(new URLSpan(h.mHref), where, len,
687                              Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
688             }
689         }
690     }
691 
endHeader(SpannableStringBuilder text)692     private static void endHeader(SpannableStringBuilder text) {
693         int len = text.length();
694         Object obj = getLast(text, Header.class);
695 
696         int where = text.getSpanStart(obj);
697 
698         text.removeSpan(obj);
699 
700         // Back off not to change only the text, not the blank line.
701         while (len > where && text.charAt(len - 1) == '\n') {
702             len--;
703         }
704 
705         if (where != len) {
706             Header h = (Header) obj;
707 
708             text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
709                          where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
710             text.setSpan(new StyleSpan(Typeface.BOLD),
711                          where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
712         }
713     }
714 
setDocumentLocator(Locator locator)715     public void setDocumentLocator(Locator locator) {
716     }
717 
startDocument()718     public void startDocument() throws SAXException {
719     }
720 
endDocument()721     public void endDocument() throws SAXException {
722     }
723 
startPrefixMapping(String prefix, String uri)724     public void startPrefixMapping(String prefix, String uri) throws SAXException {
725     }
726 
endPrefixMapping(String prefix)727     public void endPrefixMapping(String prefix) throws SAXException {
728     }
729 
startElement(String uri, String localName, String qName, Attributes attributes)730     public void startElement(String uri, String localName, String qName, Attributes attributes)
731             throws SAXException {
732         handleStartTag(localName, attributes);
733     }
734 
endElement(String uri, String localName, String qName)735     public void endElement(String uri, String localName, String qName) throws SAXException {
736         handleEndTag(localName);
737     }
738 
characters(char ch[], int start, int length)739     public void characters(char ch[], int start, int length) throws SAXException {
740         StringBuilder sb = new StringBuilder();
741 
742         /*
743          * Ignore whitespace that immediately follows other whitespace;
744          * newlines count as spaces.
745          */
746 
747         for (int i = 0; i < length; i++) {
748             char c = ch[i + start];
749 
750             if (c == ' ' || c == '\n') {
751                 char pred;
752                 int len = sb.length();
753 
754                 if (len == 0) {
755                     len = mSpannableStringBuilder.length();
756 
757                     if (len == 0) {
758                         pred = '\n';
759                     } else {
760                         pred = mSpannableStringBuilder.charAt(len - 1);
761                     }
762                 } else {
763                     pred = sb.charAt(len - 1);
764                 }
765 
766                 if (pred != ' ' && pred != '\n') {
767                     sb.append(' ');
768                 }
769             } else {
770                 sb.append(c);
771             }
772         }
773 
774         mSpannableStringBuilder.append(sb);
775     }
776 
ignorableWhitespace(char ch[], int start, int length)777     public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
778     }
779 
processingInstruction(String target, String data)780     public void processingInstruction(String target, String data) throws SAXException {
781     }
782 
skippedEntity(String name)783     public void skippedEntity(String name) throws SAXException {
784     }
785 
786     private static class Bold { }
787     private static class Italic { }
788     private static class Underline { }
789     private static class Big { }
790     private static class Small { }
791     private static class Monospace { }
792     private static class Blockquote { }
793     private static class Super { }
794     private static class Sub { }
795 
796     private static class Font {
797         public String mColor;
798         public String mFace;
799 
Font(String color, String face)800         public Font(String color, String face) {
801             mColor = color;
802             mFace = face;
803         }
804     }
805 
806     private static class Href {
807         public String mHref;
808 
Href(String href)809         public Href(String href) {
810             mHref = href;
811         }
812     }
813 
814     private static class Header {
815         private int mLevel;
816 
Header(int level)817         public Header(int level) {
818             mLevel = level;
819         }
820     }
821 
822     private static HashMap<String,Integer> COLORS = buildColorMap();
823 
buildColorMap()824     private static HashMap<String,Integer> buildColorMap() {
825         HashMap<String,Integer> map = new HashMap<String,Integer>();
826         map.put("aqua", 0x00FFFF);
827         map.put("black", 0x000000);
828         map.put("blue", 0x0000FF);
829         map.put("fuchsia", 0xFF00FF);
830         map.put("green", 0x008000);
831         map.put("grey", 0x808080);
832         map.put("lime", 0x00FF00);
833         map.put("maroon", 0x800000);
834         map.put("navy", 0x000080);
835         map.put("olive", 0x808000);
836         map.put("purple", 0x800080);
837         map.put("red", 0xFF0000);
838         map.put("silver", 0xC0C0C0);
839         map.put("teal", 0x008080);
840         map.put("white", 0xFFFFFF);
841         map.put("yellow", 0xFFFF00);
842         return map;
843     }
844 
845     /**
846      * Converts an HTML color (named or numeric) to an integer RGB value.
847      *
848      * @param color Non-null color string.
849      * @return A color value, or {@code -1} if the color string could not be interpreted.
850      */
getHtmlColor(String color)851     private static int getHtmlColor(String color) {
852         Integer i = COLORS.get(color.toLowerCase());
853         if (i != null) {
854             return i;
855         } else {
856             try {
857                 return XmlUtils.convertValueToInt(color, -1);
858             } catch (NumberFormatException nfe) {
859                 return -1;
860             }
861         }
862       }
863 
864 }
865