summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJorge Ruesga <jorge@ruesga.com>2015-05-01 21:35:23 +0200
committerSteve Kondik <steve@cyngn.com>2015-10-18 14:05:32 -0700
commit08ace26ed605946d788ce56f5c9aefc65131a63b (patch)
treef1014dc39087078bf19111b76427ab58ba78ad4b
parent3b1b30873e9c07139e2cc9fdaa796151592fea69 (diff)
downloadandroid_packages_apps_Email-08ace26ed605946d788ce56f5c9aefc65131a63b.tar.gz
android_packages_apps_Email-08ace26ed605946d788ce56f5c9aefc65131a63b.tar.bz2
android_packages_apps_Email-08ace26ed605946d788ce56f5c9aefc65131a63b.zip
email: imap push
Change-Id: I8a184a5644e4322ee65d969e14cd47fe119f5df2 Signed-off-by: Jorge Ruesga <jorge@ruesga.com>
-rwxr-xr-xemailcommon/src/com/android/emailcommon/provider/Account.java52
-rwxr-xr-xemailcommon/src/com/android/emailcommon/provider/EmailContent.java8
-rw-r--r--emailcommon/src/com/android/emailcommon/provider/Mailbox.java3
-rw-r--r--emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java6
-rw-r--r--provider_src/com/android/email/EmailConnectivityManager.java7
-rw-r--r--provider_src/com/android/email/activity/setup/AccountSettingsUtils.java2
-rw-r--r--provider_src/com/android/email/mail/store/ImapConnection.java85
-rw-r--r--provider_src/com/android/email/mail/store/ImapFolder.java377
-rw-r--r--provider_src/com/android/email/mail/store/ImapStore.java9
-rw-r--r--provider_src/com/android/email/mail/store/Pop3Store.java4
-rw-r--r--provider_src/com/android/email/mail/store/imap/ImapConstants.java4
-rw-r--r--provider_src/com/android/email/mail/store/imap/ImapList.java2
-rw-r--r--provider_src/com/android/email/mail/store/imap/ImapResponse.java7
-rw-r--r--provider_src/com/android/email/mail/store/imap/ImapResponseParser.java37
-rw-r--r--provider_src/com/android/email/mail/transport/MailTransport.java8
-rw-r--r--provider_src/com/android/email/provider/DBHelper.java53
-rw-r--r--provider_src/com/android/email/provider/EmailProvider.java71
-rw-r--r--provider_src/com/android/email/provider/Utilities.java19
-rw-r--r--provider_src/com/android/email/service/EmailBroadcastProcessorService.java4
-rwxr-xr-xprovider_src/com/android/email/service/EmailServiceStub.java8
-rw-r--r--provider_src/com/android/email/service/ImapService.java1132
-rw-r--r--provider_src/com/android/email/service/LegacyImapSyncAdapterService.java136
-rw-r--r--provider_src/com/android/email/service/PopImapSyncAdapterService.java46
-rw-r--r--res/xml/services.xml4
-rw-r--r--res/xml/syncadapter_legacy_imap.xml1
-rw-r--r--src/com/android/email/activity/setup/AccountCheckSettingsFragment.java4
-rw-r--r--src/com/android/email/activity/setup/AccountSettingsFragment.java40
-rw-r--r--src/com/android/email/activity/setup/AccountSetupFinal.java2
-rw-r--r--src/com/android/email/activity/setup/AccountSetupOptionsFragment.java24
29 files changed, 2097 insertions, 58 deletions
diff --git a/emailcommon/src/com/android/emailcommon/provider/Account.java b/emailcommon/src/com/android/emailcommon/provider/Account.java
index 5a3ab7f3a..b9a21c805 100755
--- a/emailcommon/src/com/android/emailcommon/provider/Account.java
+++ b/emailcommon/src/com/android/emailcommon/provider/Account.java
@@ -31,6 +31,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
+import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.utility.Utility;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
@@ -111,22 +112,26 @@ public final class Account extends EmailContent implements Parcelable {
// Sentinel values for the mSyncInterval field of both Account records
public static final int CHECK_INTERVAL_NEVER = -1;
public static final int CHECK_INTERVAL_PUSH = -2;
+ public static final int CHECK_INTERVAL_DEFAULT_PULL = 15;
public static Uri CONTENT_URI;
public static Uri RESET_NEW_MESSAGE_COUNT_URI;
public static Uri NOTIFIER_URI;
+ public static Uri SYNC_SETTING_CHANGED_URI;
public static void initAccount() {
CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account");
RESET_NEW_MESSAGE_COUNT_URI = Uri.parse(EmailContent.CONTENT_URI + "/resetNewMessageCount");
NOTIFIER_URI = Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/account");
+ SYNC_SETTING_CHANGED_URI = Uri.parse(
+ EmailContent.CONTENT_SYNC_SETTING_CHANGED_URI + "/account");
}
public String mDisplayName;
public String mEmailAddress;
public String mSyncKey;
public int mSyncLookback;
- public int mSyncInterval;
+ private int mSyncInterval;
public long mHostAuthKeyRecv;
public long mHostAuthKeySend;
public int mFlags;
@@ -139,6 +144,7 @@ public final class Account extends EmailContent implements Parcelable {
public String mSignature;
public long mPolicyKey;
public long mPingDuration;
+ public int mCapabilities;
@VisibleForTesting
static final String JSON_TAG_HOST_AUTH_RECV = "hostAuthRecv";
@@ -171,6 +177,7 @@ public final class Account extends EmailContent implements Parcelable {
public static final int CONTENT_POLICY_KEY_COLUMN = 14;
public static final int CONTENT_PING_DURATION_COLUMN = 15;
public static final int CONTENT_MAX_ATTACHMENT_SIZE_COLUMN = 16;
+ public static final int CONTENT_CAPABILITIES_COLUMN = 17;
public static final String[] CONTENT_PROJECTION = {
AttachmentColumns._ID, AccountColumns.DISPLAY_NAME,
@@ -181,7 +188,7 @@ public final class Account extends EmailContent implements Parcelable {
AccountColumns.RINGTONE_URI, AccountColumns.PROTOCOL_VERSION,
AccountColumns.SECURITY_SYNC_KEY,
AccountColumns.SIGNATURE, AccountColumns.POLICY_KEY, AccountColumns.PING_DURATION,
- AccountColumns.MAX_ATTACHMENT_SIZE
+ AccountColumns.MAX_ATTACHMENT_SIZE, AccountColumns.CAPABILITIES
};
public static final int ACCOUNT_FLAGS_COLUMN_ID = 0;
@@ -279,6 +286,7 @@ public final class Account extends EmailContent implements Parcelable {
mSignature = cursor.getString(CONTENT_SIGNATURE_COLUMN);
mPolicyKey = cursor.getLong(CONTENT_POLICY_KEY_COLUMN);
mPingDuration = cursor.getLong(CONTENT_PING_DURATION_COLUMN);
+ mCapabilities = cursor.getInt(CONTENT_CAPABILITIES_COLUMN);
}
public boolean isTemporary() {
@@ -358,6 +366,11 @@ public final class Account extends EmailContent implements Parcelable {
* TODO define sentinel values for "never", "push", etc. See Account.java
*/
public int getSyncInterval() {
+ // Fixed unsynced value and account capability. Change to default pull value
+ if (!hasCapability(EmailServiceProxy.CAPABILITY_PUSH)
+ && mSyncInterval == CHECK_INTERVAL_PUSH) {
+ return CHECK_INTERVAL_DEFAULT_PULL;
+ }
return mSyncInterval;
}
@@ -367,7 +380,13 @@ public final class Account extends EmailContent implements Parcelable {
* @param minutes the number of minutes between polling checks
*/
public void setSyncInterval(int minutes) {
- mSyncInterval = minutes;
+ // Fixed unsynced value and account capability. Change to default pull value
+ if (!hasCapability(EmailServiceProxy.CAPABILITY_PUSH)
+ && mSyncInterval == CHECK_INTERVAL_PUSH) {
+ mSyncInterval = CHECK_INTERVAL_DEFAULT_PULL;
+ } else {
+ mSyncInterval = minutes;
+ }
}
/**
@@ -403,6 +422,20 @@ public final class Account extends EmailContent implements Parcelable {
}
/**
+ * @return the current account capabilities.
+ */
+ public int getCapabilities() {
+ return mCapabilities;
+ }
+
+ /**
+ * Set the account capabilities. Be sure to call save() to commit to database.
+ */
+ public void setCapabilities(int value) {
+ mCapabilities = value;
+ }
+
+ /**
* @return the flags for this account
*/
public int getFlags() {
@@ -749,6 +782,7 @@ public final class Account extends EmailContent implements Parcelable {
values.put(AccountColumns.SIGNATURE, mSignature);
values.put(AccountColumns.POLICY_KEY, mPolicyKey);
values.put(AccountColumns.PING_DURATION, mPingDuration);
+ values.put(AccountColumns.CAPABILITIES, mCapabilities);
return values;
}
@@ -779,6 +813,7 @@ public final class Account extends EmailContent implements Parcelable {
json.putOpt(AccountColumns.PROTOCOL_VERSION, mProtocolVersion);
json.putOpt(AccountColumns.SIGNATURE, mSignature);
json.put(AccountColumns.PING_DURATION, mPingDuration);
+ json.put(AccountColumns.CAPABILITIES, mCapabilities);
return json;
} catch (final JSONException e) {
LogUtils.d(LogUtils.TAG, e, "Exception while serializing Account");
@@ -817,6 +852,7 @@ public final class Account extends EmailContent implements Parcelable {
a.mSignature = json.optString(AccountColumns.SIGNATURE);
// POLICY_KEY is not stored
a.mPingDuration = json.optInt(AccountColumns.PING_DURATION, 0);
+ a.mCapabilities = json.optInt(AccountColumns.CAPABILITIES, 0);
return a;
} catch (final JSONException e) {
LogUtils.d(LogUtils.TAG, e, "Exception while deserializing Account");
@@ -843,6 +879,14 @@ public final class Account extends EmailContent implements Parcelable {
}
/**
+ * Returns whether or not the capability is supported by the account.
+ * @see EmailServiceProxy#CAPABILITY_*
+ */
+ public boolean hasCapability(int capability) {
+ return (mCapabilities & capability) != 0;
+ }
+
+ /**
* Supports Parcelable
*/
@Override
@@ -903,6 +947,7 @@ public final class Account extends EmailContent implements Parcelable {
} else {
dest.writeByte((byte)0);
}
+ dest.writeInt(mCapabilities);
}
/**
@@ -937,6 +982,7 @@ public final class Account extends EmailContent implements Parcelable {
if (in.readByte() == 1) {
mHostAuthSend = new HostAuth(in);
}
+ mCapabilities = in.readInt();
}
/**
diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
index f1fcb0dcf..dd4e7eb85 100755
--- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
+++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
@@ -145,6 +145,8 @@ public abstract class EmailContent {
// delete, or update) and is intended as an optimization for use by clients of message list
// cursors (initially, the email AppWidget).
public static String NOTIFIER_AUTHORITY;
+ // The sync settings changed authority is used to notify when a sync setting changed (interval)
+ public static String SYNC_SETTING_CHANGED_AUTHORITY;
public static Uri CONTENT_URI;
public static final String PARAMETER_LIMIT = "limit";
@@ -153,6 +155,7 @@ public abstract class EmailContent {
*/
public static final String SUPPRESS_COMBINED_ACCOUNT_PARAM = "suppress_combined";
public static Uri CONTENT_NOTIFIER_URI;
+ public static Uri CONTENT_SYNC_SETTING_CHANGED_URI;
public static Uri PICK_TRASH_FOLDER_URI;
public static Uri PICK_SENT_FOLDER_URI;
public static Uri MAILBOX_NOTIFICATION_URI;
@@ -175,8 +178,11 @@ public abstract class EmailContent {
AUTHORITY = EMAIL_PACKAGE_NAME + ".provider";
LogUtils.d("EmailContent", "init for " + AUTHORITY);
NOTIFIER_AUTHORITY = EMAIL_PACKAGE_NAME + ".notifier";
+ SYNC_SETTING_CHANGED_AUTHORITY = EMAIL_PACKAGE_NAME + ".sync_setting_changed";
CONTENT_URI = Uri.parse("content://" + AUTHORITY);
CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY);
+ CONTENT_SYNC_SETTING_CHANGED_URI = Uri.parse(
+ "content://" + SYNC_SETTING_CHANGED_AUTHORITY);
PICK_TRASH_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickTrashFolder");
PICK_SENT_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickSentFolder");
MAILBOX_NOTIFICATION_URI = Uri.parse("content://" + AUTHORITY + "/mailboxNotification");
@@ -1724,6 +1730,8 @@ public abstract class EmailContent {
public static final String PING_DURATION = "pingDuration";
// Automatically fetch pop3 attachments
public static final String AUTO_FETCH_ATTACHMENTS = "autoFetchAttachments";
+ // Account capabilities (check EmailServiceProxy#CAPABILITY_*)
+ public static final String CAPABILITIES = "capabilities";
}
public interface QuickResponseColumns extends BaseColumns {
diff --git a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java
index c726b94c3..75f840e64 100644
--- a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java
+++ b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java
@@ -78,10 +78,13 @@ public class Mailbox extends EmailContent implements EmailContent.MailboxColumns
public static Uri CONTENT_URI;
public static Uri MESSAGE_COUNT_URI;
+ public static Uri SYNC_SETTING_CHANGED_URI;
public static void initMailbox() {
CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailbox");
MESSAGE_COUNT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailboxCount");
+ SYNC_SETTING_CHANGED_URI = Uri.parse(
+ EmailContent.CONTENT_SYNC_SETTING_CHANGED_URI + "/mailbox");
}
private static String formatMailboxIdExtra(final int index) {
diff --git a/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java
index 1bbec7867..0c4c8e2a3 100644
--- a/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java
+++ b/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java
@@ -68,6 +68,12 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService {
public static final String VALIDATE_BUNDLE_PROTOCOL_VERSION = "validate_protocol_version";
public static final String VALIDATE_BUNDLE_REDIRECT_ADDRESS = "validate_redirect_address";
+ // Service capabilities
+ public static final String SETTINGS_BUNDLE_CAPABILITIES = "settings_capabilities";
+
+ // List of common interesting services capabilities
+ public static final int CAPABILITY_PUSH = 1 << 0;
+
private Object mReturn = null;
private IEmailService mService;
private final boolean isRemote;
diff --git a/provider_src/com/android/email/EmailConnectivityManager.java b/provider_src/com/android/email/EmailConnectivityManager.java
index 90a511f06..be930c910 100644
--- a/provider_src/com/android/email/EmailConnectivityManager.java
+++ b/provider_src/com/android/email/EmailConnectivityManager.java
@@ -165,6 +165,13 @@ public class EmailConnectivityManager extends BroadcastReceiver {
return info.getType();
}
+ static public boolean isConnected(Context context) {
+ ConnectivityManager cm =
+ (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ return info != null && info.isConnected();
+ }
+
public void waitForConnectivity() {
// If we're unregistered, throw an exception
if (!mRegistered) {
diff --git a/provider_src/com/android/email/activity/setup/AccountSettingsUtils.java b/provider_src/com/android/email/activity/setup/AccountSettingsUtils.java
index dbbd51ee7..cf677ddc0 100644
--- a/provider_src/com/android/email/activity/setup/AccountSettingsUtils.java
+++ b/provider_src/com/android/email/activity/setup/AccountSettingsUtils.java
@@ -107,7 +107,7 @@ public class AccountSettingsUtils {
cv.put(AccountColumns.DISPLAY_NAME, account.getDisplayName());
cv.put(AccountColumns.SENDER_NAME, account.getSenderName());
cv.put(AccountColumns.SIGNATURE, account.getSignature());
- cv.put(AccountColumns.SYNC_INTERVAL, account.mSyncInterval);
+ cv.put(AccountColumns.SYNC_INTERVAL, account.getSyncInterval());
cv.put(AccountColumns.FLAGS, account.mFlags);
cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback);
cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey);
diff --git a/provider_src/com/android/email/mail/store/ImapConnection.java b/provider_src/com/android/email/mail/store/ImapConnection.java
index bf4bb2a4c..bef5f346e 100644
--- a/provider_src/com/android/email/mail/store/ImapConnection.java
+++ b/provider_src/com/android/email/mail/store/ImapConnection.java
@@ -36,6 +36,7 @@ import com.android.emailcommon.mail.MessagingException;
import com.android.mail.utils.LogUtils;
import java.io.IOException;
+import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -50,6 +51,15 @@ class ImapConnection {
// Always check in FALSE
private static final boolean DEBUG_FORCE_SEND_ID = false;
+ // RFC 2177 defines that IDLE connections must be refreshed at least every 29 minutes
+ public static final int PING_IDLE_TIMEOUT = 29 * 60 * 1000;
+
+ // Special timeout for DONE operations
+ public static final int DONE_TIMEOUT = 5 * 1000;
+
+ // Time to wait between the first idle message and triggering the changes
+ private static final int IDLE_OP_READ_TIMEOUT = 500;
+
/** ID capability per RFC 2971*/
public static final int CAPABILITY_ID = 1 << 0;
/** NAMESPACE capability per RFC 2342 */
@@ -58,6 +68,8 @@ class ImapConnection {
public static final int CAPABILITY_STARTTLS = 1 << 2;
/** UIDPLUS capability per RFC 4315 */
public static final int CAPABILITY_UIDPLUS = 1 << 3;
+ /** IDLE capability per RFC 2177 */
+ public static final int CAPABILITY_IDLE = 1 << 4;
/** The capabilities supported; a set of CAPABILITY_* values. */
private int mCapabilities;
@@ -69,6 +81,8 @@ class ImapConnection {
private String mAccessToken;
private String mIdPhrase = null;
+ private boolean mIdling = false;
+
/** # of command/response lines to log upon crash. */
private static final int DISCOURSE_LOGGER_SIZE = 64;
private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
@@ -210,10 +224,23 @@ class ImapConnection {
mImapStore = null;
}
+ int getReadTimeout() throws IOException {
+ if (mTransport == null) {
+ return MailTransport.SOCKET_READ_TIMEOUT;
+ }
+ return mTransport.getReadTimeout();
+ }
+
+ void setReadTimeout(int timeout) throws IOException {
+ if (mTransport != null) {
+ mTransport.setReadTimeout(timeout);
+ }
+ }
+
/**
* Returns whether or not the specified capability is supported by the server.
*/
- private boolean isCapable(int capability) {
+ public boolean isCapable(int capability) {
return (mCapabilities & capability) != 0;
}
@@ -235,6 +262,9 @@ class ImapConnection {
if (capabilities.contains(ImapConstants.STARTTLS)) {
mCapabilities |= CAPABILITY_STARTTLS;
}
+ if (capabilities.contains(ImapConstants.IDLE)) {
+ mCapabilities |= CAPABILITY_IDLE;
+ }
}
/**
@@ -273,6 +303,12 @@ class ImapConnection {
*/
String sendCommand(String command, boolean sensitive)
throws MessagingException, IOException {
+ // Don't allow any command other than DONE when idling
+ if (mIdling && !command.equals(ImapConstants.DONE)) {
+ return null;
+ }
+ mIdling = command.equals(ImapConstants.IDLE);
+
LogUtils.d(Logging.LOG_TAG, "sendCommand %s", (sensitive ? IMAP_REDACTED_LOG : command));
open();
return sendCommandInternal(command, sensitive);
@@ -284,7 +320,13 @@ class ImapConnection {
throw new IOException("Null transport");
}
String tag = Integer.toString(mNextCommandTag.incrementAndGet());
- String commandToSend = tag + " " + command;
+ final String commandToSend;
+ if (command.equals(ImapConstants.DONE)) {
+ // Do not send a tag for DONE command
+ commandToSend = command;
+ } else {
+ commandToSend = tag + " " + command;
+ }
mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
return tag;
@@ -327,6 +369,11 @@ class ImapConnection {
return executeSimpleCommand(command, false);
}
+ List<ImapResponse> executeIdleCommand() throws IOException, MessagingException {
+ mParser.expectIdlingResponse();
+ return executeSimpleCommand(ImapConstants.IDLE, false);
+ }
+
/**
* Read and return all of the responses from the most recent command sent to the server
*
@@ -336,13 +383,35 @@ class ImapConnection {
*/
List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
final List<ImapResponse> responses = new ArrayList<ImapResponse>();
- ImapResponse response;
- do {
- response = mParser.readResponse();
- responses.add(response);
- } while (!response.isTagged());
+ ImapResponse response = null;
+ boolean idling = false;
+ boolean throwSocketTimeoutEx = true;
+ int lastSocketTimeout = getReadTimeout();
+ try {
+ do {
+ response = mParser.readResponse();
+ if (idling) {
+ setReadTimeout(IDLE_OP_READ_TIMEOUT);
+ throwSocketTimeoutEx = false;
+ }
+ responses.add(response);
+ if (response.isIdling()) {
+ idling = true;
+ }
+ } while (idling || !response.isTagged());
+ } catch (SocketTimeoutException ex) {
+ if (throwSocketTimeoutEx) {
+ throw ex;
+ }
+ } finally {
+ mParser.resetIdlingStatus();
+ if (lastSocketTimeout != getReadTimeout()) {
+ setReadTimeout(lastSocketTimeout);
+ }
+ }
- if (!response.isOk()) {
+ // When idling, any response is valid; otherwise it must be OK
+ if (!response.isOk() && !idling) {
final String toString = response.toString();
final String status = response.getStatusOrEmpty().getString();
final String alert = response.getAlertTextOrEmpty().getString();
diff --git a/provider_src/com/android/email/mail/store/ImapFolder.java b/provider_src/com/android/email/mail/store/ImapFolder.java
index 2eefdfec3..eb0535d2a 100644
--- a/provider_src/com/android/email/mail/store/ImapFolder.java
+++ b/provider_src/com/android/email/mail/store/ImapFolder.java
@@ -52,6 +52,8 @@ import com.android.emailcommon.utility.Utility;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
+import static com.android.emailcommon.Logging.LOG_TAG;
+
import org.apache.commons.io.IOUtils;
import java.io.File;
@@ -60,6 +62,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.SocketTimeoutException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -68,13 +71,39 @@ import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.TimeZone;
-class ImapFolder extends Folder {
+public class ImapFolder extends Folder {
private final static Flag[] PERMANENT_FLAGS =
{ Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
private static final int COPY_BUFFER_SIZE = 16*1024;
+ public interface IdleCallback {
+ /**
+ * Invoked when the connection enters idle mode
+ */
+ public void onIdled();
+ /**
+ * Invoked when a new change is communicated by the server.
+ *
+ * @param needSync whether a sync is required
+ * @param fetchMessages list of message UIDs to update
+ */
+ public void onNewServerChange(boolean needSync, List<String> fetchMessages);
+ /**
+ * Connection to socket timed out. The idle connection needs
+ * to be considered broken when this is called.
+ */
+ public void onTimeout();
+ /**
+ * Something went wrong while waiting for push data.
+ *
+ * @param ex the exception detected
+ */
+ public void onException(MessagingException ex);
+ }
+
private final ImapStore mStore;
private final String mName;
private int mMessageCount = -1;
@@ -86,6 +115,22 @@ class ImapFolder extends Folder {
/** A set of hashes that can be used to track dirtiness */
Object mHash[];
+ private final Object mIdleSync = new Object();
+ private boolean mIdling;
+ private boolean mIdlingCancelled;
+ private boolean mDiscardIdlingConnection;
+ private Thread mIdleReader;
+
+ private static final String[] IDLE_STATUSES = {
+ ImapConstants.UIDVALIDITY, ImapConstants.UIDNEXT
+ };
+ private Map<String, String> mIdleStatuses = new HashMap<>();
+
+ private static class ImapIdleChanges {
+ public boolean mRequiredSync = false;
+ public ArrayList<String> mMessageToFetch = new ArrayList<>();
+ }
+
/*package*/ ImapFolder(ImapStore store, String name) {
mStore = store;
mName = name;
@@ -176,6 +221,159 @@ class ImapFolder extends Folder {
return mName;
}
+ public void startIdling(final IdleCallback callback) throws MessagingException {
+ checkOpen();
+ synchronized (mIdleSync) {
+ if (mIdling) {
+ throw new MessagingException("Folder " + mName + " is in IDLE state already.");
+ }
+ mIdling = true;
+ mIdlingCancelled = false;
+ mDiscardIdlingConnection = false;
+ }
+
+ // Run idle in background
+ mIdleReader = new Thread() {
+ @Override
+ public void run() {
+ try {
+ // Get some info before start idling
+ mIdleStatuses = getStatuses(IDLE_STATUSES);
+
+ // We setup the max time specified in RFC 2177 to re-issue
+ // an idle request to the server
+ mConnection.setReadTimeout(ImapConnection.PING_IDLE_TIMEOUT);
+ mConnection.destroyResponses();
+
+ // Enter now in idle status (we hold a connection with
+ // the server to listen for new changes)
+ synchronized (mIdleSync) {
+ if (mIdlingCancelled) {
+ return;
+ }
+ }
+
+ if (callback != null) {
+ callback.onIdled();
+ }
+ List<ImapResponse> responses = mConnection.executeIdleCommand();
+
+ // Check whether IDLE was successful (first response is an idling response)
+ if (responses.isEmpty() || (mIdling && !responses.get(0).isIdling())) {
+ if (callback != null) {
+ callback.onException(new MessagingException(
+ MessagingException.SERVER_ERROR, "Cannot idle"));
+ }
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+ return;
+ }
+
+ // Exit idle if we are still in that state
+ boolean cancelled = false;
+ boolean discardConnection = false;
+ synchronized (mIdleSync) {
+ if (!mIdlingCancelled) {
+ try {
+ mConnection.setReadTimeout(ImapConnection.DONE_TIMEOUT);
+ mConnection.executeSimpleCommand(ImapConstants.DONE);
+ } catch (MessagingException me) {
+ // Ignore this exception caused by messages in the queue
+ }
+ }
+
+ cancelled = mIdlingCancelled;
+ discardConnection = mDiscardIdlingConnection;
+ }
+
+ if (!cancelled && callback != null) {
+ // Notify that new changes exists in the server. Remove
+ // the idling status response since is only relevant for the protocol
+ // We have to enter in idle
+ ImapIdleChanges changes = extractImapChanges(
+ new ArrayList<Object>(responses.subList(1, responses.size())));
+ callback.onNewServerChange(changes.mRequiredSync, changes.mMessageToFetch);
+ }
+
+ if (discardConnection) {
+ // Return the connection to the pool
+ close(false);
+ }
+
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+
+ } catch (MessagingException me) {
+ close(false);
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+ if (callback != null) {
+ callback.onException(me);
+ }
+
+ } catch (SocketTimeoutException ste) {
+ close(false);
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+ if (callback != null) {
+ callback.onTimeout();
+ }
+
+ } catch (IOException ioe) {
+ close(false);
+ synchronized (mIdleSync) {
+ mIdling = false;
+ }
+ if (callback != null) {
+ callback.onException(ioExceptionHandler(mConnection, ioe));
+ }
+
+ }
+ }
+ };
+ mIdleReader.setName("IdleReader " + mStore.getAccount().mId + ":" + mName);
+ mIdleReader.start();
+ }
+
+ public void stopIdling(boolean discardConnection) throws MessagingException {
+ if (!isOpen()) {
+ throw new MessagingException("Folder " + mName + " is not open.");
+ }
+ synchronized (mIdleSync) {
+ if (!mIdling) {
+ throw new MessagingException("Folder " + mName + " isn't in IDLE state.");
+ }
+ try {
+ mIdlingCancelled = true;
+ mDiscardIdlingConnection = discardConnection;
+ // We can read responses here because we can block the buffer. Read commands
+ // are always done by startListener method (blocking idle)
+ mConnection.sendCommand(ImapConstants.DONE, false);
+
+ } catch (MessagingException me) {
+ // Treat IOERROR messaging exception as IOException
+ if (me.getExceptionType() == MessagingException.IOERROR) {
+ close(false);
+ throw me;
+ }
+
+ } catch (IOException ioe) {
+ throw ioExceptionHandler(mConnection, ioe);
+
+ }
+ }
+ }
+
+ public boolean isIdling() {
+ synchronized (mIdleSync) {
+ return mIdling;
+ }
+ }
+
@Override
public boolean exists() throws MessagingException {
if (mExists) {
@@ -373,6 +571,58 @@ class ImapFolder extends Folder {
}
}
+ public Map<String, String> getStatuses(String[] statuses) throws MessagingException {
+ checkOpen();
+ Map<String, String> allReturnStatuses = new HashMap<>();
+ try {
+ String flags = TextUtils.join(" ", statuses);
+ final List<ImapResponse> responses = mConnection.executeSimpleCommand(
+ String.format(Locale.US,
+ ImapConstants.STATUS + " \"%s\" (%s)",
+ ImapStore.encodeFolderName(mName, mStore.mPathPrefix), flags));
+ // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292)
+ for (ImapResponse response : responses) {
+ if (response.isDataResponse(0, ImapConstants.STATUS)) {
+ ImapList list = response.getListOrEmpty(2);
+ int count = list.size();
+ for (int i = 0; i < count; i += 2) {
+ String key = list.getStringOrEmpty(i).getString();
+ String value = list.getStringOrEmpty(i + 1).getString();
+ allReturnStatuses.put(key, value);
+ }
+ }
+ }
+ } catch (IOException ioe) {
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ return allReturnStatuses;
+ }
+
+ private List<String> getNewMessagesFromUid(String uid) throws MessagingException {
+ checkOpen();
+ List<String> nextMSNs = new ArrayList<>();
+ try {
+ final List<ImapResponse> responses = mConnection.executeSimpleCommand(
+ ImapConstants.SEARCH + " " + ImapConstants.UID + " " + uid + ":*");
+ // S: * SEARCH 1 2 3
+ for (ImapResponse response : responses) {
+ if (response.isDataResponse(0, ImapConstants.SEARCH)) {
+ int count = response.size();
+ for (int i = 1; i < count; i++) {
+ nextMSNs.add(response.getStringOrEmpty(i).getString());
+ }
+ }
+ }
+ } catch (IOException ioe) {
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ return nextMSNs;
+ }
+
@Override
public void delete(boolean recurse) {
throw new Error("ImapStore.delete() not yet implemented");
@@ -1270,7 +1520,9 @@ class ImapFolder extends Folder {
if (DebugUtils.DEBUG) {
LogUtils.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
}
- connection.close();
+ if (connection != null) {
+ connection.close();
+ }
if (connection == mConnection) {
mConnection = null; // To prevent close() from returning the connection to the pool.
close(false);
@@ -1278,6 +1530,127 @@ class ImapFolder extends Folder {
return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
}
+ private ImapIdleChanges extractImapChanges(List<Object> changes) throws MessagingException {
+ // Process the changes and fill the idle changes structure.
+ // Basically we should look for the next commands in this method:
+ //
+ // OK DONE
+ // No more changes
+ // n EXISTS
+ // Indicates that the mailbox changed => ignore
+ // n EXPUNGE
+ // Indicates a message were completely deleted => a full sync is required
+ // n RECENT
+ // New messages waiting in the server => use UIDNEXT to search for the new messages.
+ // If isn't possible to retrieve the new UID messages, then a full sync is required
+ // n FETCH (UID X FLAGS (...))
+ // a message has changed and requires to fetch only X message
+ // (something change on that item). If UID is not present, a conversion
+ // from MSN to UID is required
+
+ final ImapIdleChanges imapIdleChanges = new ImapIdleChanges();
+
+ int count = changes.size();
+ if (Logging.LOGD) {
+ for (int i = 0; i < count; i++) {
+ ImapResponse change = (ImapResponse) changes.get(i);
+ if (Logging.LOGD) {
+ LogUtils.d(Logging.LOG_TAG, "Received: " + change.toString());
+ }
+ }
+ }
+
+ // We can't ask to the server, because the responses will be destroyed. We need
+ // to compute and fetch any related after we have all the responses processed
+ boolean hasNewMessages = false;
+ List<String> msns = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ ImapResponse change = (ImapResponse) changes.get(i);
+ if (change.isOk() || change.isNo() || change.isBad()) {
+ // No more processing. DONE included
+ break;
+ }
+ try {
+ ImapElement element = change.getElementOrNone(1);
+ if (element.equals(ImapElement.NONE)) {
+ continue;
+ }
+ if (!element.isString()) {
+ continue;
+ }
+
+ ImapString op = (ImapString) element;
+ if (op.is(ImapConstants.DONE)) {
+ break;
+ } else if (op.is(ImapConstants.EXISTS)) {
+ continue;
+ } else if (op.is(ImapConstants.EXPUNGE)) {
+ imapIdleChanges.mRequiredSync = true;
+ } else if (op.is(ImapConstants.RECENT)) {
+ hasNewMessages = true;
+ } else if (op.is(ImapConstants.FETCH)
+ && change.getElementOrNone(2).isList()) {
+ ImapList messageFlags = (ImapList) change.getElementOrNone(2);
+ String uid = ((ImapString) messageFlags.getKeyedStringOrEmpty(
+ ImapConstants.UID, true)).getString();
+ if (!TextUtils.isEmpty(uid) &&
+ !imapIdleChanges.mMessageToFetch.contains(uid)) {
+ imapIdleChanges.mMessageToFetch.add(uid);
+ } else {
+ msns.add(change.getStringOrEmpty(0).getString());
+ }
+ } else {
+ if (Logging.LOGD) {
+ LogUtils.w(LOG_TAG, "Unrecognized imap change (" + change
+ + ") for mailbox " + mName);
+ }
+ }
+
+ } catch (Exception ex) {
+ if (Logging.LOGD) {
+ LogUtils.e(LOG_TAG, "Failure processing imap change (" + change
+ + ") for mailbox " + mName, ex);
+ }
+ }
+ }
+
+ // Check whether UIDVALIDITY changed - if yes, a full sync request is required
+ // NOTE: This needs to happen after parsing all responses; otherwise
+ // getStatuses will destroy the response
+ Map<String, String> statuses = getStatuses(new String[] { ImapConstants.UIDVALIDITY });
+ String oldUidValidity = mIdleStatuses.get(ImapConstants.UIDVALIDITY);
+ String newUidValidity = statuses.get(ImapConstants.UIDVALIDITY);
+ if (!TextUtils.equals(oldUidValidity, newUidValidity)) {
+ imapIdleChanges.mMessageToFetch.clear();
+ imapIdleChanges.mRequiredSync = true;
+ return imapIdleChanges;
+ }
+
+ // Recover the UIDs of new messages in case we don't do a full sync anyway
+ if (!imapIdleChanges.mRequiredSync) {
+ try {
+ // Retrieve new message UIDs
+ String uidNext = mIdleStatuses.get(ImapConstants.UIDNEXT);
+ if (hasNewMessages && !TextUtils.isEmpty(uidNext)) {
+ msns.addAll(getNewMessagesFromUid(uidNext));
+ }
+
+ // Transform MSNs to UIDs
+ for (String msn : msns) {
+ String[] uids = searchForUids(String.format(Locale.US, "%s:%s", msn, msn));
+ imapIdleChanges.mMessageToFetch.add(uids[0]);
+ }
+ } catch (MessagingException ex) {
+ // Server doesn't support UID. We have to do a full sync (since
+ // we don't know what message changed)
+ imapIdleChanges.mMessageToFetch.clear();
+ imapIdleChanges.mRequiredSync = true;
+ }
+ }
+
+ return imapIdleChanges;
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof ImapFolder) {
diff --git a/provider_src/com/android/email/mail/store/ImapStore.java b/provider_src/com/android/email/mail/store/ImapStore.java
index 5fc83e001..ddabe1c4a 100644
--- a/provider_src/com/android/email/mail/store/ImapStore.java
+++ b/provider_src/com/android/email/mail/store/ImapStore.java
@@ -501,6 +501,14 @@ public class ImapStore extends Store {
connection.destroyResponses();
}
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
+
+ // Shared capabilities (check EmailProxyServices for available shared capabilities)
+ int capabilities = 0;
+ if (connection.isCapable(ImapConnection.CAPABILITY_IDLE)) {
+ capabilities |= EmailServiceProxy.CAPABILITY_PUSH;
+ }
+ bundle.putInt(EmailServiceProxy.SETTINGS_BUNDLE_CAPABILITIES, capabilities);
+
return bundle;
}
@@ -556,6 +564,7 @@ public class ImapStore extends Store {
while ((connection = mConnectionPool.poll()) != null) {
try {
connection.setStore(this);
+ connection.setReadTimeout(MailTransport.SOCKET_READ_TIMEOUT);
connection.executeSimpleCommand(ImapConstants.NOOP);
break;
} catch (MessagingException e) {
diff --git a/provider_src/com/android/email/mail/store/Pop3Store.java b/provider_src/com/android/email/mail/store/Pop3Store.java
index 4ea75ccf3..b0aa9a2eb 100644
--- a/provider_src/com/android/email/mail/store/Pop3Store.java
+++ b/provider_src/com/android/email/mail/store/Pop3Store.java
@@ -186,6 +186,10 @@ public class Pop3Store extends Store {
ioe.getMessage());
}
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
+
+ // No special capabilities
+ bundle.putInt(EmailServiceProxy.SETTINGS_BUNDLE_CAPABILITIES, 0);
+
return bundle;
}
diff --git a/provider_src/com/android/email/mail/store/imap/ImapConstants.java b/provider_src/com/android/email/mail/store/imap/ImapConstants.java
index 9f4d59290..9c94fcf31 100644
--- a/provider_src/com/android/email/mail/store/imap/ImapConstants.java
+++ b/provider_src/com/android/email/mail/store/imap/ImapConstants.java
@@ -46,6 +46,7 @@ public final class ImapConstants {
public static final String COPYUID = "COPYUID";
public static final String CREATE = "CREATE";
public static final String DELETE = "DELETE";
+ public static final String DONE = "DONE";
public static final String EXAMINE = "EXAMINE";
public static final String EXISTS = "EXISTS";
public static final String EXPUNGE = "EXPUNGE";
@@ -58,6 +59,8 @@ public final class ImapConstants {
public static final String FLAGS = "FLAGS";
public static final String FLAGS_SILENT = "FLAGS.SILENT";
public static final String ID = "ID";
+ public static final String IDLE = "IDLE";
+ public static final String IDLING = "idling";
public static final String INBOX = "INBOX";
public static final String INTERNALDATE = "INTERNALDATE";
public static final String LIST = "LIST";
@@ -73,6 +76,7 @@ public final class ImapConstants {
public static final String PREAUTH = "PREAUTH";
public static final String READ_ONLY = "READ-ONLY";
public static final String READ_WRITE = "READ-WRITE";
+ public static final String RECENT = "RECENT";
public static final String RENAME = "RENAME";
public static final String RFC822_SIZE = "RFC822.SIZE";
public static final String SEARCH = "SEARCH";
diff --git a/provider_src/com/android/email/mail/store/imap/ImapList.java b/provider_src/com/android/email/mail/store/imap/ImapList.java
index e28355989..2ddf8227f 100644
--- a/provider_src/com/android/email/mail/store/imap/ImapList.java
+++ b/provider_src/com/android/email/mail/store/imap/ImapList.java
@@ -180,7 +180,7 @@ public class ImapList extends ImapElement {
@Override
public String toString() {
- return mList.toString();
+ return mList != null ? mList.toString() : "[null]";
}
/**
diff --git a/provider_src/com/android/email/mail/store/imap/ImapResponse.java b/provider_src/com/android/email/mail/store/imap/ImapResponse.java
index 9f975f7bf..292ff92b2 100644
--- a/provider_src/com/android/email/mail/store/imap/ImapResponse.java
+++ b/provider_src/com/android/email/mail/store/imap/ImapResponse.java
@@ -77,6 +77,13 @@ public class ImapResponse extends ImapList {
}
/**
+ * @return whether it's an IDLE response.
+ */
+ public boolean isIdling() {
+ return is(0, ImapConstants.IDLING);
+ }
+
+ /**
* @return whether it's an {@code responseType} data response. (i.e. not tagged).
* @param index where {@code responseType} should appear. e.g. 1 for "FETCH"
* @param responseType e.g. "FETCH"
diff --git a/provider_src/com/android/email/mail/store/imap/ImapResponseParser.java b/provider_src/com/android/email/mail/store/imap/ImapResponseParser.java
index 8dd1cf610..5efea3109 100644
--- a/provider_src/com/android/email/mail/store/imap/ImapResponseParser.java
+++ b/provider_src/com/android/email/mail/store/imap/ImapResponseParser.java
@@ -66,6 +66,9 @@ public class ImapResponseParser {
*/
private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
+ private boolean mIdling;
+ private boolean mExpectIdlingResponse;
+
/**
* Exception thrown when we receive BYE. It derives from IOException, so it'll be treated
* in the same way EOF does.
@@ -168,10 +171,17 @@ public class ImapResponseParser {
} catch (RuntimeException e) {
// Parser crash -- log network activities.
onParseError(e);
+ mIdling = false;
throw e;
} catch (IOException e) {
// Network error, or received an unexpected char.
- onParseError(e);
+ // If we are idling don't parse the error, just let the upper layers
+ // handle the exception
+ if (!mIdling) {
+ onParseError(e);
+ } else {
+ mIdling = false;
+ }
throw e;
}
@@ -242,6 +252,14 @@ public class ImapResponseParser {
return ret;
}
+ public void resetIdlingStatus() {
+ mIdling = false;
+ }
+
+ public void expectIdlingResponse() {
+ mExpectIdlingResponse = true;
+ }
+
/**
* Parse and return the response line.
*/
@@ -263,11 +281,26 @@ public class ImapResponseParser {
responseToDestroy = new ImapResponse(null, true);
// If it's continuation request, we don't really care what's in it.
- responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+ // NOTE: specs say the server is supposed to respond to the IDLE command
+ // with a continuation request response. To simplify internal handling,
+ // we'll always construct same response (ignoring the server text response).
+ // Our implementation always returns "+ idling".
+ if (mExpectIdlingResponse) {
+ // Discard the server message and put what we expected
+ readUntilEol();
+ responseToDestroy.add(new ImapSimpleString(ImapConstants.IDLING));
+ } else {
+ responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+ }
// Response has successfully been built. Let's return it.
responseToReturn = responseToDestroy;
responseToDestroy = null;
+
+ mIdling = responseToReturn.isIdling();
+ if (mIdling) {
+ mExpectIdlingResponse = true;
+ }
} else {
// Status response or response data
final String tag;
diff --git a/provider_src/com/android/email/mail/transport/MailTransport.java b/provider_src/com/android/email/mail/transport/MailTransport.java
index 26801f93f..1767f19a7 100644
--- a/provider_src/com/android/email/mail/transport/MailTransport.java
+++ b/provider_src/com/android/email/mail/transport/MailTransport.java
@@ -191,6 +191,14 @@ public class MailTransport {
}
}
+ public int getReadTimeout() throws IOException {
+ return mSocket.getSoTimeout();
+ }
+
+ public void setReadTimeout(int timeout) throws IOException {
+ mSocket.setSoTimeout(timeout);
+ }
+
/**
* Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
* service but is not in the public API.
diff --git a/provider_src/com/android/email/provider/DBHelper.java b/provider_src/com/android/email/provider/DBHelper.java
index 18a4548fe..fa70e6f4f 100644
--- a/provider_src/com/android/email/provider/DBHelper.java
+++ b/provider_src/com/android/email/provider/DBHelper.java
@@ -58,6 +58,7 @@ import com.android.emailcommon.provider.MessageStateChange;
import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.provider.QuickResponse;
import com.android.emailcommon.provider.SuggestedContact;
+import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.LegacyPolicySet;
import com.android.emailcommon.service.SyncWindow;
import com.android.mail.providers.UIProvider;
@@ -187,7 +188,8 @@ public final class DBHelper {
// Version 127: Force mFlags to contain the correct flags for EAS accounts given a protocol
// version above 12.0
// Version 129: Update all IMAP INBOX mailboxes to force synchronization
- public static final int DATABASE_VERSION = 129;
+ // Version 130: Account capabilities (check EmailServiceProxy#CAPABILITY_*)
+ public static final int DATABASE_VERSION = 130;
// Any changes to the database format *must* include update-in-place code.
// Original version: 2
@@ -525,7 +527,8 @@ public final class DBHelper {
+ AccountColumns.POLICY_KEY + " integer, "
+ AccountColumns.MAX_ATTACHMENT_SIZE + " integer, "
+ AccountColumns.PING_DURATION + " integer, "
- + AccountColumns.AUTO_FETCH_ATTACHMENTS + " integer"
+ + AccountColumns.AUTO_FETCH_ATTACHMENTS + " integer, "
+ + AccountColumns.CAPABILITIES + " integer default 0"
+ ");";
db.execSQL("create table " + Account.TABLE_NAME + s);
// Deleting an account deletes associated Mailboxes and HostAuth's
@@ -1562,6 +1565,52 @@ public final class DBHelper {
+ HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + "='imap'));");
}
+ if (oldVersion <= 130) {
+ //Account capabilities (check EmailServiceProxy#CAPABILITY_*)
+ try {
+ // Create capabilities field
+ db.execSQL("alter table " + Account.TABLE_NAME
+ + " add column " + AccountColumns.CAPABILITIES
+ + " integer" + " default 0;");
+
+ // Update all accounts with the appropriate capabilities
+ Cursor c = db.rawQuery("select " + Account.TABLE_NAME + "."
+ + AccountColumns._ID + ", " + HostAuth.TABLE_NAME + "."
+ + HostAuthColumns.PROTOCOL + " from " + Account.TABLE_NAME + ", "
+ + HostAuth.TABLE_NAME + " where " + Account.TABLE_NAME + "."
+ + AccountColumns.HOST_AUTH_KEY_RECV + " = " + HostAuth.TABLE_NAME
+ + "." + HostAuthColumns._ID + ";", null);
+ if (c != null) {
+ try {
+ while(c.moveToNext()) {
+ long id = c.getLong(c.getColumnIndexOrThrow(AccountColumns._ID));
+ String protocol = c.getString(c.getColumnIndexOrThrow(
+ HostAuthColumns.PROTOCOL));
+
+ int capabilities = 0;
+ if (protocol.equals(LEGACY_SCHEME_IMAP)
+ || protocol.equals(LEGACY_SCHEME_EAS)) {
+ // Don't know yet if the imap server supports the IDLE
+ // capability, but since this is upgrading the account,
+ // just assume that all imap servers supports the push
+ // capability and let disable it by the IMAP service
+ capabilities |= EmailServiceProxy.CAPABILITY_PUSH;
+ }
+ final ContentValues cv = new ContentValues(1);
+ cv.put(AccountColumns.CAPABILITIES, capabilities);
+ db.update(Account.TABLE_NAME, cv, AccountColumns._ID + " = ?",
+ new String[]{String.valueOf(id)});
+ }
+ } finally {
+ c.close();
+ }
+ }
+ } catch (final SQLException e) {
+ // Shouldn't be needed unless we're debugging and interrupt the process
+ LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v129 to v130", e);
+ }
+ }
+
// Due to a bug in commit 44a064e5f16ddaac25f2acfc03c118f65bc48aec,
// AUTO_FETCH_ATTACHMENTS column could not be available in the Account table.
// Since cm12 and up doesn't use this column, we are leave as is it. In case
diff --git a/provider_src/com/android/email/provider/EmailProvider.java b/provider_src/com/android/email/provider/EmailProvider.java
index 338c9fc4b..8e4e7b6a1 100644
--- a/provider_src/com/android/email/provider/EmailProvider.java
+++ b/provider_src/com/android/email/provider/EmailProvider.java
@@ -189,11 +189,11 @@ public class EmailProvider extends ContentProvider
"vnd.android.cursor.item/email-attachment";
/** Appended to the notification URI for delete operations */
- private static final String NOTIFICATION_OP_DELETE = "delete";
+ public static final String NOTIFICATION_OP_DELETE = "delete";
/** Appended to the notification URI for insert operations */
- private static final String NOTIFICATION_OP_INSERT = "insert";
+ public static final String NOTIFICATION_OP_INSERT = "insert";
/** Appended to the notification URI for update operations */
- private static final String NOTIFICATION_OP_UPDATE = "update";
+ public static final String NOTIFICATION_OP_UPDATE = "update";
/** The query string to trigger a folder refresh. */
protected static String QUERY_UIREFRESH = "uirefresh";
@@ -833,6 +833,7 @@ public class EmailProvider extends ContentProvider
// Notify all notifier cursors
sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id);
+ sendSyncSettingChanged(getBaseSyncSettingChangedUri(match), NOTIFICATION_OP_DELETE, id);
// Notify all email content cursors
notifyUI(EmailContent.CONTENT_URI, null);
@@ -1075,6 +1076,7 @@ public class EmailProvider extends ContentProvider
// Notify all notifier cursors
sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id);
+ sendSyncSettingChanged(getBaseSyncSettingChangedUri(match), NOTIFICATION_OP_INSERT, id);
// Notify all existing cursors.
notifyUI(EmailContent.CONTENT_URI, null);
@@ -1924,7 +1926,7 @@ public class EmailProvider extends ContentProvider
private static final int INDEX_SYNC_KEY = 2;
/**
- * Restart push if we need it (currently only for Exchange accounts).
+ * Restart push if we need it.
* @param context A {@link Context}.
* @param db The {@link SQLiteDatabase}.
* @param id The id of the thing we're looking for.
@@ -1937,9 +1939,13 @@ public class EmailProvider extends ContentProvider
try {
if (c.moveToFirst()) {
final String protocol = c.getString(INDEX_PROTOCOL);
- // Only restart push for EAS accounts that have completed initial sync.
- if (context.getString(R.string.protocol_eas).equals(protocol) &&
- !EmailContent.isInitialSyncKey(c.getString(INDEX_SYNC_KEY))) {
+ final String syncKey = c.getString(INDEX_SYNC_KEY);
+ final boolean supportsPush =
+ context.getString(R.string.protocol_eas).equals(protocol) ||
+ context.getString(R.string.protocol_legacy_imap).equals(protocol);
+
+ // Only restart push for EAS or IMAP accounts that have completed initial sync.
+ if (supportsPush && !EmailContent.isInitialSyncKey(syncKey)) {
final String emailAddress = c.getString(INDEX_EMAIL_ADDRESS);
final android.accounts.Account account =
getAccountManagerAccount(context, emailAddress, protocol);
@@ -2010,6 +2016,7 @@ public class EmailProvider extends ContentProvider
final SQLiteDatabase db = getDatabase(context);
final int table = match >> BASE_SHIFT;
int result;
+ boolean syncSettingChanged = false;
// We do NOT allow setting of unreadCount/messageCount via the provider
// These columns are maintained via triggers
@@ -2159,6 +2166,14 @@ public class EmailProvider extends ContentProvider
}
} else if (match == MESSAGE_ID) {
db.execSQL(UPDATED_MESSAGE_DELETE + id);
+ } else if (match == MAILBOX_ID) {
+ if (values.containsKey(MailboxColumns.SYNC_INTERVAL)) {
+ syncSettingChanged = true;
+ }
+ } else if (match == ACCOUNT_ID) {
+ if (values.containsKey(AccountColumns.SYNC_INTERVAL)) {
+ syncSettingChanged = true;
+ }
}
result = db.update(tableName, values, whereWithId(id, selection),
selectionArgs);
@@ -2293,6 +2308,10 @@ public class EmailProvider extends ContentProvider
TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
LogUtils.w(TAG, new Throwable(), "attachment with blank location");
}
+ } else if (match == MAILBOX) {
+ if (values.containsKey(MailboxColumns.SYNC_INTERVAL)) {
+ syncSettingChanged = true;
+ }
}
result = db.update(tableName, values, selection, selectionArgs);
break;
@@ -2314,6 +2333,10 @@ public class EmailProvider extends ContentProvider
// Notify all notifier cursors if some records where changed in the database
if (result > 0) {
sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id);
+ if (syncSettingChanged) {
+ sendSyncSettingChanged(getBaseSyncSettingChangedUri(match),
+ NOTIFICATION_OP_UPDATE, id);
+ }
notifyUI(notificationUri, null);
}
return result;
@@ -2544,6 +2567,21 @@ public class EmailProvider extends ContentProvider
return baseUri;
}
+ private static Uri getBaseSyncSettingChangedUri(int match) {
+ Uri baseUri = null;
+ switch (match) {
+ case ACCOUNT:
+ case ACCOUNT_ID:
+ baseUri = Account.SYNC_SETTING_CHANGED_URI;
+ break;
+ case MAILBOX:
+ case MAILBOX_ID:
+ baseUri = Mailbox.SYNC_SETTING_CHANGED_URI;
+ break;
+ }
+ return baseUri;
+ }
+
/**
* Sends a change notification to any cursors observers of the given base URI. The final
* notification URI is dynamically built to contain the specified information. It will be
@@ -2582,6 +2620,25 @@ public class EmailProvider extends ContentProvider
}
}
+ private void sendSyncSettingChanged(Uri baseUri, String op, String id) {
+ if (baseUri == null) return;
+
+ // Append the operation, if specified
+ if (op != null) {
+ baseUri = baseUri.buildUpon().appendEncodedPath(op).build();
+ }
+
+ long longId = 0L;
+ try {
+ longId = Long.valueOf(id);
+ } catch (NumberFormatException ignore) {}
+ if (longId > 0) {
+ notifyUI(baseUri, id);
+ } else {
+ notifyUI(baseUri, null);
+ }
+ }
+
private void sendMessageListDataChangedNotification() {
final Context context = getContext();
final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED);
diff --git a/provider_src/com/android/email/provider/Utilities.java b/provider_src/com/android/email/provider/Utilities.java
index c3b7ec93a..e28c873c4 100644
--- a/provider_src/com/android/email/provider/Utilities.java
+++ b/provider_src/com/android/email/provider/Utilities.java
@@ -40,6 +40,7 @@ import com.android.emailcommon.utility.ConversionUtilities;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
+import java.io.InputStream;
import java.io.IOException;
import java.util.ArrayList;
@@ -118,8 +119,9 @@ public class Utilities {
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
+ // Don't close the viewables attachment InputStream yet
final ConversionUtilities.BodyFieldData data =
- ConversionUtilities.parseBodyFields(viewables);
+ ConversionUtilities.parseBodyFields(viewables, false);
// set body and local message values
localMessage.setFlags(data.isQuotedReply, data.isQuotedForward);
@@ -166,6 +168,21 @@ public class Utilities {
localMessage.mFlagAttachment = true;
}
+ // Close any parts that may still be open
+ for (final Part part : viewables) {
+ if (part.getBody() == null) {
+ continue;
+ }
+ try {
+ InputStream is = part.getBody().getInputStream();
+ if (is != null) {
+ is.close();
+ }
+ } catch (IOException io) {
+ // Ignore
+ }
+ }
+
// One last update of message with two updated flags
localMessage.mFlagLoaded = loadStatus;
diff --git a/provider_src/com/android/email/service/EmailBroadcastProcessorService.java b/provider_src/com/android/email/service/EmailBroadcastProcessorService.java
index 7aa54673e..e91a49ea1 100644
--- a/provider_src/com/android/email/service/EmailBroadcastProcessorService.java
+++ b/provider_src/com/android/email/service/EmailBroadcastProcessorService.java
@@ -293,6 +293,10 @@ public class EmailBroadcastProcessorService extends IntentService {
private void onBootCompleted() {
performOneTimeInitialization();
reconcileAndStartServices();
+
+ // This is an special case to start IMAP PUSH via its adapter
+ Intent imap = new Intent(this, LegacyImapSyncAdapterService.class);
+ startService(imap);
}
private void reconcileAndStartServices() {
diff --git a/provider_src/com/android/email/service/EmailServiceStub.java b/provider_src/com/android/email/service/EmailServiceStub.java
index e4e757ba9..0a1264dd4 100755
--- a/provider_src/com/android/email/service/EmailServiceStub.java
+++ b/provider_src/com/android/email/service/EmailServiceStub.java
@@ -288,6 +288,12 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm
mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMainMailboxKey);
}
+ if (message.mServerId == null) {
+ cb.loadAttachmentStatus(messageId, attachmentId,
+ EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
+ return;
+ }
+
if (account == null || mailbox == null) {
// If the account/mailbox are gone, just report success; the UI handles this
cb.loadAttachmentStatus(messageId, attachmentId,
@@ -416,7 +422,6 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm
// actually occurs.
mailbox.mUiSyncStatus = Mailbox.SYNC_STATUS_INITIAL_SYNC_NEEDED;
}
- mailbox.save(mContext);
if (type == Mailbox.TYPE_INBOX) {
inboxId = mailbox.mId;
@@ -425,6 +430,7 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm
// should start marked
mailbox.mSyncInterval = 1;
}
+ mailbox.save(mContext);
}
}
diff --git a/provider_src/com/android/email/service/ImapService.java b/provider_src/com/android/email/service/ImapService.java
index 34ecfccc7..dcd123fea 100644
--- a/provider_src/com/android/email/service/ImapService.java
+++ b/provider_src/com/android/email/service/ImapService.java
@@ -16,28 +16,46 @@
package com.android.email.service;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.database.ContentObserver;
import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
import android.net.TrafficStats;
import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.RemoteException;
import android.os.SystemClock;
+import android.provider.BaseColumns;
import android.text.TextUtils;
import android.text.format.DateUtils;
+import android.util.SparseArray;
+import android.util.SparseLongArray;
import com.android.email.DebugUtils;
+import com.android.email.EmailConnectivityManager;
import com.android.email.LegacyConversions;
import com.android.email.NotificationController;
import com.android.email.NotificationControllerCreatorHolder;
import com.android.email.R;
import com.android.email.mail.Store;
+import com.android.email.mail.store.ImapFolder;
+import com.android.email.provider.EmailProvider;
import com.android.email.provider.Utilities;
import com.android.emailcommon.Logging;
+
+import static com.android.emailcommon.Logging.LOG_TAG;
+
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.internet.MimeUtility;
import com.android.emailcommon.mail.AuthenticationFailedException;
@@ -58,6 +76,7 @@ import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceStatus;
+import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.service.SyncWindow;
import com.android.emailcommon.utility.AttachmentUtilities;
@@ -70,12 +89,13 @@ import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
public class ImapService extends Service {
// TODO get these from configurations or settings.
private static final long QUICK_SYNC_WINDOW_MILLIS = DateUtils.DAY_IN_MILLIS;
private static final long FULL_SYNC_INTERVAL_MILLIS = 4 * DateUtils.HOUR_IN_MILLIS;
- private static final String TAG = "ImapService";
// The maximum number of messages to fetch in a single command.
private static final int MAX_MESSAGES_TO_FETCH = 500;
@@ -88,6 +108,10 @@ public class ImapService extends Service {
private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED };
private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED };
+ // Kick idle connection every 25 minutes
+ private static final int KICK_IDLE_CONNETION_TIMEOUT = 25 * 60 * 1000;
+ private static final int ALARM_REQUEST_KICK_IDLE_CODE = 1000;
+
/**
* Simple cache for last search result mailbox by account and serverId, since the most common
* case will be repeated use of the same mailbox
@@ -104,6 +128,8 @@ public class ImapService extends Service {
private static final HashMap<Long, SortableMessage[]> sSearchResults =
new HashMap<Long, SortableMessage[]>();
+ private static final ExecutorService sExecutor = Executors.newCachedThreadPool();
+
/**
* We write this into the serverId field of messages that will never be upsynced.
*/
@@ -122,34 +148,643 @@ public class ImapService extends Service {
private static final String EXTRA_MESSAGE_ID = "org.codeaurora.email.intent.extra.MESSAGE_ID";
private static final String EXTRA_MESSAGE_INFO =
"org.codeaurora.email.intent.extra.MESSAGE_INFO";
+ private static final String ACTION_KICK_IDLE_CONNECTION =
+ "com.android.email.intent.action.KICK_IDLE_CONNECTION";
+ private static final String EXTRA_MAILBOX = "com.android.email.intent.extra.MAILBOX";
+
+ private static final long RESCHEDULE_PING_DELAY = 150L;
+ private static final long MAX_PING_DELAY = 30 * 60 * 1000L;
+ private static final SparseLongArray sPingDelay = new SparseLongArray();
+
+ private static String sLegacyImapProtocol;
private static String sMessageDecodeErrorString;
+ private static boolean mSyncLock;
+
/**
* Used in ImapFolder for base64 errors. Cached here because ImapFolder does not have access
* to a Context object.
+ *
* @return Error string or empty string
*/
public static String getMessageDecodeErrorString() {
return sMessageDecodeErrorString == null ? "" : sMessageDecodeErrorString;
}
+ private static class ImapIdleListener implements ImapFolder.IdleCallback {
+ private final Context mContext;
+
+ private final Store mStore;
+ private final Mailbox mMailbox;
+
+ public ImapIdleListener(Context context, Store store, Mailbox mailbox) {
+ super();
+ mContext = context;
+ mStore = store;
+ mMailbox = mailbox;
+ }
+
+ @Override
+ public void onIdled() {
+ scheduleKickIdleConnection();
+ }
+
+ @Override
+ public void onNewServerChange(final boolean needSync, final List<String> fetchMessages) {
+ // Instead of checking every received change, request a sync of the mailbox
+ if (Logging.LOGD) {
+ LogUtils.d(LOG_TAG, "Server notified new changes for mailbox " + mMailbox.mId);
+ }
+ cancelKickIdleConnection();
+ resetPingDelay();
+
+ // Request a sync but wait a bit for new incoming messages from server
+ sExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ // Selectively process all the retrieved changes
+ processImapIdleChangesLocked(mContext, mStore.getAccount(), mMailbox,
+ needSync, fetchMessages);
+ }
+ });
+ }
+
+ @Override
+ public void onTimeout() {
+ // Timeout reschedule a new ping
+ LogUtils.i(LOG_TAG, "Ping timeout for mailbox " + mMailbox.mId + ". Reschedule.");
+ cancelKickIdleConnection();
+ internalUnregisterFolderIdle();
+ reschedulePing(RESCHEDULE_PING_DELAY);
+ resetPingDelay();
+ }
+
+ @Override
+ public void onException(MessagingException ex) {
+ // Reschedule a new ping
+ LogUtils.e(LOG_TAG, "Ping exception for mailbox " + mMailbox.mId, ex);
+ cancelKickIdleConnection();
+ internalUnregisterFolderIdle();
+ reschedulePing(increasePingDelay());
+ }
+
+ private void internalUnregisterFolderIdle() {
+ ImapIdleFolderHolder holder = ImapIdleFolderHolder.getInstance();
+ synchronized (holder.mIdledFolders) {
+ holder.mIdledFolders.remove((int) mMailbox.mId);
+ }
+ }
+
+ private void reschedulePing(final long delay) {
+ // Check for connectivity before reschedule
+ ConnectivityManager cm =
+ (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+ if (activeNetwork == null || !activeNetwork.isConnected()) {
+ return;
+ }
+
+ sExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ LogUtils.i(LOG_TAG, "Reschedule delayed ping (" + delay +
+ ") for mailbox " + mMailbox.mId);
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException ie) {
+ }
+
+ try {
+ // Check that the account is ready for push
+ Account account = Account.restoreAccountWithId(
+ mContext, mMailbox.mAccountKey);
+ if (account.getSyncInterval() != Account.CHECK_INTERVAL_PUSH) {
+ LogUtils.i(LOG_TAG, "Account isn't declared for push: " + account.mId);
+ return;
+ }
+
+ ImapIdleFolderHolder holder = ImapIdleFolderHolder.getInstance();
+ holder.registerMailboxForIdle(mContext, account, mMailbox);
+ } catch (MessagingException ex) {
+ LogUtils.w(LOG_TAG, "Failed to register mailbox for idle. Reschedule.", ex);
+ reschedulePing(increasePingDelay());
+ }
+ }
+ });
+ }
+
+ private void resetPingDelay() {
+ int index = sPingDelay.indexOfKey((int) mMailbox.mId);
+ if (index >= 0) {
+ sPingDelay.removeAt(index);
+ }
+ }
+
+ private long increasePingDelay() {
+ long delay = Math.max(RESCHEDULE_PING_DELAY, sPingDelay.get((int) mMailbox.mId));
+ delay = Math.min(MAX_PING_DELAY, delay * 2);
+ sPingDelay.put((int) mMailbox.mId, delay);
+ return delay;
+ }
+
+ private void scheduleKickIdleConnection() {
+ PendingIntent pi = getKickIdleConnectionPendingIntent();
+ long due = System.currentTimeMillis() + KICK_IDLE_CONNETION_TIMEOUT;
+ AlarmManager am = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ am.set(AlarmManager.RTC, due, pi);
+ }
+
+ private void cancelKickIdleConnection() {
+ AlarmManager am = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ am.cancel(getKickIdleConnectionPendingIntent());
+ }
+
+ private PendingIntent getKickIdleConnectionPendingIntent() {
+ int requestCode = ALARM_REQUEST_KICK_IDLE_CODE + (int) mMailbox.mId;
+ Intent i = new Intent(mContext, ImapService.class);
+ i.setAction(ACTION_KICK_IDLE_CONNECTION);
+ i.putExtra(EXTRA_MAILBOX, mMailbox.mId);
+ return PendingIntent.getService(mContext, requestCode,
+ i, PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+ }
+
+ private static class ImapIdleFolderHolder {
+ private static ImapIdleFolderHolder sInstance;
+ private SparseArray<ImapFolder> mIdledFolders = new SparseArray<>();
+
+ private static ImapIdleFolderHolder getInstance() {
+ if (sInstance == null) {
+ sInstance = new ImapIdleFolderHolder();
+ }
+ return sInstance;
+ }
+
+ private boolean isMailboxIdled(long mailboxId) {
+ synchronized (mIdledFolders) {
+ ImapFolder folder = mIdledFolders.get((int) mailboxId);
+ return folder != null && folder.isIdling();
+ }
+ }
+
+ private boolean registerMailboxForIdle(Context context, Account account, Mailbox mailbox)
+ throws MessagingException {
+ synchronized (mIdledFolders) {
+ if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ LogUtils.i(LOG_TAG, "Mailbox is not a valid idle folder: " + mailbox.mId);
+ return false;
+ }
+
+ // Check that the account is ready for push
+ if (account.getSyncInterval() != Account.CHECK_INTERVAL_PUSH) {
+ LogUtils.d(LOG_TAG, "Account is not configured as push: " + account.mId);
+ return false;
+ }
+
+ // Check that the folder isn't already registered
+ if (isMailboxIdled(mailbox.mId)) {
+ LogUtils.i(LOG_TAG, "Mailbox is idled already: " + mailbox.mId);
+ return true;
+ }
+
+ if (!EmailConnectivityManager.isConnected(context)) {
+ LogUtils.i(LOG_TAG, "No available connection to register "
+ + "mailbox for idle: " + mailbox.mId);
+ return false;
+ }
+
+ // And now just idle the folder
+ try {
+ Store remoteStore = Store.getInstance(account, context);
+ ImapFolder folder = mIdledFolders.get((int) mailbox.mId);
+ if (folder == null) {
+ folder = (ImapFolder) remoteStore.getFolder(mailbox.mServerId);
+ mIdledFolders.put((int) mailbox.mId, folder);
+ }
+ folder.open(OpenMode.READ_WRITE);
+ folder.startIdling(new ImapIdleListener(context, remoteStore, mailbox));
+
+ LogUtils.i(LOG_TAG, "Registered idle for mailbox " + mailbox.mId);
+ return true;
+ } catch (Exception ex) {
+ LogUtils.i(LOG_TAG, "Failed to register idle for mailbox " + mailbox.mId, ex);
+ }
+ return false;
+ }
+ }
+
+ private void unregisterIdledMailboxLocked(long mailboxId, boolean remove)
+ throws MessagingException {
+ synchronized (mIdledFolders) {
+ unregisterIdledMailbox(mailboxId, remove, true);
+ }
+ }
+
+ private void unregisterIdledMailbox(long mailboxId, boolean remove, boolean disconnect)
+ throws MessagingException {
+ // Check that the folder is already registered
+ if (!isMailboxIdled(mailboxId)) {
+ LogUtils.i(LOG_TAG, "Mailbox isn't idled yet: " + mailboxId);
+ return;
+ }
+
+ // Stop idling
+ ImapFolder folder = mIdledFolders.get((int) mailboxId);
+ if (disconnect) {
+ folder.stopIdling(remove);
+ }
+ if (remove) {
+ mIdledFolders.remove((int) mailboxId);
+ }
+
+ LogUtils.i(LOG_TAG, "Unregister idle for mailbox " + mailboxId);
+ }
+
+ private void registerAccountForIdle(Context context, Account account)
+ throws MessagingException {
+ // Check that the account is ready for push
+ if (account.getSyncInterval() != Account.CHECK_INTERVAL_PUSH) {
+ LogUtils.d(LOG_TAG, "Account is not configured as push: " + account.mId);
+ return;
+ }
+
+ LogUtils.i(LOG_TAG, "Register idle for account " + account.mId);
+ Cursor c = Mailbox.getLoopBackMailboxIdsForSync(
+ context.getContentResolver(), account.mId);
+ if (c != null) {
+ try {
+ boolean hasSyncMailboxes = false;
+ while (c.moveToNext()) {
+ long mailboxId = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ boolean registered = isMailboxIdled(mailboxId);
+ if (!registered) {
+ registered = registerMailboxForIdle(context, account, mailbox);
+ }
+ if (!hasSyncMailboxes && registered) {
+ hasSyncMailboxes = registered;
+ }
+ }
+
+ // Sync the inbox
+ if (!hasSyncMailboxes) {
+ final long inboxId = Mailbox.findMailboxOfType(
+ context, account.mId, Mailbox.TYPE_INBOX);
+ if (inboxId != Mailbox.NO_MAILBOX) {
+ final Mailbox inbox = Mailbox.restoreMailboxWithId(context, inboxId);
+ if (!isMailboxIdled(inbox.mId)) {;
+ registerMailboxForIdle(context, account, inbox);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ private void kickAccountIdledMailboxes(Context context, Account account)
+ throws MessagingException {
+ synchronized (mIdledFolders) {
+ unregisterAccountIdledMailboxes(context, account.mId, true);
+ registerAccountForIdle(context, account);
+ }
+ }
+
+ private void kickIdledMailbox(Context context, Mailbox mailbox, Account account)
+ throws MessagingException {
+ synchronized (mIdledFolders) {
+ unregisterIdledMailboxLocked(mailbox.mId, true);
+ registerMailboxForIdle(context, account, mailbox);
+ }
+ }
+
+ private void unregisterAccountIdledMailboxes(Context context, long accountId,
+ boolean remove) {
+ LogUtils.i(LOG_TAG, "Unregister idle for account " + accountId);
+
+ synchronized (mIdledFolders) {
+ int count = mIdledFolders.size() - 1;
+ for (int index = count; index >= 0; index--) {
+ long mailboxId = mIdledFolders.keyAt(index);
+ try {
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (mailbox == null || mailbox.mAccountKey == accountId) {
+ unregisterIdledMailbox(mailboxId, remove, true);
+
+ LogUtils.i(LOG_TAG, "Unregister idle for mailbox " + mailboxId);
+ }
+ } catch (MessagingException ex) {
+ LogUtils.w(LOG_TAG, "Failed to unregister mailbox "
+ + mailboxId + " for account " + accountId);
+ }
+ }
+ }
+ }
+
+ private void unregisterAllIdledMailboxes(final boolean disconnect) {
+ // Run away from the UI thread
+ sExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mIdledFolders) {
+ LogUtils.i(LOG_TAG, "Unregister all idle mailboxes");
+
+ int count = mIdledFolders.size() - 1;
+ for (int index = count; index >= 0; index--) {
+ long mailboxId = mIdledFolders.keyAt(index);
+ try {
+ unregisterIdledMailbox(mailboxId, true, disconnect);
+ } catch (MessagingException ex) {
+ LogUtils.w(LOG_TAG, "Failed to unregister mailbox " + mailboxId);
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+
+ private static class ImapEmailConnectivityManager extends EmailConnectivityManager {
+ private final Context mContext;
+ private final Handler mHandler;
+ private final IEmailService mService;
+
+ private final Runnable mRegisterIdledFolderRunnable = new Runnable() {
+ @Override
+ public void run() {
+ sExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ ImapService.registerAllImapIdleMailboxes(mContext, mService);
+
+ // Since we could have missed some changes, request a sync
+ // for the IDLEd accounts
+ ContentResolver cr = mContext.getContentResolver();
+ Cursor c = cr.query(Account.CONTENT_URI,
+ Account.CONTENT_PROJECTION, null, null, null);
+ if (c != null) {
+ try {
+ while (c.moveToNext()) {
+ final Account account = new Account();
+ account.restore(c);
+
+ // Only imap push accounts
+ if (account.getSyncInterval() != Account.CHECK_INTERVAL_PUSH) {
+ continue;
+ }
+ if (!isLegacyImapProtocol(mContext, account)) {
+ continue;
+ }
+
+ // Request a "recents" sync
+ ImapService.requestSync(mContext,
+ account, Mailbox.NO_MAILBOX, false);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+ });
+ }
+ };
+
+ public ImapEmailConnectivityManager(Context context, IEmailService service) {
+ super(context, LOG_TAG);
+ mContext = context;
+ mHandler = new Handler();
+ mService = service;
+ }
+
+ @Override
+ public void onConnectivityRestored(int networkType) {
+ // Restore idled folders. Execute in background
+ if (Logging.LOGD) {
+ LogUtils.d(Logging.LOG_TAG, "onConnectivityRestored ("
+ + "networkType=" + networkType + ")");
+ }
+
+ // Hold the register a bit to trying to avoid unstable networking
+ mHandler.removeCallbacks(mRegisterIdledFolderRunnable);
+ mHandler.postDelayed(mRegisterIdledFolderRunnable, 10000);
+ }
+
+ @Override
+ public void onConnectivityLost(int networkType) {
+ // Unlink idled folders. Execute in background
+ if (Logging.LOGD) {
+ LogUtils.d(Logging.LOG_TAG, "onConnectivityLost ("
+ + "networkType=" + networkType + ")");
+ }
+ sExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ // Only remove references. We have no network to kill idled
+ // connections
+ ImapIdleFolderHolder.getInstance().unregisterAllIdledMailboxes(false);
+ }
+ });
+ }
+ }
+
+ private static class LocalChangesContentObserver extends ContentObserver {
+ private Context mContext;
+
+ public LocalChangesContentObserver(Context context, Handler handler) {
+ super(handler);
+ mContext = context;
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ // what changed?
+ try {
+ List<String> segments = uri.getPathSegments();
+ final String type = segments.get(0);
+ final String op = segments.get(1);
+ final long id = Long.parseLong(uri.getLastPathSegment());
+
+ // Run the changes processor outside the ui thread
+ sExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ // Apply the change
+ if (type.equals("account")) {
+ processAccountChanged(op, id);
+ } else if (type.equals("mailbox")) {
+ processMailboxChanged(op, id);
+ } else if (type.equals("message")) {
+ processMessageChanged(op, id);
+ }
+ }
+ });
+ } catch (Exception ex) {
+ return;
+ }
+ }
+
+ private void processAccountChanged(String op, long id) {
+ // For delete operations we can't fetch the account, so process it first
+ if (op.equals(EmailProvider.NOTIFICATION_OP_DELETE)) {
+ ImapIdleFolderHolder.getInstance()
+ .unregisterAccountIdledMailboxes(mContext, id, true);
+ stopImapPushServiceIfNecessary(mContext);
+ return;
+ }
+
+ Account account = Account.restoreAccountWithId(mContext, id);
+ if (account == null) {
+ return;
+ }
+ if (!isLegacyImapProtocol(mContext, account)) {
+ // The account isn't an imap account
+ return;
+ }
+
+ try {
+ final ImapIdleFolderHolder holder = ImapIdleFolderHolder.getInstance();
+ if (op.equals(EmailProvider.NOTIFICATION_OP_UPDATE)) {
+ holder.kickAccountIdledMailboxes(mContext, account);
+ } else if (op.equals(EmailProvider.NOTIFICATION_OP_INSERT)) {
+ if (account.getSyncInterval() == Account.CHECK_INTERVAL_PUSH) {
+ holder.registerAccountForIdle(mContext, account);
+ }
+ }
+ } catch (MessagingException me) {
+ LogUtils.e(LOG_TAG, "Failed to process imap account " + id + " changes.", me);
+ }
+
+ // Check if service should be started/stopped
+ stopImapPushServiceIfNecessary(mContext);
+ }
+
+ private void processMailboxChanged(String op, long id) {
+ // For delete operations we can't fetch the mailbox, so process it first
+ if (op.equals(EmailProvider.NOTIFICATION_OP_DELETE)) {
+ try {
+ ImapIdleFolderHolder.getInstance().unregisterIdledMailboxLocked(id, true);
+ } catch (MessagingException me) {
+ LogUtils.e(LOG_TAG, "Failed to process imap mailbox " + id + " changes.", me);
+ }
+ return;
+ }
+
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, id);
+ if (mailbox == null) {
+ return;
+ }
+ Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
+ if (account == null) {
+ return;
+ }
+ if (!isLegacyImapProtocol(mContext, account)) {
+ // The account isn't an imap account
+ return;
+ }
+
+ try {
+ final ImapIdleFolderHolder holder = ImapIdleFolderHolder.getInstance();
+ if (op.equals(EmailProvider.NOTIFICATION_OP_UPDATE)) {
+ // Only apply if syncInterval has changed
+ boolean registered = holder.isMailboxIdled(id);
+ boolean toRegister = mailbox.mSyncInterval == 1
+ && account.getSyncInterval() == Account.CHECK_INTERVAL_PUSH;
+ if (registered != toRegister) {
+ if (registered) {
+ holder.unregisterIdledMailboxLocked(id, true);
+ }
+ if (toRegister) {
+ holder.registerMailboxForIdle(mContext, account, mailbox);
+ }
+ }
+ } else if (op.equals(EmailProvider.NOTIFICATION_OP_INSERT)) {
+ if (account.getSyncInterval() == Account.CHECK_INTERVAL_PUSH) {
+ holder.registerMailboxForIdle(mContext, account, mailbox);
+ }
+ }
+ } catch (MessagingException me) {
+ LogUtils.e(LOG_TAG, "Failed to process imap mailbox " + id + " changes.", me);
+ }
+ }
+
+ private void processMessageChanged(String op, long id) {
+ if (mSyncLock) {
+ return;
+ }
+ EmailContent.Message msg = EmailContent.Message.restoreMessageWithId(mContext, id);
+ if (msg == null) {
+ return;
+ }
+ Account account = Account.restoreAccountWithId(mContext, msg.mAccountKey);
+ if (account == null) {
+ return;
+ }
+ if (!isLegacyImapProtocol(mContext, account)) {
+ // The account isn't an imap account
+ return;
+ }
+ if (account.getSyncInterval() != Account.CHECK_INTERVAL_PUSH) {
+ return;
+ }
+
+ try {
+ Store remoteStore = Store.getInstance(account, mContext);
+ processPendingActionsSynchronous(mContext, account, remoteStore, false);
+ } catch (MessagingException me) {
+ LogUtils.e(LOG_TAG, "Failed to process imap message " + id + " changes.", me);
+ }
+ }
+ }
+
+ private ImapEmailConnectivityManager mConnectivityManager;
+ private LocalChangesContentObserver mLocalChangesObserver;
+ private Handler mServiceHandler;
+
@Override
public void onCreate() {
super.onCreate();
sMessageDecodeErrorString = getString(R.string.message_decode_error);
+ mServiceHandler = new Handler();
+
+ // Initialize the email provider and the listeners/observers
+ EmailContent.init(this);
+ mConnectivityManager = new ImapEmailConnectivityManager(this, mBinder);
+ mLocalChangesObserver = new LocalChangesContentObserver(this, mServiceHandler);
+
+ // Register observers
+ getContentResolver().registerContentObserver(
+ Account.SYNC_SETTING_CHANGED_URI, true, mLocalChangesObserver);
+ getContentResolver().registerContentObserver(
+ Mailbox.SYNC_SETTING_CHANGED_URI, true, mLocalChangesObserver);
+ getContentResolver().registerContentObserver(
+ EmailContent.Message.NOTIFIER_URI, true, mLocalChangesObserver);
+ }
+
+ @Override
+ public void onDestroy() {
+ // Unregister services
+ ImapIdleFolderHolder.getInstance().unregisterAllIdledMailboxes(true);
+ mConnectivityManager.unregister();
+ getContentResolver().unregisterContentObserver(mLocalChangesObserver);
+
+ super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return Service.START_STICKY;
+ }
final String action = intent.getAction();
if (Logging.LOGD) {
LogUtils.d(Logging.LOG_TAG, "Action: ", action);
}
final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
- Context context = getApplicationContext();
+ final Context context = getApplicationContext();
if (ACTION_CHECK_MAIL.equals(action)) {
final long inboxId = Mailbox.findMailboxOfType(context, accountId,
Mailbox.TYPE_INBOX);
@@ -181,6 +816,10 @@ public class ImapService extends Service {
Account.getAccountForMessageId(context, messageId), remoteStore, true);
} catch (Exception e) {
LogUtils.d(Logging.LOG_TAG, "RemoteException " + e);
+ } finally {
+ if (remoteStore != null) {
+ remoteStore.closeConnections();
+ }
}
} else if (ACTION_MESSAGE_READ.equals(action)) {
final long messageId = intent.getLongExtra(EXTRA_MESSAGE_ID, -1);
@@ -202,6 +841,10 @@ public class ImapService extends Service {
Account.getAccountForMessageId(context, messageId), remoteStore, true);
} catch (Exception e){
LogUtils.d(Logging.LOG_TAG, "RemoteException " + e);
+ } finally {
+ if (remoteStore != null) {
+ remoteStore.closeConnections();
+ }
}
} else if (ACTION_MOVE_MESSAGE.equals(action)) {
final long messageId = intent.getLongExtra(EXTRA_MESSAGE_ID, -1);
@@ -226,6 +869,10 @@ public class ImapService extends Service {
Account.getAccountForMessageId(context, messageId),remoteStore, true);
} catch (Exception e){
LogUtils.d(Logging.LOG_TAG, "RemoteException " + e);
+ } finally {
+ if (remoteStore != null) {
+ remoteStore.closeConnections();
+ }
}
} else if (ACTION_SEND_PENDING_MAIL.equals(action)) {
if (Logging.LOGD) {
@@ -240,6 +887,48 @@ public class ImapService extends Service {
} catch (Exception e) {
LogUtils.e(Logging.LOG_TAG, "RemoteException " + e);
}
+ } else if (ACTION_KICK_IDLE_CONNECTION.equals(action)) {
+ if (Logging.LOGD) {
+ LogUtils.d(Logging.LOG_TAG, "action: Send Pending Mail "+accountId);
+ }
+ final long mailboxId = intent.getLongExtra(EXTRA_MAILBOX, -1);
+ if (mailboxId <= -1 ) {
+ return START_NOT_STICKY;
+ }
+
+ sExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (mailbox == null) {
+ return;
+ }
+ Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
+ if (account == null) {
+ return;
+ }
+
+ Store remoteStore = null;
+ try {
+ // Since we were idling, just perform a full sync of the mailbox to ensure
+ // we have all the items before kick the connection
+ remoteStore = Store.getInstance(account, context);
+ synchronizeMailboxGeneric(context, account, remoteStore,
+ mailbox, false, true);
+
+ // Kick mailbox
+ ImapIdleFolderHolder holder = ImapIdleFolderHolder.getInstance();
+ holder.kickIdledMailbox(context, mailbox, account);
+ } catch (Exception e) {
+ LogUtils.e(Logging.LOG_TAG,"Failed to kick idled connection "
+ + "for mailbox " + mailboxId, e);
+ } finally {
+ if (remoteStore != null) {
+ remoteStore.closeConnections();
+ }
+ }
+ }
+ });
}
return Service.START_STICKY;
@@ -259,6 +948,26 @@ public class ImapService extends Service {
}
return 0;
}
+
+ @Override
+ public void pushModify(long accountId) throws RemoteException {
+ final Context context = ImapService.this;
+ final Account account = Account.restoreAccountWithId(context, accountId);
+ if (account.getSyncInterval() != Account.CHECK_INTERVAL_PUSH) {
+ LogUtils.i(LOG_TAG,"Idle (pushModify) isn't avaliable for account " + accountId);
+ ImapIdleFolderHolder holder = ImapIdleFolderHolder.getInstance();
+ holder.unregisterAccountIdledMailboxes(context, account.mId, true);
+ return;
+ }
+
+ LogUtils.i(LOG_TAG,"Register idle (pushModify) account " + accountId);
+ try {
+ ImapIdleFolderHolder holder = ImapIdleFolderHolder.getInstance();
+ holder.registerAccountForIdle(context, account);
+ } catch (MessagingException ex) {
+ LogUtils.d(LOG_TAG, "Failed to modify push for account " + accountId);
+ }
+ }
};
@Override
@@ -267,6 +976,89 @@ public class ImapService extends Service {
return mBinder;
}
+ protected static void registerAllImapIdleMailboxes(Context context, IEmailService service) {
+ ContentResolver cr = context.getContentResolver();
+ Cursor c = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null);
+ if (c != null) {
+ try {
+ while (c.moveToNext()) {
+ final Account account = new Account();
+ account.restore(c);
+
+ // Only imap push accounts
+ if (account.getSyncInterval() != Account.CHECK_INTERVAL_PUSH) {
+ continue;
+ }
+ if (!isLegacyImapProtocol(context, account)) {
+ continue;
+ }
+
+ try {
+ service.pushModify(account.mId);
+ } catch (RemoteException ex) {
+ LogUtils.d(LOG_TAG, "Failed to call pushModify for account " + account.mId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ private static void requestSync(Context context, Account account, long mailbox, boolean full) {
+ if (Logging.LOGD) {
+ LogUtils.d(LOG_TAG, "Request sync due to idle response for mailbox " + mailbox);
+ }
+
+ final EmailServiceUtils.EmailServiceInfo info = EmailServiceUtils.getServiceInfoForAccount(
+ context, account.mId);
+ final android.accounts.Account acct = new android.accounts.Account(
+ account.mEmailAddress, info.accountType);
+ Bundle extras = null;
+ if (mailbox != Mailbox.NO_MAILBOX) {
+ extras = Mailbox.createSyncBundle(mailbox);
+ } else {
+ extras = new Bundle();
+ }
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, full);
+ ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
+ }
+
+ protected static final void stopImapPushServiceIfNecessary(Context context) {
+ ContentResolver cr = context.getContentResolver();
+ Cursor c = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,null, null, null);
+ if (c != null) {
+ try {
+ while (c.moveToNext()) {
+ final Account account = new Account();
+ account.restore(c);
+
+ // Only imap push accounts
+ if (account.getSyncInterval() != Account.CHECK_INTERVAL_PUSH ||
+ !ImapService.isLegacyImapProtocol(context, account)) {
+ continue;
+ }
+
+ return;
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ // Stop the service
+ context.stopService(new Intent(context, LegacyImapSyncAdapterService.class));
+ }
+
+ public static boolean isLegacyImapProtocol(Context ctx, Account acct) {
+ if (sLegacyImapProtocol == null) {
+ sLegacyImapProtocol = ctx.getString(R.string.protocol_legacy_imap);
+ }
+ return acct.getProtocol(ctx).equals(sLegacyImapProtocol);
+ }
+
/**
* Start foreground synchronization of the specified folder. This is called by
* synchronizeMailbox or checkMail.
@@ -278,13 +1070,23 @@ public class ImapService extends Service {
final Account account, final Mailbox folder, final boolean loadMore,
final boolean uiRefresh) throws MessagingException {
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
- final NotificationController nc =
- NotificationControllerCreatorHolder.getInstance(context);
+ final NotificationController nc = NotificationControllerCreatorHolder.getInstance(context);
Store remoteStore = null;
+ ImapIdleFolderHolder imapHolder = ImapIdleFolderHolder.getInstance();
try {
+ mSyncLock = true;
+
+ // Unregister the imap idle
+ if (account.getSyncInterval() == Account.CHECK_INTERVAL_PUSH) {
+ imapHolder.unregisterIdledMailboxLocked(folder.mId, false);
+ } else {
+ imapHolder.unregisterAccountIdledMailboxes(context, account.mId, false);
+ }
+
remoteStore = Store.getInstance(account, context);
processPendingActionsSynchronous(context, account, remoteStore, uiRefresh);
synchronizeMailboxGeneric(context, account, remoteStore, folder, loadMore, uiRefresh);
+
// Clear authentication notification for this account
nc.cancelLoginFailedNotification(account.mId);
} catch (MessagingException e) {
@@ -297,9 +1099,16 @@ public class ImapService extends Service {
}
throw e;
} finally {
+ mSyncLock = false;
+
if (remoteStore != null) {
remoteStore.closeConnections();
}
+
+ // Register the imap idle again
+ if (account.getSyncInterval() == Account.CHECK_INTERVAL_PUSH) {
+ imapHolder.registerMailboxForIdle(context, account, folder);
+ }
}
// TODO: Rather than use exceptions as logic above, return the status and handle it
// correctly in caller.
@@ -825,6 +1634,255 @@ public class ImapService extends Service {
remoteFolder.close(false);
}
+ private synchronized static void processImapFetchChanges(Context ctx, Account acct,
+ Mailbox mailbox, List<String> uids) throws MessagingException {
+
+ PowerManager pm = (PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
+ PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "Imap IDLE Sync WakeLock");
+
+ NotificationController nc = null;
+ Store remoteStore = null;
+ ImapIdleFolderHolder imapHolder = null;
+
+ try {
+ mSyncLock = true;
+ wl.acquire();
+
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(ctx, acct));
+ nc = NotificationControllerCreatorHolder.getInstance(ctx);
+
+ remoteStore = Store.getInstance(acct, ctx);
+ imapHolder = ImapIdleFolderHolder.getInstance();
+
+ final ContentResolver resolver = ctx.getContentResolver();
+
+ // Don't sync if account is not set to idled
+ if (acct.getSyncInterval() != Account.CHECK_INTERVAL_PUSH) {
+ return;
+ }
+
+ // 1. Open the remote store & folder
+ ImapFolder remoteFolder;
+ synchronized (imapHolder.mIdledFolders) {
+ remoteFolder = imapHolder.mIdledFolders.get((int) mailbox.mId);
+ }
+ if (remoteFolder == null || remoteFolder.isIdling()) {
+ remoteFolder = (ImapFolder) remoteStore.getFolder(mailbox.mServerId);
+ }
+ if (!remoteFolder.exists()) {
+ return;
+ }
+ remoteFolder.open(OpenMode.READ_WRITE);
+ if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
+ return;
+ }
+
+ // 1.- Retrieve the messages
+ Message[] remoteMessages = remoteFolder.getMessages(
+ uids.toArray(new String[uids.size()]), null);
+
+ // 2.- Refresh flags
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.FLAGS);
+ remoteFolder.fetch(remoteMessages, fp, null);
+
+ boolean remoteSupportsSeen = false;
+ boolean remoteSupportsFlagged = false;
+ boolean remoteSupportsAnswered = false;
+ for (Flag flag : remoteFolder.getPermanentFlags()) {
+ if (flag == Flag.SEEN) {
+ remoteSupportsSeen = true;
+ }
+ if (flag == Flag.FLAGGED) {
+ remoteSupportsFlagged = true;
+ }
+ if (flag == Flag.ANSWERED) {
+ remoteSupportsAnswered = true;
+ }
+ }
+
+ // 3.- Retrieve a reference of the local messages
+ HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<>();
+ for (Message remoteMessage : remoteMessages) {
+ Cursor localUidCursor = null;
+ try {
+ localUidCursor = resolver.query(
+ EmailContent.Message.CONTENT_URI,
+ LocalMessageInfo.PROJECTION,
+ EmailContent.MessageColumns.ACCOUNT_KEY + "=?"
+ + " AND " + MessageColumns.MAILBOX_KEY + "=?"
+ + " AND " + MessageColumns.SERVER_ID + ">=?",
+ new String[] {
+ String.valueOf(acct.mId),
+ String.valueOf(mailbox.mId),
+ String.valueOf(remoteMessage.getUid()) },
+ null);
+ if (localUidCursor != null && localUidCursor.moveToNext()) {
+ LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
+ localMessageMap.put(info.mServerId, info);
+ }
+ } finally {
+ if (localUidCursor != null) {
+ localUidCursor.close();
+ }
+ }
+ }
+
+ // 5.- Add to the list of new messages
+ final ArrayList<Long> unseenMessages = new ArrayList<Long>();
+ final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
+ for (Message remoteMessage : remoteMessages) {
+ LocalMessageInfo localMessage = localMessageMap.get(remoteMessage.getUid());
+
+ // localMessage == null -> message has never been created (not even headers)
+ // mFlagLoaded = UNLOADED -> message created, but none of body loaded
+ // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
+ // mFlagLoaded = COMPLETE -> message body has been completely loaded
+ // mFlagLoaded = DELETED -> message has been deleted
+ // Only the first two of these are "unsynced", so let's retrieve them
+ if (localMessage == null ||
+ (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED) ||
+ (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_PARTIAL)) {
+ unsyncedMessages.add(remoteMessage);
+ }
+ }
+
+ // 6. Download basic info about the new/unloaded messages (if any)
+ /*
+ * Fetch the flags and envelope only of the new messages. This is intended to get us
+ * critical data as fast as possible, and then we'll fill in the details.
+ */
+ if (unsyncedMessages.size() > 0) {
+ downloadFlagAndEnvelope(ctx, acct, mailbox, remoteFolder, unsyncedMessages,
+ localMessageMap, unseenMessages);
+ }
+
+ // 7. Update SEEN/FLAGGED/ANSWERED (star) flags
+ if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
+ for (Message remoteMessage : remoteMessages) {
+ LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
+ if (localMessageInfo == null) {
+ continue;
+ }
+ boolean localSeen = localMessageInfo.mFlagRead;
+ boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
+ boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
+ boolean localFlagged = localMessageInfo.mFlagFavorite;
+ boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
+ boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
+ int localFlags = localMessageInfo.mFlags;
+ boolean localAnswered = (localFlags &
+ EmailContent.Message.FLAG_REPLIED_TO) != 0;
+ boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
+ boolean newAnswered = (remoteSupportsAnswered &&
+ (localAnswered != remoteAnswered));
+ if (newSeen || newFlagged || newAnswered) {
+ Uri uri = ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
+ ContentValues updateValues = new ContentValues();
+ updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
+ updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
+ if (remoteAnswered) {
+ localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
+ } else {
+ localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
+ }
+ updateValues.put(MessageColumns.FLAGS, localFlags);
+ resolver.update(uri, updateValues, null, null);
+ }
+ }
+ }
+
+ // 8.- Remove remote deleted messages
+ for (final Message remoteMessage : remoteMessages) {
+ if (remoteMessage.isSet(Flag.DELETED)) {
+ LocalMessageInfo info = localMessageMap.get(remoteMessage.getUid());
+ if (info == null) {
+ continue;
+ }
+
+ // Delete associated data (attachment files)
+ // Attachment & Body records are auto-deleted when we delete the Message record
+ AttachmentUtilities.deleteAllAttachmentFiles(ctx, acct.mId, info.mId);
+
+ // Delete the message itself
+ final Uri uriToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, info.mId);
+ resolver.delete(uriToDelete, null, null);
+
+ // Delete extra rows (e.g. updated or deleted)
+ final Uri updateRowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, info.mId);
+ resolver.delete(updateRowToDelete, null, null);
+ final Uri deleteRowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.DELETED_CONTENT_URI, info.mId);
+ resolver.delete(deleteRowToDelete, null, null);
+ }
+ }
+
+ // 9.- Load unsynced messages
+ loadUnsyncedMessages(ctx, acct, remoteFolder, unsyncedMessages, mailbox);
+
+ // 10. Remove messages that are in the local store but no in the current sync window
+ int syncLookBack = mailbox.mSyncLookback == SyncWindow.SYNC_WINDOW_ACCOUNT
+ ? acct.mSyncLookback
+ : mailbox.mSyncLookback;
+ long endDate = System.currentTimeMillis() -
+ (SyncWindow.toDays(syncLookBack) * DateUtils.DAY_IN_MILLIS);
+ LogUtils.d(Logging.LOG_TAG, "full sync: original window: now - " + endDate);
+ for (final LocalMessageInfo info : localMessageMap.values()) {
+ // If this message is inside our sync window, and we cannot find it in our list
+ // of remote messages, then we know it's been deleted from the server.
+ if (info.mTimestamp < endDate) {
+ // Delete associated data (attachment files)
+ // Attachment & Body records are auto-deleted when we delete the Message record
+ AttachmentUtilities.deleteAllAttachmentFiles(ctx, acct.mId, info.mId);
+
+ // Delete the message itself
+ final Uri uriToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.CONTENT_URI, info.mId);
+ resolver.delete(uriToDelete, null, null);
+
+ // Delete extra rows (e.g. updated or deleted)
+ final Uri updateRowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.UPDATED_CONTENT_URI, info.mId);
+ resolver.delete(updateRowToDelete, null, null);
+ final Uri deleteRowToDelete = ContentUris.withAppendedId(
+ EmailContent.Message.DELETED_CONTENT_URI, info.mId);
+ resolver.delete(deleteRowToDelete, null, null);
+ }
+ }
+
+ // Clear authentication notification for this account
+ nc.cancelLoginFailedNotification(acct.mId);
+
+ } catch (MessagingException ex) {
+ if (Logging.LOGD) {
+ LogUtils.d(Logging.LOG_TAG, "processImapFetchChanges", ex);
+ }
+ if (ex instanceof AuthenticationFailedException) {
+ // Generate authentication notification
+ if (nc != null) {
+ nc.showLoginFailedNotificationSynchronous(acct.mId, true /* incoming */);
+ }
+ }
+ throw ex;
+ } finally {
+ mSyncLock = false;
+ wl.release();
+
+ if (remoteStore != null) {
+ remoteStore.closeConnections();
+
+ // Register the imap idle again
+ if (imapHolder != null && acct.getSyncInterval() == Account.CHECK_INTERVAL_PUSH) {
+ imapHolder.registerMailboxForIdle(ctx, acct, mailbox);
+ }
+ }
+ }
+ }
+
/**
* Find messages in the updated table that need to be written back to server.
*
@@ -1701,4 +2759,70 @@ public class ImapService extends Service {
return numSearchResults;
}
+
+ private static synchronized void processImapIdleChangesLocked(Context context, Account account,
+ Mailbox mailbox, boolean needSync, List<String> fetchMessages) {
+
+ // Process local to server changes first
+ Store remoteStore = null;
+ try {
+ remoteStore = Store.getInstance(account, context);
+ processPendingActionsSynchronous(context, account, remoteStore, false);
+ } catch (MessagingException me) {
+ // Ignore
+ } finally {
+ if (remoteStore != null) {
+ remoteStore.closeConnections();
+ }
+ }
+
+ // If the request rebased the maximum time without a full sync, then instead of fetch
+ // the changes just perform a full sync
+ final long timeSinceLastFullSync = SystemClock.elapsedRealtime() -
+ mailbox.mLastFullSyncTime;
+ final boolean fullSync = timeSinceLastFullSync >= FULL_SYNC_INTERVAL_MILLIS
+ || timeSinceLastFullSync < 0;
+ if (fullSync) {
+ needSync = true;
+ fetchMessages.clear();
+
+ if (Logging.LOGD) {
+ LogUtils.d(LOG_TAG, "Full sync required for mailbox " + mailbox.mId
+ + " because is exceded the maximum time without a full sync.");
+ }
+ }
+
+ final int msgToFetchSize = fetchMessages.size();
+ if (Logging.LOGD) {
+ LogUtils.d(LOG_TAG, "Processing IDLE changes for mailbox " + mailbox.mId
+ + ": need sync " + needSync + ", " + msgToFetchSize + " fetch messages");
+ }
+
+ boolean syncRequested = false;
+ try {
+ // Sync fetch messages only if we are not going to perform a full sync
+ if (msgToFetchSize > 0 && msgToFetchSize < MAX_MESSAGES_TO_FETCH && !needSync) {
+ processImapFetchChanges(context, account, mailbox, fetchMessages);
+ }
+ if (needSync || msgToFetchSize > MAX_MESSAGES_TO_FETCH) {
+ // With idle we fetched as much as possible. If as resync is required, then
+ // if should be a full sync
+ requestSync(context, account, mailbox.mId, true);
+ syncRequested = true;
+ }
+ } catch (MessagingException ex) {
+ LogUtils.w(LOG_TAG, "Failed to process imap idle changes for mailbox " + mailbox.mId);
+ }
+
+ // In case no sync happens, re-add idle status
+ try {
+ if (!syncRequested && account.getSyncInterval() == Account.CHECK_INTERVAL_PUSH) {
+ final ImapIdleFolderHolder holder = ImapIdleFolderHolder.getInstance();
+ holder.registerMailboxForIdle(context, account, mailbox);
+ }
+ } catch (MessagingException ex) {
+ LogUtils.w(LOG_TAG, "Failed to readd imap idle after no sync " +
+ "for mailbox " + mailbox.mId);
+ }
+ }
}
diff --git a/provider_src/com/android/email/service/LegacyImapSyncAdapterService.java b/provider_src/com/android/email/service/LegacyImapSyncAdapterService.java
index 1f6b6195e..1b5a36b61 100644
--- a/provider_src/com/android/email/service/LegacyImapSyncAdapterService.java
+++ b/provider_src/com/android/email/service/LegacyImapSyncAdapterService.java
@@ -16,5 +16,139 @@
package com.android.email.service;
+import static com.android.emailcommon.Logging.LOG_TAG;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ComponentName;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SyncResult;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.text.format.DateUtils;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.IEmailService;
+import com.android.mail.utils.LogUtils;
+
public class LegacyImapSyncAdapterService extends PopImapSyncAdapterService {
-} \ No newline at end of file
+
+ // The call to ServiceConnection.onServiceConnected is asynchronous to bindService. It's
+ // possible for that to be delayed if, in which case, a call to onPerformSync
+ // could occur before we have a connection to the service.
+ // In onPerformSync, if we don't yet have our ImapService, we will wait for up to 10
+ // seconds for it to appear. If it takes longer than that, we will fail the sync.
+ private static final long MAX_WAIT_FOR_SERVICE_MS = 10 * DateUtils.SECOND_IN_MILLIS;
+
+ private static final ExecutorService sExecutor = Executors.newCachedThreadPool();
+
+ private IEmailService mImapService;
+
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ if (Logging.LOGD) {
+ LogUtils.v(LOG_TAG, "onServiceConnected");
+ }
+ synchronized (mConnection) {
+ mImapService = IEmailService.Stub.asInterface(binder);
+ mConnection.notify();
+
+ // We need to run this task in the background (not in UI-Thread)
+ sExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ final Context context = LegacyImapSyncAdapterService.this;
+ ImapService.registerAllImapIdleMailboxes(context, mImapService);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mImapService = null;
+ }
+ };
+
+ protected class ImapSyncAdapterImpl extends SyncAdapterImpl {
+ public ImapSyncAdapterImpl(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onPerformSync(android.accounts.Account account, Bundle extras,
+ String authority, ContentProviderClient provider, SyncResult syncResult) {
+
+ final Context context = LegacyImapSyncAdapterService.this;
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "Imap Sync WakeLock");
+ try {
+ wl.acquire();
+
+ if (!waitForService()) {
+ // The service didn't connect, nothing we can do.
+ return;
+ }
+
+ if (!Mailbox.isPushOnlyExtras(extras)) {
+ super.onPerformSync(account, extras, authority, provider, syncResult);
+ }
+
+ // Check if IMAP push service is necessary
+ ImapService.stopImapPushServiceIfNecessary(context);
+
+ } finally {
+ wl.release();
+ }
+ }
+ }
+
+ public AbstractThreadedSyncAdapter getSyncAdapter() {
+ return new ImapSyncAdapterImpl(getApplicationContext());
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ bindService(new Intent(this, ImapService.class), mConnection, Context.BIND_AUTO_CREATE);
+ startService(new Intent(this, LegacyImapSyncAdapterService.class));
+ }
+
+ @Override
+ public void onDestroy() {
+ unbindService(mConnection);
+ super.onDestroy();
+ }
+
+ private final boolean waitForService() {
+ synchronized(mConnection) {
+ if (mImapService == null) {
+ if (Logging.LOGD) {
+ LogUtils.v(LOG_TAG, "ImapService not yet connected");
+ }
+ try {
+ mConnection.wait(MAX_WAIT_FOR_SERVICE_MS);
+ } catch (InterruptedException e) {
+ LogUtils.wtf(LOG_TAG, "InterrupedException waiting for ImapService to connect");
+ return false;
+ }
+ if (mImapService == null) {
+ LogUtils.wtf(LOG_TAG, "timed out waiting for ImapService to connect");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/provider_src/com/android/email/service/PopImapSyncAdapterService.java b/provider_src/com/android/email/service/PopImapSyncAdapterService.java
index 432cdd107..5e19478dd 100644
--- a/provider_src/com/android/email/service/PopImapSyncAdapterService.java
+++ b/provider_src/com/android/email/service/PopImapSyncAdapterService.java
@@ -49,7 +49,7 @@ import java.util.ArrayList;
public class PopImapSyncAdapterService extends Service {
private static final String TAG = "PopImapSyncService";
- private SyncAdapterImpl mSyncAdapter = null;
+ private AbstractThreadedSyncAdapter mSyncAdapter = null;
private static String sPop3Protocol;
private static String sLegacyImapProtocol;
@@ -58,7 +58,7 @@ public class PopImapSyncAdapterService extends Service {
super();
}
- private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
+ static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
public SyncAdapterImpl(Context context) {
super(context, true /* autoInitialize */);
}
@@ -71,10 +71,14 @@ public class PopImapSyncAdapterService extends Service {
}
}
+ public AbstractThreadedSyncAdapter getSyncAdapter() {
+ return new SyncAdapterImpl(getApplicationContext());
+ }
+
@Override
public void onCreate() {
super.onCreate();
- mSyncAdapter = new SyncAdapterImpl(getApplicationContext());
+ mSyncAdapter = getSyncAdapter();
}
@Override
@@ -101,14 +105,14 @@ public class PopImapSyncAdapterService extends Service {
return false;
}
- private static void sync(final Context context, final long mailboxId,
+ private static boolean sync(final Context context, final long mailboxId,
final Bundle extras, final SyncResult syncResult, final boolean uiRefresh,
final int deltaMessageCount) {
TempDirectory.setTempDirectory(context);
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
- if (mailbox == null) return;
+ if (mailbox == null) return false;
Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
- if (account == null) return;
+ if (account == null) return false;
ContentResolver resolver = context.getContentResolver();
if ((mailbox.mType != Mailbox.TYPE_OUTBOX) &&
!loadsFromServer(context, mailbox, account)) {
@@ -116,7 +120,7 @@ public class PopImapSyncAdapterService extends Service {
// updates table and return
resolver.delete(Message.UPDATED_CONTENT_URI, MessageColumns.MAILBOX_KEY + "=?",
new String[] {Long.toString(mailbox.mId)});
- return;
+ return true;
}
LogUtils.d(TAG, "About to sync mailbox: " + mailbox.mDisplayName);
@@ -147,6 +151,7 @@ public class PopImapSyncAdapterService extends Service {
}
EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, status, 0,
lastSyncResult);
+ return true;
}
} catch (MessagingException e) {
final int type = e.getExceptionType();
@@ -186,6 +191,7 @@ public class PopImapSyncAdapterService extends Service {
values.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
resolver.update(mailboxUri, values, null, null);
}
+ return false;
}
/**
@@ -247,7 +253,8 @@ public class PopImapSyncAdapterService extends Service {
// from the account settings. Otherwise just sync the inbox.
if (info.offerLookback) {
mailboxIds = getLoopBackMailboxIdsForSync(context, acct);
- } else {
+ }
+ if (mailboxIds.length == 0) {
final long inboxId = Mailbox.findMailboxOfType(context, acct.mId,
Mailbox.TYPE_INBOX);
if (inboxId != Mailbox.NO_MAILBOX) {
@@ -262,9 +269,20 @@ public class PopImapSyncAdapterService extends Service {
extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
int deltaMessageCount =
extras.getInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, 0);
+ boolean success = mailboxIds.length > 0;
for (long mailboxId : mailboxIds) {
- sync(context, mailboxId, extras, syncResult, uiRefresh,
- deltaMessageCount);
+ boolean result = sync(context, mailboxId, extras, syncResult,
+ uiRefresh, deltaMessageCount);
+ if (!result) {
+ success = false;
+ }
+ }
+
+ // Initial sync performed?
+ if (success) {
+ // All mailboxes (that need a sync) are now synced. Assume we
+ // have a valid sync key, in case this account has push support
+ markAsInitialSyncKey(context, acct.mId);
}
}
}
@@ -278,6 +296,14 @@ public class PopImapSyncAdapterService extends Service {
}
}
+ private static void markAsInitialSyncKey(Context context, long accountId) {
+ ContentResolver resolver = context.getContentResolver();
+ Uri accountUri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
+ ContentValues values = new ContentValues();
+ values.put(AccountColumns.SYNC_KEY, "1");
+ resolver.update(accountUri, values, null, null);
+ }
+
private static boolean isLegacyImapProtocol(Context ctx, Account acct) {
if (sLegacyImapProtocol == null) {
sLegacyImapProtocol = ctx.getString(R.string.protocol_legacy_imap);
diff --git a/res/xml/services.xml b/res/xml/services.xml
index 1bd882306..aaebd6c90 100644
--- a/res/xml/services.xml
+++ b/res/xml/services.xml
@@ -74,8 +74,8 @@
email:serviceClass="com.android.email.service.ImapService"
email:port="143"
email:portSsl="993"
- email:syncIntervalStrings="@array/account_settings_check_frequency_entries"
- email:syncIntervals="@array/account_settings_check_frequency_values"
+ email:syncIntervalStrings="@array/account_settings_check_frequency_entries_push"
+ email:syncIntervals="@array/account_settings_check_frequency_values_push"
email:defaultSyncInterval="mins15"
email:offerTls="true"
diff --git a/res/xml/syncadapter_legacy_imap.xml b/res/xml/syncadapter_legacy_imap.xml
index 6ad6ee140..09be31a6d 100644
--- a/res/xml/syncadapter_legacy_imap.xml
+++ b/res/xml/syncadapter_legacy_imap.xml
@@ -23,5 +23,6 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="@string/authority_email_provider"
android:accountType="@string/account_manager_type_legacy_imap"
+ android:allowParallelSyncs="true"
android:supportsUploading="true"
/>
diff --git a/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java b/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java
index a9c1a9691..f11f54cd2 100644
--- a/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java
+++ b/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java
@@ -417,6 +417,10 @@ public class AccountCheckSettingsFragment extends Fragment {
EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE);
return new MessagingException(resultCode, errorMessage);
}
+
+ // Save account capabilities
+ mAccount.mCapabilities = bundle.getInt(
+ EmailServiceProxy.SETTINGS_BUNDLE_CAPABILITIES, 0);
}
final EmailServiceInfo info;
diff --git a/src/com/android/email/activity/setup/AccountSettingsFragment.java b/src/com/android/email/activity/setup/AccountSettingsFragment.java
index 97d4f9ea8..26e476455 100644
--- a/src/com/android/email/activity/setup/AccountSettingsFragment.java
+++ b/src/com/android/email/activity/setup/AccountSettingsFragment.java
@@ -69,6 +69,7 @@ import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.Policy;
+import com.android.emailcommon.service.EmailServiceProxy;
import com.android.mail.preferences.AccountPreferences;
import com.android.mail.preferences.FolderPreferences;
import com.android.mail.preferences.FolderPreferences.NotificationLight;
@@ -84,7 +85,9 @@ import com.android.mail.utils.LogUtils;
import com.android.mail.utils.NotificationUtils;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -243,10 +246,7 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
final CharSequence [] syncIntervals =
savedInstanceState.getCharSequenceArray(SAVESTATE_SYNC_INTERVALS);
mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY);
- if (mCheckFrequency != null) {
- mCheckFrequency.setEntries(syncIntervalStrings);
- mCheckFrequency.setEntryValues(syncIntervals);
- }
+ fillCheckFrecuency(syncIntervalStrings, syncIntervals);
}
}
@@ -382,16 +382,15 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
final android.accounts.Account androidAcct = new android.accounts.Account(
mAccount.mEmailAddress, mServiceInfo.accountType);
if (Integer.parseInt(summary) == Account.CHECK_INTERVAL_NEVER) {
- // Disable syncing from the account manager. Leave the current sync frequency
- // in the database.
+ // Disable syncing from the account manager.
ContentResolver.setSyncAutomatically(androidAcct, EmailContent.AUTHORITY,
false);
} else {
// Enable syncing from the account manager.
ContentResolver.setSyncAutomatically(androidAcct, EmailContent.AUTHORITY,
true);
- cv.put(AccountColumns.SYNC_INTERVAL, Integer.parseInt(summary));
}
+ cv.put(AccountColumns.SYNC_INTERVAL, Integer.parseInt(summary));
}
} else if (key.equals(PREFERENCE_SYNC_WINDOW)) {
final String summary = newValue.toString();
@@ -749,8 +748,7 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
R.string.preferences_signature_summary_not_set);
mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY);
- mCheckFrequency.setEntries(mServiceInfo.syncIntervalStrings);
- mCheckFrequency.setEntryValues(mServiceInfo.syncIntervals);
+ fillCheckFrecuency(mServiceInfo.syncIntervalStrings, mServiceInfo.syncIntervals);
if (mServiceInfo.syncContacts || mServiceInfo.syncCalendar) {
// This account allows syncing of contacts and/or calendar, so we will always have
// separate preferences to enable or disable syncing of email, contacts, and calendar.
@@ -1182,4 +1180,28 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
}
mInboxLights.setOn(notificationLight.mOn);
}
+
+ private void fillCheckFrecuency(CharSequence[] labels, CharSequence[] values) {
+ if (mCheckFrequency == null) {
+ return;
+ }
+
+ // Check push capability prior to include as an option
+ if (mAccount != null) {
+ boolean hasPushCapability = mAccount.hasCapability(EmailServiceProxy.CAPABILITY_PUSH);
+ List<CharSequence> valuesList = new ArrayList<>(Arrays.asList(values));
+ int checkIntervalPushPos = valuesList.indexOf(
+ String.valueOf(Account.CHECK_INTERVAL_PUSH));
+ if (!hasPushCapability && checkIntervalPushPos != -1) {
+ List<CharSequence> labelsList = new ArrayList<>(Arrays.asList(labels));
+ labelsList.remove(checkIntervalPushPos);
+ valuesList.remove(checkIntervalPushPos);
+ labels = labelsList.toArray(new CharSequence[labelsList.size()]);
+ values = valuesList.toArray(new CharSequence[valuesList.size()]);
+ }
+ }
+ mCheckFrequency.setEntries(labels);
+ mCheckFrequency.setEntryValues(values);
+ mCheckFrequency.setDefaultValue(values);
+ }
}
diff --git a/src/com/android/email/activity/setup/AccountSetupFinal.java b/src/com/android/email/activity/setup/AccountSetupFinal.java
index adaa32aa1..5386a7e4a 100644
--- a/src/com/android/email/activity/setup/AccountSetupFinal.java
+++ b/src/com/android/email/activity/setup/AccountSetupFinal.java
@@ -917,7 +917,7 @@ public class AccountSetupFinal extends AccountSetupActivity
public void setDefaultsForProtocol(Account account) {
final EmailServiceUtils.EmailServiceInfo info = mSetupData.getIncomingServiceInfo(this);
if (info == null) return;
- account.mSyncInterval = info.defaultSyncInterval;
+ account.setSyncInterval(info.defaultSyncInterval);
account.mSyncLookback = info.defaultLookback;
if (info.offerLocalDeletes) {
account.setDeletePolicy(info.defaultLocalDeletes);
diff --git a/src/com/android/email/activity/setup/AccountSetupOptionsFragment.java b/src/com/android/email/activity/setup/AccountSetupOptionsFragment.java
index 9d048c119..287a0d323 100644
--- a/src/com/android/email/activity/setup/AccountSetupOptionsFragment.java
+++ b/src/com/android/email/activity/setup/AccountSetupOptionsFragment.java
@@ -29,8 +29,13 @@ import com.android.email.activity.UiUtilities;
import com.android.email.service.EmailServiceUtils;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.Policy;
+import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.SyncWindow;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
public class AccountSetupOptionsFragment extends AccountSetupFragment {
private Spinner mCheckFrequencyView;
private Spinner mSyncWindowView;
@@ -90,11 +95,24 @@ public class AccountSetupOptionsFragment extends AccountSetupFragment {
final CharSequence[] frequencyEntries = serviceInfo.syncIntervalStrings;
// Now create the array used by the sync interval Spinner
- final SpinnerOption[] checkFrequencies = new SpinnerOption[frequencyEntries.length];
+ int checkIntervalPushPos = -1;
+ SpinnerOption[] checkFrequencies = new SpinnerOption[frequencyEntries.length];
for (int i = 0; i < frequencyEntries.length; i++) {
- checkFrequencies[i] = new SpinnerOption(
- Integer.valueOf(frequencyValues[i].toString()), frequencyEntries[i].toString());
+ Integer value = Integer.valueOf(frequencyValues[i].toString());
+ if (value.intValue() == Account.CHECK_INTERVAL_PUSH) {
+ checkIntervalPushPos = i;
+ }
+ checkFrequencies[i] = new SpinnerOption(value, frequencyEntries[i].toString());
}
+
+ // Ensure that push capability is supported by the server
+ boolean hasPushCapability = account.hasCapability(EmailServiceProxy.CAPABILITY_PUSH);
+ if (!hasPushCapability && checkIntervalPushPos != -1) {
+ List<SpinnerOption> options = new ArrayList<>(Arrays.asList(checkFrequencies));
+ options.remove(checkIntervalPushPos);
+ checkFrequencies = options.toArray(new SpinnerOption[options.size()]);
+ }
+
final ArrayAdapter<SpinnerOption> checkFrequenciesAdapter =
new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_item,
checkFrequencies);