mirror of
https://github.com/moparisthebest/k-9
synced 2024-11-11 20:15:03 -05:00
1372 lines
41 KiB
Java
1372 lines
41 KiB
Java
package com.fsck.k9.helper;
|
|
|
|
import android.text.*;
|
|
import android.text.Html.TagHandler;
|
|
import android.util.Log;
|
|
import com.fsck.k9.K9;
|
|
import org.xml.sax.XMLReader;
|
|
|
|
import java.io.IOException;
|
|
import java.io.StringReader;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.Locale;
|
|
import java.util.Set;
|
|
import java.util.regex.Matcher;
|
|
|
|
/**
|
|
* Contains common routines to convert html to text and vice versa.
|
|
*/
|
|
public class HtmlConverter {
|
|
/**
|
|
* 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);
|
|
|
|
StringReader reader = new StringReader(text);
|
|
StringBuilder buff = new StringBuilder(text.length() + TEXT_TO_HTML_EXTRA_BUFFER_LENGTH);
|
|
|
|
buff.append(htmlifyMessageHeader());
|
|
|
|
int c;
|
|
try {
|
|
while ((c = reader.read()) != -1) {
|
|
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((char)c);
|
|
}//switch
|
|
}
|
|
} catch (IOException e) {
|
|
//Should never happen
|
|
Log.e(K9.LOG_TAG, "Could not read string to convert text to HTML:", e);
|
|
}
|
|
|
|
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);
|
|
}
|
|
StringReader reader = new StringReader(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.
|
|
try {
|
|
int c;
|
|
while ((c = reader.read()) != -1) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
//Should never happen
|
|
Log.e(K9.LOG_TAG, "Could not read string to convert text to HTML:", e);
|
|
}
|
|
// 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 />");
|
|
|
|
/*
|
|
* Unwrap multi-line paragraphs into single line paragraphs that are
|
|
* wrapped when displayed. But try to avoid unwrapping consecutive lines
|
|
* of text that are not paragraphs, such as lists of system log entries
|
|
* or long URLs that are on their own line.
|
|
*/
|
|
text = text.replaceAll("(?m)^([^\r\n]{4,}[\\s\\w,:;+/])(?:\r\n|\n|\r)(?=[a-z]\\S{0,10}[\\s\\n\\r])", "$1 ");
|
|
|
|
// Compress four or more newlines down to two newlines
|
|
text = text.replaceAll("(?m)(\r\n|\n|\r){4,}", "\r\n\r\n");
|
|
|
|
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 >
|
|
text = text.replaceAll("<gt>", ">");
|
|
|
|
return text;
|
|
}
|
|
|
|
private static void appendchar(StringBuilder buff, int c) {
|
|
switch (c) {
|
|
case '&':
|
|
buff.append("&");
|
|
break;
|
|
case '<':
|
|
buff.append("<");
|
|
break;
|
|
case '>':
|
|
// We use a token here which can't occur in htmlified text because > 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 > 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.
|
|
*/
|
|
private static void linkifyText(final String text, final StringBuffer outputBuffer) {
|
|
String prepared = text.replaceAll(Regex.BITCOIN_URI_PATTERN, "<a href=\"$0\">$0</a>");
|
|
|
|
Matcher m = Regex.WEB_URL_PATTERN.matcher(prepared);
|
|
while (m.find()) {
|
|
int start = m.start();
|
|
if (start == 0 || (start != 0 && text.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 ', 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("'", "'");
|
|
}
|
|
|
|
/**
|
|
* 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");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|