0 <= usedCharacters <= 50
).
+ * @return the specified text if encoding is not necessary or an encoded
+ * word or a sequence of encoded words otherwise.
+ */
+ public static String encodeIfNecessary(String text, Usage usage,
+ int usedCharacters) {
+ if (hasToBeEncoded(text, usedCharacters))
+ return encodeEncodedWord(text, usage, usedCharacters);
+ else
+ return text;
+ }
+
+ /**
+ * Determines if the specified string has to encoded into an encoded-word.
+ * Returns true
if the text contains characters that don't
+ * fall into the printable ASCII character set or if the text contains a
+ * 'word' (sequence of non-whitespace characters) longer than 77 characters
+ * (including characters already used up in the line).
+ *
+ * @param text
+ * text to analyze.
+ * @param usedCharacters
+ * number of characters already used up (0 <= usedCharacters <= 50
).
+ * @return true
if the specified text has to be encoded into
+ * an encoded-word, false
otherwise.
+ */
+ public static boolean hasToBeEncoded(String text, int usedCharacters) {
+ if (text == null)
+ throw new IllegalArgumentException();
+ if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS)
+ throw new IllegalArgumentException();
+
+ int nonWhiteSpaceCount = usedCharacters;
+
+ for (int idx = 0; idx < text.length(); idx++) {
+ char ch = text.charAt(idx);
+ if (ch == '\t' || ch == ' ') {
+ nonWhiteSpaceCount = 0;
+ } else {
+ nonWhiteSpaceCount++;
+ if (nonWhiteSpaceCount > 77) {
+ // Line cannot be folded into multiple lines with no more
+ // than 78 characters each. Encoding as encoded-words makes
+ // that possible. One character has to be reserved for
+ // folding white space; that leaves 77 characters.
+ return true;
+ }
+
+ if (ch < 32 || ch >= 127) {
+ // non-printable ascii character has to be encoded
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Encodes the specified text into an encoded word or a sequence of encoded
+ * words separated by space. The text is separated into a sequence of
+ * encoded words if it does not fit in a single one.
+ * + * The charset to encode the specified text into a byte array and the + * encoding to use for the encoded-word are detected automatically. + *
+ * This method assumes that zero characters have already been used up in the + * current line. + * + * @param text + * text to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @return the encoded word (or sequence of encoded words if the given text + * does not fit in a single encoded word). + * @see #hasToBeEncoded(String, int) + */ + public static String encodeEncodedWord(String text, Usage usage) { + return encodeEncodedWord(text, usage, 0, null, null); + } + + /** + * Encodes the specified text into an encoded word or a sequence of encoded + * words separated by space. The text is separated into a sequence of + * encoded words if it does not fit in a single one. + *
+ * The charset to encode the specified text into a byte array and the
+ * encoding to use for the encoded-word are detected automatically.
+ *
+ * @param text
+ * text to encode.
+ * @param usage
+ * whether the encoded-word is to be used to replace a text token
+ * or a word entity (see RFC 822).
+ * @param usedCharacters
+ * number of characters already used up (0 <= usedCharacters <= 50
).
+ * @return the encoded word (or sequence of encoded words if the given text
+ * does not fit in a single encoded word).
+ * @see #hasToBeEncoded(String, int)
+ */
+ public static String encodeEncodedWord(String text, Usage usage,
+ int usedCharacters) {
+ return encodeEncodedWord(text, usage, usedCharacters, null, null);
+ }
+
+ /**
+ * Encodes the specified text into an encoded word or a sequence of encoded
+ * words separated by space. The text is separated into a sequence of
+ * encoded words if it does not fit in a single one.
+ *
+ * @param text
+ * text to encode.
+ * @param usage
+ * whether the encoded-word is to be used to replace a text token
+ * or a word entity (see RFC 822).
+ * @param usedCharacters
+ * number of characters already used up (0 <= usedCharacters <= 50
).
+ * @param charset
+ * the Java charset that should be used to encode the specified
+ * string into a byte array. A suitable charset is detected
+ * automatically if this parameter is null
.
+ * @param encoding
+ * the encoding to use for the encoded-word (either B or Q). A
+ * suitable encoding is automatically chosen if this parameter is
+ * null
.
+ * @return the encoded word (or sequence of encoded words if the given text
+ * does not fit in a single encoded word).
+ * @see #hasToBeEncoded(String, int)
+ */
+ public static String encodeEncodedWord(String text, Usage usage,
+ int usedCharacters, Charset charset, Encoding encoding) {
+ if (text == null)
+ throw new IllegalArgumentException();
+ if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS)
+ throw new IllegalArgumentException();
+
+ if (charset == null)
+ charset = determineCharset(text);
+
+ String mimeCharset = CharsetUtil.toMimeCharset(charset.name());
+ if (mimeCharset == null) {
+ // cannot happen if charset was originally null
+ throw new IllegalArgumentException("Unsupported charset");
+ }
+
+ byte[] bytes = encode(text, charset);
+
+ if (encoding == null)
+ encoding = determineEncoding(bytes, usage);
+
+ if (encoding == Encoding.B) {
+ String prefix = ENC_WORD_PREFIX + mimeCharset + "?B?";
+ return encodeB(prefix, text, usedCharacters, charset, bytes);
+ } else {
+ String prefix = ENC_WORD_PREFIX + mimeCharset + "?Q?";
+ return encodeQ(prefix, text, usage, usedCharacters, charset, bytes);
+ }
+ }
+
+ /**
+ * Encodes the specified byte array using the B encoding defined in RFC
+ * 2047.
+ *
+ * @param bytes
+ * byte array to encode.
+ * @return encoded string.
+ */
+ public static String encodeB(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+
+ int idx = 0;
+ final int end = bytes.length;
+ for (; idx < end - 2; idx += 3) {
+ int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8
+ | bytes[idx + 2] & 0xff;
+ sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data & 0x3f]);
+ }
+
+ if (idx == end - 2) {
+ int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8;
+ sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]);
+ sb.append(BASE64_PAD);
+
+ } else if (idx == end - 1) {
+ int data = (bytes[idx] & 0xff) << 16;
+ sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]);
+ sb.append(BASE64_PAD);
+ sb.append(BASE64_PAD);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Encodes the specified byte array using the Q encoding defined in RFC
+ * 2047.
+ *
+ * @param bytes
+ * byte array to encode.
+ * @param usage
+ * whether the encoded-word is to be used to replace a text token
+ * or a word entity (see RFC 822).
+ * @return encoded string.
+ */
+ public static String encodeQ(byte[] bytes, Usage usage) {
+ BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS
+ : Q_RESTRICTED_CHARS;
+
+ StringBuilder sb = new StringBuilder();
+
+ final int end = bytes.length;
+ for (int idx = 0; idx < end; idx++) {
+ int v = bytes[idx] & 0xff;
+ if (v == 32) {
+ sb.append('_');
+ } else if (!qChars.get(v)) {
+ sb.append('=');
+ sb.append(hexDigit(v >>> 4));
+ sb.append(hexDigit(v & 0xf));
+ } else {
+ sb.append((char) v);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Tests whether the specified string is a token as defined in RFC 2045
+ * section 5.1.
+ *
+ * @param str
+ * string to test.
+ * @return true
if the specified string is a RFC 2045 token,
+ * false
otherwise.
+ */
+ public static boolean isToken(String str) {
+ // token := 1*true
if the specified character is a whitespace
+ * character (CR, LF, SP or HT).
+ *
+ * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J
+ *
+ * @param ch
+ * character to test.
+ * @return true
if the specified character is a whitespace
+ * character, false
otherwise.
+ */
+ public static boolean isWhitespace(char ch) {
+ return ch == SP || ch == HT || ch == CR || ch == LF;
+ }
+
+ /**
+ * Returns true
if the specified string consists entirely of
+ * whitespace characters.
+ *
+ * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J
+ *
+ * @param s
+ * string to test.
+ * @return true
if the specified string consists entirely of
+ * whitespace characters, false
otherwise.
+ */
+ public static boolean isWhitespace(final String s) {
+ if (s == null) {
+ throw new IllegalArgumentException("String may not be null");
+ }
+ final int len = s.length();
+ for (int i = 0; i < len; i++) {
+ if (!isWhitespace(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Determines if the VM supports encoding (chars to bytes) the
* specified character set. NOTE: the given character set name may
diff --git a/tests/src/com/android/email/mail/internet/MimeMessageTest.java b/tests/src/com/android/email/mail/internet/MimeMessageTest.java
index d1fde4a6e..45f223e4c 100644
--- a/tests/src/com/android/email/mail/internet/MimeMessageTest.java
+++ b/tests/src/com/android/email/mail/internet/MimeMessageTest.java
@@ -35,6 +35,26 @@ import junit.framework.TestCase;
*/
@SmallTest
public class MimeMessageTest extends TestCase {
+
+ /** up arrow, down arrow, left arrow, right arrow */
+ private final String SHORT_UNICODE = "\u2191\u2193\u2190\u2192";
+ private final String SHORT_UNICODE_ENCODED = "=?UTF-8?B?4oaR4oaT4oaQ4oaS?=";
+
+ /** a string without any unicode */
+ private final String SHORT_PLAIN = "abcd";
+
+ /** longer unicode strings */
+ private final String LONG_UNICODE_16 = SHORT_UNICODE + SHORT_UNICODE +
+ SHORT_UNICODE + SHORT_UNICODE;
+ private final String LONG_UNICODE_64 = LONG_UNICODE_16 + LONG_UNICODE_16 +
+ LONG_UNICODE_16 + LONG_UNICODE_16;
+
+ /** longer plain strings (with fold points) */
+ private final String LONG_PLAIN_16 = "abcdefgh ijklmno";
+ private final String LONG_PLAIN_64 =
+ LONG_PLAIN_16 + LONG_PLAIN_16 + LONG_PLAIN_16 + LONG_PLAIN_16;
+ private final String LONG_PLAIN_256 =
+ LONG_PLAIN_64 + LONG_PLAIN_64 + LONG_PLAIN_64 + LONG_PLAIN_64;
// TODO: more tests.
@@ -98,7 +118,7 @@ public class MimeMessageTest extends TestCase {
assertEquals("set and get Message-ID", testId2, message2.getMessageId());
}
- /*
+ /**
* Confirm getContentID() correctly works.
*/
public void testGetContentId() throws MessagingException {
@@ -116,4 +136,80 @@ public class MimeMessageTest extends TestCase {
message.setHeader(MimeHeader.HEADER_CONTENT_ID, "<" + cid1 + ">");
assertEquals(cid1, message.getContentId());
}
+
+ /**
+ * Confirm that setSubject() works with plain strings
+ */
+ public void testSetSubjectPlain() throws MessagingException {
+ MimeMessage message = new MimeMessage();
+
+ message.setSubject(SHORT_PLAIN);
+
+ // test 1: readback
+ assertEquals("plain subjects", SHORT_PLAIN, message.getSubject());
+
+ // test 2: raw readback is not escaped
+ String rawHeader = message.getFirstHeader("Subject");
+ assertEquals("plain subject not encoded", -1, rawHeader.indexOf("=?"));
+
+ // test 3: long subject (shouldn't fold)
+ message.setSubject(LONG_PLAIN_64);
+ rawHeader = message.getFirstHeader("Subject");
+ String[] split = rawHeader.split("\r\n");
+ assertEquals("64 shouldn't fold", 1, split.length);
+
+ // test 4: very long subject (should fold)
+ message.setSubject(LONG_PLAIN_256);
+ rawHeader = message.getFirstHeader("Subject");
+ split = rawHeader.split("\r\n");
+ assertTrue("long subject should fold", split.length > 1);
+ for (String s : split) {
+ assertTrue("split lines max length 78", s.length() <= 76); // 76+\r\n = 78
+ String trimmed = s.trim();
+ assertFalse("split lines are not encoded", trimmed.startsWith("=?"));
+ }
+ }
+
+ /**
+ * Confirm that setSubject() works with unicode strings
+ */
+ public void testSetSubject() throws MessagingException {
+ MimeMessage message = new MimeMessage();
+
+ message.setSubject(SHORT_UNICODE);
+
+ // test 1: readback in unicode
+ assertEquals("unicode readback", SHORT_UNICODE, message.getSubject());
+
+ // test 2: raw readback is escaped
+ String rawHeader = message.getFirstHeader("Subject");
+ assertEquals("raw readback", SHORT_UNICODE_ENCODED, rawHeader);
+ }
+
+ /**
+ * Confirm folding operations on unicode subjects
+ */
+ public void testSetLongSubject() throws MessagingException {
+ MimeMessage message = new MimeMessage();
+
+ // test 1: long unicode - readback in unicode
+ message.setSubject(LONG_UNICODE_16);
+ assertEquals("unicode readback 16", LONG_UNICODE_16, message.getSubject());
+
+ // test 2: longer unicode (will fold)
+ message.setSubject(LONG_UNICODE_64);
+ assertEquals("unicode readback 64", LONG_UNICODE_64, message.getSubject());
+
+ // test 3: check folding & encoding
+ String rawHeader = message.getFirstHeader("Subject");
+ String[] split = rawHeader.split("\r\n");
+ assertTrue("long subject should fold", split.length > 1);
+ for (String s : split) {
+ assertTrue("split lines max length 78", s.length() <= 76); // 76+\r\n = 78
+ String trimmed = s.trim();
+ assertTrue("split lines are encoded",
+ trimmed.startsWith("=?") && trimmed.endsWith("?="));
+ }
+ }
+
}
\ No newline at end of file
diff --git a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java
index e894161be..b39e4cf0d 100644
--- a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java
+++ b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java
@@ -35,11 +35,120 @@ import junit.framework.TestCase;
@SmallTest
public class MimeUtilityTest extends TestCase {
- // TODO: tests for unfold(String s)
+ /** up arrow, down arrow, left arrow, right arrow */
+ private final String SHORT_UNICODE = "\u2191\u2193\u2190\u2192";
+ private final String SHORT_UNICODE_ENCODED = "=?UTF-8?B?4oaR4oaT4oaQ4oaS?=";
+
+ /** a string without any unicode */
+ private final String SHORT_PLAIN = "abcd";
+
+ /** a typical no-param header */
+ private final String HEADER_NO_PARAMETER =
+ "header";
+ /** a typical multi-param header */
+ private final String HEADER_MULTI_PARAMETER =
+ "header; Param1Name=Param1Value; Param2Name=Param2Value";
+
+ /**
+ * Test that decode/unfold is efficient when it can be
+ */
+ public void testEfficientUnfoldAndDecode() {
+ String result1 = MimeUtility.unfold(SHORT_PLAIN);
+ String result2 = MimeUtility.decode(SHORT_PLAIN);
+ String result3 = MimeUtility.unfoldAndDecode(SHORT_PLAIN);
+
+ assertSame(SHORT_PLAIN, result1);
+ assertSame(SHORT_PLAIN, result2);
+ assertSame(SHORT_PLAIN, result3);
+ }
+
+ // TODO: more tests for unfold(String s)
+
+ /**
+ * Test that decode is working for simple strings
+ */
+ public void testDecodeSimple() {
+ String result1 = MimeUtility.decode(SHORT_UNICODE_ENCODED);
+ assertEquals(SHORT_UNICODE, result1);
+ }
+
// TODO: tests for decode(String s)
+
+ /**
+ * Test that unfoldAndDecode is working for simple strings
+ */
+ public void testUnfoldAndDecodeSimple() {
+ String result1 = MimeUtility.unfoldAndDecode(SHORT_UNICODE_ENCODED);
+ assertEquals(SHORT_UNICODE, result1);
+ }
+
// TODO: tests for unfoldAndDecode(String s)
- // TODO: tests for foldAndEncode(String s)
- // TODO: tests for getHeaderParameter(String header, String name)
+
+ /**
+ * Test that fold/encode is efficient when it can be
+ */
+ public void testEfficientFoldAndEncode() {
+ String result1 = MimeUtility.foldAndEncode(SHORT_PLAIN);
+ String result2 = MimeUtility.foldAndEncode2(SHORT_PLAIN, 10);
+ String result3 = MimeUtility.fold(SHORT_PLAIN, 10);
+
+ assertSame(SHORT_PLAIN, result1);
+ assertSame(SHORT_PLAIN, result2);
+ assertSame(SHORT_PLAIN, result3);
+ }
+
+ // TODO: more tests for foldAndEncode(String s)
+
+ /**
+ * Test that foldAndEncode2 is working for simple strings
+ */
+ public void testFoldAndEncode2() {
+ String result1 = MimeUtility.foldAndEncode2(SHORT_UNICODE, 10);
+ assertEquals(SHORT_UNICODE_ENCODED, result1);
+ }
+
+ // TODO: more tests for foldAndEncode2(String s)
+ // TODO: more tests for fold(String s, int usedCharacters)
+
+ /**
+ * Basic tests of getHeaderParameter()
+ *
+ * Typical header value: multipart/mixed; boundary="----E5UGTXUQQJV80DR8SJ88F79BRA4S8K"
+ *
+ * Function spec says:
+ * if header is null: return null
+ * if name is null: if params, return first param. else return full field
+ * else: if param is found (case insensitive) return it
+ * else return null
+ */
+ public void testGetHeaderParameter() {
+ // if header is null, return null
+ assertNull("null header check", MimeUtility.getHeaderParameter(null, "name"));
+
+ // if name is null, return first param or full header
+ // NOTE: The docs are wrong - it returns the header (no params) in that case
+// assertEquals("null name first param per docs", "Param1Value",
+// MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, null));
+ assertEquals("null name first param per code", "header",
+ MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, null));
+ assertEquals("null name full header", HEADER_NO_PARAMETER,
+ MimeUtility.getHeaderParameter(HEADER_NO_PARAMETER, null));
+
+ // find name
+ assertEquals("get 1st param", "Param1Value",
+ MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "Param1Name"));
+ assertEquals("get 2nd param", "Param2Value",
+ MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "Param2Name"));
+ assertEquals("get missing param", null,
+ MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "Param3Name"));
+
+ // case insensitivity
+ assertEquals("get 2nd param all LC", "Param2Value",
+ MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "param2name"));
+ assertEquals("get 2nd param all UC", "Param2Value",
+ MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "PARAM2NAME"));
+ }
+
// TODO: tests for findFirstPartByMimeType(Part part, String mimeType)
/** Tests for findPartByContentId(Part part, String contentId) */
From 8dcff90a5e878d96f9a782b5af40ce05d3323cbc Mon Sep 17 00:00:00 2001
From: Eric Fischer <>
Date: Tue, 24 Mar 2009 21:02:50 -0700
Subject: [PATCH 03/27] Automated import from
//branches/donutburger/...@141868,141868
---
res/values-cs/strings.xml | 23 ++++++++++++-----------
res/values-de/strings.xml | 23 ++++++++++++-----------
res/values-es/strings.xml | 23 ++++++++++++-----------
res/values-fr/strings.xml | 23 ++++++++++++-----------
res/values-it/strings.xml | 23 ++++++++++++-----------
res/values-ja/strings.xml | 31 ++++++++++++++++---------------
res/values-ko/strings.xml | 23 ++++++++++++-----------
res/values-nb/strings.xml | 15 ++++++++-------
res/values-nl/strings.xml | 23 ++++++++++++-----------
res/values-pl/strings.xml | 23 ++++++++++++-----------
res/values-ru/strings.xml | 23 ++++++++++++-----------
res/values-zh-rCN/strings.xml | 23 ++++++++++++-----------
res/values-zh-rTW/strings.xml | 23 ++++++++++++-----------
13 files changed, 156 insertions(+), 143 deletions(-)
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index f38314c38..2bed10a90 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -50,11 +50,15 @@
For protocols that do not support push mode, be sure that push="true" is not + * set by the stores.xml definition file(s). This function need not be implemented. + * + *
For protocols that do support push mode, this will be called at startup (boot) time + * so that the Store can launch its own underlying connection service. It will also be called + * any time the user changes the settings for the account (because the user may switch back + * to polling mode (or disable checking completely). + * + *
This API will be called repeatedly, even after push mode has already been started or
+ * stopped. Stores that support push mode should return quickly if the configuration has not
+ * changed.
+ *
+ * @param enablePushMode start or stop push mode delivery
+ */
+ public void enablePushModeDelivery(boolean enablePushMode) {
+ // does nothing for non-push protocols
+ }
+
/**
* Delete Store and its corresponding resources.
* @throws MessagingException
diff --git a/src/com/android/email/mail/exchange/ExchangeStoreExample.java b/src/com/android/email/mail/exchange/ExchangeStoreExample.java
index d70ca0658..444fb68b8 100644
--- a/src/com/android/email/mail/exchange/ExchangeStoreExample.java
+++ b/src/com/android/email/mail/exchange/ExchangeStoreExample.java
@@ -16,11 +16,14 @@
package com.android.email.mail.exchange;
+import com.android.email.Email;
import com.android.email.mail.Folder;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Store;
import android.content.Context;
+import android.util.Config;
+import android.util.Log;
import java.net.URI;
import java.net.URISyntaxException;
@@ -36,12 +39,15 @@ import java.util.HashMap;
* to res/xml/stores.xml
*/
public class ExchangeStoreExample extends Store {
+ public static final String LOG_TAG = "ExchangeStoreExample";
private final Context mContext;
private URI mUri;
private final ExchangeTransportExample mTransport;
private final HashMap Note, may be called multiple times, even after push mode has been started or stopped.
+ *
+ * @param enablePushMode start or stop push mode delivery
+ */
+ @Override
+ public void enablePushModeDelivery(boolean enablePushMode) {
+ if (Config.LOGD && Email.DEBUG) {
+ if (enablePushMode && !mPushModeRunning) {
+ Log.d(Email.LOG_TAG, "start push mode");
+ } else if (!enablePushMode && mPushModeRunning) {
+ Log.d(Email.LOG_TAG, "stop push mode");
+ } else {
+ Log.d(Email.LOG_TAG, enablePushMode ?
+ "push mode already started" : "push mode already stopped");
+ }
+ }
+ mPushModeRunning = enablePushMode;
+ }
+
/**
* Get class of SettingActivity for this Store class.
* @return Activity class that has class method actionEditIncomingSettings().
diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java
index 9cd8c0169..7d1fb00a5 100644
--- a/src/com/android/email/service/MailService.java
+++ b/src/com/android/email/service/MailService.java
@@ -16,8 +16,16 @@
package com.android.email.service;
-import java.util.ArrayList;
-import java.util.HashMap;
+import com.android.email.Account;
+import com.android.email.Email;
+import com.android.email.MessagingController;
+import com.android.email.MessagingListener;
+import com.android.email.Preferences;
+import com.android.email.R;
+import com.android.email.activity.Accounts;
+import com.android.email.activity.FolderMessageList;
+import com.android.email.mail.MessagingException;
+import com.android.email.mail.Store;
import android.app.AlarmManager;
import android.app.Notification;
@@ -33,14 +41,8 @@ import android.text.TextUtils;
import android.util.Config;
import android.util.Log;
-import com.android.email.Account;
-import com.android.email.Email;
-import com.android.email.MessagingController;
-import com.android.email.MessagingListener;
-import com.android.email.Preferences;
-import com.android.email.R;
-import com.android.email.activity.Accounts;
-import com.android.email.activity.FolderMessageList;
+import java.util.ArrayList;
+import java.util.HashMap;
/**
*/
@@ -48,6 +50,8 @@ public class MailService extends Service {
private static final String ACTION_CHECK_MAIL = "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
private static final String ACTION_RESCHEDULE = "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
private static final String ACTION_CANCEL = "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
+
+ private static final String EXTRA_CHECK_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
private Listener mListener = new Listener();
@@ -66,6 +70,21 @@ public class MailService extends Service {
i.setAction(MailService.ACTION_CANCEL);
context.startService(i);
}
+
+ /**
+ * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
+ * messages. Note: Although this is not a blocking call, it will start the MessagingController
+ * which will attempt to load the new messages. So the Store should expect to be opened and
+ * fetched from shortly after making this call.
+ *
+ * @param storeUri the Uri of the store that is reporting new messages
+ */
+ public static void actionNotifyNewMessages(Context context, String storeUri) {
+ Intent i = new Intent(ACTION_CHECK_MAIL);
+ i.setClass(context, MailService.class);
+ i.putExtra(EXTRA_CHECK_ACCOUNT, storeUri);
+ context.startService(i);
+ }
@Override
public void onStart(Intent intent, int startId) {
@@ -84,11 +103,21 @@ public class MailService extends Service {
// accounts that should not have been checked at all.
// Also note: Due to the organization of this service, you must gather the accounts
// and make a single call to controller.checkMail().
+
+ // TODO: Notification for single push account will fire up checks on all other
+ // accounts. This needs to be cleaned up for better efficiency.
+ String specificStoreUri = intent.getStringExtra(EXTRA_CHECK_ACCOUNT);
+
ArrayList
@@ -87,6 +101,8 @@ public class Account implements Serializable {
* Refresh the account from the stored settings.
*/
public void refresh(Preferences preferences) {
+ mPreferences = preferences;
+
mStoreUri = Utility.base64Decode(preferences.mSharedPreferences.getString(mUuid
+ ".storeUri", null));
mLocalStoreUri = preferences.mSharedPreferences.getString(mUuid + ".localStoreUri", null);
@@ -129,6 +145,9 @@ public class Account implements Serializable {
mVibrate = preferences.mSharedPreferences.getBoolean(mUuid + ".vibrate", false);
mRingtoneUri = preferences.mSharedPreferences.getString(mUuid + ".ringtone",
"content://settings/system/notification_sound");
+
+ mStorePersistent = preferences.mSharedPreferences.getString(
+ mUuid + PREF_TAG_STORE_PERSISTENT, null);
}
public String getUuid() {
@@ -223,7 +242,8 @@ public class Account implements Serializable {
editor.remove(mUuid + ".accountNumber");
editor.remove(mUuid + ".vibrate");
editor.remove(mUuid + ".ringtone");
-
+ editor.remove(mUuid + PREF_TAG_STORE_PERSISTENT);
+
// also delete any deprecated fields
editor.remove(mUuid + ".transportUri");
@@ -231,6 +251,8 @@ public class Account implements Serializable {
}
public void save(Preferences preferences) {
+ mPreferences = preferences;
+
if (!preferences.mSharedPreferences.getString("accountUuids", "").contains(mUuid)) {
/*
* When the account is first created we assign it a unique account number. The
@@ -284,12 +306,17 @@ public class Account implements Serializable {
editor.putBoolean(mUuid + ".vibrate", mVibrate);
editor.putString(mUuid + ".ringtone", mRingtoneUri);
+ // The following fields are *not* written because they need to be more fine-grained
+ // and not risk rewriting with old data.
+ // editor.putString(mUuid + PREF_TAG_STORE_PERSISTENT, mStorePersistent);
+
// also delete any deprecated fields
editor.remove(mUuid + ".transportUri");
editor.commit();
}
+ @Override
public String toString() {
return mDescription;
}
@@ -380,6 +407,44 @@ public class Account implements Serializable {
return mAccountNumber;
}
+ /**
+ * Provides a small place for Stores to store persistent data. This will need to be
+ * expanded in the future, but is sufficient for now.
+ * @param storeData Data to persist. All data must be encoded into a string,
+ * so use base64 or some other encoding if necessary.
+ */
+ public void setPersistentString(String storeData) {
+ synchronized (this.getClass()) {
+ mStorePersistent = mPreferences.mSharedPreferences.getString(
+ mUuid + PREF_TAG_STORE_PERSISTENT, null);
+ if ((mStorePersistent == null && storeData != null) ||
+ (mStorePersistent != null && !mStorePersistent.equals(storeData))) {
+ mStorePersistent = storeData;
+ SharedPreferences.Editor editor = mPreferences.mSharedPreferences.edit();
+ editor.putString(mUuid + PREF_TAG_STORE_PERSISTENT, mStorePersistent);
+ editor.commit();
+ }
+ }
+ }
+
+ /**
+ * @return the data saved by the Store, or null if never set.
+ */
+ public String getPersistentString() {
+ synchronized (this.getClass()) {
+ mStorePersistent = mPreferences.mSharedPreferences.getString(
+ mUuid + PREF_TAG_STORE_PERSISTENT, null);
+ }
+ return mStorePersistent;
+ }
+
+ /**
+ * @return An implementation of Store.PersistentDataCallbacks
+ */
+ public Store.PersistentDataCallbacks getStoreCallbacks() {
+ return this;
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof Account) {
diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java
index 46dd9f3fb..cacaad043 100644
--- a/src/com/android/email/MessagingController.java
+++ b/src/com/android/email/MessagingController.java
@@ -189,7 +189,7 @@ public class MessagingController implements Runnable {
l.listFoldersStarted(account);
}
try {
- Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication);
+ Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null);
Folder[] localFolders = localStore.getPersonalNamespaces();
if (localFolders == null || localFolders.length == 0) {
@@ -210,13 +210,13 @@ public class MessagingController implements Runnable {
put("listFolders", listener, new Runnable() {
public void run() {
try {
- Store store = Store.getInstance(account.getStoreUri(), mApplication);
+ Store store = Store.getInstance(account.getStoreUri(), mApplication,
+ account.getStoreCallbacks());
Folder[] remoteFolders = store.getPersonalNamespaces();
Store localStore = Store.getInstance(
- account.getLocalStoreUri(),
- mApplication);
+ account.getLocalStoreUri(), mApplication, null);
HashSet