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