email: imap push

Change-Id: I8a184a5644e4322ee65d969e14cd47fe119f5df2
Signed-off-by: Jorge Ruesga <jorge@ruesga.com>
This commit is contained in:
Jorge Ruesga 2015-05-01 21:35:23 +02:00 committed by Steve Kondik
parent 3b1b30873e
commit 08ace26ed6
29 changed files with 2097 additions and 58 deletions

View File

@ -31,6 +31,7 @@ import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.os.RemoteException; import android.os.RemoteException;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.utility.Utility; import com.android.emailcommon.utility.Utility;
import com.android.mail.utils.LogUtils; import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting; 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 // Sentinel values for the mSyncInterval field of both Account records
public static final int CHECK_INTERVAL_NEVER = -1; public static final int CHECK_INTERVAL_NEVER = -1;
public static final int CHECK_INTERVAL_PUSH = -2; 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 CONTENT_URI;
public static Uri RESET_NEW_MESSAGE_COUNT_URI; public static Uri RESET_NEW_MESSAGE_COUNT_URI;
public static Uri NOTIFIER_URI; public static Uri NOTIFIER_URI;
public static Uri SYNC_SETTING_CHANGED_URI;
public static void initAccount() { public static void initAccount() {
CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account"); CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account");
RESET_NEW_MESSAGE_COUNT_URI = Uri.parse(EmailContent.CONTENT_URI + "/resetNewMessageCount"); RESET_NEW_MESSAGE_COUNT_URI = Uri.parse(EmailContent.CONTENT_URI + "/resetNewMessageCount");
NOTIFIER_URI = Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/account"); 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 mDisplayName;
public String mEmailAddress; public String mEmailAddress;
public String mSyncKey; public String mSyncKey;
public int mSyncLookback; public int mSyncLookback;
public int mSyncInterval; private int mSyncInterval;
public long mHostAuthKeyRecv; public long mHostAuthKeyRecv;
public long mHostAuthKeySend; public long mHostAuthKeySend;
public int mFlags; public int mFlags;
@ -139,6 +144,7 @@ public final class Account extends EmailContent implements Parcelable {
public String mSignature; public String mSignature;
public long mPolicyKey; public long mPolicyKey;
public long mPingDuration; public long mPingDuration;
public int mCapabilities;
@VisibleForTesting @VisibleForTesting
static final String JSON_TAG_HOST_AUTH_RECV = "hostAuthRecv"; 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_POLICY_KEY_COLUMN = 14;
public static final int CONTENT_PING_DURATION_COLUMN = 15; public static final int CONTENT_PING_DURATION_COLUMN = 15;
public static final int CONTENT_MAX_ATTACHMENT_SIZE_COLUMN = 16; public static final int CONTENT_MAX_ATTACHMENT_SIZE_COLUMN = 16;
public static final int CONTENT_CAPABILITIES_COLUMN = 17;
public static final String[] CONTENT_PROJECTION = { public static final String[] CONTENT_PROJECTION = {
AttachmentColumns._ID, AccountColumns.DISPLAY_NAME, AttachmentColumns._ID, AccountColumns.DISPLAY_NAME,
@ -181,7 +188,7 @@ public final class Account extends EmailContent implements Parcelable {
AccountColumns.RINGTONE_URI, AccountColumns.PROTOCOL_VERSION, AccountColumns.RINGTONE_URI, AccountColumns.PROTOCOL_VERSION,
AccountColumns.SECURITY_SYNC_KEY, AccountColumns.SECURITY_SYNC_KEY,
AccountColumns.SIGNATURE, AccountColumns.POLICY_KEY, AccountColumns.PING_DURATION, 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; 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); mSignature = cursor.getString(CONTENT_SIGNATURE_COLUMN);
mPolicyKey = cursor.getLong(CONTENT_POLICY_KEY_COLUMN); mPolicyKey = cursor.getLong(CONTENT_POLICY_KEY_COLUMN);
mPingDuration = cursor.getLong(CONTENT_PING_DURATION_COLUMN); mPingDuration = cursor.getLong(CONTENT_PING_DURATION_COLUMN);
mCapabilities = cursor.getInt(CONTENT_CAPABILITIES_COLUMN);
} }
public boolean isTemporary() { 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 * TODO define sentinel values for "never", "push", etc. See Account.java
*/ */
public int getSyncInterval() { 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; return mSyncInterval;
} }
@ -367,7 +380,13 @@ public final class Account extends EmailContent implements Parcelable {
* @param minutes the number of minutes between polling checks * @param minutes the number of minutes between polling checks
*/ */
public void setSyncInterval(int minutes) { 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;
}
} }
/** /**
@ -402,6 +421,20 @@ public final class Account extends EmailContent implements Parcelable {
mPingDuration = value; mPingDuration = value;
} }
/**
* @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 * @return the flags for this account
*/ */
@ -749,6 +782,7 @@ public final class Account extends EmailContent implements Parcelable {
values.put(AccountColumns.SIGNATURE, mSignature); values.put(AccountColumns.SIGNATURE, mSignature);
values.put(AccountColumns.POLICY_KEY, mPolicyKey); values.put(AccountColumns.POLICY_KEY, mPolicyKey);
values.put(AccountColumns.PING_DURATION, mPingDuration); values.put(AccountColumns.PING_DURATION, mPingDuration);
values.put(AccountColumns.CAPABILITIES, mCapabilities);
return values; return values;
} }
@ -779,6 +813,7 @@ public final class Account extends EmailContent implements Parcelable {
json.putOpt(AccountColumns.PROTOCOL_VERSION, mProtocolVersion); json.putOpt(AccountColumns.PROTOCOL_VERSION, mProtocolVersion);
json.putOpt(AccountColumns.SIGNATURE, mSignature); json.putOpt(AccountColumns.SIGNATURE, mSignature);
json.put(AccountColumns.PING_DURATION, mPingDuration); json.put(AccountColumns.PING_DURATION, mPingDuration);
json.put(AccountColumns.CAPABILITIES, mCapabilities);
return json; return json;
} catch (final JSONException e) { } catch (final JSONException e) {
LogUtils.d(LogUtils.TAG, e, "Exception while serializing Account"); 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); a.mSignature = json.optString(AccountColumns.SIGNATURE);
// POLICY_KEY is not stored // POLICY_KEY is not stored
a.mPingDuration = json.optInt(AccountColumns.PING_DURATION, 0); a.mPingDuration = json.optInt(AccountColumns.PING_DURATION, 0);
a.mCapabilities = json.optInt(AccountColumns.CAPABILITIES, 0);
return a; return a;
} catch (final JSONException e) { } catch (final JSONException e) {
LogUtils.d(LogUtils.TAG, e, "Exception while deserializing Account"); LogUtils.d(LogUtils.TAG, e, "Exception while deserializing Account");
@ -842,6 +878,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 * Supports Parcelable
*/ */
@ -903,6 +947,7 @@ public final class Account extends EmailContent implements Parcelable {
} else { } else {
dest.writeByte((byte)0); dest.writeByte((byte)0);
} }
dest.writeInt(mCapabilities);
} }
/** /**
@ -937,6 +982,7 @@ public final class Account extends EmailContent implements Parcelable {
if (in.readByte() == 1) { if (in.readByte() == 1) {
mHostAuthSend = new HostAuth(in); mHostAuthSend = new HostAuth(in);
} }
mCapabilities = in.readInt();
} }
/** /**

View File

@ -145,6 +145,8 @@ public abstract class EmailContent {
// delete, or update) and is intended as an optimization for use by clients of message list // delete, or update) and is intended as an optimization for use by clients of message list
// cursors (initially, the email AppWidget). // cursors (initially, the email AppWidget).
public static String NOTIFIER_AUTHORITY; 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 Uri CONTENT_URI;
public static final String PARAMETER_LIMIT = "limit"; 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 final String SUPPRESS_COMBINED_ACCOUNT_PARAM = "suppress_combined";
public static Uri CONTENT_NOTIFIER_URI; 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_TRASH_FOLDER_URI;
public static Uri PICK_SENT_FOLDER_URI; public static Uri PICK_SENT_FOLDER_URI;
public static Uri MAILBOX_NOTIFICATION_URI; public static Uri MAILBOX_NOTIFICATION_URI;
@ -175,8 +178,11 @@ public abstract class EmailContent {
AUTHORITY = EMAIL_PACKAGE_NAME + ".provider"; AUTHORITY = EMAIL_PACKAGE_NAME + ".provider";
LogUtils.d("EmailContent", "init for " + AUTHORITY); LogUtils.d("EmailContent", "init for " + AUTHORITY);
NOTIFIER_AUTHORITY = EMAIL_PACKAGE_NAME + ".notifier"; NOTIFIER_AUTHORITY = EMAIL_PACKAGE_NAME + ".notifier";
SYNC_SETTING_CHANGED_AUTHORITY = EMAIL_PACKAGE_NAME + ".sync_setting_changed";
CONTENT_URI = Uri.parse("content://" + AUTHORITY); CONTENT_URI = Uri.parse("content://" + AUTHORITY);
CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_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_TRASH_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickTrashFolder");
PICK_SENT_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickSentFolder"); PICK_SENT_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickSentFolder");
MAILBOX_NOTIFICATION_URI = Uri.parse("content://" + AUTHORITY + "/mailboxNotification"); MAILBOX_NOTIFICATION_URI = Uri.parse("content://" + AUTHORITY + "/mailboxNotification");
@ -1724,6 +1730,8 @@ public abstract class EmailContent {
public static final String PING_DURATION = "pingDuration"; public static final String PING_DURATION = "pingDuration";
// Automatically fetch pop3 attachments // Automatically fetch pop3 attachments
public static final String AUTO_FETCH_ATTACHMENTS = "autoFetchAttachments"; public static final String AUTO_FETCH_ATTACHMENTS = "autoFetchAttachments";
// Account capabilities (check EmailServiceProxy#CAPABILITY_*)
public static final String CAPABILITIES = "capabilities";
} }
public interface QuickResponseColumns extends BaseColumns { public interface QuickResponseColumns extends BaseColumns {

View File

@ -78,10 +78,13 @@ public class Mailbox extends EmailContent implements EmailContent.MailboxColumns
public static Uri CONTENT_URI; public static Uri CONTENT_URI;
public static Uri MESSAGE_COUNT_URI; public static Uri MESSAGE_COUNT_URI;
public static Uri SYNC_SETTING_CHANGED_URI;
public static void initMailbox() { public static void initMailbox() {
CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailbox"); CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailbox");
MESSAGE_COUNT_URI = Uri.parse(EmailContent.CONTENT_URI + "/mailboxCount"); 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) { private static String formatMailboxIdExtra(final int index) {

View File

@ -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_PROTOCOL_VERSION = "validate_protocol_version";
public static final String VALIDATE_BUNDLE_REDIRECT_ADDRESS = "validate_redirect_address"; 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 Object mReturn = null;
private IEmailService mService; private IEmailService mService;
private final boolean isRemote; private final boolean isRemote;

View File

@ -165,6 +165,13 @@ public class EmailConnectivityManager extends BroadcastReceiver {
return info.getType(); 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() { public void waitForConnectivity() {
// If we're unregistered, throw an exception // If we're unregistered, throw an exception
if (!mRegistered) { if (!mRegistered) {

View File

@ -107,7 +107,7 @@ public class AccountSettingsUtils {
cv.put(AccountColumns.DISPLAY_NAME, account.getDisplayName()); cv.put(AccountColumns.DISPLAY_NAME, account.getDisplayName());
cv.put(AccountColumns.SENDER_NAME, account.getSenderName()); cv.put(AccountColumns.SENDER_NAME, account.getSenderName());
cv.put(AccountColumns.SIGNATURE, account.getSignature()); 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.FLAGS, account.mFlags);
cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback); cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback);
cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey); cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey);

View File

@ -36,6 +36,7 @@ import com.android.emailcommon.mail.MessagingException;
import com.android.mail.utils.LogUtils; import com.android.mail.utils.LogUtils;
import java.io.IOException; import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -50,6 +51,15 @@ class ImapConnection {
// Always check in FALSE // Always check in FALSE
private static final boolean DEBUG_FORCE_SEND_ID = 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*/ /** ID capability per RFC 2971*/
public static final int CAPABILITY_ID = 1 << 0; public static final int CAPABILITY_ID = 1 << 0;
/** NAMESPACE capability per RFC 2342 */ /** NAMESPACE capability per RFC 2342 */
@ -58,6 +68,8 @@ class ImapConnection {
public static final int CAPABILITY_STARTTLS = 1 << 2; public static final int CAPABILITY_STARTTLS = 1 << 2;
/** UIDPLUS capability per RFC 4315 */ /** UIDPLUS capability per RFC 4315 */
public static final int CAPABILITY_UIDPLUS = 1 << 3; 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. */ /** The capabilities supported; a set of CAPABILITY_* values. */
private int mCapabilities; private int mCapabilities;
@ -69,6 +81,8 @@ class ImapConnection {
private String mAccessToken; private String mAccessToken;
private String mIdPhrase = null; private String mIdPhrase = null;
private boolean mIdling = false;
/** # of command/response lines to log upon crash. */ /** # of command/response lines to log upon crash. */
private static final int DISCOURSE_LOGGER_SIZE = 64; private static final int DISCOURSE_LOGGER_SIZE = 64;
private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE); private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
@ -210,10 +224,23 @@ class ImapConnection {
mImapStore = null; 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. * 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; return (mCapabilities & capability) != 0;
} }
@ -235,6 +262,9 @@ class ImapConnection {
if (capabilities.contains(ImapConstants.STARTTLS)) { if (capabilities.contains(ImapConstants.STARTTLS)) {
mCapabilities |= CAPABILITY_STARTTLS; mCapabilities |= CAPABILITY_STARTTLS;
} }
if (capabilities.contains(ImapConstants.IDLE)) {
mCapabilities |= CAPABILITY_IDLE;
}
} }
/** /**
@ -273,6 +303,12 @@ class ImapConnection {
*/ */
String sendCommand(String command, boolean sensitive) String sendCommand(String command, boolean sensitive)
throws MessagingException, IOException { 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)); LogUtils.d(Logging.LOG_TAG, "sendCommand %s", (sensitive ? IMAP_REDACTED_LOG : command));
open(); open();
return sendCommandInternal(command, sensitive); return sendCommandInternal(command, sensitive);
@ -284,7 +320,13 @@ class ImapConnection {
throw new IOException("Null transport"); throw new IOException("Null transport");
} }
String tag = Integer.toString(mNextCommandTag.incrementAndGet()); 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); mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend); mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
return tag; return tag;
@ -327,6 +369,11 @@ class ImapConnection {
return executeSimpleCommand(command, false); 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 * 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 { List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
final List<ImapResponse> responses = new ArrayList<ImapResponse>(); final List<ImapResponse> responses = new ArrayList<ImapResponse>();
ImapResponse response; ImapResponse response = null;
do { boolean idling = false;
response = mParser.readResponse(); boolean throwSocketTimeoutEx = true;
responses.add(response); int lastSocketTimeout = getReadTimeout();
} while (!response.isTagged()); 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 toString = response.toString();
final String status = response.getStatusOrEmpty().getString(); final String status = response.getStatusOrEmpty().getString();
final String alert = response.getAlertTextOrEmpty().getString(); final String alert = response.getAlertTextOrEmpty().getString();

View File

@ -52,6 +52,8 @@ import com.android.emailcommon.utility.Utility;
import com.android.mail.utils.LogUtils; import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import static com.android.emailcommon.Logging.LOG_TAG;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import java.io.File; import java.io.File;
@ -60,6 +62,7 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -68,13 +71,39 @@ import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.TimeZone; import java.util.TimeZone;
class ImapFolder extends Folder { public class ImapFolder extends Folder {
private final static Flag[] PERMANENT_FLAGS = private final static Flag[] PERMANENT_FLAGS =
{ Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED }; { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
private static final int COPY_BUFFER_SIZE = 16*1024; 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 ImapStore mStore;
private final String mName; private final String mName;
private int mMessageCount = -1; private int mMessageCount = -1;
@ -86,6 +115,22 @@ class ImapFolder extends Folder {
/** A set of hashes that can be used to track dirtiness */ /** A set of hashes that can be used to track dirtiness */
Object mHash[]; 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) { /*package*/ ImapFolder(ImapStore store, String name) {
mStore = store; mStore = store;
mName = name; mName = name;
@ -176,6 +221,159 @@ class ImapFolder extends Folder {
return mName; 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 @Override
public boolean exists() throws MessagingException { public boolean exists() throws MessagingException {
if (mExists) { 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 @Override
public void delete(boolean recurse) { public void delete(boolean recurse) {
throw new Error("ImapStore.delete() not yet implemented"); throw new Error("ImapStore.delete() not yet implemented");
@ -1270,7 +1520,9 @@ class ImapFolder extends Folder {
if (DebugUtils.DEBUG) { if (DebugUtils.DEBUG) {
LogUtils.d(Logging.LOG_TAG, "IO Exception detected: ", ioe); LogUtils.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
} }
connection.close(); if (connection != null) {
connection.close();
}
if (connection == mConnection) { if (connection == mConnection) {
mConnection = null; // To prevent close() from returning the connection to the pool. mConnection = null; // To prevent close() from returning the connection to the pool.
close(false); close(false);
@ -1278,6 +1530,127 @@ class ImapFolder extends Folder {
return new MessagingException(MessagingException.IOERROR, "IO Error", ioe); 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o instanceof ImapFolder) { if (o instanceof ImapFolder) {

View File

@ -501,6 +501,14 @@ public class ImapStore extends Store {
connection.destroyResponses(); connection.destroyResponses();
} }
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); 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; return bundle;
} }
@ -556,6 +564,7 @@ public class ImapStore extends Store {
while ((connection = mConnectionPool.poll()) != null) { while ((connection = mConnectionPool.poll()) != null) {
try { try {
connection.setStore(this); connection.setStore(this);
connection.setReadTimeout(MailTransport.SOCKET_READ_TIMEOUT);
connection.executeSimpleCommand(ImapConstants.NOOP); connection.executeSimpleCommand(ImapConstants.NOOP);
break; break;
} catch (MessagingException e) { } catch (MessagingException e) {

View File

@ -186,6 +186,10 @@ public class Pop3Store extends Store {
ioe.getMessage()); ioe.getMessage());
} }
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
// No special capabilities
bundle.putInt(EmailServiceProxy.SETTINGS_BUNDLE_CAPABILITIES, 0);
return bundle; return bundle;
} }

View File

@ -46,6 +46,7 @@ public final class ImapConstants {
public static final String COPYUID = "COPYUID"; public static final String COPYUID = "COPYUID";
public static final String CREATE = "CREATE"; public static final String CREATE = "CREATE";
public static final String DELETE = "DELETE"; public static final String DELETE = "DELETE";
public static final String DONE = "DONE";
public static final String EXAMINE = "EXAMINE"; public static final String EXAMINE = "EXAMINE";
public static final String EXISTS = "EXISTS"; public static final String EXISTS = "EXISTS";
public static final String EXPUNGE = "EXPUNGE"; 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 = "FLAGS";
public static final String FLAGS_SILENT = "FLAGS.SILENT"; public static final String FLAGS_SILENT = "FLAGS.SILENT";
public static final String ID = "ID"; 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 INBOX = "INBOX";
public static final String INTERNALDATE = "INTERNALDATE"; public static final String INTERNALDATE = "INTERNALDATE";
public static final String LIST = "LIST"; public static final String LIST = "LIST";
@ -73,6 +76,7 @@ public final class ImapConstants {
public static final String PREAUTH = "PREAUTH"; public static final String PREAUTH = "PREAUTH";
public static final String READ_ONLY = "READ-ONLY"; public static final String READ_ONLY = "READ-ONLY";
public static final String READ_WRITE = "READ-WRITE"; public static final String READ_WRITE = "READ-WRITE";
public static final String RECENT = "RECENT";
public static final String RENAME = "RENAME"; public static final String RENAME = "RENAME";
public static final String RFC822_SIZE = "RFC822.SIZE"; public static final String RFC822_SIZE = "RFC822.SIZE";
public static final String SEARCH = "SEARCH"; public static final String SEARCH = "SEARCH";

View File

@ -180,7 +180,7 @@ public class ImapList extends ImapElement {
@Override @Override
public String toString() { public String toString() {
return mList.toString(); return mList != null ? mList.toString() : "[null]";
} }
/** /**

View File

@ -76,6 +76,13 @@ public class ImapResponse extends ImapList {
return is(0, ImapConstants.NO); return is(0, ImapConstants.NO);
} }
/**
* @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). * @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 index where {@code responseType} should appear. e.g. 1 for "FETCH"

View File

@ -66,6 +66,9 @@ public class ImapResponseParser {
*/ */
private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>(); 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 * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated
* in the same way EOF does. * in the same way EOF does.
@ -168,10 +171,17 @@ public class ImapResponseParser {
} catch (RuntimeException e) { } catch (RuntimeException e) {
// Parser crash -- log network activities. // Parser crash -- log network activities.
onParseError(e); onParseError(e);
mIdling = false;
throw e; throw e;
} catch (IOException e) { } catch (IOException e) {
// Network error, or received an unexpected char. // 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; throw e;
} }
@ -242,6 +252,14 @@ public class ImapResponseParser {
return ret; return ret;
} }
public void resetIdlingStatus() {
mIdling = false;
}
public void expectIdlingResponse() {
mExpectIdlingResponse = true;
}
/** /**
* Parse and return the response line. * Parse and return the response line.
*/ */
@ -263,11 +281,26 @@ public class ImapResponseParser {
responseToDestroy = new ImapResponse(null, true); responseToDestroy = new ImapResponse(null, true);
// If it's continuation request, we don't really care what's in it. // 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. // Response has successfully been built. Let's return it.
responseToReturn = responseToDestroy; responseToReturn = responseToDestroy;
responseToDestroy = null; responseToDestroy = null;
mIdling = responseToReturn.isIdling();
if (mIdling) {
mExpectIdlingResponse = true;
}
} else { } else {
// Status response or response data // Status response or response data
final String tag; final String tag;

View File

@ -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 * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
* service but is not in the public API. * service but is not in the public API.

View File

@ -58,6 +58,7 @@ import com.android.emailcommon.provider.MessageStateChange;
import com.android.emailcommon.provider.Policy; import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.provider.QuickResponse; import com.android.emailcommon.provider.QuickResponse;
import com.android.emailcommon.provider.SuggestedContact; import com.android.emailcommon.provider.SuggestedContact;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.LegacyPolicySet; import com.android.emailcommon.service.LegacyPolicySet;
import com.android.emailcommon.service.SyncWindow; import com.android.emailcommon.service.SyncWindow;
import com.android.mail.providers.UIProvider; 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 127: Force mFlags to contain the correct flags for EAS accounts given a protocol
// version above 12.0 // version above 12.0
// Version 129: Update all IMAP INBOX mailboxes to force synchronization // 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. // Any changes to the database format *must* include update-in-place code.
// Original version: 2 // Original version: 2
@ -525,7 +527,8 @@ public final class DBHelper {
+ AccountColumns.POLICY_KEY + " integer, " + AccountColumns.POLICY_KEY + " integer, "
+ AccountColumns.MAX_ATTACHMENT_SIZE + " integer, " + AccountColumns.MAX_ATTACHMENT_SIZE + " integer, "
+ AccountColumns.PING_DURATION + " 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); db.execSQL("create table " + Account.TABLE_NAME + s);
// Deleting an account deletes associated Mailboxes and HostAuth's // Deleting an account deletes associated Mailboxes and HostAuth's
@ -1562,6 +1565,52 @@ public final class DBHelper {
+ HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + "='imap'));"); + 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, // Due to a bug in commit 44a064e5f16ddaac25f2acfc03c118f65bc48aec,
// AUTO_FETCH_ATTACHMENTS column could not be available in the Account table. // 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 // Since cm12 and up doesn't use this column, we are leave as is it. In case

View File

@ -189,11 +189,11 @@ public class EmailProvider extends ContentProvider
"vnd.android.cursor.item/email-attachment"; "vnd.android.cursor.item/email-attachment";
/** Appended to the notification URI for delete operations */ /** 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 */ /** 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 */ /** 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. */ /** The query string to trigger a folder refresh. */
protected static String QUERY_UIREFRESH = "uirefresh"; protected static String QUERY_UIREFRESH = "uirefresh";
@ -833,6 +833,7 @@ public class EmailProvider extends ContentProvider
// Notify all notifier cursors // Notify all notifier cursors
sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id); sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id);
sendSyncSettingChanged(getBaseSyncSettingChangedUri(match), NOTIFICATION_OP_DELETE, id);
// Notify all email content cursors // Notify all email content cursors
notifyUI(EmailContent.CONTENT_URI, null); notifyUI(EmailContent.CONTENT_URI, null);
@ -1075,6 +1076,7 @@ public class EmailProvider extends ContentProvider
// Notify all notifier cursors // Notify all notifier cursors
sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id); sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id);
sendSyncSettingChanged(getBaseSyncSettingChangedUri(match), NOTIFICATION_OP_INSERT, id);
// Notify all existing cursors. // Notify all existing cursors.
notifyUI(EmailContent.CONTENT_URI, null); notifyUI(EmailContent.CONTENT_URI, null);
@ -1924,7 +1926,7 @@ public class EmailProvider extends ContentProvider
private static final int INDEX_SYNC_KEY = 2; 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 context A {@link Context}.
* @param db The {@link SQLiteDatabase}. * @param db The {@link SQLiteDatabase}.
* @param id The id of the thing we're looking for. * @param id The id of the thing we're looking for.
@ -1937,9 +1939,13 @@ public class EmailProvider extends ContentProvider
try { try {
if (c.moveToFirst()) { if (c.moveToFirst()) {
final String protocol = c.getString(INDEX_PROTOCOL); final String protocol = c.getString(INDEX_PROTOCOL);
// Only restart push for EAS accounts that have completed initial sync. final String syncKey = c.getString(INDEX_SYNC_KEY);
if (context.getString(R.string.protocol_eas).equals(protocol) && final boolean supportsPush =
!EmailContent.isInitialSyncKey(c.getString(INDEX_SYNC_KEY))) { 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 String emailAddress = c.getString(INDEX_EMAIL_ADDRESS);
final android.accounts.Account account = final android.accounts.Account account =
getAccountManagerAccount(context, emailAddress, protocol); getAccountManagerAccount(context, emailAddress, protocol);
@ -2010,6 +2016,7 @@ public class EmailProvider extends ContentProvider
final SQLiteDatabase db = getDatabase(context); final SQLiteDatabase db = getDatabase(context);
final int table = match >> BASE_SHIFT; final int table = match >> BASE_SHIFT;
int result; int result;
boolean syncSettingChanged = false;
// We do NOT allow setting of unreadCount/messageCount via the provider // We do NOT allow setting of unreadCount/messageCount via the provider
// These columns are maintained via triggers // These columns are maintained via triggers
@ -2159,6 +2166,14 @@ public class EmailProvider extends ContentProvider
} }
} else if (match == MESSAGE_ID) { } else if (match == MESSAGE_ID) {
db.execSQL(UPDATED_MESSAGE_DELETE + 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), result = db.update(tableName, values, whereWithId(id, selection),
selectionArgs); selectionArgs);
@ -2293,6 +2308,10 @@ public class EmailProvider extends ContentProvider
TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) { TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
LogUtils.w(TAG, new Throwable(), "attachment with blank 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); result = db.update(tableName, values, selection, selectionArgs);
break; break;
@ -2314,6 +2333,10 @@ public class EmailProvider extends ContentProvider
// Notify all notifier cursors if some records where changed in the database // Notify all notifier cursors if some records where changed in the database
if (result > 0) { if (result > 0) {
sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id); sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id);
if (syncSettingChanged) {
sendSyncSettingChanged(getBaseSyncSettingChangedUri(match),
NOTIFICATION_OP_UPDATE, id);
}
notifyUI(notificationUri, null); notifyUI(notificationUri, null);
} }
return result; return result;
@ -2544,6 +2567,21 @@ public class EmailProvider extends ContentProvider
return baseUri; 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 * 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 * 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() { private void sendMessageListDataChangedNotification() {
final Context context = getContext(); final Context context = getContext();
final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED); final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED);

View File

@ -40,6 +40,7 @@ import com.android.emailcommon.utility.ConversionUtilities;
import com.android.mail.utils.LogUtils; import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils; import com.android.mail.utils.Utils;
import java.io.InputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -118,8 +119,9 @@ public class Utilities {
ArrayList<Part> attachments = new ArrayList<Part>(); ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments); MimeUtility.collectParts(message, viewables, attachments);
// Don't close the viewables attachment InputStream yet
final ConversionUtilities.BodyFieldData data = final ConversionUtilities.BodyFieldData data =
ConversionUtilities.parseBodyFields(viewables); ConversionUtilities.parseBodyFields(viewables, false);
// set body and local message values // set body and local message values
localMessage.setFlags(data.isQuotedReply, data.isQuotedForward); localMessage.setFlags(data.isQuotedReply, data.isQuotedForward);
@ -166,6 +168,21 @@ public class Utilities {
localMessage.mFlagAttachment = true; 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 // One last update of message with two updated flags
localMessage.mFlagLoaded = loadStatus; localMessage.mFlagLoaded = loadStatus;

View File

@ -293,6 +293,10 @@ public class EmailBroadcastProcessorService extends IntentService {
private void onBootCompleted() { private void onBootCompleted() {
performOneTimeInitialization(); performOneTimeInitialization();
reconcileAndStartServices(); 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() { private void reconcileAndStartServices() {

View File

@ -288,6 +288,12 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm
mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMainMailboxKey); 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 (account == null || mailbox == null) {
// If the account/mailbox are gone, just report success; the UI handles this // If the account/mailbox are gone, just report success; the UI handles this
cb.loadAttachmentStatus(messageId, attachmentId, cb.loadAttachmentStatus(messageId, attachmentId,
@ -416,7 +422,6 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm
// actually occurs. // actually occurs.
mailbox.mUiSyncStatus = Mailbox.SYNC_STATUS_INITIAL_SYNC_NEEDED; mailbox.mUiSyncStatus = Mailbox.SYNC_STATUS_INITIAL_SYNC_NEEDED;
} }
mailbox.save(mContext);
if (type == Mailbox.TYPE_INBOX) { if (type == Mailbox.TYPE_INBOX) {
inboxId = mailbox.mId; inboxId = mailbox.mId;
@ -425,6 +430,7 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm
// should start marked // should start marked
mailbox.mSyncInterval = 1; mailbox.mSyncInterval = 1;
} }
mailbox.save(mContext);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -16,5 +16,139 @@
package com.android.email.service; 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 { public class LegacyImapSyncAdapterService extends PopImapSyncAdapterService {
}
// 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;
}
}

View File

@ -49,7 +49,7 @@ import java.util.ArrayList;
public class PopImapSyncAdapterService extends Service { public class PopImapSyncAdapterService extends Service {
private static final String TAG = "PopImapSyncService"; private static final String TAG = "PopImapSyncService";
private SyncAdapterImpl mSyncAdapter = null; private AbstractThreadedSyncAdapter mSyncAdapter = null;
private static String sPop3Protocol; private static String sPop3Protocol;
private static String sLegacyImapProtocol; private static String sLegacyImapProtocol;
@ -58,7 +58,7 @@ public class PopImapSyncAdapterService extends Service {
super(); super();
} }
private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter { static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
public SyncAdapterImpl(Context context) { public SyncAdapterImpl(Context context) {
super(context, true /* autoInitialize */); super(context, true /* autoInitialize */);
} }
@ -71,10 +71,14 @@ public class PopImapSyncAdapterService extends Service {
} }
} }
public AbstractThreadedSyncAdapter getSyncAdapter() {
return new SyncAdapterImpl(getApplicationContext());
}
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
mSyncAdapter = new SyncAdapterImpl(getApplicationContext()); mSyncAdapter = getSyncAdapter();
} }
@Override @Override
@ -101,14 +105,14 @@ public class PopImapSyncAdapterService extends Service {
return false; 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 Bundle extras, final SyncResult syncResult, final boolean uiRefresh,
final int deltaMessageCount) { final int deltaMessageCount) {
TempDirectory.setTempDirectory(context); TempDirectory.setTempDirectory(context);
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
if (mailbox == null) return; if (mailbox == null) return false;
Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
if (account == null) return; if (account == null) return false;
ContentResolver resolver = context.getContentResolver(); ContentResolver resolver = context.getContentResolver();
if ((mailbox.mType != Mailbox.TYPE_OUTBOX) && if ((mailbox.mType != Mailbox.TYPE_OUTBOX) &&
!loadsFromServer(context, mailbox, account)) { !loadsFromServer(context, mailbox, account)) {
@ -116,7 +120,7 @@ public class PopImapSyncAdapterService extends Service {
// updates table and return // updates table and return
resolver.delete(Message.UPDATED_CONTENT_URI, MessageColumns.MAILBOX_KEY + "=?", resolver.delete(Message.UPDATED_CONTENT_URI, MessageColumns.MAILBOX_KEY + "=?",
new String[] {Long.toString(mailbox.mId)}); new String[] {Long.toString(mailbox.mId)});
return; return true;
} }
LogUtils.d(TAG, "About to sync mailbox: " + mailbox.mDisplayName); 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, EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, status, 0,
lastSyncResult); lastSyncResult);
return true;
} }
} catch (MessagingException e) { } catch (MessagingException e) {
final int type = e.getExceptionType(); final int type = e.getExceptionType();
@ -186,6 +191,7 @@ public class PopImapSyncAdapterService extends Service {
values.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); values.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
resolver.update(mailboxUri, values, null, null); 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. // from the account settings. Otherwise just sync the inbox.
if (info.offerLookback) { if (info.offerLookback) {
mailboxIds = getLoopBackMailboxIdsForSync(context, acct); mailboxIds = getLoopBackMailboxIdsForSync(context, acct);
} else { }
if (mailboxIds.length == 0) {
final long inboxId = Mailbox.findMailboxOfType(context, acct.mId, final long inboxId = Mailbox.findMailboxOfType(context, acct.mId,
Mailbox.TYPE_INBOX); Mailbox.TYPE_INBOX);
if (inboxId != Mailbox.NO_MAILBOX) { if (inboxId != Mailbox.NO_MAILBOX) {
@ -262,9 +269,20 @@ public class PopImapSyncAdapterService extends Service {
extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false); extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
int deltaMessageCount = int deltaMessageCount =
extras.getInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, 0); extras.getInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, 0);
boolean success = mailboxIds.length > 0;
for (long mailboxId : mailboxIds) { for (long mailboxId : mailboxIds) {
sync(context, mailboxId, extras, syncResult, uiRefresh, boolean result = sync(context, mailboxId, extras, syncResult,
deltaMessageCount); 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) { private static boolean isLegacyImapProtocol(Context ctx, Account acct) {
if (sLegacyImapProtocol == null) { if (sLegacyImapProtocol == null) {
sLegacyImapProtocol = ctx.getString(R.string.protocol_legacy_imap); sLegacyImapProtocol = ctx.getString(R.string.protocol_legacy_imap);

View File

@ -74,8 +74,8 @@
email:serviceClass="com.android.email.service.ImapService" email:serviceClass="com.android.email.service.ImapService"
email:port="143" email:port="143"
email:portSsl="993" email:portSsl="993"
email:syncIntervalStrings="@array/account_settings_check_frequency_entries" email:syncIntervalStrings="@array/account_settings_check_frequency_entries_push"
email:syncIntervals="@array/account_settings_check_frequency_values" email:syncIntervals="@array/account_settings_check_frequency_values_push"
email:defaultSyncInterval="mins15" email:defaultSyncInterval="mins15"
email:offerTls="true" email:offerTls="true"

View File

@ -23,5 +23,6 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="@string/authority_email_provider" android:contentAuthority="@string/authority_email_provider"
android:accountType="@string/account_manager_type_legacy_imap" android:accountType="@string/account_manager_type_legacy_imap"
android:allowParallelSyncs="true"
android:supportsUploading="true" android:supportsUploading="true"
/> />

View File

@ -417,6 +417,10 @@ public class AccountCheckSettingsFragment extends Fragment {
EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE); EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE);
return new MessagingException(resultCode, errorMessage); return new MessagingException(resultCode, errorMessage);
} }
// Save account capabilities
mAccount.mCapabilities = bundle.getInt(
EmailServiceProxy.SETTINGS_BUNDLE_CAPABILITIES, 0);
} }
final EmailServiceInfo info; final EmailServiceInfo info;

View File

@ -69,6 +69,7 @@ import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.Policy; import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.mail.preferences.AccountPreferences; import com.android.mail.preferences.AccountPreferences;
import com.android.mail.preferences.FolderPreferences; import com.android.mail.preferences.FolderPreferences;
import com.android.mail.preferences.FolderPreferences.NotificationLight; import com.android.mail.preferences.FolderPreferences.NotificationLight;
@ -84,7 +85,9 @@ import com.android.mail.utils.LogUtils;
import com.android.mail.utils.NotificationUtils; import com.android.mail.utils.NotificationUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -243,10 +246,7 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
final CharSequence [] syncIntervals = final CharSequence [] syncIntervals =
savedInstanceState.getCharSequenceArray(SAVESTATE_SYNC_INTERVALS); savedInstanceState.getCharSequenceArray(SAVESTATE_SYNC_INTERVALS);
mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY); mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY);
if (mCheckFrequency != null) { fillCheckFrecuency(syncIntervalStrings, syncIntervals);
mCheckFrequency.setEntries(syncIntervalStrings);
mCheckFrequency.setEntryValues(syncIntervals);
}
} }
} }
@ -382,16 +382,15 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
final android.accounts.Account androidAcct = new android.accounts.Account( final android.accounts.Account androidAcct = new android.accounts.Account(
mAccount.mEmailAddress, mServiceInfo.accountType); mAccount.mEmailAddress, mServiceInfo.accountType);
if (Integer.parseInt(summary) == Account.CHECK_INTERVAL_NEVER) { if (Integer.parseInt(summary) == Account.CHECK_INTERVAL_NEVER) {
// Disable syncing from the account manager. Leave the current sync frequency // Disable syncing from the account manager.
// in the database.
ContentResolver.setSyncAutomatically(androidAcct, EmailContent.AUTHORITY, ContentResolver.setSyncAutomatically(androidAcct, EmailContent.AUTHORITY,
false); false);
} else { } else {
// Enable syncing from the account manager. // Enable syncing from the account manager.
ContentResolver.setSyncAutomatically(androidAcct, EmailContent.AUTHORITY, ContentResolver.setSyncAutomatically(androidAcct, EmailContent.AUTHORITY,
true); true);
cv.put(AccountColumns.SYNC_INTERVAL, Integer.parseInt(summary));
} }
cv.put(AccountColumns.SYNC_INTERVAL, Integer.parseInt(summary));
} }
} else if (key.equals(PREFERENCE_SYNC_WINDOW)) { } else if (key.equals(PREFERENCE_SYNC_WINDOW)) {
final String summary = newValue.toString(); final String summary = newValue.toString();
@ -749,8 +748,7 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
R.string.preferences_signature_summary_not_set); R.string.preferences_signature_summary_not_set);
mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY); mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY);
mCheckFrequency.setEntries(mServiceInfo.syncIntervalStrings); fillCheckFrecuency(mServiceInfo.syncIntervalStrings, mServiceInfo.syncIntervals);
mCheckFrequency.setEntryValues(mServiceInfo.syncIntervals);
if (mServiceInfo.syncContacts || mServiceInfo.syncCalendar) { if (mServiceInfo.syncContacts || mServiceInfo.syncCalendar) {
// This account allows syncing of contacts and/or calendar, so we will always have // 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. // 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); 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);
}
} }

View File

@ -917,7 +917,7 @@ public class AccountSetupFinal extends AccountSetupActivity
public void setDefaultsForProtocol(Account account) { public void setDefaultsForProtocol(Account account) {
final EmailServiceUtils.EmailServiceInfo info = mSetupData.getIncomingServiceInfo(this); final EmailServiceUtils.EmailServiceInfo info = mSetupData.getIncomingServiceInfo(this);
if (info == null) return; if (info == null) return;
account.mSyncInterval = info.defaultSyncInterval; account.setSyncInterval(info.defaultSyncInterval);
account.mSyncLookback = info.defaultLookback; account.mSyncLookback = info.defaultLookback;
if (info.offerLocalDeletes) { if (info.offerLocalDeletes) {
account.setDeletePolicy(info.defaultLocalDeletes); account.setDeletePolicy(info.defaultLocalDeletes);

View File

@ -29,8 +29,13 @@ import com.android.email.activity.UiUtilities;
import com.android.email.service.EmailServiceUtils; import com.android.email.service.EmailServiceUtils;
import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.Policy; import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.SyncWindow; import com.android.emailcommon.service.SyncWindow;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AccountSetupOptionsFragment extends AccountSetupFragment { public class AccountSetupOptionsFragment extends AccountSetupFragment {
private Spinner mCheckFrequencyView; private Spinner mCheckFrequencyView;
private Spinner mSyncWindowView; private Spinner mSyncWindowView;
@ -90,11 +95,24 @@ public class AccountSetupOptionsFragment extends AccountSetupFragment {
final CharSequence[] frequencyEntries = serviceInfo.syncIntervalStrings; final CharSequence[] frequencyEntries = serviceInfo.syncIntervalStrings;
// Now create the array used by the sync interval Spinner // 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++) { for (int i = 0; i < frequencyEntries.length; i++) {
checkFrequencies[i] = new SpinnerOption( Integer value = Integer.valueOf(frequencyValues[i].toString());
Integer.valueOf(frequencyValues[i].toString()), frequencyEntries[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 = final ArrayAdapter<SpinnerOption> checkFrequenciesAdapter =
new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_item, new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_item,
checkFrequencies); checkFrequencies);