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