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