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.graphics.Color; 20 import com.android.internal.util.ArrayUtils; 21 import org.ccil.cowan.tagsoup.HTMLSchema; 22 import org.ccil.cowan.tagsoup.Parser; 23 import org.xml.sax.Attributes; 24 import org.xml.sax.ContentHandler; 25 import org.xml.sax.InputSource; 26 import org.xml.sax.Locator; 27 import org.xml.sax.SAXException; 28 import org.xml.sax.XMLReader; 29 30 import android.content.res.ColorStateList; 31 import android.content.res.Resources; 32 import android.graphics.Typeface; 33 import android.graphics.drawable.Drawable; 34 import android.text.style.AbsoluteSizeSpan; 35 import android.text.style.AlignmentSpan; 36 import android.text.style.CharacterStyle; 37 import android.text.style.ForegroundColorSpan; 38 import android.text.style.ImageSpan; 39 import android.text.style.ParagraphStyle; 40 import android.text.style.QuoteSpan; 41 import android.text.style.RelativeSizeSpan; 42 import android.text.style.StrikethroughSpan; 43 import android.text.style.StyleSpan; 44 import android.text.style.SubscriptSpan; 45 import android.text.style.SuperscriptSpan; 46 import android.text.style.TextAppearanceSpan; 47 import android.text.style.TypefaceSpan; 48 import android.text.style.URLSpan; 49 import android.text.style.UnderlineSpan; 50 51 import java.io.IOException; 52 import java.io.StringReader; 53 54 /** 55 * This class processes HTML strings into displayable styled text. 56 * Not all HTML tags are supported. 57 */ 58 public class Html { 59 /** 60 * Retrieves images for HTML <img> tags. 61 */ 62 public static interface ImageGetter { 63 /** 64 * This method is called when the HTML parser encounters an 65 * <img> tag. The <code>source</code> argument is the 66 * string from the "src" attribute; the return value should be 67 * a Drawable representation of the image or <code>null</code> 68 * for a generic replacement image. Make sure you call 69 * setBounds() on your Drawable if it doesn't already have 70 * its bounds set. 71 */ getDrawable(String source)72 public Drawable getDrawable(String source); 73 } 74 75 /** 76 * Is notified when HTML tags are encountered that the parser does 77 * not know how to interpret. 78 */ 79 public static interface TagHandler { 80 /** 81 * This method will be called whenn the HTML parser encounters 82 * a tag that it does not know how to interpret. 83 */ handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader)84 public void handleTag(boolean opening, String tag, 85 Editable output, XMLReader xmlReader); 86 } 87 Html()88 private Html() { } 89 90 /** 91 * Returns displayable styled text from the provided HTML string. 92 * Any <img> tags in the HTML will display as a generic 93 * replacement image which your program can then go through and 94 * replace with real images. 95 * 96 * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. 97 */ fromHtml(String source)98 public static Spanned fromHtml(String source) { 99 return fromHtml(source, null, null); 100 } 101 102 /** 103 * Lazy initialization holder for HTML parser. This class will 104 * a) be preloaded by the zygote, or b) not loaded until absolutely 105 * necessary. 106 */ 107 private static class HtmlParser { 108 private static final HTMLSchema schema = new HTMLSchema(); 109 } 110 111 /** 112 * Returns displayable styled text from the provided HTML string. 113 * Any <img> tags in the HTML will use the specified ImageGetter 114 * to request a representation of the image (use null if you don't 115 * want this) and the specified TagHandler to handle unknown tags 116 * (specify null if you don't want this). 117 * 118 * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. 119 */ fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler)120 public static Spanned fromHtml(String source, ImageGetter imageGetter, 121 TagHandler tagHandler) { 122 Parser parser = new Parser(); 123 try { 124 parser.setProperty(Parser.schemaProperty, HtmlParser.schema); 125 } catch (org.xml.sax.SAXNotRecognizedException e) { 126 // Should not happen. 127 throw new RuntimeException(e); 128 } catch (org.xml.sax.SAXNotSupportedException e) { 129 // Should not happen. 130 throw new RuntimeException(e); 131 } 132 133 HtmlToSpannedConverter converter = 134 new HtmlToSpannedConverter(source, imageGetter, tagHandler, 135 parser); 136 return converter.convert(); 137 } 138 139 /** 140 * Returns an HTML representation of the provided Spanned text. A best effort is 141 * made to add HTML tags corresponding to spans. Also note that HTML metacharacters 142 * (such as "<" and "&") within the input text are escaped. 143 * 144 * @param text input text to convert 145 * @return string containing input converted to HTML 146 */ toHtml(Spanned text)147 public static String toHtml(Spanned text) { 148 StringBuilder out = new StringBuilder(); 149 withinHtml(out, text); 150 return out.toString(); 151 } 152 153 /** 154 * Returns an HTML escaped representation of the given plain text. 155 */ escapeHtml(CharSequence text)156 public static String escapeHtml(CharSequence text) { 157 StringBuilder out = new StringBuilder(); 158 withinStyle(out, text, 0, text.length()); 159 return out.toString(); 160 } 161 withinHtml(StringBuilder out, Spanned text)162 private static void withinHtml(StringBuilder out, Spanned text) { 163 int len = text.length(); 164 165 int next; 166 for (int i = 0; i < text.length(); i = next) { 167 next = text.nextSpanTransition(i, len, ParagraphStyle.class); 168 ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class); 169 String elements = " "; 170 boolean needDiv = false; 171 172 for(int j = 0; j < style.length; j++) { 173 if (style[j] instanceof AlignmentSpan) { 174 Layout.Alignment align = 175 ((AlignmentSpan) style[j]).getAlignment(); 176 needDiv = true; 177 if (align == Layout.Alignment.ALIGN_CENTER) { 178 elements = "align=\"center\" " + elements; 179 } else if (align == Layout.Alignment.ALIGN_OPPOSITE) { 180 elements = "align=\"right\" " + elements; 181 } else { 182 elements = "align=\"left\" " + elements; 183 } 184 } 185 } 186 if (needDiv) { 187 out.append("<div ").append(elements).append(">"); 188 } 189 190 withinDiv(out, text, i, next); 191 192 if (needDiv) { 193 out.append("</div>"); 194 } 195 } 196 } 197 withinDiv(StringBuilder out, Spanned text, int start, int end)198 private static void withinDiv(StringBuilder out, Spanned text, 199 int start, int end) { 200 int next; 201 for (int i = start; i < end; i = next) { 202 next = text.nextSpanTransition(i, end, QuoteSpan.class); 203 QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class); 204 205 for (QuoteSpan quote : quotes) { 206 out.append("<blockquote>"); 207 } 208 209 withinBlockquote(out, text, i, next); 210 211 for (QuoteSpan quote : quotes) { 212 out.append("</blockquote>\n"); 213 } 214 } 215 } 216 getOpenParaTagWithDirection(Spanned text, int start, int end)217 private static String getOpenParaTagWithDirection(Spanned text, int start, int end) { 218 final int len = end - start; 219 final byte[] levels = ArrayUtils.newUnpaddedByteArray(len); 220 final char[] buffer = TextUtils.obtain(len); 221 TextUtils.getChars(text, start, end, buffer, 0); 222 223 int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len, 224 false /* no info */); 225 switch(paraDir) { 226 case Layout.DIR_RIGHT_TO_LEFT: 227 return "<p dir=\"rtl\">"; 228 case Layout.DIR_LEFT_TO_RIGHT: 229 default: 230 return "<p dir=\"ltr\">"; 231 } 232 } 233 withinBlockquote(StringBuilder out, Spanned text, int start, int end)234 private static void withinBlockquote(StringBuilder out, Spanned text, 235 int start, int end) { 236 out.append(getOpenParaTagWithDirection(text, start, end)); 237 238 int next; 239 for (int i = start; i < end; i = next) { 240 next = TextUtils.indexOf(text, '\n', i, end); 241 if (next < 0) { 242 next = end; 243 } 244 245 int nl = 0; 246 247 while (next < end && text.charAt(next) == '\n') { 248 nl++; 249 next++; 250 } 251 252 if (withinParagraph(out, text, i, next - nl, nl, next == end)) { 253 /* Paragraph should be closed */ 254 out.append("</p>\n"); 255 out.append(getOpenParaTagWithDirection(text, next, end)); 256 } 257 } 258 259 out.append("</p>\n"); 260 } 261 262 /* Returns true if the caller should close and reopen the paragraph. */ withinParagraph(StringBuilder out, Spanned text, int start, int end, int nl, boolean last)263 private static boolean withinParagraph(StringBuilder out, Spanned text, 264 int start, int end, int nl, 265 boolean last) { 266 int next; 267 for (int i = start; i < end; i = next) { 268 next = text.nextSpanTransition(i, end, CharacterStyle.class); 269 CharacterStyle[] style = text.getSpans(i, next, 270 CharacterStyle.class); 271 272 for (int j = 0; j < style.length; j++) { 273 if (style[j] instanceof StyleSpan) { 274 int s = ((StyleSpan) style[j]).getStyle(); 275 276 if ((s & Typeface.BOLD) != 0) { 277 out.append("<b>"); 278 } 279 if ((s & Typeface.ITALIC) != 0) { 280 out.append("<i>"); 281 } 282 } 283 if (style[j] instanceof TypefaceSpan) { 284 String s = ((TypefaceSpan) style[j]).getFamily(); 285 286 if ("monospace".equals(s)) { 287 out.append("<tt>"); 288 } 289 } 290 if (style[j] instanceof SuperscriptSpan) { 291 out.append("<sup>"); 292 } 293 if (style[j] instanceof SubscriptSpan) { 294 out.append("<sub>"); 295 } 296 if (style[j] instanceof UnderlineSpan) { 297 out.append("<u>"); 298 } 299 if (style[j] instanceof StrikethroughSpan) { 300 out.append("<strike>"); 301 } 302 if (style[j] instanceof URLSpan) { 303 out.append("<a href=\""); 304 out.append(((URLSpan) style[j]).getURL()); 305 out.append("\">"); 306 } 307 if (style[j] instanceof ImageSpan) { 308 out.append("<img src=\""); 309 out.append(((ImageSpan) style[j]).getSource()); 310 out.append("\">"); 311 312 // Don't output the dummy character underlying the image. 313 i = next; 314 } 315 if (style[j] instanceof AbsoluteSizeSpan) { 316 out.append("<font size =\""); 317 out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6); 318 out.append("\">"); 319 } 320 if (style[j] instanceof ForegroundColorSpan) { 321 out.append("<font color =\"#"); 322 String color = Integer.toHexString(((ForegroundColorSpan) 323 style[j]).getForegroundColor() + 0x01000000); 324 while (color.length() < 6) { 325 color = "0" + color; 326 } 327 out.append(color); 328 out.append("\">"); 329 } 330 } 331 332 withinStyle(out, text, i, next); 333 334 for (int j = style.length - 1; j >= 0; j--) { 335 if (style[j] instanceof ForegroundColorSpan) { 336 out.append("</font>"); 337 } 338 if (style[j] instanceof AbsoluteSizeSpan) { 339 out.append("</font>"); 340 } 341 if (style[j] instanceof URLSpan) { 342 out.append("</a>"); 343 } 344 if (style[j] instanceof StrikethroughSpan) { 345 out.append("</strike>"); 346 } 347 if (style[j] instanceof UnderlineSpan) { 348 out.append("</u>"); 349 } 350 if (style[j] instanceof SubscriptSpan) { 351 out.append("</sub>"); 352 } 353 if (style[j] instanceof SuperscriptSpan) { 354 out.append("</sup>"); 355 } 356 if (style[j] instanceof TypefaceSpan) { 357 String s = ((TypefaceSpan) style[j]).getFamily(); 358 359 if (s.equals("monospace")) { 360 out.append("</tt>"); 361 } 362 } 363 if (style[j] instanceof StyleSpan) { 364 int s = ((StyleSpan) style[j]).getStyle(); 365 366 if ((s & Typeface.BOLD) != 0) { 367 out.append("</b>"); 368 } 369 if ((s & Typeface.ITALIC) != 0) { 370 out.append("</i>"); 371 } 372 } 373 } 374 } 375 376 if (nl == 1) { 377 out.append("<br>\n"); 378 return false; 379 } else { 380 for (int i = 2; i < nl; i++) { 381 out.append("<br>"); 382 } 383 return !last; 384 } 385 } 386 withinStyle(StringBuilder out, CharSequence text, int start, int end)387 private static void withinStyle(StringBuilder out, CharSequence text, 388 int start, int end) { 389 for (int i = start; i < end; i++) { 390 char c = text.charAt(i); 391 392 if (c == '<') { 393 out.append("<"); 394 } else if (c == '>') { 395 out.append(">"); 396 } else if (c == '&') { 397 out.append("&"); 398 } else if (c >= 0xD800 && c <= 0xDFFF) { 399 if (c < 0xDC00 && i + 1 < end) { 400 char d = text.charAt(i + 1); 401 if (d >= 0xDC00 && d <= 0xDFFF) { 402 i++; 403 int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00; 404 out.append("&#").append(codepoint).append(";"); 405 } 406 } 407 } else if (c > 0x7E || c < ' ') { 408 out.append("&#").append((int) c).append(";"); 409 } else if (c == ' ') { 410 while (i + 1 < end && text.charAt(i + 1) == ' ') { 411 out.append(" "); 412 i++; 413 } 414 415 out.append(' '); 416 } else { 417 out.append(c); 418 } 419 } 420 } 421 } 422 423 class HtmlToSpannedConverter implements ContentHandler { 424 425 private static final float[] HEADER_SIZES = { 426 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, 427 }; 428 429 private String mSource; 430 private XMLReader mReader; 431 private SpannableStringBuilder mSpannableStringBuilder; 432 private Html.ImageGetter mImageGetter; 433 private Html.TagHandler mTagHandler; 434 HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser)435 public HtmlToSpannedConverter( 436 String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, 437 Parser parser) { 438 mSource = source; 439 mSpannableStringBuilder = new SpannableStringBuilder(); 440 mImageGetter = imageGetter; 441 mTagHandler = tagHandler; 442 mReader = parser; 443 } 444 convert()445 public Spanned convert() { 446 447 mReader.setContentHandler(this); 448 try { 449 mReader.parse(new InputSource(new StringReader(mSource))); 450 } catch (IOException e) { 451 // We are reading from a string. There should not be IO problems. 452 throw new RuntimeException(e); 453 } catch (SAXException e) { 454 // TagSoup doesn't throw parse exceptions. 455 throw new RuntimeException(e); 456 } 457 458 // Fix flags and range for paragraph-type markup. 459 Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class); 460 for (int i = 0; i < obj.length; i++) { 461 int start = mSpannableStringBuilder.getSpanStart(obj[i]); 462 int end = mSpannableStringBuilder.getSpanEnd(obj[i]); 463 464 // If the last line of the range is blank, back off by one. 465 if (end - 2 >= 0) { 466 if (mSpannableStringBuilder.charAt(end - 1) == '\n' && 467 mSpannableStringBuilder.charAt(end - 2) == '\n') { 468 end--; 469 } 470 } 471 472 if (end == start) { 473 mSpannableStringBuilder.removeSpan(obj[i]); 474 } else { 475 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH); 476 } 477 } 478 479 return mSpannableStringBuilder; 480 } 481 handleStartTag(String tag, Attributes attributes)482 private void handleStartTag(String tag, Attributes attributes) { 483 if (tag.equalsIgnoreCase("br")) { 484 // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br> 485 // so we can safely emite the linebreaks when we handle the close tag. 486 } else if (tag.equalsIgnoreCase("p")) { 487 handleP(mSpannableStringBuilder); 488 } else if (tag.equalsIgnoreCase("div")) { 489 handleP(mSpannableStringBuilder); 490 } else if (tag.equalsIgnoreCase("strong")) { 491 start(mSpannableStringBuilder, new Bold()); 492 } else if (tag.equalsIgnoreCase("b")) { 493 start(mSpannableStringBuilder, new Bold()); 494 } else if (tag.equalsIgnoreCase("em")) { 495 start(mSpannableStringBuilder, new Italic()); 496 } else if (tag.equalsIgnoreCase("cite")) { 497 start(mSpannableStringBuilder, new Italic()); 498 } else if (tag.equalsIgnoreCase("dfn")) { 499 start(mSpannableStringBuilder, new Italic()); 500 } else if (tag.equalsIgnoreCase("i")) { 501 start(mSpannableStringBuilder, new Italic()); 502 } else if (tag.equalsIgnoreCase("big")) { 503 start(mSpannableStringBuilder, new Big()); 504 } else if (tag.equalsIgnoreCase("small")) { 505 start(mSpannableStringBuilder, new Small()); 506 } else if (tag.equalsIgnoreCase("font")) { 507 startFont(mSpannableStringBuilder, attributes); 508 } else if (tag.equalsIgnoreCase("blockquote")) { 509 handleP(mSpannableStringBuilder); 510 start(mSpannableStringBuilder, new Blockquote()); 511 } else if (tag.equalsIgnoreCase("tt")) { 512 start(mSpannableStringBuilder, new Monospace()); 513 } else if (tag.equalsIgnoreCase("a")) { 514 startA(mSpannableStringBuilder, attributes); 515 } else if (tag.equalsIgnoreCase("u")) { 516 start(mSpannableStringBuilder, new Underline()); 517 } else if (tag.equalsIgnoreCase("sup")) { 518 start(mSpannableStringBuilder, new Super()); 519 } else if (tag.equalsIgnoreCase("sub")) { 520 start(mSpannableStringBuilder, new Sub()); 521 } else if (tag.length() == 2 && 522 Character.toLowerCase(tag.charAt(0)) == 'h' && 523 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { 524 handleP(mSpannableStringBuilder); 525 start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1')); 526 } else if (tag.equalsIgnoreCase("img")) { 527 startImg(mSpannableStringBuilder, attributes, mImageGetter); 528 } else if (mTagHandler != null) { 529 mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader); 530 } 531 } 532 handleEndTag(String tag)533 private void handleEndTag(String tag) { 534 if (tag.equalsIgnoreCase("br")) { 535 handleBr(mSpannableStringBuilder); 536 } else if (tag.equalsIgnoreCase("p")) { 537 handleP(mSpannableStringBuilder); 538 } else if (tag.equalsIgnoreCase("div")) { 539 handleP(mSpannableStringBuilder); 540 } else if (tag.equalsIgnoreCase("strong")) { 541 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); 542 } else if (tag.equalsIgnoreCase("b")) { 543 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); 544 } else if (tag.equalsIgnoreCase("em")) { 545 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); 546 } else if (tag.equalsIgnoreCase("cite")) { 547 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); 548 } else if (tag.equalsIgnoreCase("dfn")) { 549 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); 550 } else if (tag.equalsIgnoreCase("i")) { 551 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); 552 } else if (tag.equalsIgnoreCase("big")) { 553 end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f)); 554 } else if (tag.equalsIgnoreCase("small")) { 555 end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f)); 556 } else if (tag.equalsIgnoreCase("font")) { 557 endFont(mSpannableStringBuilder); 558 } else if (tag.equalsIgnoreCase("blockquote")) { 559 handleP(mSpannableStringBuilder); 560 end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan()); 561 } else if (tag.equalsIgnoreCase("tt")) { 562 end(mSpannableStringBuilder, Monospace.class, 563 new TypefaceSpan("monospace")); 564 } else if (tag.equalsIgnoreCase("a")) { 565 endA(mSpannableStringBuilder); 566 } else if (tag.equalsIgnoreCase("u")) { 567 end(mSpannableStringBuilder, Underline.class, new UnderlineSpan()); 568 } else if (tag.equalsIgnoreCase("sup")) { 569 end(mSpannableStringBuilder, Super.class, new SuperscriptSpan()); 570 } else if (tag.equalsIgnoreCase("sub")) { 571 end(mSpannableStringBuilder, Sub.class, new SubscriptSpan()); 572 } else if (tag.length() == 2 && 573 Character.toLowerCase(tag.charAt(0)) == 'h' && 574 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { 575 handleP(mSpannableStringBuilder); 576 endHeader(mSpannableStringBuilder); 577 } else if (mTagHandler != null) { 578 mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader); 579 } 580 } 581 handleP(SpannableStringBuilder text)582 private static void handleP(SpannableStringBuilder text) { 583 int len = text.length(); 584 585 if (len >= 1 && text.charAt(len - 1) == '\n') { 586 if (len >= 2 && text.charAt(len - 2) == '\n') { 587 return; 588 } 589 590 text.append("\n"); 591 return; 592 } 593 594 if (len != 0) { 595 text.append("\n\n"); 596 } 597 } 598 handleBr(SpannableStringBuilder text)599 private static void handleBr(SpannableStringBuilder text) { 600 text.append("\n"); 601 } 602 getLast(Spanned text, Class kind)603 private static Object getLast(Spanned text, Class kind) { 604 /* 605 * This knows that the last returned object from getSpans() 606 * will be the most recently added. 607 */ 608 Object[] objs = text.getSpans(0, text.length(), kind); 609 610 if (objs.length == 0) { 611 return null; 612 } else { 613 return objs[objs.length - 1]; 614 } 615 } 616 start(SpannableStringBuilder text, Object mark)617 private static void start(SpannableStringBuilder text, Object mark) { 618 int len = text.length(); 619 text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); 620 } 621 end(SpannableStringBuilder text, Class kind, Object repl)622 private static void end(SpannableStringBuilder text, Class kind, 623 Object repl) { 624 int len = text.length(); 625 Object obj = getLast(text, kind); 626 int where = text.getSpanStart(obj); 627 628 text.removeSpan(obj); 629 630 if (where != len) { 631 text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 632 } 633 } 634 startImg(SpannableStringBuilder text, Attributes attributes, Html.ImageGetter img)635 private static void startImg(SpannableStringBuilder text, 636 Attributes attributes, Html.ImageGetter img) { 637 String src = attributes.getValue("", "src"); 638 Drawable d = null; 639 640 if (img != null) { 641 d = img.getDrawable(src); 642 } 643 644 if (d == null) { 645 d = Resources.getSystem(). 646 getDrawable(com.android.internal.R.drawable.unknown_image); 647 d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); 648 } 649 650 int len = text.length(); 651 text.append("\uFFFC"); 652 653 text.setSpan(new ImageSpan(d, src), len, text.length(), 654 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 655 } 656 startFont(SpannableStringBuilder text, Attributes attributes)657 private static void startFont(SpannableStringBuilder text, 658 Attributes attributes) { 659 String color = attributes.getValue("", "color"); 660 String face = attributes.getValue("", "face"); 661 662 int len = text.length(); 663 text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK); 664 } 665 endFont(SpannableStringBuilder text)666 private static void endFont(SpannableStringBuilder text) { 667 int len = text.length(); 668 Object obj = getLast(text, Font.class); 669 int where = text.getSpanStart(obj); 670 671 text.removeSpan(obj); 672 673 if (where != len) { 674 Font f = (Font) obj; 675 676 if (!TextUtils.isEmpty(f.mColor)) { 677 if (f.mColor.startsWith("@")) { 678 Resources res = Resources.getSystem(); 679 String name = f.mColor.substring(1); 680 int colorRes = res.getIdentifier(name, "color", "android"); 681 if (colorRes != 0) { 682 ColorStateList colors = res.getColorStateList(colorRes, null); 683 text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null), 684 where, len, 685 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 686 } 687 } else { 688 int c = Color.getHtmlColor(f.mColor); 689 if (c != -1) { 690 text.setSpan(new ForegroundColorSpan(c | 0xFF000000), 691 where, len, 692 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 693 } 694 } 695 } 696 697 if (f.mFace != null) { 698 text.setSpan(new TypefaceSpan(f.mFace), where, len, 699 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 700 } 701 } 702 } 703 startA(SpannableStringBuilder text, Attributes attributes)704 private static void startA(SpannableStringBuilder text, Attributes attributes) { 705 String href = attributes.getValue("", "href"); 706 707 int len = text.length(); 708 text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK); 709 } 710 endA(SpannableStringBuilder text)711 private static void endA(SpannableStringBuilder text) { 712 int len = text.length(); 713 Object obj = getLast(text, Href.class); 714 int where = text.getSpanStart(obj); 715 716 text.removeSpan(obj); 717 718 if (where != len) { 719 Href h = (Href) obj; 720 721 if (h.mHref != null) { 722 text.setSpan(new URLSpan(h.mHref), where, len, 723 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 724 } 725 } 726 } 727 endHeader(SpannableStringBuilder text)728 private static void endHeader(SpannableStringBuilder text) { 729 int len = text.length(); 730 Object obj = getLast(text, Header.class); 731 732 int where = text.getSpanStart(obj); 733 734 text.removeSpan(obj); 735 736 // Back off not to change only the text, not the blank line. 737 while (len > where && text.charAt(len - 1) == '\n') { 738 len--; 739 } 740 741 if (where != len) { 742 Header h = (Header) obj; 743 744 text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]), 745 where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 746 text.setSpan(new StyleSpan(Typeface.BOLD), 747 where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 748 } 749 } 750 setDocumentLocator(Locator locator)751 public void setDocumentLocator(Locator locator) { 752 } 753 startDocument()754 public void startDocument() throws SAXException { 755 } 756 endDocument()757 public void endDocument() throws SAXException { 758 } 759 startPrefixMapping(String prefix, String uri)760 public void startPrefixMapping(String prefix, String uri) throws SAXException { 761 } 762 endPrefixMapping(String prefix)763 public void endPrefixMapping(String prefix) throws SAXException { 764 } 765 startElement(String uri, String localName, String qName, Attributes attributes)766 public void startElement(String uri, String localName, String qName, Attributes attributes) 767 throws SAXException { 768 handleStartTag(localName, attributes); 769 } 770 endElement(String uri, String localName, String qName)771 public void endElement(String uri, String localName, String qName) throws SAXException { 772 handleEndTag(localName); 773 } 774 characters(char ch[], int start, int length)775 public void characters(char ch[], int start, int length) throws SAXException { 776 StringBuilder sb = new StringBuilder(); 777 778 /* 779 * Ignore whitespace that immediately follows other whitespace; 780 * newlines count as spaces. 781 */ 782 783 for (int i = 0; i < length; i++) { 784 char c = ch[i + start]; 785 786 if (c == ' ' || c == '\n') { 787 char pred; 788 int len = sb.length(); 789 790 if (len == 0) { 791 len = mSpannableStringBuilder.length(); 792 793 if (len == 0) { 794 pred = '\n'; 795 } else { 796 pred = mSpannableStringBuilder.charAt(len - 1); 797 } 798 } else { 799 pred = sb.charAt(len - 1); 800 } 801 802 if (pred != ' ' && pred != '\n') { 803 sb.append(' '); 804 } 805 } else { 806 sb.append(c); 807 } 808 } 809 810 mSpannableStringBuilder.append(sb); 811 } 812 ignorableWhitespace(char ch[], int start, int length)813 public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { 814 } 815 processingInstruction(String target, String data)816 public void processingInstruction(String target, String data) throws SAXException { 817 } 818 skippedEntity(String name)819 public void skippedEntity(String name) throws SAXException { 820 } 821 822 private static class Bold { } 823 private static class Italic { } 824 private static class Underline { } 825 private static class Big { } 826 private static class Small { } 827 private static class Monospace { } 828 private static class Blockquote { } 829 private static class Super { } 830 private static class Sub { } 831 832 private static class Font { 833 public String mColor; 834 public String mFace; 835 Font(String color, String face)836 public Font(String color, String face) { 837 mColor = color; 838 mFace = face; 839 } 840 } 841 842 private static class Href { 843 public String mHref; 844 Href(String href)845 public Href(String href) { 846 mHref = href; 847 } 848 } 849 850 private static class Header { 851 private int mLevel; 852 Header(int level)853 public Header(int level) { 854 mLevel = level; 855 } 856 } 857 } 858