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