diff --git a/res/values/strings.xml b/res/values/strings.xml
index d2866406a..2d370b6a6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -349,6 +349,10 @@
>
+
+
+ %1$s - %2$s
+
Set up email
diff --git a/src/com/android/email/LegacyConversions.java b/src/com/android/email/LegacyConversions.java
index 8979af115..7d7e884bd 100644
--- a/src/com/android/email/LegacyConversions.java
+++ b/src/com/android/email/LegacyConversions.java
@@ -45,6 +45,7 @@ import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
+import android.text.TextUtils;
import android.util.Log;
import java.io.File;
@@ -217,11 +218,18 @@ public class LegacyConversions {
}
// write the combined data to the body part
- if (sbText != null && sbText.length() != 0) {
- body.mTextContent = sbText.toString();
+ if (!TextUtils.isEmpty(sbText)) {
+ String text = sbText.toString();
+ body.mTextContent = text;
+ localMessage.mSnippet = Snippet.fromPlainText(text);
}
- if (sbHtml != null && sbHtml.length() != 0) {
+ if (!TextUtils.isEmpty(sbHtml)) {
+ String text = sbHtml.toString();
+ body.mHtmlContent = text;
body.mHtmlContent = sbHtml.toString();
+ if (localMessage.mSnippet == null) {
+ localMessage.mSnippet = Snippet.fromHtmlText(text);
+ }
}
if (sbHtmlReply != null && sbHtmlReply.length() != 0) {
body.mHtmlReply = sbHtmlReply.toString();
diff --git a/src/com/android/email/Snippet.java b/src/com/android/email/Snippet.java
new file mode 100644
index 000000000..95285c3ca
--- /dev/null
+++ b/src/com/android/email/Snippet.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email;
+
+import android.text.TextUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Class to generate a short 'snippet' from either plain text or html text
+ *
+ * If the sync protocol can get plain text, that's great, but we'll still strip out extraneous
+ * whitespace. If it's HTML, we'll 1) strip out tags, 2) turn entities into the appropriate
+ * characters, and 3) strip out extraneous whitespace, all in one pass
+ *
+ * Why not use an existing class? The best answer is performance; yet another answer is
+ * correctness (e.g. Html.textFromHtml simply doesn't generate well-stripped text). But performance
+ * is key; we frequently sync text that is 10K or (much) longer, yet we really only care about a
+ * small amount of text for the snippet. So it's critically important that we just stop when we've
+ * gotten enough; existing methods that exist will go through the entire incoming string, at great
+ * (and useless) expense.
+ */
+public class Snippet {
+ // This is how many chars we'll allow in a snippet
+ private static final int MAX_PLAIN_TEXT_SCAN_LENGTH = 200;
+ // For some reason, isWhitespace() returns false with the following...
+ /*package*/ static final char NON_BREAKING_SPACE_CHARACTER = (char)160;
+
+ // Note: ESCAPE_STRINGS is taken from the StringUtil class which is part of the
+ // unbundled_google package
+ static final Map ESCAPE_STRINGS;
+ static {
+ // HTML character entity references as defined in HTML 4
+ // see http://www.w3.org/TR/REC-html40/sgml/entities.html
+ ESCAPE_STRINGS = new HashMap(252);
+
+ ESCAPE_STRINGS.put(" ", '\u00A0');
+ ESCAPE_STRINGS.put("¡", '\u00A1');
+ ESCAPE_STRINGS.put("¢", '\u00A2');
+ ESCAPE_STRINGS.put("£", '\u00A3');
+ ESCAPE_STRINGS.put("¤", '\u00A4');
+ ESCAPE_STRINGS.put("¥", '\u00A5');
+ ESCAPE_STRINGS.put("¦", '\u00A6');
+ ESCAPE_STRINGS.put("§", '\u00A7');
+ ESCAPE_STRINGS.put("¨", '\u00A8');
+ ESCAPE_STRINGS.put("©", '\u00A9');
+ ESCAPE_STRINGS.put("ª", '\u00AA');
+ ESCAPE_STRINGS.put("«", '\u00AB');
+ ESCAPE_STRINGS.put("¬", '\u00AC');
+ ESCAPE_STRINGS.put("­", '\u00AD');
+ ESCAPE_STRINGS.put("®", '\u00AE');
+ ESCAPE_STRINGS.put("¯", '\u00AF');
+ ESCAPE_STRINGS.put("°", '\u00B0');
+ ESCAPE_STRINGS.put("±", '\u00B1');
+ ESCAPE_STRINGS.put("²", '\u00B2');
+ ESCAPE_STRINGS.put("³", '\u00B3');
+ ESCAPE_STRINGS.put("´", '\u00B4');
+ ESCAPE_STRINGS.put("µ", '\u00B5');
+ ESCAPE_STRINGS.put("¶", '\u00B6');
+ ESCAPE_STRINGS.put("·", '\u00B7');
+ ESCAPE_STRINGS.put("¸", '\u00B8');
+ ESCAPE_STRINGS.put("¹", '\u00B9');
+ ESCAPE_STRINGS.put("º", '\u00BA');
+ ESCAPE_STRINGS.put("»", '\u00BB');
+ ESCAPE_STRINGS.put("¼", '\u00BC');
+ ESCAPE_STRINGS.put("½", '\u00BD');
+ ESCAPE_STRINGS.put("¾", '\u00BE');
+ ESCAPE_STRINGS.put("¿", '\u00BF');
+ ESCAPE_STRINGS.put("À", '\u00C0');
+ ESCAPE_STRINGS.put("Á", '\u00C1');
+ ESCAPE_STRINGS.put("Â", '\u00C2');
+ ESCAPE_STRINGS.put("Ã", '\u00C3');
+ ESCAPE_STRINGS.put("Ä", '\u00C4');
+ ESCAPE_STRINGS.put("Å", '\u00C5');
+ ESCAPE_STRINGS.put("Æ", '\u00C6');
+ ESCAPE_STRINGS.put("Ç", '\u00C7');
+ ESCAPE_STRINGS.put("È", '\u00C8');
+ ESCAPE_STRINGS.put("É", '\u00C9');
+ ESCAPE_STRINGS.put("Ê", '\u00CA');
+ ESCAPE_STRINGS.put("Ë", '\u00CB');
+ ESCAPE_STRINGS.put("Ì", '\u00CC');
+ ESCAPE_STRINGS.put("Í", '\u00CD');
+ ESCAPE_STRINGS.put("Î", '\u00CE');
+ ESCAPE_STRINGS.put("Ï", '\u00CF');
+ ESCAPE_STRINGS.put("Ð", '\u00D0');
+ ESCAPE_STRINGS.put("Ñ", '\u00D1');
+ ESCAPE_STRINGS.put("Ò", '\u00D2');
+ ESCAPE_STRINGS.put("Ó", '\u00D3');
+ ESCAPE_STRINGS.put("Ô", '\u00D4');
+ ESCAPE_STRINGS.put("Õ", '\u00D5');
+ ESCAPE_STRINGS.put("Ö", '\u00D6');
+ ESCAPE_STRINGS.put("×", '\u00D7');
+ ESCAPE_STRINGS.put("Ø", '\u00D8');
+ ESCAPE_STRINGS.put("Ù", '\u00D9');
+ ESCAPE_STRINGS.put("Ú", '\u00DA');
+ ESCAPE_STRINGS.put("Û", '\u00DB');
+ ESCAPE_STRINGS.put("Ü", '\u00DC');
+ ESCAPE_STRINGS.put("Ý", '\u00DD');
+ ESCAPE_STRINGS.put("Þ", '\u00DE');
+ ESCAPE_STRINGS.put("ß", '\u00DF');
+ ESCAPE_STRINGS.put("à", '\u00E0');
+ ESCAPE_STRINGS.put("á", '\u00E1');
+ ESCAPE_STRINGS.put("â", '\u00E2');
+ ESCAPE_STRINGS.put("ã", '\u00E3');
+ ESCAPE_STRINGS.put("ä", '\u00E4');
+ ESCAPE_STRINGS.put("å", '\u00E5');
+ ESCAPE_STRINGS.put("æ", '\u00E6');
+ ESCAPE_STRINGS.put("ç", '\u00E7');
+ ESCAPE_STRINGS.put("è", '\u00E8');
+ ESCAPE_STRINGS.put("é", '\u00E9');
+ ESCAPE_STRINGS.put("ê", '\u00EA');
+ ESCAPE_STRINGS.put("ë", '\u00EB');
+ ESCAPE_STRINGS.put("ì", '\u00EC');
+ ESCAPE_STRINGS.put("í", '\u00ED');
+ ESCAPE_STRINGS.put("î", '\u00EE');
+ ESCAPE_STRINGS.put("ï", '\u00EF');
+ ESCAPE_STRINGS.put("ð", '\u00F0');
+ ESCAPE_STRINGS.put("ñ", '\u00F1');
+ ESCAPE_STRINGS.put("ò", '\u00F2');
+ ESCAPE_STRINGS.put("ó", '\u00F3');
+ ESCAPE_STRINGS.put("ô", '\u00F4');
+ ESCAPE_STRINGS.put("õ", '\u00F5');
+ ESCAPE_STRINGS.put("ö", '\u00F6');
+ ESCAPE_STRINGS.put("÷", '\u00F7');
+ ESCAPE_STRINGS.put("ø", '\u00F8');
+ ESCAPE_STRINGS.put("ù", '\u00F9');
+ ESCAPE_STRINGS.put("ú", '\u00FA');
+ ESCAPE_STRINGS.put("û", '\u00FB');
+ ESCAPE_STRINGS.put("ü", '\u00FC');
+ ESCAPE_STRINGS.put("ý", '\u00FD');
+ ESCAPE_STRINGS.put("þ", '\u00FE');
+ ESCAPE_STRINGS.put("ÿ", '\u00FF');
+ ESCAPE_STRINGS.put("&fnof", '\u0192');
+ ESCAPE_STRINGS.put("&Alpha", '\u0391');
+ ESCAPE_STRINGS.put("&Beta", '\u0392');
+ ESCAPE_STRINGS.put("&Gamma", '\u0393');
+ ESCAPE_STRINGS.put("&Delta", '\u0394');
+ ESCAPE_STRINGS.put("&Epsilon", '\u0395');
+ ESCAPE_STRINGS.put("&Zeta", '\u0396');
+ ESCAPE_STRINGS.put("&Eta", '\u0397');
+ ESCAPE_STRINGS.put("&Theta", '\u0398');
+ ESCAPE_STRINGS.put("&Iota", '\u0399');
+ ESCAPE_STRINGS.put("&Kappa", '\u039A');
+ ESCAPE_STRINGS.put("&Lambda", '\u039B');
+ ESCAPE_STRINGS.put("&Mu", '\u039C');
+ ESCAPE_STRINGS.put("&Nu", '\u039D');
+ ESCAPE_STRINGS.put("&Xi", '\u039E');
+ ESCAPE_STRINGS.put("&Omicron", '\u039F');
+ ESCAPE_STRINGS.put("&Pi", '\u03A0');
+ ESCAPE_STRINGS.put("&Rho", '\u03A1');
+ ESCAPE_STRINGS.put("&Sigma", '\u03A3');
+ ESCAPE_STRINGS.put("&Tau", '\u03A4');
+ ESCAPE_STRINGS.put("&Upsilon", '\u03A5');
+ ESCAPE_STRINGS.put("&Phi", '\u03A6');
+ ESCAPE_STRINGS.put("&Chi", '\u03A7');
+ ESCAPE_STRINGS.put("&Psi", '\u03A8');
+ ESCAPE_STRINGS.put("&Omega", '\u03A9');
+ ESCAPE_STRINGS.put("&alpha", '\u03B1');
+ ESCAPE_STRINGS.put("&beta", '\u03B2');
+ ESCAPE_STRINGS.put("&gamma", '\u03B3');
+ ESCAPE_STRINGS.put("&delta", '\u03B4');
+ ESCAPE_STRINGS.put("&epsilon", '\u03B5');
+ ESCAPE_STRINGS.put("&zeta", '\u03B6');
+ ESCAPE_STRINGS.put("&eta", '\u03B7');
+ ESCAPE_STRINGS.put("&theta", '\u03B8');
+ ESCAPE_STRINGS.put("&iota", '\u03B9');
+ ESCAPE_STRINGS.put("&kappa", '\u03BA');
+ ESCAPE_STRINGS.put("&lambda", '\u03BB');
+ ESCAPE_STRINGS.put("&mu", '\u03BC');
+ ESCAPE_STRINGS.put("&nu", '\u03BD');
+ ESCAPE_STRINGS.put("&xi", '\u03BE');
+ ESCAPE_STRINGS.put("&omicron", '\u03BF');
+ ESCAPE_STRINGS.put("&pi", '\u03C0');
+ ESCAPE_STRINGS.put("&rho", '\u03C1');
+ ESCAPE_STRINGS.put("&sigmaf", '\u03C2');
+ ESCAPE_STRINGS.put("&sigma", '\u03C3');
+ ESCAPE_STRINGS.put("&tau", '\u03C4');
+ ESCAPE_STRINGS.put("&upsilon", '\u03C5');
+ ESCAPE_STRINGS.put("&phi", '\u03C6');
+ ESCAPE_STRINGS.put("&chi", '\u03C7');
+ ESCAPE_STRINGS.put("&psi", '\u03C8');
+ ESCAPE_STRINGS.put("&omega", '\u03C9');
+ ESCAPE_STRINGS.put("&thetasym", '\u03D1');
+ ESCAPE_STRINGS.put("&upsih", '\u03D2');
+ ESCAPE_STRINGS.put("&piv", '\u03D6');
+ ESCAPE_STRINGS.put("&bull", '\u2022');
+ ESCAPE_STRINGS.put("&hellip", '\u2026');
+ ESCAPE_STRINGS.put("&prime", '\u2032');
+ ESCAPE_STRINGS.put("&Prime", '\u2033');
+ ESCAPE_STRINGS.put("&oline", '\u203E');
+ ESCAPE_STRINGS.put("&frasl", '\u2044');
+ ESCAPE_STRINGS.put("&weierp", '\u2118');
+ ESCAPE_STRINGS.put("&image", '\u2111');
+ ESCAPE_STRINGS.put("&real", '\u211C');
+ ESCAPE_STRINGS.put("&trade", '\u2122');
+ ESCAPE_STRINGS.put("&alefsym", '\u2135');
+ ESCAPE_STRINGS.put("&larr", '\u2190');
+ ESCAPE_STRINGS.put("&uarr", '\u2191');
+ ESCAPE_STRINGS.put("&rarr", '\u2192');
+ ESCAPE_STRINGS.put("&darr", '\u2193');
+ ESCAPE_STRINGS.put("&harr", '\u2194');
+ ESCAPE_STRINGS.put("&crarr", '\u21B5');
+ ESCAPE_STRINGS.put("&lArr", '\u21D0');
+ ESCAPE_STRINGS.put("&uArr", '\u21D1');
+ ESCAPE_STRINGS.put("&rArr", '\u21D2');
+ ESCAPE_STRINGS.put("&dArr", '\u21D3');
+ ESCAPE_STRINGS.put("&hArr", '\u21D4');
+ ESCAPE_STRINGS.put("&forall", '\u2200');
+ ESCAPE_STRINGS.put("&part", '\u2202');
+ ESCAPE_STRINGS.put("&exist", '\u2203');
+ ESCAPE_STRINGS.put("&empty", '\u2205');
+ ESCAPE_STRINGS.put("&nabla", '\u2207');
+ ESCAPE_STRINGS.put("&isin", '\u2208');
+ ESCAPE_STRINGS.put("¬in", '\u2209');
+ ESCAPE_STRINGS.put("&ni", '\u220B');
+ ESCAPE_STRINGS.put("&prod", '\u220F');
+ ESCAPE_STRINGS.put("&sum", '\u2211');
+ ESCAPE_STRINGS.put("&minus", '\u2212');
+ ESCAPE_STRINGS.put("&lowast", '\u2217');
+ ESCAPE_STRINGS.put("&radic", '\u221A');
+ ESCAPE_STRINGS.put("&prop", '\u221D');
+ ESCAPE_STRINGS.put("&infin", '\u221E');
+ ESCAPE_STRINGS.put("&ang", '\u2220');
+ ESCAPE_STRINGS.put("&and", '\u2227');
+ ESCAPE_STRINGS.put("&or", '\u2228');
+ ESCAPE_STRINGS.put("&cap", '\u2229');
+ ESCAPE_STRINGS.put("&cup", '\u222A');
+ ESCAPE_STRINGS.put("&int", '\u222B');
+ ESCAPE_STRINGS.put("&there4", '\u2234');
+ ESCAPE_STRINGS.put("&sim", '\u223C');
+ ESCAPE_STRINGS.put("&cong", '\u2245');
+ ESCAPE_STRINGS.put("&asymp", '\u2248');
+ ESCAPE_STRINGS.put("&ne", '\u2260');
+ ESCAPE_STRINGS.put("&equiv", '\u2261');
+ ESCAPE_STRINGS.put("&le", '\u2264');
+ ESCAPE_STRINGS.put("&ge", '\u2265');
+ ESCAPE_STRINGS.put("&sub", '\u2282');
+ ESCAPE_STRINGS.put("&sup", '\u2283');
+ ESCAPE_STRINGS.put("&nsub", '\u2284');
+ ESCAPE_STRINGS.put("&sube", '\u2286');
+ ESCAPE_STRINGS.put("&supe", '\u2287');
+ ESCAPE_STRINGS.put("&oplus", '\u2295');
+ ESCAPE_STRINGS.put("&otimes", '\u2297');
+ ESCAPE_STRINGS.put("&perp", '\u22A5');
+ ESCAPE_STRINGS.put("&sdot", '\u22C5');
+ ESCAPE_STRINGS.put("&lceil", '\u2308');
+ ESCAPE_STRINGS.put("&rceil", '\u2309');
+ ESCAPE_STRINGS.put("&lfloor", '\u230A');
+ ESCAPE_STRINGS.put("&rfloor", '\u230B');
+ ESCAPE_STRINGS.put("&lang", '\u2329');
+ ESCAPE_STRINGS.put("&rang", '\u232A');
+ ESCAPE_STRINGS.put("&loz", '\u25CA');
+ ESCAPE_STRINGS.put("&spades", '\u2660');
+ ESCAPE_STRINGS.put("&clubs", '\u2663');
+ ESCAPE_STRINGS.put("&hearts", '\u2665');
+ ESCAPE_STRINGS.put("&diams", '\u2666');
+ ESCAPE_STRINGS.put(""", '\u0022');
+ ESCAPE_STRINGS.put("&", '\u0026');
+ ESCAPE_STRINGS.put("<", '\u003C');
+ ESCAPE_STRINGS.put(">", '\u003E');
+ ESCAPE_STRINGS.put("&OElig", '\u0152');
+ ESCAPE_STRINGS.put("&oelig", '\u0153');
+ ESCAPE_STRINGS.put("&Scaron", '\u0160');
+ ESCAPE_STRINGS.put("&scaron", '\u0161');
+ ESCAPE_STRINGS.put("&Yuml", '\u0178');
+ ESCAPE_STRINGS.put("&circ", '\u02C6');
+ ESCAPE_STRINGS.put("&tilde", '\u02DC');
+ ESCAPE_STRINGS.put("&ensp", '\u2002');
+ ESCAPE_STRINGS.put("&emsp", '\u2003');
+ ESCAPE_STRINGS.put("&thinsp", '\u2009');
+ ESCAPE_STRINGS.put("&zwnj", '\u200C');
+ ESCAPE_STRINGS.put("&zwj", '\u200D');
+ ESCAPE_STRINGS.put("&lrm", '\u200E');
+ ESCAPE_STRINGS.put("&rlm", '\u200F');
+ ESCAPE_STRINGS.put("&ndash", '\u2013');
+ ESCAPE_STRINGS.put("&mdash", '\u2014');
+ ESCAPE_STRINGS.put("&lsquo", '\u2018');
+ ESCAPE_STRINGS.put("&rsquo", '\u2019');
+ ESCAPE_STRINGS.put("&sbquo", '\u201A');
+ ESCAPE_STRINGS.put("&ldquo", '\u201C');
+ ESCAPE_STRINGS.put("&rdquo", '\u201D');
+ ESCAPE_STRINGS.put("&bdquo", '\u201E');
+ ESCAPE_STRINGS.put("&dagger", '\u2020');
+ ESCAPE_STRINGS.put("&Dagger", '\u2021');
+ ESCAPE_STRINGS.put("&permil", '\u2030');
+ ESCAPE_STRINGS.put("&lsaquo", '\u2039');
+ ESCAPE_STRINGS.put("&rsaquo", '\u203A');
+ ESCAPE_STRINGS.put("&euro", '\u20AC');
+ }
+
+ public static String fromHtmlText(String text) {
+ return fromText(text, true);
+ }
+
+ public static String fromPlainText(String text) {
+ return fromText(text, false);
+ }
+
+ public static String fromText(String text, boolean stripHtml) {
+ // Handle null and empty string
+ if (TextUtils.isEmpty(text)) return "";
+
+ final int length = text.length();
+ // Use char[] instead of StringBuilder purely for performance; fewer method calls, etc.
+ char[] buffer = new char[MAX_PLAIN_TEXT_SCAN_LENGTH];
+ // skipCount is an array of a single int; that int is set inside stripHtmlEntity and is
+ // used to determine how many characters can be "skipped" due to the transformation of the
+ // entity to a single character. When Java allows multiple return values, we can make this
+ // much cleaner :-)
+ int[] skipCount = new int[1];
+ int bufferCount = 0;
+ // Start with space as last character to avoid leading whitespace
+ char last = ' ';
+ // Indicates whether we're in the middle of an HTML tag
+ boolean inTag = false;
+
+ // Walk through the text until we're done with the input OR we've got a large enough snippet
+ for (int i = 0; i < length && bufferCount < MAX_PLAIN_TEXT_SCAN_LENGTH; i++) {
+ char c = text.charAt(i);
+ if (stripHtml && !inTag && (c == '<')) {
+ // Find tags to strip; they will begin with ')) {
+ // Terminate stripping here
+ inTag = false;
+ continue;
+ }
+
+ if (inTag) {
+ // We just skip by everything while we're in a tag
+ continue;
+ } else if (stripHtml && (c == '&')) {
+ // Handle a possible HTML entity here
+ // We always get back a character to use; we also get back a "skip count",
+ // indicating how many characters were eaten from the entity
+ c = stripHtmlEntity(text, i, skipCount);
+ i += skipCount[0];
+ }
+
+ if (Character.isWhitespace(c) || (c == NON_BREAKING_SPACE_CHARACTER)) {
+ // The idea is to find the content in the message, not the whitespace, so we'll
+ // turn any combination of contiguous whitespace into a single space
+ if (last == ' ') {
+ continue;
+ } else {
+ // Make every whitespace character a simple space
+ c = ' ';
+ }
+ } else if ((c == '-' || c == '=') && (last == c)) {
+ // Lots of messages (especially digests) have whole lines of --- or ===
+ // We'll get rid of those duplicates here
+ continue;
+ }
+
+ // After all that, maybe we've got a character for our snippet
+ buffer[bufferCount++] = c;
+ last = c;
+ }
+
+ // Lose trailing space and return our snippet
+ if ((bufferCount > 0) && (last == ' ')) {
+ bufferCount--;
+ }
+ return new String(buffer, 0, bufferCount);
+ }
+
+ static /*package*/ char stripHtmlEntity(String text, int pos, int[] skipCount) {
+ int length = text.length();
+ // Ugly, but we store our skip count in this array; we can't use a static here, because
+ // multiple threads might be calling in
+ skipCount[0] = 0;
+ // All entities are <= 8 characters long, so that's how far we'll look for one (+ & and ;)
+ int end = pos + 10;
+ String entity = null;
+ // Isolate the entity
+ for (int i = pos; (i < length) && (i < end); i++) {
+ if (text.charAt(i) == ';') {
+ entity = text.substring(pos, i);
+ break;
+ }
+ }
+ if (entity == null) {
+ // This wasn't really an HTML entity
+ return '&';
+ } else {
+ // Skip count is the length of the entity
+ Character mapping = ESCAPE_STRINGS.get(entity);
+ int entityLength = entity.length();
+ if (mapping != null) {
+ skipCount[0] = entityLength;
+ return mapping;
+ } else if ((entityLength > 2) && (entity.charAt(1) == '#')) {
+ // nn; means ascii nn (decimal) and nn means ascii nn (hex)
+ char c = '?';
+ try {
+ int i;
+ if ((entity.charAt(2) == 'x') && (entityLength > 3)) {
+ i = Integer.parseInt(entity.substring(3), 16);
+ } else {
+ i = Integer.parseInt(entity.substring(2));
+ }
+ c = (char)i;
+ } catch (NumberFormatException e) {
+ // We'll just return the ? in this case
+ }
+ skipCount[0] = entityLength;
+ return c;
+ }
+ }
+ // Worst case, we return the original start character, ampersand
+ return '&';
+ }
+
+}
diff --git a/src/com/android/email/activity/MessagesAdapter.java b/src/com/android/email/activity/MessagesAdapter.java
index f9e1d1e2e..1cc965414 100644
--- a/src/com/android/email/activity/MessagesAdapter.java
+++ b/src/com/android/email/activity/MessagesAdapter.java
@@ -28,13 +28,13 @@ import android.content.Context;
import android.content.Loader;
import android.content.res.ColorStateList;
import android.content.res.Resources;
-import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
+import android.content.res.Resources.Theme;
import android.database.Cursor;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
-import android.os.Handler;
+import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -59,7 +59,7 @@ import java.util.Set;
EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP,
MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT,
- MessageColumns.FLAGS,
+ MessageColumns.FLAGS, MessageColumns.SNIPPET
};
public static final int COLUMN_ID = 0;
@@ -72,6 +72,7 @@ import java.util.Set;
public static final int COLUMN_FAVORITE = 7;
public static final int COLUMN_ATTACHMENTS = 8;
public static final int COLUMN_FLAGS = 9;
+ public static final int COLUMN_SNIPPET = 10;
private static final int ITEM_BACKGROUND_SELECTED = 0xFFB0FFB0; // TODO color not finalized
@@ -184,6 +185,15 @@ import java.util.Set;
TextView subjectView = (TextView) view.findViewById(R.id.subject);
text = cursor.getString(COLUMN_SUBJECT);
+ // Add in the snippet if we have one
+ // TODO Should this be spanned text?
+ // The mocks show, for new messages, only the real subject in bold...
+ // Would it be easier to simply use a 2nd TextView? This would also allow ellipsizing an
+ // overly-long subject, to let the beautiful snippet shine through.
+ String snippet = cursor.getString(COLUMN_SNIPPET);
+ if (!TextUtils.isEmpty(snippet)) {
+ text = context.getString(R.string.message_list_snippet, text, snippet);
+ }
subjectView.setText(text);
boolean hasInvitation =
diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java
index da0e4bd37..0d3174237 100644
--- a/src/com/android/email/provider/EmailContent.java
+++ b/src/com/android/email/provider/EmailContent.java
@@ -16,6 +16,7 @@
package com.android.email.provider;
+import com.android.email.Snippet;
import com.android.email.Utility;
import android.content.ContentProviderOperation;
@@ -426,9 +427,10 @@ public abstract class EmailContent {
public static final String CC_LIST = "ccList";
public static final String BCC_LIST = "bccList";
public static final String REPLY_TO_LIST = "replyToList";
-
// Meeting invitation related information (for now, start time in ms)
public static final String MEETING_INFO = "meetingInfo";
+ // A text "snippet" derived from the body of the message
+ public static final String SNIPPET = "snippet";
}
public static final class Message extends EmailContent implements SyncColumns, MessageColumns {
@@ -468,6 +470,7 @@ public abstract class EmailContent {
public static final int CONTENT_REPLY_TO_COLUMN = 18;
public static final int CONTENT_SERVER_TIMESTAMP_COLUMN = 19;
public static final int CONTENT_MEETING_INFO_COLUMN = 20;
+ public static final int CONTENT_SNIPPET_COLUMN = 21;
public static final String[] CONTENT_PROJECTION = new String[] {
RECORD_ID,
@@ -480,7 +483,8 @@ public abstract class EmailContent {
MessageColumns.ACCOUNT_KEY, MessageColumns.FROM_LIST,
MessageColumns.TO_LIST, MessageColumns.CC_LIST,
MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST,
- SyncColumns.SERVER_TIMESTAMP, MessageColumns.MEETING_INFO
+ SyncColumns.SERVER_TIMESTAMP, MessageColumns.MEETING_INFO,
+ MessageColumns.SNIPPET
};
public static final int LIST_ID_COLUMN = 0;
@@ -495,6 +499,7 @@ public abstract class EmailContent {
public static final int LIST_MAILBOX_KEY_COLUMN = 9;
public static final int LIST_ACCOUNT_KEY_COLUMN = 10;
public static final int LIST_SERVER_ID_COLUMN = 11;
+ public static final int LIST_SNIPPET_COLUMN = 12;
// Public projection for common list columns
public static final String[] LIST_PROJECTION = new String[] {
@@ -504,7 +509,7 @@ public abstract class EmailContent {
MessageColumns.FLAG_LOADED, MessageColumns.FLAG_FAVORITE,
MessageColumns.FLAG_ATTACHMENT, MessageColumns.FLAGS,
MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
- SyncColumns.SERVER_ID
+ SyncColumns.SERVER_ID, MessageColumns.SNIPPET
};
public static final int ID_COLUMNS_ID_COLUMN = 0;
@@ -551,6 +556,8 @@ public abstract class EmailContent {
// For now, just the start time of a meeting invite, in ms
public String mMeetingInfo;
+ public String mSnippet;
+
// The following transient members may be used while building and manipulating messages,
// but they are NOT persisted directly by EmailProvider
transient public String mText;
@@ -632,6 +639,8 @@ public abstract class EmailContent {
values.put(MessageColumns.MEETING_INFO, mMeetingInfo);
+ values.put(MessageColumns.SNIPPET, mSnippet);
+
return values;
}
@@ -676,6 +685,7 @@ public abstract class EmailContent {
mBcc = c.getString(CONTENT_BCC_LIST_COLUMN);
mReplyTo = c.getString(CONTENT_REPLY_TO_COLUMN);
mMeetingInfo = c.getString(CONTENT_MEETING_INFO_COLUMN);
+ mSnippet = c.getString(CONTENT_SNIPPET_COLUMN);
return this;
}
@@ -746,6 +756,12 @@ public abstract class EmailContent {
public void addSaveOps(ArrayList ops) {
// First, save the message
ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(mBaseUri);
+ // Generate the snippet here, before we create the CPO for Message
+ if (mText != null) {
+ mSnippet = Snippet.fromPlainText(mText);
+ } else if (mHtml != null) {
+ mSnippet = Snippet.fromHtmlText(mHtml);
+ }
ops.add(b.withValues(toContentValues()).build());
// Create and save the body
diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java
index 75abd0726..68516eb08 100644
--- a/src/com/android/email/provider/EmailProvider.java
+++ b/src/com/android/email/provider/EmailProvider.java
@@ -90,7 +90,8 @@ public class EmailProvider extends ContentProvider {
// Version 11: Add content and flags to attachment table
// Version 12: Add content_bytes to attachment table. content is deprecated.
// Version 13: Add messageCount to Mailbox table.
- public static final int DATABASE_VERSION = 13;
+ // Version 14: Add snippet to Message table
+ public static final int DATABASE_VERSION = 14;
// Any changes to the database format *must* include update-in-place code.
// Original version: 2
@@ -314,7 +315,8 @@ public class EmailProvider extends ContentProvider {
+ MessageColumns.CC_LIST + " text, "
+ MessageColumns.BCC_LIST + " text, "
+ MessageColumns.REPLY_TO_LIST + " text, "
- + MessageColumns.MEETING_INFO + " text"
+ + MessageColumns.MEETING_INFO + " text, "
+ + MessageColumns.SNIPPET + " text"
+ ");";
// This String and the following String MUST have the same columns, except for the type
@@ -849,6 +851,17 @@ public class EmailProvider extends ContentProvider {
}
oldVersion = 13;
}
+ if (oldVersion == 13) {
+ try {
+ db.execSQL("alter table " + Message.TABLE_NAME
+ + " add column " + Message.SNIPPET
+ +" text" + ";");
+ } catch (SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ Log.w(TAG, "Exception upgrading EmailProvider.db from 13 to 14 " + e);
+ }
+ oldVersion = 14;
+ }
}
@Override
diff --git a/tests/src/com/android/email/SnippetTests.java b/tests/src/com/android/email/SnippetTests.java
new file mode 100644
index 000000000..6b3b06ba5
--- /dev/null
+++ b/tests/src/com/android/email/SnippetTests.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This is a series of unit tests for snippet creation
+ *
+ * You can run this entire test case with:
+ * runtest -c com.android.email.SnippetTests email
+ */
+package com.android.email;
+
+import android.test.AndroidTestCase;
+
+public class SnippetTests extends AndroidTestCase {
+
+ public void testPlainSnippet() {
+ // Test the simplest cases
+ assertEquals("", Snippet.fromPlainText(null));
+ assertEquals("", Snippet.fromPlainText(""));
+
+ // Test handling leading, trailing, and duplicated whitespace
+ // Just test common whitespace characters; we calls Character.isWhitespace() internally, so
+ // other whitespace should be fine as well
+ assertEquals("", Snippet.fromPlainText(" \n\r\t\r\t\n"));
+ char c = Snippet.NON_BREAKING_SPACE_CHARACTER;
+ assertEquals("foo", Snippet.fromPlainText(c + "\r\n\tfoo \n\t\r" + c));
+ assertEquals("foo bar", Snippet.fromPlainText(c + "\r\n\tfoo \r\n bar\n\t\r" + c));
+
+ // Handle duplicated - and =
+ assertEquals("Foo-Bar=Bletch", Snippet.fromPlainText("Foo-----Bar=======Bletch"));
+
+ // We shouldn't muck with HTML entities
+ assertEquals(" >", Snippet.fromPlainText(" >"));
+ }
+
+ public void testHtmlSnippet() {
+ // Test the simplest cases
+ assertEquals("", Snippet.fromHtmlText(null));
+ assertEquals("", Snippet.fromHtmlText(""));
+
+ // Test handling leading, trailing, and duplicated whitespace
+ // Just test common whitespace characters; we calls Character.isWhitespace() internally, so
+ // other whitespace should be fine as well
+ assertEquals("", Snippet.fromHtmlText(" \n\r\t\r\t\n"));
+ char c = Snippet.NON_BREAKING_SPACE_CHARACTER;
+ assertEquals("foo", Snippet.fromHtmlText(c + "\r\n\tfoo \n\t\r" + c));
+ assertEquals("foo bar", Snippet.fromHtmlText(c + "\r\n\tfoo \r\n bar\n\t\r" + c));
+
+ // Handle duplicated - and =
+ assertEquals("Foo-Bar=Bletch", Snippet.fromPlainText("Foo-----Bar=======Bletch"));
+
+ // We should catch HTML entities in these tests
+ assertEquals(">", Snippet.fromHtmlText(" >"));
+ assertEquals("&<> \"", Snippet.fromHtmlText("&<> ""));
+ // Test for decimal and hex entities
+ assertEquals("ABC", Snippet.fromHtmlText("ABC"));
+ assertEquals("ABC", Snippet.fromHtmlText("ABC"));
+
+ // Test for stripping simple tags
+ assertEquals("Hi there", Snippet.fromHtmlText("Hi there"));
+ // TODO: Add tests here if/when we find problematic HTML
+ }
+
+ public void testStripHtmlEntityEdgeCases() {
+ int[] skipCount = new int[1];
+ // Bare & isn't an entity
+ char c = Snippet.stripHtmlEntity("&", 0, skipCount);
+ assertEquals(c, '&');
+ assertEquals(0, skipCount[0]);
+ // Also not legal
+ c = Snippet.stripHtmlEntity("&;", 0, skipCount);
+ assertEquals(c, '&');
+ assertEquals(0, skipCount[0]);
+ // This is an entity, but shouldn't be found
+ c = Snippet.stripHtmlEntity("&nosuch;", 0, skipCount);
+ assertEquals(c, '&');
+ assertEquals(0, skipCount[0]);
+ // This is too long for an entity, even though it starts like a valid one
+ c = Snippet.stripHtmlEntity(" andmore;", 0, skipCount);
+ assertEquals(c, '&');
+ assertEquals(0, skipCount[0]);
+ // Illegal decimal entities
+ c = Snippet.stripHtmlEntity("ABC", 0, skipCount);
+ assertEquals(c, '&');
+ assertEquals(0, skipCount[0]);
+ c = Snippet.stripHtmlEntity("B", 0, skipCount);
+ assertEquals(c, '&');
+ assertEquals(0, skipCount[0]);
+ // Illegal hex entities
+ c = Snippet.stripHtmlEntity("઼", 0, skipCount);
+ assertEquals(c, '&');
+ assertEquals(0, skipCount[0]);
+ // Illegal hex entities
+ c = Snippet.stripHtmlEntity("G", 0, skipCount);
+ assertEquals(c, '&');
+ assertEquals(0, skipCount[0]);
+ }
+ }
diff --git a/tests/src/com/android/email/provider/ProviderTestUtils.java b/tests/src/com/android/email/provider/ProviderTestUtils.java
index 2a6407ad5..a5a3339d5 100644
--- a/tests/src/com/android/email/provider/ProviderTestUtils.java
+++ b/tests/src/com/android/email/provider/ProviderTestUtils.java
@@ -20,6 +20,7 @@ import com.android.email.Utility;
import com.android.email.mail.transport.Rfc822Output;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Attachment;
+import com.android.email.provider.EmailContent.Body;
import com.android.email.provider.EmailContent.HostAuth;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.Message;
@@ -190,6 +191,31 @@ public class ProviderTestUtils extends Assert {
return message;
}
+ /**
+ * Create a test body
+ *
+ * @param messageId the message this body belongs to
+ * @param textContent the plain text for the body
+ * @param htmlContent the html text for the body
+ * @param saveIt if true, write the new attachment directly to the DB
+ * @param context use this context
+ */
+ public static Body setupBody(long messageId, String textContent, String htmlContent,
+ boolean saveIt, Context context) {
+ Body body = new Body();
+ body.mMessageKey = messageId;
+ body.mTextContent = textContent;
+ body.mHtmlContent = htmlContent;
+ body.mTextReply = "text reply " + messageId;
+ body.mHtmlReply = "html reply " + messageId;
+ body.mSourceKey = messageId + 0x1000;
+ body.mIntroText = "intro text " + messageId;
+ if (saveIt) {
+ body.save(context);
+ }
+ return body;
+ }
+
/**
* Create a test attachment. A few fields are specified by params, and all other fields
* are generated using pseudo-unique values.
@@ -364,6 +390,8 @@ public class ProviderTestUtils extends Assert {
assertEquals(caller + " mMeetingInfo", expect.mMeetingInfo, actual.mMeetingInfo);
+ assertEquals(caller + " mSnippet", expect.mSnippet, actual.mSnippet);
+
assertEquals(caller + " mText", expect.mText, actual.mText);
assertEquals(caller + " mHtml", expect.mHtml, actual.mHtml);
assertEquals(caller + " mTextReply", expect.mTextReply, actual.mTextReply);
diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java
index badbb6b74..c1289cdc2 100644
--- a/tests/src/com/android/email/provider/ProviderTests.java
+++ b/tests/src/com/android/email/provider/ProviderTests.java
@@ -16,6 +16,7 @@
package com.android.email.provider;
+import com.android.email.Snippet;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.Attachment;
@@ -518,6 +519,34 @@ public class ProviderTests extends ProviderTestCase2 {
}
}
+ /**
+ * Test that saving a message creates the proper snippet for that message
+ */
+ public void testMessageSaveAddsSnippet() {
+ Account account = ProviderTestUtils.setupAccount("message-snippet", true, mMockContext);
+ Mailbox box = ProviderTestUtils.setupMailbox("box1", account.mId, true, mMockContext);
+
+ // Create a message without a body, unsaved
+ Message message = ProviderTestUtils.setupMessage("message", account.mId, box.mId, false,
+ false, mMockContext);
+ message.mText = "This is some text";
+ message.mHtml = "This is some text";
+ message.save(mMockContext);
+ Message restoredMessage = Message.restoreMessageWithId(mMockContext, message.mId);
+ // We should have the plain text as the snippet
+ assertEquals(restoredMessage.mSnippet, Snippet.fromPlainText(message.mText));
+
+ // Start again
+ message = ProviderTestUtils.setupMessage("message", account.mId, box.mId, false,
+ false, mMockContext);
+ message.mText = null;
+ message.mHtml = "This is some text";
+ message.save(mMockContext);
+ restoredMessage = Message.restoreMessageWithId(mMockContext, message.mId);
+ // We should have the plain text as the snippet
+ assertEquals(restoredMessage.mSnippet, Snippet.fromHtmlText(message.mHtml));
+ }
+
/**
* TODO: update account
*/