• 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.app.ActivityThread;
20 import android.app.Application;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.res.Resources;
23 import android.graphics.Color;
24 import android.graphics.Typeface;
25 import android.graphics.drawable.Drawable;
26 import android.os.Build;
27 import android.text.style.AbsoluteSizeSpan;
28 import android.text.style.AlignmentSpan;
29 import android.text.style.BackgroundColorSpan;
30 import android.text.style.BulletSpan;
31 import android.text.style.CharacterStyle;
32 import android.text.style.ForegroundColorSpan;
33 import android.text.style.ImageSpan;
34 import android.text.style.ParagraphStyle;
35 import android.text.style.QuoteSpan;
36 import android.text.style.RelativeSizeSpan;
37 import android.text.style.StrikethroughSpan;
38 import android.text.style.StyleSpan;
39 import android.text.style.SubscriptSpan;
40 import android.text.style.SuperscriptSpan;
41 import android.text.style.TypefaceSpan;
42 import android.text.style.URLSpan;
43 import android.text.style.UnderlineSpan;
44 
45 import com.android.internal.util.XmlUtils;
46 
47 import org.ccil.cowan.tagsoup.HTMLSchema;
48 import org.ccil.cowan.tagsoup.Parser;
49 import org.xml.sax.Attributes;
50 import org.xml.sax.ContentHandler;
51 import org.xml.sax.InputSource;
52 import org.xml.sax.Locator;
53 import org.xml.sax.SAXException;
54 import org.xml.sax.XMLReader;
55 
56 import java.io.IOException;
57 import java.io.StringReader;
58 import java.util.HashMap;
59 import java.util.Locale;
60 import java.util.Map;
61 import java.util.regex.Matcher;
62 import java.util.regex.Pattern;
63 
64 /**
65  * This class processes HTML strings into displayable styled text.
66  * Not all HTML tags are supported.
67  */
68 public class Html {
69     /**
70      * Retrieves images for HTML <img> tags.
71      */
72     public static interface ImageGetter {
73         /**
74          * This method is called when the HTML parser encounters an
75          * &lt;img&gt; tag.  The <code>source</code> argument is the
76          * string from the "src" attribute; the return value should be
77          * a Drawable representation of the image or <code>null</code>
78          * for a generic replacement image.  Make sure you call
79          * setBounds() on your Drawable if it doesn't already have
80          * its bounds set.
81          */
getDrawable(String source)82         public Drawable getDrawable(String source);
83     }
84 
85     /**
86      * Is notified when HTML tags are encountered that the parser does
87      * not know how to interpret.
88      */
89     public static interface TagHandler {
90         /**
91          * This method will be called whenn the HTML parser encounters
92          * a tag that it does not know how to interpret.
93          */
handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader)94         public void handleTag(boolean opening, String tag,
95                                  Editable output, XMLReader xmlReader);
96     }
97 
98     /**
99      * Option for {@link #toHtml(Spanned, int)}: Wrap consecutive lines of text delimited by '\n'
100      * inside &lt;p&gt; elements. {@link BulletSpan}s are ignored.
101      */
102     public static final int TO_HTML_PARAGRAPH_LINES_CONSECUTIVE = 0x00000000;
103 
104     /**
105      * Option for {@link #toHtml(Spanned, int)}: Wrap each line of text delimited by '\n' inside a
106      * &lt;p&gt; or a &lt;li&gt; element. This allows {@link ParagraphStyle}s attached to be
107      * encoded as CSS styles within the corresponding &lt;p&gt; or &lt;li&gt; element.
108      */
109     public static final int TO_HTML_PARAGRAPH_LINES_INDIVIDUAL = 0x00000001;
110 
111     /**
112      * Flag indicating that texts inside &lt;p&gt; elements will be separated from other texts with
113      * one newline character by default.
114      */
115     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH = 0x00000001;
116 
117     /**
118      * Flag indicating that texts inside &lt;h1&gt;~&lt;h6&gt; elements will be separated from
119      * other texts with one newline character by default.
120      */
121     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_HEADING = 0x00000002;
122 
123     /**
124      * Flag indicating that texts inside &lt;li&gt; elements will be separated from other texts
125      * with one newline character by default.
126      */
127     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM = 0x00000004;
128 
129     /**
130      * Flag indicating that texts inside &lt;ul&gt; elements will be separated from other texts
131      * with one newline character by default.
132      */
133     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST = 0x00000008;
134 
135     /**
136      * Flag indicating that texts inside &lt;div&gt; elements will be separated from other texts
137      * with one newline character by default.
138      */
139     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_DIV = 0x00000010;
140 
141     /**
142      * Flag indicating that texts inside &lt;blockquote&gt; elements will be separated from other
143      * texts with one newline character by default.
144      */
145     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE = 0x00000020;
146 
147     /**
148      * Flag indicating that CSS color values should be used instead of those defined in
149      * {@link Color}.
150      */
151     public static final int FROM_HTML_OPTION_USE_CSS_COLORS = 0x00000100;
152 
153     /**
154      * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level
155      * elements with blank lines (two newline characters) in between. This is the legacy behavior
156      * prior to N.
157      */
158     public static final int FROM_HTML_MODE_LEGACY = 0x00000000;
159 
160     /**
161      * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level
162      * elements with line breaks (single newline character) in between. This inverts the
163      * {@link Spanned} to HTML string conversion done with the option
164      * {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}.
165      */
166     public static final int FROM_HTML_MODE_COMPACT =
167             FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH
168             | FROM_HTML_SEPARATOR_LINE_BREAK_HEADING
169             | FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM
170             | FROM_HTML_SEPARATOR_LINE_BREAK_LIST
171             | FROM_HTML_SEPARATOR_LINE_BREAK_DIV
172             | FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE;
173 
174     /**
175      * The bit which indicates if lines delimited by '\n' will be grouped into &lt;p&gt; elements.
176      */
177     private static final int TO_HTML_PARAGRAPH_FLAG = 0x00000001;
178 
Html()179     private Html() { }
180 
181     /**
182      * Returns displayable styled text from the provided HTML string with the legacy flags
183      * {@link #FROM_HTML_MODE_LEGACY}.
184      *
185      * @deprecated use {@link #fromHtml(String, int)} instead.
186      */
187     @Deprecated
fromHtml(String source)188     public static Spanned fromHtml(String source) {
189         return fromHtml(source, FROM_HTML_MODE_LEGACY, null, null);
190     }
191 
192     /**
193      * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; tags in the
194      * HTML will display as a generic replacement image which your program can then go through and
195      * replace with real images.
196      *
197      * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
198      */
fromHtml(String source, int flags)199     public static Spanned fromHtml(String source, int flags) {
200         return fromHtml(source, flags, null, null);
201     }
202 
203     /**
204      * Lazy initialization holder for HTML parser. This class will
205      * a) be preloaded by the zygote, or b) not loaded until absolutely
206      * necessary.
207      */
208     private static class HtmlParser {
209         private static final HTMLSchema schema = new HTMLSchema();
210     }
211 
212     /**
213      * Returns displayable styled text from the provided HTML string with the legacy flags
214      * {@link #FROM_HTML_MODE_LEGACY}.
215      *
216      * @deprecated use {@link #fromHtml(String, int, ImageGetter, TagHandler)} instead.
217      */
218     @Deprecated
fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler)219     public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {
220         return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler);
221     }
222 
223     /**
224      * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; tags in the
225      * HTML will use the specified ImageGetter to request a representation of the image (use null
226      * if you don't want this) and the specified TagHandler to handle unknown tags (specify null if
227      * you don't want this).
228      *
229      * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
230      */
fromHtml(String source, int flags, ImageGetter imageGetter, TagHandler tagHandler)231     public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter,
232             TagHandler tagHandler) {
233         Parser parser = new Parser();
234         try {
235             parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
236         } catch (org.xml.sax.SAXNotRecognizedException e) {
237             // Should not happen.
238             throw new RuntimeException(e);
239         } catch (org.xml.sax.SAXNotSupportedException e) {
240             // Should not happen.
241             throw new RuntimeException(e);
242         }
243 
244         HtmlToSpannedConverter converter =
245                 new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags);
246         return converter.convert();
247     }
248 
249     /**
250      * @deprecated use {@link #toHtml(Spanned, int)} instead.
251      */
252     @Deprecated
toHtml(Spanned text)253     public static String toHtml(Spanned text) {
254         return toHtml(text, TO_HTML_PARAGRAPH_LINES_CONSECUTIVE);
255     }
256 
257     /**
258      * Returns an HTML representation of the provided Spanned text. A best effort is
259      * made to add HTML tags corresponding to spans. Also note that HTML metacharacters
260      * (such as "&lt;" and "&amp;") within the input text are escaped.
261      *
262      * @param text input text to convert
263      * @param option one of {@link #TO_HTML_PARAGRAPH_LINES_CONSECUTIVE} or
264      *     {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}
265      * @return string containing input converted to HTML
266      */
toHtml(Spanned text, int option)267     public static String toHtml(Spanned text, int option) {
268         StringBuilder out = new StringBuilder();
269         withinHtml(out, text, option);
270         return out.toString();
271     }
272 
273     /**
274      * Returns an HTML escaped representation of the given plain text.
275      */
escapeHtml(CharSequence text)276     public static String escapeHtml(CharSequence text) {
277         StringBuilder out = new StringBuilder();
278         withinStyle(out, text, 0, text.length());
279         return out.toString();
280     }
281 
withinHtml(StringBuilder out, Spanned text, int option)282     private static void withinHtml(StringBuilder out, Spanned text, int option) {
283         if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) {
284             encodeTextAlignmentByDiv(out, text, option);
285             return;
286         }
287 
288         withinDiv(out, text, 0, text.length(), option);
289     }
290 
encodeTextAlignmentByDiv(StringBuilder out, Spanned text, int option)291     private static void encodeTextAlignmentByDiv(StringBuilder out, Spanned text, int option) {
292         int len = text.length();
293 
294         int next;
295         for (int i = 0; i < len; i = next) {
296             next = text.nextSpanTransition(i, len, ParagraphStyle.class);
297             ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
298             String elements = " ";
299             boolean needDiv = false;
300 
301             for(int j = 0; j < style.length; j++) {
302                 if (style[j] instanceof AlignmentSpan) {
303                     Layout.Alignment align =
304                         ((AlignmentSpan) style[j]).getAlignment();
305                     needDiv = true;
306                     if (align == Layout.Alignment.ALIGN_CENTER) {
307                         elements = "align=\"center\" " + elements;
308                     } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
309                         elements = "align=\"right\" " + elements;
310                     } else {
311                         elements = "align=\"left\" " + elements;
312                     }
313                 }
314             }
315             if (needDiv) {
316                 out.append("<div ").append(elements).append(">");
317             }
318 
319             withinDiv(out, text, i, next, option);
320 
321             if (needDiv) {
322                 out.append("</div>");
323             }
324         }
325     }
326 
withinDiv(StringBuilder out, Spanned text, int start, int end, int option)327     private static void withinDiv(StringBuilder out, Spanned text, int start, int end,
328             int option) {
329         int next;
330         for (int i = start; i < end; i = next) {
331             next = text.nextSpanTransition(i, end, QuoteSpan.class);
332             QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
333 
334             for (QuoteSpan quote : quotes) {
335                 out.append("<blockquote>");
336             }
337 
338             withinBlockquote(out, text, i, next, option);
339 
340             for (QuoteSpan quote : quotes) {
341                 out.append("</blockquote>\n");
342             }
343         }
344     }
345 
getTextDirection(Spanned text, int start, int end)346     private static String getTextDirection(Spanned text, int start, int end) {
347         if (TextDirectionHeuristics.FIRSTSTRONG_LTR.isRtl(text, start, end - start)) {
348             return " dir=\"rtl\"";
349         } else {
350             return " dir=\"ltr\"";
351         }
352     }
353 
getTextStyles(Spanned text, int start, int end, boolean forceNoVerticalMargin, boolean includeTextAlign)354     private static String getTextStyles(Spanned text, int start, int end,
355             boolean forceNoVerticalMargin, boolean includeTextAlign) {
356         String margin = null;
357         String textAlign = null;
358 
359         if (forceNoVerticalMargin) {
360             margin = "margin-top:0; margin-bottom:0;";
361         }
362         if (includeTextAlign) {
363             final AlignmentSpan[] alignmentSpans = text.getSpans(start, end, AlignmentSpan.class);
364 
365             // Only use the last AlignmentSpan with flag SPAN_PARAGRAPH
366             for (int i = alignmentSpans.length - 1; i >= 0; i--) {
367                 AlignmentSpan s = alignmentSpans[i];
368                 if ((text.getSpanFlags(s) & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH) {
369                     final Layout.Alignment alignment = s.getAlignment();
370                     if (alignment == Layout.Alignment.ALIGN_NORMAL) {
371                         textAlign = "text-align:start;";
372                     } else if (alignment == Layout.Alignment.ALIGN_CENTER) {
373                         textAlign = "text-align:center;";
374                     } else if (alignment == Layout.Alignment.ALIGN_OPPOSITE) {
375                         textAlign = "text-align:end;";
376                     }
377                     break;
378                 }
379             }
380         }
381 
382         if (margin == null && textAlign == null) {
383             return "";
384         }
385 
386         final StringBuilder style = new StringBuilder(" style=\"");
387         if (margin != null && textAlign != null) {
388             style.append(margin).append(" ").append(textAlign);
389         } else if (margin != null) {
390             style.append(margin);
391         } else if (textAlign != null) {
392             style.append(textAlign);
393         }
394 
395         return style.append("\"").toString();
396     }
397 
withinBlockquote(StringBuilder out, Spanned text, int start, int end, int option)398     private static void withinBlockquote(StringBuilder out, Spanned text, int start, int end,
399             int option) {
400         if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) {
401             withinBlockquoteConsecutive(out, text, start, end);
402         } else {
403             withinBlockquoteIndividual(out, text, start, end);
404         }
405     }
406 
withinBlockquoteIndividual(StringBuilder out, Spanned text, int start, int end)407     private static void withinBlockquoteIndividual(StringBuilder out, Spanned text, int start,
408             int end) {
409         boolean isInList = false;
410         int next;
411         for (int i = start; i <= end; i = next) {
412             next = TextUtils.indexOf(text, '\n', i, end);
413             if (next < 0) {
414                 next = end;
415             }
416 
417             if (next == i) {
418                 if (isInList) {
419                     // Current paragraph is no longer a list item; close the previously opened list
420                     isInList = false;
421                     out.append("</ul>\n");
422                 }
423                 out.append("<br>\n");
424             } else {
425                 boolean isListItem = false;
426                 ParagraphStyle[] paragraphStyles = text.getSpans(i, next, ParagraphStyle.class);
427                 for (ParagraphStyle paragraphStyle : paragraphStyles) {
428                     final int spanFlags = text.getSpanFlags(paragraphStyle);
429                     if ((spanFlags & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH
430                             && paragraphStyle instanceof BulletSpan) {
431                         isListItem = true;
432                         break;
433                     }
434                 }
435 
436                 if (isListItem && !isInList) {
437                     // Current paragraph is the first item in a list
438                     isInList = true;
439                     out.append("<ul")
440                             .append(getTextStyles(text, i, next, true, false))
441                             .append(">\n");
442                 }
443 
444                 if (isInList && !isListItem) {
445                     // Current paragraph is no longer a list item; close the previously opened list
446                     isInList = false;
447                     out.append("</ul>\n");
448                 }
449 
450                 String tagType = isListItem ? "li" : "p";
451                 out.append("<").append(tagType)
452                         .append(getTextDirection(text, i, next))
453                         .append(getTextStyles(text, i, next, !isListItem, true))
454                         .append(">");
455 
456                 withinParagraph(out, text, i, next);
457 
458                 out.append("</");
459                 out.append(tagType);
460                 out.append(">\n");
461 
462                 if (next == end && isInList) {
463                     isInList = false;
464                     out.append("</ul>\n");
465                 }
466             }
467 
468             next++;
469         }
470     }
471 
withinBlockquoteConsecutive(StringBuilder out, Spanned text, int start, int end)472     private static void withinBlockquoteConsecutive(StringBuilder out, Spanned text, int start,
473             int end) {
474         out.append("<p").append(getTextDirection(text, start, end)).append(">");
475 
476         int next;
477         for (int i = start; i < end; i = next) {
478             next = TextUtils.indexOf(text, '\n', i, end);
479             if (next < 0) {
480                 next = end;
481             }
482 
483             int nl = 0;
484 
485             while (next < end && text.charAt(next) == '\n') {
486                 nl++;
487                 next++;
488             }
489 
490             withinParagraph(out, text, i, next - nl);
491 
492             if (nl == 1) {
493                 out.append("<br>\n");
494             } else {
495                 for (int j = 2; j < nl; j++) {
496                     out.append("<br>");
497                 }
498                 if (next != end) {
499                     /* Paragraph should be closed and reopened */
500                     out.append("</p>\n");
501                     out.append("<p").append(getTextDirection(text, start, end)).append(">");
502                 }
503             }
504         }
505 
506         out.append("</p>\n");
507     }
508 
withinParagraph(StringBuilder out, Spanned text, int start, int end)509     private static void withinParagraph(StringBuilder out, Spanned text, int start, int end) {
510         int next;
511         for (int i = start; i < end; i = next) {
512             next = text.nextSpanTransition(i, end, CharacterStyle.class);
513             CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
514 
515             for (int j = 0; j < style.length; j++) {
516                 if (style[j] instanceof StyleSpan) {
517                     int s = ((StyleSpan) style[j]).getStyle();
518 
519                     if ((s & Typeface.BOLD) != 0) {
520                         out.append("<b>");
521                     }
522                     if ((s & Typeface.ITALIC) != 0) {
523                         out.append("<i>");
524                     }
525                 }
526                 if (style[j] instanceof TypefaceSpan) {
527                     String s = ((TypefaceSpan) style[j]).getFamily();
528 
529                     if ("monospace".equals(s)) {
530                         out.append("<tt>");
531                     }
532                 }
533                 if (style[j] instanceof SuperscriptSpan) {
534                     out.append("<sup>");
535                 }
536                 if (style[j] instanceof SubscriptSpan) {
537                     out.append("<sub>");
538                 }
539                 if (style[j] instanceof UnderlineSpan) {
540                     out.append("<u>");
541                 }
542                 if (style[j] instanceof StrikethroughSpan) {
543                     out.append("<span style=\"text-decoration:line-through;\">");
544                 }
545                 if (style[j] instanceof URLSpan) {
546                     out.append("<a href=\"");
547                     out.append(((URLSpan) style[j]).getURL());
548                     out.append("\">");
549                 }
550                 if (style[j] instanceof ImageSpan) {
551                     out.append("<img src=\"");
552                     out.append(((ImageSpan) style[j]).getSource());
553                     out.append("\">");
554 
555                     // Don't output the placeholder character underlying the image.
556                     i = next;
557                 }
558                 if (style[j] instanceof AbsoluteSizeSpan) {
559                     AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);
560                     float sizeDip = s.getSize();
561                     if (!s.getDip()) {
562                         Application application = ActivityThread.currentApplication();
563                         sizeDip /= application.getResources().getDisplayMetrics().density;
564                     }
565 
566                     // px in CSS is the equivalance of dip in Android
567                     out.append(String.format("<span style=\"font-size:%.0fpx\";>", sizeDip));
568                 }
569                 if (style[j] instanceof RelativeSizeSpan) {
570                     float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();
571                     out.append(String.format("<span style=\"font-size:%.2fem;\">", sizeEm));
572                 }
573                 if (style[j] instanceof ForegroundColorSpan) {
574                     int color = ((ForegroundColorSpan) style[j]).getForegroundColor();
575                     out.append(String.format("<span style=\"color:#%06X;\">", 0xFFFFFF & color));
576                 }
577                 if (style[j] instanceof BackgroundColorSpan) {
578                     int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();
579                     out.append(String.format("<span style=\"background-color:#%06X;\">",
580                             0xFFFFFF & color));
581                 }
582             }
583 
584             withinStyle(out, text, i, next);
585 
586             for (int j = style.length - 1; j >= 0; j--) {
587                 if (style[j] instanceof BackgroundColorSpan) {
588                     out.append("</span>");
589                 }
590                 if (style[j] instanceof ForegroundColorSpan) {
591                     out.append("</span>");
592                 }
593                 if (style[j] instanceof RelativeSizeSpan) {
594                     out.append("</span>");
595                 }
596                 if (style[j] instanceof AbsoluteSizeSpan) {
597                     out.append("</span>");
598                 }
599                 if (style[j] instanceof URLSpan) {
600                     out.append("</a>");
601                 }
602                 if (style[j] instanceof StrikethroughSpan) {
603                     out.append("</span>");
604                 }
605                 if (style[j] instanceof UnderlineSpan) {
606                     out.append("</u>");
607                 }
608                 if (style[j] instanceof SubscriptSpan) {
609                     out.append("</sub>");
610                 }
611                 if (style[j] instanceof SuperscriptSpan) {
612                     out.append("</sup>");
613                 }
614                 if (style[j] instanceof TypefaceSpan) {
615                     String s = ((TypefaceSpan) style[j]).getFamily();
616 
617                     if (s.equals("monospace")) {
618                         out.append("</tt>");
619                     }
620                 }
621                 if (style[j] instanceof StyleSpan) {
622                     int s = ((StyleSpan) style[j]).getStyle();
623 
624                     if ((s & Typeface.BOLD) != 0) {
625                         out.append("</b>");
626                     }
627                     if ((s & Typeface.ITALIC) != 0) {
628                         out.append("</i>");
629                     }
630                 }
631             }
632         }
633     }
634 
635     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
withinStyle(StringBuilder out, CharSequence text, int start, int end)636     private static void withinStyle(StringBuilder out, CharSequence text,
637                                     int start, int end) {
638         for (int i = start; i < end; i++) {
639             char c = text.charAt(i);
640 
641             if (c == '<') {
642                 out.append("&lt;");
643             } else if (c == '>') {
644                 out.append("&gt;");
645             } else if (c == '&') {
646                 out.append("&amp;");
647             } else if (c >= 0xD800 && c <= 0xDFFF) {
648                 if (c < 0xDC00 && i + 1 < end) {
649                     char d = text.charAt(i + 1);
650                     if (d >= 0xDC00 && d <= 0xDFFF) {
651                         i++;
652                         int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
653                         out.append("&#").append(codepoint).append(";");
654                     }
655                 }
656             } else if (c > 0x7E || c < ' ') {
657                 out.append("&#").append((int) c).append(";");
658             } else if (c == ' ') {
659                 while (i + 1 < end && text.charAt(i + 1) == ' ') {
660                     out.append("&nbsp;");
661                     i++;
662                 }
663 
664                 out.append(' ');
665             } else {
666                 out.append(c);
667             }
668         }
669     }
670 }
671 
672 class HtmlToSpannedConverter implements ContentHandler {
673 
674     private static final float[] HEADING_SIZES = {
675         1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
676     };
677 
678     private String mSource;
679     private XMLReader mReader;
680     private SpannableStringBuilder mSpannableStringBuilder;
681     private Html.ImageGetter mImageGetter;
682     private Html.TagHandler mTagHandler;
683     private int mFlags;
684 
685     private static Pattern sTextAlignPattern;
686     private static Pattern sForegroundColorPattern;
687     private static Pattern sBackgroundColorPattern;
688     private static Pattern sTextDecorationPattern;
689 
690     /**
691      * Name-value mapping of HTML/CSS colors which have different values in {@link Color}.
692      */
693     private static final Map<String, Integer> sColorMap;
694 
695     static {
696         sColorMap = new HashMap<>();
697         sColorMap.put("darkgray", 0xFFA9A9A9);
698         sColorMap.put("gray", 0xFF808080);
699         sColorMap.put("lightgray", 0xFFD3D3D3);
700         sColorMap.put("darkgrey", 0xFFA9A9A9);
701         sColorMap.put("grey", 0xFF808080);
702         sColorMap.put("lightgrey", 0xFFD3D3D3);
703         sColorMap.put("green", 0xFF008000);
704     }
705 
getTextAlignPattern()706     private static Pattern getTextAlignPattern() {
707         if (sTextAlignPattern == null) {
708             sTextAlignPattern = Pattern.compile("(?:\\s+|\\A)text-align\\s*:\\s*(\\S*)\\b");
709         }
710         return sTextAlignPattern;
711     }
712 
getForegroundColorPattern()713     private static Pattern getForegroundColorPattern() {
714         if (sForegroundColorPattern == null) {
715             sForegroundColorPattern = Pattern.compile(
716                     "(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b");
717         }
718         return sForegroundColorPattern;
719     }
720 
getBackgroundColorPattern()721     private static Pattern getBackgroundColorPattern() {
722         if (sBackgroundColorPattern == null) {
723             sBackgroundColorPattern = Pattern.compile(
724                     "(?:\\s+|\\A)background(?:-color)?\\s*:\\s*(\\S*)\\b");
725         }
726         return sBackgroundColorPattern;
727     }
728 
getTextDecorationPattern()729     private static Pattern getTextDecorationPattern() {
730         if (sTextDecorationPattern == null) {
731             sTextDecorationPattern = Pattern.compile(
732                     "(?:\\s+|\\A)text-decoration\\s*:\\s*(\\S*)\\b");
733         }
734         return sTextDecorationPattern;
735     }
736 
HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser, int flags)737     public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter,
738             Html.TagHandler tagHandler, Parser parser, int flags) {
739         mSource = source;
740         mSpannableStringBuilder = new SpannableStringBuilder();
741         mImageGetter = imageGetter;
742         mTagHandler = tagHandler;
743         mReader = parser;
744         mFlags = flags;
745     }
746 
convert()747     public Spanned convert() {
748 
749         mReader.setContentHandler(this);
750         try {
751             mReader.parse(new InputSource(new StringReader(mSource)));
752         } catch (IOException e) {
753             // We are reading from a string. There should not be IO problems.
754             throw new RuntimeException(e);
755         } catch (SAXException e) {
756             // TagSoup doesn't throw parse exceptions.
757             throw new RuntimeException(e);
758         }
759 
760         // Fix flags and range for paragraph-type markup.
761         Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
762         for (int i = 0; i < obj.length; i++) {
763             int start = mSpannableStringBuilder.getSpanStart(obj[i]);
764             int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
765 
766             // If the last line of the range is blank, back off by one.
767             if (end - 2 >= 0) {
768                 if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
769                     mSpannableStringBuilder.charAt(end - 2) == '\n') {
770                     end--;
771                 }
772             }
773 
774             if (end == start) {
775                 mSpannableStringBuilder.removeSpan(obj[i]);
776             } else {
777                 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
778             }
779         }
780 
781         return mSpannableStringBuilder;
782     }
783 
handleStartTag(String tag, Attributes attributes)784     private void handleStartTag(String tag, Attributes attributes) {
785         if (tag.equalsIgnoreCase("br")) {
786             // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
787             // so we can safely emit the linebreaks when we handle the close tag.
788         } else if (tag.equalsIgnoreCase("p")) {
789             startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());
790             startCssStyle(mSpannableStringBuilder, attributes);
791         } else if (tag.equalsIgnoreCase("ul")) {
792             startBlockElement(mSpannableStringBuilder, attributes, getMarginList());
793         } else if (tag.equalsIgnoreCase("li")) {
794             startLi(mSpannableStringBuilder, attributes);
795         } else if (tag.equalsIgnoreCase("div")) {
796             startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv());
797         } else if (tag.equalsIgnoreCase("span")) {
798             startCssStyle(mSpannableStringBuilder, attributes);
799         } else if (tag.equalsIgnoreCase("strong")) {
800             start(mSpannableStringBuilder, new Bold());
801         } else if (tag.equalsIgnoreCase("b")) {
802             start(mSpannableStringBuilder, new Bold());
803         } else if (tag.equalsIgnoreCase("em")) {
804             start(mSpannableStringBuilder, new Italic());
805         } else if (tag.equalsIgnoreCase("cite")) {
806             start(mSpannableStringBuilder, new Italic());
807         } else if (tag.equalsIgnoreCase("dfn")) {
808             start(mSpannableStringBuilder, new Italic());
809         } else if (tag.equalsIgnoreCase("i")) {
810             start(mSpannableStringBuilder, new Italic());
811         } else if (tag.equalsIgnoreCase("big")) {
812             start(mSpannableStringBuilder, new Big());
813         } else if (tag.equalsIgnoreCase("small")) {
814             start(mSpannableStringBuilder, new Small());
815         } else if (tag.equalsIgnoreCase("font")) {
816             startFont(mSpannableStringBuilder, attributes);
817         } else if (tag.equalsIgnoreCase("blockquote")) {
818             startBlockquote(mSpannableStringBuilder, attributes);
819         } else if (tag.equalsIgnoreCase("tt")) {
820             start(mSpannableStringBuilder, new Monospace());
821         } else if (tag.equalsIgnoreCase("a")) {
822             startA(mSpannableStringBuilder, attributes);
823         } else if (tag.equalsIgnoreCase("u")) {
824             start(mSpannableStringBuilder, new Underline());
825         } else if (tag.equalsIgnoreCase("del")) {
826             start(mSpannableStringBuilder, new Strikethrough());
827         } else if (tag.equalsIgnoreCase("s")) {
828             start(mSpannableStringBuilder, new Strikethrough());
829         } else if (tag.equalsIgnoreCase("strike")) {
830             start(mSpannableStringBuilder, new Strikethrough());
831         } else if (tag.equalsIgnoreCase("sup")) {
832             start(mSpannableStringBuilder, new Super());
833         } else if (tag.equalsIgnoreCase("sub")) {
834             start(mSpannableStringBuilder, new Sub());
835         } else if (tag.length() == 2 &&
836                 Character.toLowerCase(tag.charAt(0)) == 'h' &&
837                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
838             startHeading(mSpannableStringBuilder, attributes, tag.charAt(1) - '1');
839         } else if (tag.equalsIgnoreCase("img")) {
840             startImg(mSpannableStringBuilder, attributes, mImageGetter);
841         } else if (mTagHandler != null) {
842             mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
843         }
844     }
845 
handleEndTag(String tag)846     private void handleEndTag(String tag) {
847         if (tag.equalsIgnoreCase("br")) {
848             handleBr(mSpannableStringBuilder);
849         } else if (tag.equalsIgnoreCase("p")) {
850             endCssStyle(mSpannableStringBuilder);
851             endBlockElement(mSpannableStringBuilder);
852         } else if (tag.equalsIgnoreCase("ul")) {
853             endBlockElement(mSpannableStringBuilder);
854         } else if (tag.equalsIgnoreCase("li")) {
855             endLi(mSpannableStringBuilder);
856         } else if (tag.equalsIgnoreCase("div")) {
857             endBlockElement(mSpannableStringBuilder);
858         } else if (tag.equalsIgnoreCase("span")) {
859             endCssStyle(mSpannableStringBuilder);
860         } else if (tag.equalsIgnoreCase("strong")) {
861             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
862         } else if (tag.equalsIgnoreCase("b")) {
863             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
864         } else if (tag.equalsIgnoreCase("em")) {
865             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
866         } else if (tag.equalsIgnoreCase("cite")) {
867             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
868         } else if (tag.equalsIgnoreCase("dfn")) {
869             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
870         } else if (tag.equalsIgnoreCase("i")) {
871             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
872         } else if (tag.equalsIgnoreCase("big")) {
873             end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
874         } else if (tag.equalsIgnoreCase("small")) {
875             end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
876         } else if (tag.equalsIgnoreCase("font")) {
877             endFont(mSpannableStringBuilder);
878         } else if (tag.equalsIgnoreCase("blockquote")) {
879             endBlockquote(mSpannableStringBuilder);
880         } else if (tag.equalsIgnoreCase("tt")) {
881             end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace"));
882         } else if (tag.equalsIgnoreCase("a")) {
883             endA(mSpannableStringBuilder);
884         } else if (tag.equalsIgnoreCase("u")) {
885             end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
886         } else if (tag.equalsIgnoreCase("del")) {
887             end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
888         } else if (tag.equalsIgnoreCase("s")) {
889             end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
890         } else if (tag.equalsIgnoreCase("strike")) {
891             end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
892         } else if (tag.equalsIgnoreCase("sup")) {
893             end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
894         } else if (tag.equalsIgnoreCase("sub")) {
895             end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
896         } else if (tag.length() == 2 &&
897                 Character.toLowerCase(tag.charAt(0)) == 'h' &&
898                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
899             endHeading(mSpannableStringBuilder);
900         } else if (mTagHandler != null) {
901             mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
902         }
903     }
904 
getMarginParagraph()905     private int getMarginParagraph() {
906         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH);
907     }
908 
getMarginHeading()909     private int getMarginHeading() {
910         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
911     }
912 
getMarginListItem()913     private int getMarginListItem() {
914         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM);
915     }
916 
getMarginList()917     private int getMarginList() {
918         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST);
919     }
920 
getMarginDiv()921     private int getMarginDiv() {
922         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV);
923     }
924 
getMarginBlockquote()925     private int getMarginBlockquote() {
926         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE);
927     }
928 
929     /**
930      * Returns the minimum number of newline characters needed before and after a given block-level
931      * element.
932      *
933      * @param flag the corresponding option flag defined in {@link Html} of a block-level element
934      */
getMargin(int flag)935     private int getMargin(int flag) {
936         if ((flag & mFlags) != 0) {
937             return 1;
938         }
939         return 2;
940     }
941 
appendNewlines(Editable text, int minNewline)942     private static void appendNewlines(Editable text, int minNewline) {
943         final int len = text.length();
944 
945         if (len == 0) {
946             return;
947         }
948 
949         int existingNewlines = 0;
950         for (int i = len - 1; i >= 0 && text.charAt(i) == '\n'; i--) {
951             existingNewlines++;
952         }
953 
954         for (int j = existingNewlines; j < minNewline; j++) {
955             text.append("\n");
956         }
957     }
958 
startBlockElement(Editable text, Attributes attributes, int margin)959     private static void startBlockElement(Editable text, Attributes attributes, int margin) {
960         final int len = text.length();
961         if (margin > 0) {
962             appendNewlines(text, margin);
963             start(text, new Newline(margin));
964         }
965 
966         String style = attributes.getValue("", "style");
967         if (style != null) {
968             Matcher m = getTextAlignPattern().matcher(style);
969             if (m.find()) {
970                 String alignment = m.group(1);
971                 if (alignment.equalsIgnoreCase("start")) {
972                     start(text, new Alignment(Layout.Alignment.ALIGN_NORMAL));
973                 } else if (alignment.equalsIgnoreCase("center")) {
974                     start(text, new Alignment(Layout.Alignment.ALIGN_CENTER));
975                 } else if (alignment.equalsIgnoreCase("end")) {
976                     start(text, new Alignment(Layout.Alignment.ALIGN_OPPOSITE));
977                 }
978             }
979         }
980     }
981 
endBlockElement(Editable text)982     private static void endBlockElement(Editable text) {
983         Newline n = getLast(text, Newline.class);
984         if (n != null) {
985             appendNewlines(text, n.mNumNewlines);
986             text.removeSpan(n);
987         }
988 
989         Alignment a = getLast(text, Alignment.class);
990         if (a != null) {
991             setSpanFromMark(text, a, new AlignmentSpan.Standard(a.mAlignment));
992         }
993     }
994 
handleBr(Editable text)995     private static void handleBr(Editable text) {
996         text.append('\n');
997     }
998 
startLi(Editable text, Attributes attributes)999     private void startLi(Editable text, Attributes attributes) {
1000         startBlockElement(text, attributes, getMarginListItem());
1001         start(text, new Bullet());
1002         startCssStyle(text, attributes);
1003     }
1004 
endLi(Editable text)1005     private static void endLi(Editable text) {
1006         endCssStyle(text);
1007         endBlockElement(text);
1008         end(text, Bullet.class, new BulletSpan());
1009     }
1010 
startBlockquote(Editable text, Attributes attributes)1011     private void startBlockquote(Editable text, Attributes attributes) {
1012         startBlockElement(text, attributes, getMarginBlockquote());
1013         start(text, new Blockquote());
1014     }
1015 
endBlockquote(Editable text)1016     private static void endBlockquote(Editable text) {
1017         endBlockElement(text);
1018         end(text, Blockquote.class, new QuoteSpan());
1019     }
1020 
startHeading(Editable text, Attributes attributes, int level)1021     private void startHeading(Editable text, Attributes attributes, int level) {
1022         startBlockElement(text, attributes, getMarginHeading());
1023         start(text, new Heading(level));
1024     }
1025 
endHeading(Editable text)1026     private static void endHeading(Editable text) {
1027         // RelativeSizeSpan and StyleSpan are CharacterStyles
1028         // Their ranges should not include the newlines at the end
1029         Heading h = getLast(text, Heading.class);
1030         if (h != null) {
1031             setSpanFromMark(text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]),
1032                     new StyleSpan(Typeface.BOLD));
1033         }
1034 
1035         endBlockElement(text);
1036     }
1037 
getLast(Spanned text, Class<T> kind)1038     private static <T> T getLast(Spanned text, Class<T> kind) {
1039         /*
1040          * This knows that the last returned object from getSpans()
1041          * will be the most recently added.
1042          */
1043         T[] objs = text.getSpans(0, text.length(), kind);
1044 
1045         if (objs.length == 0) {
1046             return null;
1047         } else {
1048             return objs[objs.length - 1];
1049         }
1050     }
1051 
setSpanFromMark(Spannable text, Object mark, Object... spans)1052     private static void setSpanFromMark(Spannable text, Object mark, Object... spans) {
1053         int where = text.getSpanStart(mark);
1054         text.removeSpan(mark);
1055         int len = text.length();
1056         if (where != len) {
1057             for (Object span : spans) {
1058                 text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1059             }
1060         }
1061     }
1062 
start(Editable text, Object mark)1063     private static void start(Editable text, Object mark) {
1064         int len = text.length();
1065         text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
1066     }
1067 
end(Editable text, Class kind, Object repl)1068     private static void end(Editable text, Class kind, Object repl) {
1069         int len = text.length();
1070         Object obj = getLast(text, kind);
1071         if (obj != null) {
1072             setSpanFromMark(text, obj, repl);
1073         }
1074     }
1075 
startCssStyle(Editable text, Attributes attributes)1076     private void startCssStyle(Editable text, Attributes attributes) {
1077         String style = attributes.getValue("", "style");
1078         if (style != null) {
1079             Matcher m = getForegroundColorPattern().matcher(style);
1080             if (m.find()) {
1081                 int c = getHtmlColor(m.group(1));
1082                 if (c != -1) {
1083                     start(text, new Foreground(c | 0xFF000000));
1084                 }
1085             }
1086 
1087             m = getBackgroundColorPattern().matcher(style);
1088             if (m.find()) {
1089                 int c = getHtmlColor(m.group(1));
1090                 if (c != -1) {
1091                     start(text, new Background(c | 0xFF000000));
1092                 }
1093             }
1094 
1095             m = getTextDecorationPattern().matcher(style);
1096             if (m.find()) {
1097                 String textDecoration = m.group(1);
1098                 if (textDecoration.equalsIgnoreCase("line-through")) {
1099                     start(text, new Strikethrough());
1100                 }
1101             }
1102         }
1103     }
1104 
endCssStyle(Editable text)1105     private static void endCssStyle(Editable text) {
1106         Strikethrough s = getLast(text, Strikethrough.class);
1107         if (s != null) {
1108             setSpanFromMark(text, s, new StrikethroughSpan());
1109         }
1110 
1111         Background b = getLast(text, Background.class);
1112         if (b != null) {
1113             setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor));
1114         }
1115 
1116         Foreground f = getLast(text, Foreground.class);
1117         if (f != null) {
1118             setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));
1119         }
1120     }
1121 
startImg(Editable text, Attributes attributes, Html.ImageGetter img)1122     private static void startImg(Editable text, Attributes attributes, Html.ImageGetter img) {
1123         String src = attributes.getValue("", "src");
1124         Drawable d = null;
1125 
1126         if (img != null) {
1127             d = img.getDrawable(src);
1128         }
1129 
1130         if (d == null) {
1131             d = Resources.getSystem().
1132                     getDrawable(com.android.internal.R.drawable.unknown_image);
1133             d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
1134         }
1135 
1136         int len = text.length();
1137         text.append("\uFFFC");
1138 
1139         text.setSpan(new ImageSpan(d, src), len, text.length(),
1140                      Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1141     }
1142 
startFont(Editable text, Attributes attributes)1143     private void startFont(Editable text, Attributes attributes) {
1144         String color = attributes.getValue("", "color");
1145         String face = attributes.getValue("", "face");
1146 
1147         if (!TextUtils.isEmpty(color)) {
1148             int c = getHtmlColor(color);
1149             if (c != -1) {
1150                 start(text, new Foreground(c | 0xFF000000));
1151             }
1152         }
1153 
1154         if (!TextUtils.isEmpty(face)) {
1155             start(text, new Font(face));
1156         }
1157     }
1158 
endFont(Editable text)1159     private static void endFont(Editable text) {
1160         Font font = getLast(text, Font.class);
1161         if (font != null) {
1162             setSpanFromMark(text, font, new TypefaceSpan(font.mFace));
1163         }
1164 
1165         Foreground foreground = getLast(text, Foreground.class);
1166         if (foreground != null) {
1167             setSpanFromMark(text, foreground,
1168                     new ForegroundColorSpan(foreground.mForegroundColor));
1169         }
1170     }
1171 
startA(Editable text, Attributes attributes)1172     private static void startA(Editable text, Attributes attributes) {
1173         String href = attributes.getValue("", "href");
1174         start(text, new Href(href));
1175     }
1176 
endA(Editable text)1177     private static void endA(Editable text) {
1178         Href h = getLast(text, Href.class);
1179         if (h != null) {
1180             if (h.mHref != null) {
1181                 setSpanFromMark(text, h, new URLSpan((h.mHref)));
1182             }
1183         }
1184     }
1185 
getHtmlColor(String color)1186     private int getHtmlColor(String color) {
1187         if ((mFlags & Html.FROM_HTML_OPTION_USE_CSS_COLORS)
1188                 == Html.FROM_HTML_OPTION_USE_CSS_COLORS) {
1189             Integer i = sColorMap.get(color.toLowerCase(Locale.US));
1190             if (i != null) {
1191                 return i;
1192             }
1193         }
1194 
1195         // If |color| is the name of a color, pass it to Color to convert it. Otherwise,
1196         // it may start with "#", "0", "0x", "+", or a digit. All of these cases are
1197         // handled below by XmlUtils. (Note that parseColor accepts colors starting
1198         // with "#", but it treats them differently from XmlUtils.)
1199         if (Character.isLetter(color.charAt(0))) {
1200             try {
1201                 return Color.parseColor(color);
1202             } catch (IllegalArgumentException e) {
1203                 return -1;
1204             }
1205         }
1206 
1207         try {
1208             return XmlUtils.convertValueToInt(color, -1);
1209         } catch (NumberFormatException nfe) {
1210             return -1;
1211         }
1212 
1213     }
1214 
setDocumentLocator(Locator locator)1215     public void setDocumentLocator(Locator locator) {
1216     }
1217 
startDocument()1218     public void startDocument() throws SAXException {
1219     }
1220 
endDocument()1221     public void endDocument() throws SAXException {
1222     }
1223 
startPrefixMapping(String prefix, String uri)1224     public void startPrefixMapping(String prefix, String uri) throws SAXException {
1225     }
1226 
endPrefixMapping(String prefix)1227     public void endPrefixMapping(String prefix) throws SAXException {
1228     }
1229 
startElement(String uri, String localName, String qName, Attributes attributes)1230     public void startElement(String uri, String localName, String qName, Attributes attributes)
1231             throws SAXException {
1232         handleStartTag(localName, attributes);
1233     }
1234 
endElement(String uri, String localName, String qName)1235     public void endElement(String uri, String localName, String qName) throws SAXException {
1236         handleEndTag(localName);
1237     }
1238 
characters(char ch[], int start, int length)1239     public void characters(char ch[], int start, int length) throws SAXException {
1240         StringBuilder sb = new StringBuilder();
1241 
1242         /*
1243          * Ignore whitespace that immediately follows other whitespace;
1244          * newlines count as spaces.
1245          */
1246 
1247         for (int i = 0; i < length; i++) {
1248             char c = ch[i + start];
1249 
1250             if (c == ' ' || c == '\n') {
1251                 char pred;
1252                 int len = sb.length();
1253 
1254                 if (len == 0) {
1255                     len = mSpannableStringBuilder.length();
1256 
1257                     if (len == 0) {
1258                         pred = '\n';
1259                     } else {
1260                         pred = mSpannableStringBuilder.charAt(len - 1);
1261                     }
1262                 } else {
1263                     pred = sb.charAt(len - 1);
1264                 }
1265 
1266                 if (pred != ' ' && pred != '\n') {
1267                     sb.append(' ');
1268                 }
1269             } else {
1270                 sb.append(c);
1271             }
1272         }
1273 
1274         mSpannableStringBuilder.append(sb);
1275     }
1276 
ignorableWhitespace(char ch[], int start, int length)1277     public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
1278     }
1279 
processingInstruction(String target, String data)1280     public void processingInstruction(String target, String data) throws SAXException {
1281     }
1282 
skippedEntity(String name)1283     public void skippedEntity(String name) throws SAXException {
1284     }
1285 
1286     private static class Bold { }
1287     private static class Italic { }
1288     private static class Underline { }
1289     private static class Strikethrough { }
1290     private static class Big { }
1291     private static class Small { }
1292     private static class Monospace { }
1293     private static class Blockquote { }
1294     private static class Super { }
1295     private static class Sub { }
1296     private static class Bullet { }
1297 
1298     private static class Font {
1299         public String mFace;
1300 
Font(String face)1301         public Font(String face) {
1302             mFace = face;
1303         }
1304     }
1305 
1306     private static class Href {
1307         public String mHref;
1308 
Href(String href)1309         public Href(String href) {
1310             mHref = href;
1311         }
1312     }
1313 
1314     private static class Foreground {
1315         private int mForegroundColor;
1316 
Foreground(int foregroundColor)1317         public Foreground(int foregroundColor) {
1318             mForegroundColor = foregroundColor;
1319         }
1320     }
1321 
1322     private static class Background {
1323         private int mBackgroundColor;
1324 
Background(int backgroundColor)1325         public Background(int backgroundColor) {
1326             mBackgroundColor = backgroundColor;
1327         }
1328     }
1329 
1330     private static class Heading {
1331         private int mLevel;
1332 
Heading(int level)1333         public Heading(int level) {
1334             mLevel = level;
1335         }
1336     }
1337 
1338     private static class Newline {
1339         private int mNumNewlines;
1340 
Newline(int numNewlines)1341         public Newline(int numNewlines) {
1342             mNumNewlines = numNewlines;
1343         }
1344     }
1345 
1346     private static class Alignment {
1347         private Layout.Alignment mAlignment;
1348 
Alignment(Layout.Alignment alignment)1349         public Alignment(Layout.Alignment alignment) {
1350             mAlignment = alignment;
1351         }
1352     }
1353 }
1354