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