Home | History | Annotate | Download | only in text
      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          * &lt;img&gt; 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          */
     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          */
     84         public void handleTag(boolean opening, String tag,
     85                                  Editable output, XMLReader xmlReader);
     86     }
     87 
     88     private Html() { }
     89 
     90     /**
     91      * Returns displayable styled text from the provided HTML string.
     92      * Any &lt;img&gt; 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      */
     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 &lt;img&gt; 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      */
    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 "&lt;" and "&amp;") within the input text are escaped.
    143      *
    144      * @param text input text to convert
    145      * @return string containing input converted to HTML
    146      */
    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      */
    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 
    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 
    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 
    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 
    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. */
    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 
    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("&lt;");
    394             } else if (c == '>') {
    395                 out.append("&gt;");
    396             } else if (c == '&') {
    397                 out.append("&amp;");
    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("&nbsp;");
    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 
    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 
    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 
    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 
    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 
    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 
    599     private static void handleBr(SpannableStringBuilder text) {
    600         text.append("\n");
    601     }
    602 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    751     public void setDocumentLocator(Locator locator) {
    752     }
    753 
    754     public void startDocument() throws SAXException {
    755     }
    756 
    757     public void endDocument() throws SAXException {
    758     }
    759 
    760     public void startPrefixMapping(String prefix, String uri) throws SAXException {
    761     }
    762 
    763     public void endPrefixMapping(String prefix) throws SAXException {
    764     }
    765 
    766     public void startElement(String uri, String localName, String qName, Attributes attributes)
    767             throws SAXException {
    768         handleStartTag(localName, attributes);
    769     }
    770 
    771     public void endElement(String uri, String localName, String qName) throws SAXException {
    772         handleEndTag(localName);
    773     }
    774 
    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 
    813     public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
    814     }
    815 
    816     public void processingInstruction(String target, String data) throws SAXException {
    817     }
    818 
    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 
    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 
    845         public Href(String href) {
    846             mHref = href;
    847         }
    848     }
    849 
    850     private static class Header {
    851         private int mLevel;
    852 
    853         public Header(int level) {
    854             mLevel = level;
    855         }
    856     }
    857 }
    858