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 * <img> 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 <p> 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 * <p> or a <li> element. This allows {@link ParagraphStyle}s attached to be 108 * encoded as CSS styles within the corresponding <p> or <li> element. 109 */ 110 public static final int TO_HTML_PARAGRAPH_LINES_INDIVIDUAL = 0x00000001; 111 112 /** 113 * Flag indicating that texts inside <p> 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 <h1>~<h6> 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 <li> 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 <ul> 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 <div> 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 <blockquote> 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 <p> 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 <img> 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 <img> 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 "<" and "&") 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("<"); 652 } else if (c == '>') { 653 out.append(">"); 654 } else if (c == '&') { 655 out.append("&"); 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(" "); 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