diff options
author | Jorge Ruesga <jorge@ruesga.com> | 2015-05-01 21:35:23 +0200 |
---|---|---|
committer | Steve Kondik <steve@cyngn.com> | 2015-10-18 14:05:32 -0700 |
commit | 08ace26ed605946d788ce56f5c9aefc65131a63b (patch) | |
tree | f1014dc39087078bf19111b76427ab58ba78ad4b /provider_src/com/android/email/mail/store/ImapFolder.java | |
parent | 3b1b30873e9c07139e2cc9fdaa796151592fea69 (diff) | |
download | android_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>
Diffstat (limited to 'provider_src/com/android/email/mail/store/ImapFolder.java')
-rw-r--r-- | provider_src/com/android/email/mail/store/ImapFolder.java | 377 |
1 files changed, 375 insertions, 2 deletions
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) { |