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             Application application = ActivityThread.currentApplication();
862             int fontWeightAdjustment =
863                     application.getResources().getConfiguration().fontWeightAdjustment;
864             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD,
865                     fontWeightAdjustment));
866         } else if (tag.equalsIgnoreCase("b")) {
867             Application application = ActivityThread.currentApplication();
868             int fontWeightAdjustment =
869                     application.getResources().getConfiguration().fontWeightAdjustment;
870             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD,
871                     fontWeightAdjustment));
872         } else if (tag.equalsIgnoreCase("em")) {
873             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
874         } else if (tag.equalsIgnoreCase("cite")) {
875             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
876         } else if (tag.equalsIgnoreCase("dfn")) {
877             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
878         } else if (tag.equalsIgnoreCase("i")) {
879             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
880         } else if (tag.equalsIgnoreCase("big")) {
881             end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
882         } else if (tag.equalsIgnoreCase("small")) {
883             end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
884         } else if (tag.equalsIgnoreCase("font")) {
885             endFont(mSpannableStringBuilder);
886         } else if (tag.equalsIgnoreCase("blockquote")) {
887             endBlockquote(mSpannableStringBuilder);
888         } else if (tag.equalsIgnoreCase("tt")) {
889             end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace"));
890         } else if (tag.equalsIgnoreCase("a")) {
891             endA(mSpannableStringBuilder);
892         } else if (tag.equalsIgnoreCase("u")) {
893             end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
894         } else if (tag.equalsIgnoreCase("del")) {
895             end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
896         } else if (tag.equalsIgnoreCase("s")) {
897             end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
898         } else if (tag.equalsIgnoreCase("strike")) {
899             end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
900         } else if (tag.equalsIgnoreCase("sup")) {
901             end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
902         } else if (tag.equalsIgnoreCase("sub")) {
903             end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
904         } else if (tag.length() == 2 &&
905                 Character.toLowerCase(tag.charAt(0)) == 'h' &&
906                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
907             endHeading(mSpannableStringBuilder);
908         } else if (mTagHandler != null) {
909             mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
910         }
911     }
912 
getMarginParagraph()913     private int getMarginParagraph() {
914         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH);
915     }
916 
getMarginHeading()917     private int getMarginHeading() {
918         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
919     }
920 
getMarginListItem()921     private int getMarginListItem() {
922         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM);
923     }
924 
getMarginList()925     private int getMarginList() {
926         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST);
927     }
928 
getMarginDiv()929     private int getMarginDiv() {
930         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV);
931     }
932 
getMarginBlockquote()933     private int getMarginBlockquote() {
934         return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE);
935     }
936 
937     /**
938      * Returns the minimum number of newline characters needed before and after a given block-level
939      * element.
940      *
941      * @param flag the corresponding option flag defined in {@link Html} of a block-level element
942      */
getMargin(int flag)943     private int getMargin(int flag) {
944         if ((flag & mFlags) != 0) {
945             return 1;
946         }
947         return 2;
948     }
949 
appendNewlines(Editable text, int minNewline)950     private static void appendNewlines(Editable text, int minNewline) {
951         final int len = text.length();
952 
953         if (len == 0) {
954             return;
955         }
956 
957         int existingNewlines = 0;
958         for (int i = len - 1; i >= 0 && text.charAt(i) == '\n'; i--) {
959             existingNewlines++;
960         }
961 
962         for (int j = existingNewlines; j < minNewline; j++) {
963             text.append("\n");
964         }
965     }
966 
startBlockElement(Editable text, Attributes attributes, int margin)967     private static void startBlockElement(Editable text, Attributes attributes, int margin) {
968         final int len = text.length();
969         if (margin > 0) {
970             appendNewlines(text, margin);
971             start(text, new Newline(margin));
972         }
973 
974         String style = attributes.getValue("", "style");
975         if (style != null) {
976             Matcher m = getTextAlignPattern().matcher(style);
977             if (m.find()) {
978                 String alignment = m.group(1);
979                 if (alignment.equalsIgnoreCase("start")) {
980                     start(text, new Alignment(Layout.Alignment.ALIGN_NORMAL));
981                 } else if (alignment.equalsIgnoreCase("center")) {
982                     start(text, new Alignment(Layout.Alignment.ALIGN_CENTER));
983                 } else if (alignment.equalsIgnoreCase("end")) {
984                     start(text, new Alignment(Layout.Alignment.ALIGN_OPPOSITE));
985                 }
986             }
987         }
988     }
989 
endBlockElement(Editable text)990     private static void endBlockElement(Editable text) {
991         Newline n = getLast(text, Newline.class);
992         if (n != null) {
993             appendNewlines(text, n.mNumNewlines);
994             text.removeSpan(n);
995         }
996 
997         Alignment a = getLast(text, Alignment.class);
998         if (a != null) {
999             setSpanFromMark(text, a, new AlignmentSpan.Standard(a.mAlignment));
1000         }
1001     }
1002 
handleBr(Editable text)1003     private static void handleBr(Editable text) {
1004         text.append('\n');
1005     }
1006 
startLi(Editable text, Attributes attributes)1007     private void startLi(Editable text, Attributes attributes) {
1008         startBlockElement(text, attributes, getMarginListItem());
1009         start(text, new Bullet());
1010         startCssStyle(text, attributes);
1011     }
1012 
endLi(Editable text)1013     private static void endLi(Editable text) {
1014         endCssStyle(text);
1015         endBlockElement(text);
1016         end(text, Bullet.class, new BulletSpan());
1017     }
1018 
startBlockquote(Editable text, Attributes attributes)1019     private void startBlockquote(Editable text, Attributes attributes) {
1020         startBlockElement(text, attributes, getMarginBlockquote());
1021         start(text, new Blockquote());
1022     }
1023 
endBlockquote(Editable text)1024     private static void endBlockquote(Editable text) {
1025         endBlockElement(text);
1026         end(text, Blockquote.class, new QuoteSpan());
1027     }
1028 
startHeading(Editable text, Attributes attributes, int level)1029     private void startHeading(Editable text, Attributes attributes, int level) {
1030         startBlockElement(text, attributes, getMarginHeading());
1031         start(text, new Heading(level));
1032     }
1033 
endHeading(Editable text)1034     private static void endHeading(Editable text) {
1035         // RelativeSizeSpan and StyleSpan are CharacterStyles
1036         // Their ranges should not include the newlines at the end
1037         Heading h = getLast(text, Heading.class);
1038         if (h != null) {
1039             Application application = ActivityThread.currentApplication();
1040             int fontWeightAdjustment =
1041                     application.getResources().getConfiguration().fontWeightAdjustment;
1042             setSpanFromMark(text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]),
1043                     new StyleSpan(Typeface.BOLD, fontWeightAdjustment));
1044         }
1045 
1046         endBlockElement(text);
1047     }
1048 
getLast(Spanned text, Class<T> kind)1049     private static <T> T getLast(Spanned text, Class<T> kind) {
1050         /*
1051          * This knows that the last returned object from getSpans()
1052          * will be the most recently added.
1053          */
1054         T[] objs = text.getSpans(0, text.length(), kind);
1055 
1056         if (objs.length == 0) {
1057             return null;
1058         } else {
1059             return objs[objs.length - 1];
1060         }
1061     }
1062 
setSpanFromMark(Spannable text, Object mark, Object... spans)1063     private static void setSpanFromMark(Spannable text, Object mark, Object... spans) {
1064         int where = text.getSpanStart(mark);
1065         text.removeSpan(mark);
1066         int len = text.length();
1067         if (where != len) {
1068             for (Object span : spans) {
1069                 text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1070             }
1071         }
1072     }
1073 
start(Editable text, Object mark)1074     private static void start(Editable text, Object mark) {
1075         int len = text.length();
1076         text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
1077     }
1078 
end(Editable text, Class kind, Object repl)1079     private static void end(Editable text, Class kind, Object repl) {
1080         int len = text.length();
1081         Object obj = getLast(text, kind);
1082         if (obj != null) {
1083             setSpanFromMark(text, obj, repl);
1084         }
1085     }
1086 
startCssStyle(Editable text, Attributes attributes)1087     private void startCssStyle(Editable text, Attributes attributes) {
1088         String style = attributes.getValue("", "style");
1089         if (style != null) {
1090             Matcher m = getForegroundColorPattern().matcher(style);
1091             if (m.find()) {
1092                 int c = getHtmlColor(m.group(1));
1093                 if (c != -1) {
1094                     start(text, new Foreground(c | 0xFF000000));
1095                 }
1096             }
1097 
1098             m = getBackgroundColorPattern().matcher(style);
1099             if (m.find()) {
1100                 int c = getHtmlColor(m.group(1));
1101                 if (c != -1) {
1102                     start(text, new Background(c | 0xFF000000));
1103                 }
1104             }
1105 
1106             m = getTextDecorationPattern().matcher(style);
1107             if (m.find()) {
1108                 String textDecoration = m.group(1);
1109                 if (textDecoration.equalsIgnoreCase("line-through")) {
1110                     start(text, new Strikethrough());
1111                 }
1112             }
1113         }
1114     }
1115 
endCssStyle(Editable text)1116     private static void endCssStyle(Editable text) {
1117         Strikethrough s = getLast(text, Strikethrough.class);
1118         if (s != null) {
1119             setSpanFromMark(text, s, new StrikethroughSpan());
1120         }
1121 
1122         Background b = getLast(text, Background.class);
1123         if (b != null) {
1124             setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor));
1125         }
1126 
1127         Foreground f = getLast(text, Foreground.class);
1128         if (f != null) {
1129             setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));
1130         }
1131     }
1132 
startImg(Editable text, Attributes attributes, Html.ImageGetter img)1133     private static void startImg(Editable text, Attributes attributes, Html.ImageGetter img) {
1134         String src = attributes.getValue("", "src");
1135         Drawable d = null;
1136 
1137         if (img != null) {
1138             d = img.getDrawable(src);
1139         }
1140 
1141         if (d == null) {
1142             d = Resources.getSystem().
1143                     getDrawable(com.android.internal.R.drawable.unknown_image);
1144             d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
1145         }
1146 
1147         int len = text.length();
1148         text.append("\uFFFC");
1149 
1150         text.setSpan(new ImageSpan(d, src), len, text.length(),
1151                      Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1152     }
1153 
startFont(Editable text, Attributes attributes)1154     private void startFont(Editable text, Attributes attributes) {
1155         String color = attributes.getValue("", "color");
1156         String face = attributes.getValue("", "face");
1157 
1158         if (!TextUtils.isEmpty(color)) {
1159             int c = getHtmlColor(color);
1160             if (c != -1) {
1161                 start(text, new Foreground(c | 0xFF000000));
1162             }
1163         }
1164 
1165         if (!TextUtils.isEmpty(face)) {
1166             start(text, new Font(face));
1167         }
1168     }
1169 
endFont(Editable text)1170     private static void endFont(Editable text) {
1171         Font font = getLast(text, Font.class);
1172         if (font != null) {
1173             setSpanFromMark(text, font, new TypefaceSpan(font.mFace));
1174         }
1175 
1176         Foreground foreground = getLast(text, Foreground.class);
1177         if (foreground != null) {
1178             setSpanFromMark(text, foreground,
1179                     new ForegroundColorSpan(foreground.mForegroundColor));
1180         }
1181     }
1182 
startA(Editable text, Attributes attributes)1183     private static void startA(Editable text, Attributes attributes) {
1184         String href = attributes.getValue("", "href");
1185         start(text, new Href(href));
1186     }
1187 
endA(Editable text)1188     private static void endA(Editable text) {
1189         Href h = getLast(text, Href.class);
1190         if (h != null) {
1191             if (h.mHref != null) {
1192                 setSpanFromMark(text, h, new URLSpan((h.mHref)));
1193             }
1194         }
1195     }
1196 
getHtmlColor(String color)1197     private int getHtmlColor(String color) {
1198         if ((mFlags & Html.FROM_HTML_OPTION_USE_CSS_COLORS)
1199                 == Html.FROM_HTML_OPTION_USE_CSS_COLORS) {
1200             Integer i = sColorMap.get(color.toLowerCase(Locale.US));
1201             if (i != null) {
1202                 return i;
1203             }
1204         }
1205 
1206         // If |color| is the name of a color, pass it to Color to convert it. Otherwise,
1207         // it may start with "#", "0", "0x", "+", or a digit. All of these cases are
1208         // handled below by XmlUtils. (Note that parseColor accepts colors starting
1209         // with "#", but it treats them differently from XmlUtils.)
1210         if (Character.isLetter(color.charAt(0))) {
1211             try {
1212                 return Color.parseColor(color);
1213             } catch (IllegalArgumentException e) {
1214                 return -1;
1215             }
1216         }
1217 
1218         try {
1219             return XmlUtils.convertValueToInt(color, -1);
1220         } catch (NumberFormatException nfe) {
1221             return -1;
1222         }
1223 
1224     }
1225 
setDocumentLocator(Locator locator)1226     public void setDocumentLocator(Locator locator) {
1227     }
1228 
startDocument()1229     public void startDocument() throws SAXException {
1230     }
1231 
endDocument()1232     public void endDocument() throws SAXException {
1233     }
1234 
startPrefixMapping(String prefix, String uri)1235     public void startPrefixMapping(String prefix, String uri) throws SAXException {
1236     }
1237 
endPrefixMapping(String prefix)1238     public void endPrefixMapping(String prefix) throws SAXException {
1239     }
1240 
startElement(String uri, String localName, String qName, Attributes attributes)1241     public void startElement(String uri, String localName, String qName, Attributes attributes)
1242             throws SAXException {
1243         handleStartTag(localName, attributes);
1244     }
1245 
endElement(String uri, String localName, String qName)1246     public void endElement(String uri, String localName, String qName) throws SAXException {
1247         handleEndTag(localName);
1248     }
1249 
characters(char ch[], int start, int length)1250     public void characters(char ch[], int start, int length) throws SAXException {
1251         StringBuilder sb = new StringBuilder();
1252 
1253         /*
1254          * Ignore whitespace that immediately follows other whitespace;
1255          * newlines count as spaces.
1256          */
1257 
1258         for (int i = 0; i < length; i++) {
1259             char c = ch[i + start];
1260 
1261             if (c == ' ' || c == '\n') {
1262                 char pred;
1263                 int len = sb.length();
1264 
1265                 if (len == 0) {
1266                     len = mSpannableStringBuilder.length();
1267 
1268                     if (len == 0) {
1269                         pred = '\n';
1270                     } else {
1271                         pred = mSpannableStringBuilder.charAt(len - 1);
1272                     }
1273                 } else {
1274                     pred = sb.charAt(len - 1);
1275                 }
1276 
1277                 if (pred != ' ' && pred != '\n') {
1278                     sb.append(' ');
1279                 }
1280             } else {
1281                 sb.append(c);
1282             }
1283         }
1284 
1285         mSpannableStringBuilder.append(sb);
1286     }
1287 
ignorableWhitespace(char ch[], int start, int length)1288     public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
1289     }
1290 
processingInstruction(String target, String data)1291     public void processingInstruction(String target, String data) throws SAXException {
1292     }
1293 
skippedEntity(String name)1294     public void skippedEntity(String name) throws SAXException {
1295     }
1296 
1297     private static class Bold { }
1298     private static class Italic { }
1299     private static class Underline { }
1300     private static class Strikethrough { }
1301     private static class Big { }
1302     private static class Small { }
1303     private static class Monospace { }
1304     private static class Blockquote { }
1305     private static class Super { }
1306     private static class Sub { }
1307     private static class Bullet { }
1308 
1309     private static class Font {
1310         public String mFace;
1311 
Font(String face)1312         public Font(String face) {
1313             mFace = face;
1314         }
1315     }
1316 
1317     private static class Href {
1318         public String mHref;
1319 
Href(String href)1320         public Href(String href) {
1321             mHref = href;
1322         }
1323     }
1324 
1325     private static class Foreground {
1326         private int mForegroundColor;
1327 
Foreground(int foregroundColor)1328         public Foreground(int foregroundColor) {
1329             mForegroundColor = foregroundColor;
1330         }
1331     }
1332 
1333     private static class Background {
1334         private int mBackgroundColor;
1335 
Background(int backgroundColor)1336         public Background(int backgroundColor) {
1337             mBackgroundColor = backgroundColor;
1338         }
1339     }
1340 
1341     private static class Heading {
1342         private int mLevel;
1343 
Heading(int level)1344         public Heading(int level) {
1345             mLevel = level;
1346         }
1347     }
1348 
1349     private static class Newline {
1350         private int mNumNewlines;
1351 
Newline(int numNewlines)1352         public Newline(int numNewlines) {
1353             mNumNewlines = numNewlines;
1354         }
1355     }
1356 
1357     private static class Alignment {
1358         private Layout.Alignment mAlignment;
1359 
Alignment(Layout.Alignment alignment)1360         public Alignment(Layout.Alignment alignment) {
1361             mAlignment = alignment;
1362         }
1363     }
1364 }
1365