k-9/k9mail/src/main/java/com/fsck/k9/helper/HtmlConverter.java

1411 lines
43 KiB
Java

package com.fsck.k9.helper;
import android.text.*;
import android.text.Html.TagHandler;
import com.fsck.k9.K9;
import org.xml.sax.XMLReader;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Contains common routines to convert html to text and vice versa.
*/
public class HtmlConverter {
/* This comprises most common used Unicode characters allowed in IRI
* as detailed in RFC 3987.
* Specifically, those two byte Unicode characters are not included.
*/
private static final String GOOD_IRI_CHAR =
"a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";
/**
* Goegular expression to match all IANA top-level domains for WEB_URL.
* List accurate as of 2011/01/12. List taken from:
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt
* This pattern is auto-generated by frameworks/base/common/tools/make-iana-tld-pattern.py
*/
private static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL =
"(?:"
+ "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ "|(?:biz|b[abdefghijmnorstvwyz])"
+ "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
+ "|d[ejkmoz]"
+ "|(?:edu|e[cegrstu])"
+ "|f[ijkmor]"
+ "|(?:gov|g[abdefghilmnpqrstuwy])"
+ "|h[kmnrtu]"
+ "|(?:info|int|i[delmnoqrst])"
+ "|(?:jobs|j[emop])"
+ "|k[eghimnprwyz]"
+ "|l[abcikrstuvy]"
+ "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
+ "|(?:name|net|n[acefgilopruz])"
+ "|(?:org|om)"
+ "|(?:pro|p[aefghklmnrstwy])"
+ "|qa"
+ "|r[eosuw]"
+ "|s[abcdeghijklmnortuvyz]"
+ "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
+ "|u[agksyz]"
+ "|v[aceginu]"
+ "|w[fs]"
+ "|(?:xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-80akhbyknj4f|xn\\-\\-9t4b11yi5a|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah)"
+ "|y[et]"
+ "|z[amw]))";
private static final String BITCOIN_URI_PATTERN =
"bitcoin:[1-9a-km-zA-HJ-NP-Z]{27,34}(\\?[a-zA-Z0-9$\\-_.+!*'(),%:@&=]*)?";
/**
* Regular expression pattern to match most part of RFC 3987
* Internationalized URLs, aka IRIs. Commonly used Unicode characters are
* added.
*/
private static final Pattern WEB_URL_PATTERN = Pattern.compile(
"((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?"
+ "((?:(?:[" + GOOD_IRI_CHAR + "][" + GOOD_IRI_CHAR + "\\-]{0,64}\\.)+" // named host
+ TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL
+ "|(?:(?:25[0-5]|2[0-4]" // or ip address
+ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]"
+ "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]"
+ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ "|[1-9][0-9]|[0-9])))"
+ "(?:\\:\\d{1,5})?)" // plus option port number
+ "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params
+ "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?"
+ "(?:\\b|$)"); // and finally, a word boundary or end of
/**
* When generating previews, Spannable objects that can't be converted into a String are
* represented as 0xfffc. When displayed, these show up as undisplayed squares. These constants
* define the object character and the replacement character.
*/
private static final char PREVIEW_OBJECT_CHARACTER = (char)0xfffc;
private static final char PREVIEW_OBJECT_REPLACEMENT = (char)0x20; // space
/**
* toHtml() converts non-breaking spaces into the UTF-8 non-breaking space, which doesn't get
* rendered properly in some clients. Replace it with a simple space.
*/
private static final char NBSP_CHARACTER = (char)0x00a0; // utf-8 non-breaking space
private static final char NBSP_REPLACEMENT = (char)0x20; // space
// Number of extra bytes to allocate in a string buffer for htmlification.
private static final int TEXT_TO_HTML_EXTRA_BUFFER_LENGTH = 512;
/**
* Convert an HTML string to a plain text string.
* @param html HTML string to convert.
* @return Plain text result.
*/
public static String htmlToText(final String html) {
return Html.fromHtml(html, null, new HtmlToTextTagHandler()).toString()
.replace(PREVIEW_OBJECT_CHARACTER, PREVIEW_OBJECT_REPLACEMENT)
.replace(NBSP_CHARACTER, NBSP_REPLACEMENT);
}
/**
* Custom tag handler to use when converting HTML messages to text. It currently handles text
* representations of HTML tags that Android's built-in parser doesn't understand and hides code
* contained in STYLE and SCRIPT blocks.
*/
private static class HtmlToTextTagHandler implements Html.TagHandler {
// List of tags whose content should be ignored.
private static final Set<String> TAGS_WITH_IGNORED_CONTENT;
static {
Set<String> set = new HashSet<String>();
set.add("style");
set.add("script");
set.add("title");
set.add("!"); // comments
TAGS_WITH_IGNORED_CONTENT = Collections.unmodifiableSet(set);
}
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
tag = tag.toLowerCase(Locale.US);
if (tag.equals("hr") && opening) {
// In the case of an <hr>, replace it with a bunch of underscores. This is roughly
// the behaviour of Outlook in Rich Text mode.
output.append("_____________________________________________\r\n");
} else if (TAGS_WITH_IGNORED_CONTENT.contains(tag)) {
handleIgnoredTag(opening, output);
}
}
private static final String IGNORED_ANNOTATION_KEY = "K9_ANNOTATION";
private static final String IGNORED_ANNOTATION_VALUE = "hiddenSpan";
/**
* When we come upon an ignored tag, we mark it with an Annotation object with a specific key
* and value as above. We don't really need to be checking these values since Html.fromHtml()
* doesn't use Annotation spans, but we should do it now to be safe in case they do start using
* it in the future.
* @param opening If this is an opening tag or not.
* @param output Spannable string that we're working with.
*/
private void handleIgnoredTag(boolean opening, Editable output) {
int len = output.length();
if (opening) {
output.setSpan(new Annotation(IGNORED_ANNOTATION_KEY, IGNORED_ANNOTATION_VALUE), len,
len, Spannable.SPAN_MARK_MARK);
} else {
Object start = getOpeningAnnotation(output);
if (start != null) {
int where = output.getSpanStart(start);
// Remove the temporary Annotation span.
output.removeSpan(start);
// Delete everything between the start of the Annotation and the end of the string
// (what we've generated so far).
output.delete(where, len);
}
}
}
/**
* Fetch the matching opening Annotation object and verify that it's the one added by K9.
* @param output Spannable string we're working with.
* @return Starting Annotation object.
*/
private Object getOpeningAnnotation(Editable output) {
Object[] objs = output.getSpans(0, output.length(), Annotation.class);
for (int i = objs.length - 1; i >= 0; i--) {
Annotation span = (Annotation) objs[i];
if (output.getSpanFlags(objs[i]) == Spannable.SPAN_MARK_MARK
&& span.getKey().equals(IGNORED_ANNOTATION_KEY)
&& span.getValue().equals(IGNORED_ANNOTATION_VALUE)) {
return objs[i];
}
}
return null;
}
}
private static final int MAX_SMART_HTMLIFY_MESSAGE_LENGTH = 1024 * 256 ;
/**
* Naively convert a text string into an HTML document.
*
* <p>
* This method avoids using regular expressions on the entire message body to save memory.
* </p>
* <p>
* No HTML headers or footers are added to the result. Headers and footers
* are added at display time in
* {@link com.fsck.k9.view#MessageWebView.setText(String) MessageWebView.setText()}
* </p>
*
* @param text
* Plain text string.
* @return HTML string.
*/
private static String simpleTextToHtml(String text) {
// Encode HTML entities to make sure we don't display something evil.
text = TextUtils.htmlEncode(text);
StringBuilder buff = new StringBuilder(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
buff.append(htmlifyMessageHeader());
for (int index = 0; index < text.length(); index++) {
char c = text.charAt(index);
switch (c) {
case '\n':
// pine treats <br> as two newlines, but <br/> as one newline. Use <br/> so our messages aren't
// doublespaced.
buff.append("<br />");
break;
case '\r':
break;
default:
buff.append(c);
}//switch
}
buff.append(htmlifyMessageFooter());
return buff.toString();
}
private static final String HTML_BLOCKQUOTE_COLOR_TOKEN = "$$COLOR$$";
private static final String HTML_BLOCKQUOTE_START = "<blockquote class=\"gmail_quote\" " +
"style=\"margin: 0pt 0pt 1ex 0.8ex; border-left: 1px solid $$COLOR$$; padding-left: 1ex;\">";
private static final String HTML_BLOCKQUOTE_END = "</blockquote>";
private static final String HTML_NEWLINE = "<br />";
/**
* Convert a text string into an HTML document.
*
* <p>
* Attempts to do smart replacement for large documents to prevent OOM
* errors.
* <p>
* No HTML headers or footers are added to the result. Headers and footers
* are added at display time in
* {@link com.fsck.k9.view#MessageWebView.setText(String) MessageWebView.setText()}
* </p>
* <p>
* To convert to a fragment, use {@link #textToHtmlFragment(String)} .
* </p>
*
* @param text
* Plain text string.
* @return HTML string.
*/
public static String textToHtml(String text) {
// Our HTMLification code is somewhat memory intensive
// and was causing lots of OOM errors on the market
// if the message is big and plain text, just do
// a trivial htmlification
if (text.length() > MAX_SMART_HTMLIFY_MESSAGE_LENGTH) {
return simpleTextToHtml(text);
}
StringBuilder buff = new StringBuilder(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
boolean isStartOfLine = true; // Are we currently at the start of a line?
int spaces = 0;
int quoteDepth = 0; // Number of DIVs deep we are.
int quotesThisLine = 0; // How deep we should be quoting for this line.
for (int index = 0; index < text.length(); index++) {
char c = text.charAt(index);
if (isStartOfLine) {
switch (c) {
case ' ':
spaces++;
break;
case '>':
quotesThisLine++;
spaces = 0;
break;
case '\n':
appendbq(buff, quotesThisLine, quoteDepth);
quoteDepth = quotesThisLine;
appendsp(buff, spaces);
spaces = 0;
appendchar(buff, c);
isStartOfLine = true;
quotesThisLine = 0;
break;
default:
isStartOfLine = false;
appendbq(buff, quotesThisLine, quoteDepth);
quoteDepth = quotesThisLine;
appendsp(buff, spaces);
spaces = 0;
appendchar(buff, c);
isStartOfLine = false;
break;
}
}
else {
appendchar(buff, c);
if (c == '\n') {
isStartOfLine = true;
quotesThisLine = 0;
}
}
}
// Close off any quotes we may have opened.
if (quoteDepth > 0) {
for (int i = quoteDepth; i > 0; i--) {
buff.append(HTML_BLOCKQUOTE_END);
}
}
text = buff.toString();
// Make newlines at the end of blockquotes nicer by putting newlines beyond the first one outside of the
// blockquote.
text = text.replaceAll(
"\\Q" + HTML_NEWLINE + "\\E((\\Q" + HTML_NEWLINE + "\\E)+?)\\Q" + HTML_BLOCKQUOTE_END + "\\E",
HTML_BLOCKQUOTE_END + "$1"
);
// Replace lines of -,= or _ with horizontal rules
text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", "<hr />");
StringBuffer sb = new StringBuffer(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
sb.append(htmlifyMessageHeader());
linkifyText(text, sb);
sb.append(htmlifyMessageFooter());
text = sb.toString();
// Above we replaced > with <gt>, now make it &gt;
text = text.replaceAll("<gt>", "&gt;");
return text;
}
private static void appendchar(StringBuilder buff, int c) {
switch (c) {
case '&':
buff.append("&amp;");
break;
case '<':
buff.append("&lt;");
break;
case '>':
// We use a token here which can't occur in htmlified text because &gt; is valid
// within links (where > is not), and linkifying links will include it if we
// do it here. We'll make another pass and change this back to &gt; after
// the linkification is done.
buff.append("<gt>");
break;
case '\r':
break;
case '\n':
// pine treats <br> as two newlines, but <br/> as one newline. Use <br/> so our messages aren't
// doublespaced.
buff.append(HTML_NEWLINE);
break;
default:
buff.append((char)c);
break;
}
}
private static void appendsp(StringBuilder buff, int spaces) {
while (spaces > 0) {
buff.append(' ');
spaces--;
}
}
private static void appendbq(StringBuilder buff, int quotesThisLine, int quoteDepth) {
// Add/remove blockquotes by comparing this line's quotes to the previous line's quotes.
if (quotesThisLine > quoteDepth) {
for (int i = quoteDepth; i < quotesThisLine; i++) {
buff.append(HTML_BLOCKQUOTE_START.replace(HTML_BLOCKQUOTE_COLOR_TOKEN, getQuoteColor(i + 1)));
}
} else if (quotesThisLine < quoteDepth) {
for (int i = quoteDepth; i > quotesThisLine; i--) {
buff.append(HTML_BLOCKQUOTE_END);
}
}
}
protected static final String QUOTE_COLOR_DEFAULT = "#ccc";
protected static final String QUOTE_COLOR_LEVEL_1 = "#729fcf";
protected static final String QUOTE_COLOR_LEVEL_2 = "#ad7fa8";
protected static final String QUOTE_COLOR_LEVEL_3 = "#8ae234";
protected static final String QUOTE_COLOR_LEVEL_4 = "#fcaf3e";
protected static final String QUOTE_COLOR_LEVEL_5 = "#e9b96e";
private static final String K9MAIL_CSS_CLASS = "k9mail";
/**
* Return an HTML hex color string for a given quote level.
* @param level Quote level
* @return Hex color string with prepended #.
*/
protected static String getQuoteColor(final int level) {
switch(level) {
case 1:
return QUOTE_COLOR_LEVEL_1;
case 2:
return QUOTE_COLOR_LEVEL_2;
case 3:
return QUOTE_COLOR_LEVEL_3;
case 4:
return QUOTE_COLOR_LEVEL_4;
case 5:
return QUOTE_COLOR_LEVEL_5;
default:
return QUOTE_COLOR_DEFAULT;
}
}
/**
* Searches for link-like text in a string and turn it into a link. Append the result to
* <tt>outputBuffer</tt>. <tt>text</tt> is not modified.
* @param text Plain text to be linkified.
* @param outputBuffer Buffer to append linked text to.
*/
protected static void linkifyText(final String text, final StringBuffer outputBuffer) {
String prepared = text.replaceAll(BITCOIN_URI_PATTERN, "<a href=\"$0\">$0</a>");
Matcher m = WEB_URL_PATTERN.matcher(prepared);
while (m.find()) {
int start = m.start();
if (start == 0 || (start != 0 && prepared.charAt(start - 1) != '@')) {
if (m.group().indexOf(':') > 0) { // With no URI-schema we may get "http:/" links with the second / missing
m.appendReplacement(outputBuffer, "<a href=\"$0\">$0</a>");
} else {
m.appendReplacement(outputBuffer, "<a href=\"http://$0\">$0</a>");
}
} else {
m.appendReplacement(outputBuffer, "$0");
}
}
m.appendTail(outputBuffer);
}
/*
* Lightweight method to check whether the message contains emoji or not.
* Useful to avoid calling the heavyweight convertEmoji2Img method.
* We don't use String.codePointAt here for performance reasons.
*/
private static boolean hasEmoji(String html) {
for (int i = 0; i < html.length(); ++i) {
char c = html.charAt(i);
if (c >= 0xDBB8 && c < 0xDBBC)
return true;
}
return false;
}
public static String convertEmoji2Img(String html) {
if (!hasEmoji(html)) {
return html;
}
StringBuilder buff = new StringBuilder(html.length() + 512);
for (int i = 0; i < html.length(); i = html.offsetByCodePoints(i, 1)) {
int codePoint = html.codePointAt(i);
String emoji = getEmojiForCodePoint(codePoint);
if (emoji != null)
buff.append("<img src=\"file:///android_asset/emoticons/").append(emoji).append(".gif\" alt=\"").append(emoji).append("\" />");
else
buff.appendCodePoint(codePoint);
}
return buff.toString();
}
private static String getEmojiForCodePoint(int codePoint) {
// Derived from http://code.google.com/p/emoji4unicode/source/browse/trunk/data/emoji4unicode.xml
// XXX: This doesn't cover all the characters. More emoticons are wanted.
switch (codePoint) {
case 0xFE000:
return "sun";
case 0xFE001:
return "cloud";
case 0xFE002:
return "rain";
case 0xFE003:
return "snow";
case 0xFE004:
return "thunder";
case 0xFE005:
return "typhoon";
case 0xFE006:
return "mist";
case 0xFE007:
return "sprinkle";
case 0xFE008:
return "night";
case 0xFE009:
return "sun";
case 0xFE00A:
return "sun";
case 0xFE00C:
return "sun";
case 0xFE010:
return "night";
case 0xFE011:
return "newmoon";
case 0xFE012:
return "moon1";
case 0xFE013:
return "moon2";
case 0xFE014:
return "moon3";
case 0xFE015:
return "fullmoon";
case 0xFE016:
return "moon2";
case 0xFE018:
return "soon";
case 0xFE019:
return "on";
case 0xFE01A:
return "end";
case 0xFE01B:
return "sandclock";
case 0xFE01C:
return "sandclock";
case 0xFE01D:
return "watch";
case 0xFE01E:
return "clock";
case 0xFE01F:
return "clock";
case 0xFE020:
return "clock";
case 0xFE021:
return "clock";
case 0xFE022:
return "clock";
case 0xFE023:
return "clock";
case 0xFE024:
return "clock";
case 0xFE025:
return "clock";
case 0xFE026:
return "clock";
case 0xFE027:
return "clock";
case 0xFE028:
return "clock";
case 0xFE029:
return "clock";
case 0xFE02A:
return "clock";
case 0xFE02B:
return "aries";
case 0xFE02C:
return "taurus";
case 0xFE02D:
return "gemini";
case 0xFE02E:
return "cancer";
case 0xFE02F:
return "leo";
case 0xFE030:
return "virgo";
case 0xFE031:
return "libra";
case 0xFE032:
return "scorpius";
case 0xFE033:
return "sagittarius";
case 0xFE034:
return "capricornus";
case 0xFE035:
return "aquarius";
case 0xFE036:
return "pisces";
case 0xFE038:
return "wave";
case 0xFE03B:
return "night";
case 0xFE03C:
return "clover";
case 0xFE03D:
return "tulip";
case 0xFE03E:
return "bud";
case 0xFE03F:
return "maple";
case 0xFE040:
return "cherryblossom";
case 0xFE042:
return "maple";
case 0xFE04E:
return "clover";
case 0xFE04F:
return "cherry";
case 0xFE050:
return "banana";
case 0xFE051:
return "apple";
case 0xFE05B:
return "apple";
case 0xFE190:
return "eye";
case 0xFE191:
return "ear";
case 0xFE193:
return "kissmark";
case 0xFE194:
return "bleah";
case 0xFE195:
return "rouge";
case 0xFE198:
return "hairsalon";
case 0xFE19A:
return "shadow";
case 0xFE19B:
return "happy01";
case 0xFE19C:
return "happy01";
case 0xFE19D:
return "happy01";
case 0xFE19E:
return "happy01";
case 0xFE1B7:
return "dog";
case 0xFE1B8:
return "cat";
case 0xFE1B9:
return "snail";
case 0xFE1BA:
return "chick";
case 0xFE1BB:
return "chick";
case 0xFE1BC:
return "penguin";
case 0xFE1BD:
return "fish";
case 0xFE1BE:
return "horse";
case 0xFE1BF:
return "pig";
case 0xFE1C8:
return "chick";
case 0xFE1C9:
return "fish";
case 0xFE1CF:
return "aries";
case 0xFE1D0:
return "dog";
case 0xFE1D8:
return "dog";
case 0xFE1D9:
return "fish";
case 0xFE1DB:
return "foot";
case 0xFE1DD:
return "chick";
case 0xFE1E0:
return "pig";
case 0xFE1E3:
return "cancer";
case 0xFE320:
return "angry";
case 0xFE321:
return "sad";
case 0xFE322:
return "wobbly";
case 0xFE323:
return "despair";
case 0xFE324:
return "wobbly";
case 0xFE325:
return "coldsweats02";
case 0xFE326:
return "gawk";
case 0xFE327:
return "lovely";
case 0xFE328:
return "smile";
case 0xFE329:
return "bleah";
case 0xFE32A:
return "bleah";
case 0xFE32B:
return "delicious";
case 0xFE32C:
return "lovely";
case 0xFE32D:
return "lovely";
case 0xFE32F:
return "happy02";
case 0xFE330:
return "happy01";
case 0xFE331:
return "coldsweats01";
case 0xFE332:
return "happy02";
case 0xFE333:
return "smile";
case 0xFE334:
return "happy02";
case 0xFE335:
return "delicious";
case 0xFE336:
return "happy01";
case 0xFE337:
return "happy01";
case 0xFE338:
return "coldsweats01";
case 0xFE339:
return "weep";
case 0xFE33A:
return "crying";
case 0xFE33B:
return "shock";
case 0xFE33C:
return "bearing";
case 0xFE33D:
return "pout";
case 0xFE33E:
return "confident";
case 0xFE33F:
return "sad";
case 0xFE340:
return "think";
case 0xFE341:
return "shock";
case 0xFE342:
return "sleepy";
case 0xFE343:
return "catface";
case 0xFE344:
return "coldsweats02";
case 0xFE345:
return "coldsweats02";
case 0xFE346:
return "bearing";
case 0xFE347:
return "wink";
case 0xFE348:
return "happy01";
case 0xFE349:
return "smile";
case 0xFE34A:
return "happy02";
case 0xFE34B:
return "lovely";
case 0xFE34C:
return "lovely";
case 0xFE34D:
return "weep";
case 0xFE34E:
return "pout";
case 0xFE34F:
return "smile";
case 0xFE350:
return "sad";
case 0xFE351:
return "ng";
case 0xFE352:
return "ok";
case 0xFE357:
return "paper";
case 0xFE359:
return "sad";
case 0xFE35A:
return "angry";
case 0xFE4B0:
return "house";
case 0xFE4B1:
return "house";
case 0xFE4B2:
return "building";
case 0xFE4B3:
return "postoffice";
case 0xFE4B4:
return "hospital";
case 0xFE4B5:
return "bank";
case 0xFE4B6:
return "atm";
case 0xFE4B7:
return "hotel";
case 0xFE4B9:
return "24hours";
case 0xFE4BA:
return "school";
case 0xFE4C1:
return "ship";
case 0xFE4C2:
return "bottle";
case 0xFE4C3:
return "fuji";
case 0xFE4C9:
return "wrench";
case 0xFE4CC:
return "shoe";
case 0xFE4CD:
return "shoe";
case 0xFE4CE:
return "eyeglass";
case 0xFE4CF:
return "t-shirt";
case 0xFE4D0:
return "denim";
case 0xFE4D1:
return "crown";
case 0xFE4D2:
return "crown";
case 0xFE4D6:
return "boutique";
case 0xFE4D7:
return "boutique";
case 0xFE4DB:
return "t-shirt";
case 0xFE4DC:
return "moneybag";
case 0xFE4DD:
return "dollar";
case 0xFE4E0:
return "dollar";
case 0xFE4E2:
return "yen";
case 0xFE4E3:
return "dollar";
case 0xFE4EF:
return "camera";
case 0xFE4F0:
return "bag";
case 0xFE4F1:
return "pouch";
case 0xFE4F2:
return "bell";
case 0xFE4F3:
return "door";
case 0xFE4F9:
return "movie";
case 0xFE4FB:
return "flair";
case 0xFE4FD:
return "sign05";
case 0xFE4FF:
return "book";
case 0xFE500:
return "book";
case 0xFE501:
return "book";
case 0xFE502:
return "book";
case 0xFE503:
return "book";
case 0xFE505:
return "spa";
case 0xFE506:
return "toilet";
case 0xFE507:
return "toilet";
case 0xFE508:
return "toilet";
case 0xFE50F:
return "ribbon";
case 0xFE510:
return "present";
case 0xFE511:
return "birthday";
case 0xFE512:
return "xmas";
case 0xFE522:
return "pocketbell";
case 0xFE523:
return "telephone";
case 0xFE524:
return "telephone";
case 0xFE525:
return "mobilephone";
case 0xFE526:
return "phoneto";
case 0xFE527:
return "memo";
case 0xFE528:
return "faxto";
case 0xFE529:
return "mail";
case 0xFE52A:
return "mailto";
case 0xFE52B:
return "mailto";
case 0xFE52C:
return "postoffice";
case 0xFE52D:
return "postoffice";
case 0xFE52E:
return "postoffice";
case 0xFE535:
return "present";
case 0xFE536:
return "pen";
case 0xFE537:
return "chair";
case 0xFE538:
return "pc";
case 0xFE539:
return "pencil";
case 0xFE53A:
return "clip";
case 0xFE53B:
return "bag";
case 0xFE53E:
return "hairsalon";
case 0xFE540:
return "memo";
case 0xFE541:
return "memo";
case 0xFE545:
return "book";
case 0xFE546:
return "book";
case 0xFE547:
return "book";
case 0xFE548:
return "memo";
case 0xFE54D:
return "book";
case 0xFE54F:
return "book";
case 0xFE552:
return "memo";
case 0xFE553:
return "foot";
case 0xFE7D0:
return "sports";
case 0xFE7D1:
return "baseball";
case 0xFE7D2:
return "golf";
case 0xFE7D3:
return "tennis";
case 0xFE7D4:
return "soccer";
case 0xFE7D5:
return "ski";
case 0xFE7D6:
return "basketball";
case 0xFE7D7:
return "motorsports";
case 0xFE7D8:
return "snowboard";
case 0xFE7D9:
return "run";
case 0xFE7DA:
return "snowboard";
case 0xFE7DC:
return "horse";
case 0xFE7DF:
return "train";
case 0xFE7E0:
return "subway";
case 0xFE7E1:
return "subway";
case 0xFE7E2:
return "bullettrain";
case 0xFE7E3:
return "bullettrain";
case 0xFE7E4:
return "car";
case 0xFE7E5:
return "rvcar";
case 0xFE7E6:
return "bus";
case 0xFE7E8:
return "ship";
case 0xFE7E9:
return "airplane";
case 0xFE7EA:
return "yacht";
case 0xFE7EB:
return "bicycle";
case 0xFE7EE:
return "yacht";
case 0xFE7EF:
return "car";
case 0xFE7F0:
return "run";
case 0xFE7F5:
return "gasstation";
case 0xFE7F6:
return "parking";
case 0xFE7F7:
return "signaler";
case 0xFE7FA:
return "spa";
case 0xFE7FC:
return "carouselpony";
case 0xFE7FF:
return "fish";
case 0xFE800:
return "karaoke";
case 0xFE801:
return "movie";
case 0xFE802:
return "movie";
case 0xFE803:
return "music";
case 0xFE804:
return "art";
case 0xFE805:
return "drama";
case 0xFE806:
return "event";
case 0xFE807:
return "ticket";
case 0xFE808:
return "slate";
case 0xFE809:
return "drama";
case 0xFE80A:
return "game";
case 0xFE813:
return "note";
case 0xFE814:
return "notes";
case 0xFE81A:
return "notes";
case 0xFE81C:
return "tv";
case 0xFE81D:
return "cd";
case 0xFE81E:
return "cd";
case 0xFE823:
return "kissmark";
case 0xFE824:
return "loveletter";
case 0xFE825:
return "ring";
case 0xFE826:
return "ring";
case 0xFE827:
return "kissmark";
case 0xFE829:
return "heart02";
case 0xFE82B:
return "freedial";
case 0xFE82C:
return "sharp";
case 0xFE82D:
return "mobaq";
case 0xFE82E:
return "one";
case 0xFE82F:
return "two";
case 0xFE830:
return "three";
case 0xFE831:
return "four";
case 0xFE832:
return "five";
case 0xFE833:
return "six";
case 0xFE834:
return "seven";
case 0xFE835:
return "eight";
case 0xFE836:
return "nine";
case 0xFE837:
return "zero";
case 0xFE960:
return "fastfood";
case 0xFE961:
return "riceball";
case 0xFE962:
return "cake";
case 0xFE963:
return "noodle";
case 0xFE964:
return "bread";
case 0xFE96A:
return "noodle";
case 0xFE973:
return "typhoon";
case 0xFE980:
return "restaurant";
case 0xFE981:
return "cafe";
case 0xFE982:
return "bar";
case 0xFE983:
return "beer";
case 0xFE984:
return "japanesetea";
case 0xFE985:
return "bottle";
case 0xFE986:
return "wine";
case 0xFE987:
return "beer";
case 0xFE988:
return "bar";
case 0xFEAF0:
return "upwardright";
case 0xFEAF1:
return "downwardright";
case 0xFEAF2:
return "upwardleft";
case 0xFEAF3:
return "downwardleft";
case 0xFEAF4:
return "up";
case 0xFEAF5:
return "down";
case 0xFEAF6:
return "leftright";
case 0xFEAF7:
return "updown";
case 0xFEB04:
return "sign01";
case 0xFEB05:
return "sign02";
case 0xFEB06:
return "sign03";
case 0xFEB07:
return "sign04";
case 0xFEB08:
return "sign05";
case 0xFEB0B:
return "sign01";
case 0xFEB0C:
return "heart01";
case 0xFEB0D:
return "heart02";
case 0xFEB0E:
return "heart03";
case 0xFEB0F:
return "heart04";
case 0xFEB10:
return "heart01";
case 0xFEB11:
return "heart02";
case 0xFEB12:
return "heart01";
case 0xFEB13:
return "heart01";
case 0xFEB14:
return "heart01";
case 0xFEB15:
return "heart01";
case 0xFEB16:
return "heart01";
case 0xFEB17:
return "heart01";
case 0xFEB18:
return "heart02";
case 0xFEB19:
return "cute";
case 0xFEB1A:
return "heart";
case 0xFEB1B:
return "spade";
case 0xFEB1C:
return "diamond";
case 0xFEB1D:
return "club";
case 0xFEB1E:
return "smoking";
case 0xFEB1F:
return "nosmoking";
case 0xFEB20:
return "wheelchair";
case 0xFEB21:
return "free";
case 0xFEB22:
return "flag";
case 0xFEB23:
return "danger";
case 0xFEB26:
return "ng";
case 0xFEB27:
return "ok";
case 0xFEB28:
return "ng";
case 0xFEB29:
return "copyright";
case 0xFEB2A:
return "tm";
case 0xFEB2B:
return "secret";
case 0xFEB2C:
return "recycle";
case 0xFEB2D:
return "r-mark";
case 0xFEB2E:
return "ban";
case 0xFEB2F:
return "empty";
case 0xFEB30:
return "pass";
case 0xFEB31:
return "full";
case 0xFEB36:
return "new";
case 0xFEB44:
return "fullmoon";
case 0xFEB48:
return "ban";
case 0xFEB55:
return "cute";
case 0xFEB56:
return "flair";
case 0xFEB57:
return "annoy";
case 0xFEB58:
return "bomb";
case 0xFEB59:
return "sleepy";
case 0xFEB5A:
return "impact";
case 0xFEB5B:
return "sweat01";
case 0xFEB5C:
return "sweat02";
case 0xFEB5D:
return "dash";
case 0xFEB5F:
return "sad";
case 0xFEB60:
return "shine";
case 0xFEB61:
return "cute";
case 0xFEB62:
return "cute";
case 0xFEB63:
return "newmoon";
case 0xFEB64:
return "newmoon";
case 0xFEB65:
return "newmoon";
case 0xFEB66:
return "newmoon";
case 0xFEB67:
return "newmoon";
case 0xFEB77:
return "shine";
case 0xFEB81:
return "id";
case 0xFEB82:
return "key";
case 0xFEB83:
return "enter";
case 0xFEB84:
return "clear";
case 0xFEB85:
return "search";
case 0xFEB86:
return "key";
case 0xFEB87:
return "key";
case 0xFEB8A:
return "key";
case 0xFEB8D:
return "search";
case 0xFEB90:
return "key";
case 0xFEB91:
return "recycle";
case 0xFEB92:
return "mail";
case 0xFEB93:
return "rock";
case 0xFEB94:
return "scissors";
case 0xFEB95:
return "paper";
case 0xFEB96:
return "punch";
case 0xFEB97:
return "good";
case 0xFEB9D:
return "paper";
case 0xFEB9F:
return "ok";
case 0xFEBA0:
return "down";
case 0xFEBA1:
return "paper";
case 0xFEE10:
return "info01";
case 0xFEE11:
return "info02";
case 0xFEE12:
return "by-d";
case 0xFEE13:
return "d-point";
case 0xFEE14:
return "appli01";
case 0xFEE15:
return "appli02";
case 0xFEE1C:
return "movie";
default:
return null;
}
}
private static String htmlifyMessageHeader() {
return "<pre class=\"" + K9MAIL_CSS_CLASS + "\">";
}
private static String htmlifyMessageFooter() {
return "</pre>";
}
/**
* Dynamically generate a CSS style for {@code <pre>} elements.
*
* <p>
* The style incorporates the user's current preference
* setting for the font family used for plain text messages.
* </p>
*
* @return
* A {@code <style>} element that can be dynamically included in the HTML
* {@code <head>} element when messages are displayed.
*/
public static String cssStylePre() {
final String font = K9.messageViewFixedWidthFont()
? "monospace"
: "sans-serif";
return "<style type=\"text/css\"> pre." + K9MAIL_CSS_CLASS +
" {white-space: pre-wrap; word-wrap:break-word; " +
"font-family: " + font + "; margin-top: 0px}</style>";
}
/**
* Convert a plain text string into an HTML fragment.
* @param text Plain text.
* @return HTML fragment.
*/
public static String textToHtmlFragment(final String text) {
// Escape the entities and add newlines.
String htmlified = TextUtils.htmlEncode(text);
// Linkify the message.
StringBuffer linkified = new StringBuffer(htmlified.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
linkifyText(htmlified, linkified);
// Add newlines and unescaping.
//
// For some reason, TextUtils.htmlEncode escapes ' into &apos;, which is technically part of the XHTML 1.0
// standard, but Gmail doesn't recognize it as an HTML entity. We unescape that here.
return linkified.toString().replaceAll("\r?\n", "<br>\r\n").replace("&apos;", "&#39;");
}
/**
* Convert HTML to a {@link Spanned} that can be used in a {@link android.widget.TextView}.
*
* @param html
* The HTML fragment to be converted.
*
* @return A {@link Spanned} containing the text in {@code html} formatted using spans.
*/
public static Spanned htmlToSpanned(String html) {
return Html.fromHtml(html, null, new ListTagHandler());
}
/**
* {@link TagHandler} that supports unordered lists.
*
* @see HtmlConverter#htmlToSpanned(String)
*/
public static class ListTagHandler implements TagHandler {
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if (tag.equals("ul")) {
if (opening) {
char lastChar = 0;
if (output.length() > 0) {
lastChar = output.charAt(output.length() - 1);
}
if (lastChar != '\n') {
output.append("\r\n");
}
} else {
output.append("\r\n");
}
}
if (tag.equals("li")) {
if (opening) {
output.append("\t• ");
} else {
output.append("\r\n");
}
}
}
}
}