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