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