Home | History | Annotate | Download | only in utils
      1 /**
      2  * Copyright (c) 2014, Google Inc.
      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 com.android.mail.utils;
     18 
     19 import android.graphics.Color;
     20 import android.graphics.Typeface;
     21 import android.text.SpannableStringBuilder;
     22 import android.text.Spanned;
     23 import android.text.style.AbsoluteSizeSpan;
     24 import android.text.style.ForegroundColorSpan;
     25 import android.text.style.QuoteSpan;
     26 import android.text.style.StyleSpan;
     27 import android.text.style.TypefaceSpan;
     28 import android.text.style.URLSpan;
     29 import android.text.style.UnderlineSpan;
     30 
     31 import com.android.mail.analytics.AnalyticsTimer;
     32 import com.google.android.mail.common.base.CharMatcher;
     33 import com.google.android.mail.common.html.parser.HTML;
     34 import com.google.android.mail.common.html.parser.HTML4;
     35 import com.google.android.mail.common.html.parser.HtmlDocument;
     36 import com.google.android.mail.common.html.parser.HtmlTree;
     37 import com.google.common.collect.Lists;
     38 
     39 import java.util.LinkedList;
     40 
     41 public class HtmlUtils {
     42 
     43     static final String LOG_TAG = LogTag.getLogTag();
     44 
     45     /**
     46      * Use our custom SpannedConverter to process the HtmlNode results from HtmlTree.
     47      * @param html
     48      * @return processed HTML as a Spanned
     49      */
     50     public static Spanned htmlToSpan(String html, HtmlTree.ConverterFactory factory) {
     51         AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.COMPOSE_HTML_TO_SPAN);
     52         // Get the html "tree"
     53         final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
     54         htmlTree.setConverterFactory(factory);
     55         final Spanned spanned = htmlTree.getSpanned();
     56         AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.COMPOSE_HTML_TO_SPAN, true,
     57                 "compose", "html_to_span", null);
     58         LogUtils.i(LOG_TAG, "htmlToSpan completed, input: %d, result: %d", html.length(),
     59                 spanned.length());
     60         return spanned;
     61     }
     62 
     63     /**
     64      * Class that handles converting the html into a Spanned.
     65      * This class will only handle a subset of the html tags. Below is the full list:
     66      *   - bold
     67      *   - italic
     68      *   - underline
     69      *   - font size
     70      *   - font color
     71      *   - font face
     72      *   - a
     73      *   - blockquote
     74      *   - p
     75      *   - div
     76      */
     77     public static class SpannedConverter implements HtmlTree.Converter<Spanned> {
     78         // Pinto normal text size is 2 while normal for AbsoluteSizeSpan is 12.
     79         // So 6 seems to be the magic number here. Html.toHtml also uses 6 as divider.
     80         private static final int WEB_TO_ANDROID_SIZE_MULTIPLIER = 6;
     81 
     82         protected final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
     83         private final LinkedList<TagWrapper> mSeenTags = Lists.newLinkedList();
     84 
     85         private final HtmlTree.DefaultPlainTextConverter mTextConverter =
     86                 new HtmlTree.DefaultPlainTextConverter();
     87         private int mTextConverterIndex = 0;
     88 
     89         @Override
     90         public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
     91             // Feed it into the plain text converter
     92             mTextConverter.addNode(n, nodeNum, endNum);
     93             if (n instanceof HtmlDocument.Tag) {
     94                 handleStart((HtmlDocument.Tag) n);
     95             } else if (n instanceof HtmlDocument.EndTag) {
     96                 handleEnd((HtmlDocument.EndTag) n);
     97             }
     98             appendPlainTextFromConverter();
     99         }
    100 
    101         private void appendPlainTextFromConverter() {
    102             String textString = mTextConverter.getObject();
    103             if (textString.length() > mTextConverterIndex) {
    104                 mBuilder.append(textString.substring(mTextConverterIndex));
    105                 mTextConverterIndex = textString.length();
    106             }
    107         }
    108 
    109         /**
    110          * Helper function to handle start tag
    111          */
    112         protected void handleStart(HtmlDocument.Tag tag) {
    113             if (!tag.isSelfTerminating()) {
    114                 // Add to the stack of tags needing closing tag
    115                 mSeenTags.push(new TagWrapper(tag, mBuilder.length()));
    116             }
    117         }
    118 
    119         /**
    120          * Helper function to handle end tag
    121          */
    122         protected void handleEnd(HtmlDocument.EndTag tag) {
    123             TagWrapper lastSeen;
    124             HTML.Element element = tag.getElement();
    125             while ((lastSeen = mSeenTags.poll()) != null && lastSeen.tag.getElement() != null &&
    126                     !lastSeen.tag.getElement().equals(element)) { }
    127 
    128             // Misformatted html, just ignore this tag
    129             if (lastSeen == null) {
    130                 return;
    131             }
    132 
    133             Object marker = null;
    134             if (HTML4.B_ELEMENT.equals(element)) {
    135                 // BOLD
    136                 marker = new StyleSpan(Typeface.BOLD);
    137             } else if (HTML4.I_ELEMENT.equals(element)) {
    138                 // ITALIC
    139                 marker = new StyleSpan(Typeface.ITALIC);
    140             } else if (HTML4.U_ELEMENT.equals(element)) {
    141                 // UNDERLINE
    142                 marker = new UnderlineSpan();
    143             } else if (HTML4.A_ELEMENT.equals(element)) {
    144                 // A HREF
    145                 HtmlDocument.TagAttribute attr = lastSeen.tag.getAttribute(HTML4.HREF_ATTRIBUTE);
    146                 // Ignore this tag if it doesn't have a link
    147                 if (attr == null) {
    148                     return;
    149                 }
    150                 marker = new URLSpan(attr.getValue());
    151             } else if (HTML4.BLOCKQUOTE_ELEMENT.equals(element)) {
    152                 // BLOCKQUOTE
    153                 marker = new QuoteSpan();
    154             } else if (HTML4.FONT_ELEMENT.equals(element)) {
    155                 // FONT SIZE/COLOR/FACE, since this can insert more than one span
    156                 // we special case it and return
    157                 handleFont(lastSeen);
    158             }
    159 
    160             final int start = lastSeen.startIndex;
    161             final int end = mBuilder.length();
    162             if (marker != null && start != end) {
    163                 mBuilder.setSpan(marker, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    164             }
    165         }
    166 
    167         /**
    168          * Helper function to handle end font tags
    169          */
    170         private void handleFont(TagWrapper wrapper) {
    171             final int start = wrapper.startIndex;
    172             final int end = mBuilder.length();
    173 
    174             // check font color
    175             HtmlDocument.TagAttribute attr = wrapper.tag.getAttribute(HTML4.COLOR_ATTRIBUTE);
    176             if (attr != null) {
    177                 int c = Color.parseColor(attr.getValue());
    178                 if (c != -1) {
    179                     mBuilder.setSpan(new ForegroundColorSpan(c | 0xFF000000), start, end,
    180                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    181                 }
    182             }
    183 
    184             // check font size
    185             attr = wrapper.tag.getAttribute(HTML4.SIZE_ATTRIBUTE);
    186             if (attr != null) {
    187                 int i = Integer.parseInt(attr.getValue());
    188                 if (i != -1) {
    189                     mBuilder.setSpan(new AbsoluteSizeSpan(i * WEB_TO_ANDROID_SIZE_MULTIPLIER,
    190                             true /* use dip */), start, end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    191                 }
    192             }
    193 
    194             // check font typeface
    195             attr = wrapper.tag.getAttribute(HTML4.FACE_ATTRIBUTE);
    196             if (attr != null) {
    197                 String[] families = attr.getValue().split(",");
    198                 for (String family : families) {
    199                     mBuilder.setSpan(new TypefaceSpan(family.trim()), start, end,
    200                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    201                 }
    202             }
    203         }
    204 
    205         @Override
    206         public int getPlainTextLength() {
    207             return mBuilder.length();
    208         }
    209 
    210         @Override
    211         public Spanned getObject() {
    212             return mBuilder;
    213         }
    214 
    215         private static class TagWrapper {
    216             final HtmlDocument.Tag tag;
    217             final int startIndex;
    218 
    219             TagWrapper(HtmlDocument.Tag tag, int startIndex) {
    220                 this.tag = tag;
    221                 this.startIndex = startIndex;
    222             }
    223         }
    224     }
    225 }
    226