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

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
// cursors (initially, the email AppWidget).
public static String NOTIFIER_AUTHORITY;
// The sync settings changed authority is used to notify when a sync setting changed (interval)
public static String SYNC_SETTING_CHANGED_AUTHORITY;
public static Uri CONTENT_URI;
public static final String PARAMETER_LIMIT = "limit";
@ -153,6 +155,7 @@ public abstract class EmailContent {
*/
public static final String SUPPRESS_COMBINED_ACCOUNT_PARAM = "suppress_combined";
public static Uri CONTENT_NOTIFIER_URI;
public static Uri CONTENT_SYNC_SETTING_CHANGED_URI;
public static Uri PICK_TRASH_FOLDER_URI;
public static Uri PICK_SENT_FOLDER_URI;
public static Uri MAILBOX_NOTIFICATION_URI;
@ -175,8 +178,11 @@ public abstract class EmailContent {
AUTHORITY = EMAIL_PACKAGE_NAME + ".provider";
LogUtils.d("EmailContent", "init for " + AUTHORITY);
NOTIFIER_AUTHORITY = EMAIL_PACKAGE_NAME + ".notifier";
SYNC_SETTING_CHANGED_AUTHORITY = EMAIL_PACKAGE_NAME + ".sync_setting_changed";
CONTENT_URI = Uri.parse("content://" + AUTHORITY);
CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY);
CONTENT_SYNC_SETTING_CHANGED_URI = Uri.parse(
"content://" + SYNC_SETTING_CHANGED_AUTHORITY);
PICK_TRASH_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickTrashFolder");
PICK_SENT_FOLDER_URI = Uri.parse("content://" + AUTHORITY + "/pickSentFolder");
MAILBOX_NOTIFICATION_URI = Uri.parse("content://" + AUTHORITY + "/mailboxNotification");
@ -1724,6 +1730,8 @@ public abstract class EmailContent {
public static final String PING_DURATION = "pingDuration";
// Automatically fetch pop3 attachments
public static final String AUTO_FETCH_ATTACHMENTS = "autoFetchAttachments";
// Account capabilities (check EmailServiceProxy#CAPABILITY_*)
public static final String CAPABILITIES = "capabilities";
}
public interface QuickResponseColumns extends BaseColumns {

View File

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

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_REDIRECT_ADDRESS = "validate_redirect_address";
// Service capabilities
public static final String SETTINGS_BUNDLE_CAPABILITIES = "settings_capabilities";
// List of common interesting services capabilities
public static final int CAPABILITY_PUSH = 1 << 0;
private Object mReturn = null;
private IEmailService mService;
private final boolean isRemote;

View File

@ -165,6 +165,13 @@ public class EmailConnectivityManager extends BroadcastReceiver {
return info.getType();
}
static public boolean isConnected(Context context) {
ConnectivityManager cm =
(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
return info != null && info.isConnected();
}
public void waitForConnectivity() {
// If we're unregistered, throw an exception
if (!mRegistered) {

View File

@ -107,7 +107,7 @@ public class AccountSettingsUtils {
cv.put(AccountColumns.DISPLAY_NAME, account.getDisplayName());
cv.put(AccountColumns.SENDER_NAME, account.getSenderName());
cv.put(AccountColumns.SIGNATURE, account.getSignature());
cv.put(AccountColumns.SYNC_INTERVAL, account.mSyncInterval);
cv.put(AccountColumns.SYNC_INTERVAL, account.getSyncInterval());
cv.put(AccountColumns.FLAGS, account.mFlags);
cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback);
cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey);

View File

@ -36,6 +36,7 @@ import com.android.emailcommon.mail.MessagingException;
import com.android.mail.utils.LogUtils;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -50,6 +51,15 @@ class ImapConnection {
// Always check in FALSE
private static final boolean DEBUG_FORCE_SEND_ID = false;
// RFC 2177 defines that IDLE connections must be refreshed at least every 29 minutes
public static final int PING_IDLE_TIMEOUT = 29 * 60 * 1000;
// Special timeout for DONE operations
public static final int DONE_TIMEOUT = 5 * 1000;
// Time to wait between the first idle message and triggering the changes
private static final int IDLE_OP_READ_TIMEOUT = 500;
/** ID capability per RFC 2971*/
public static final int CAPABILITY_ID = 1 << 0;
/** NAMESPACE capability per RFC 2342 */
@ -58,6 +68,8 @@ class ImapConnection {
public static final int CAPABILITY_STARTTLS = 1 << 2;
/** UIDPLUS capability per RFC 4315 */
public static final int CAPABILITY_UIDPLUS = 1 << 3;
/** IDLE capability per RFC 2177 */
public static final int CAPABILITY_IDLE = 1 << 4;
/** The capabilities supported; a set of CAPABILITY_* values. */
private int mCapabilities;
@ -69,6 +81,8 @@ class ImapConnection {
private String mAccessToken;
private String mIdPhrase = null;
private boolean mIdling = false;
/** # of command/response lines to log upon crash. */
private static final int DISCOURSE_LOGGER_SIZE = 64;
private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
@ -210,10 +224,23 @@ class ImapConnection {
mImapStore = null;
}
int getReadTimeout() throws IOException {
if (mTransport == null) {
return MailTransport.SOCKET_READ_TIMEOUT;
}
return mTransport.getReadTimeout();
}
void setReadTimeout(int timeout) throws IOException {
if (mTransport != null) {
mTransport.setReadTimeout(timeout);
}
}
/**
* Returns whether or not the specified capability is supported by the server.
*/
private boolean isCapable(int capability) {
public boolean isCapable(int capability) {
return (mCapabilities & capability) != 0;
}
@ -235,6 +262,9 @@ class ImapConnection {
if (capabilities.contains(ImapConstants.STARTTLS)) {
mCapabilities |= CAPABILITY_STARTTLS;
}
if (capabilities.contains(ImapConstants.IDLE)) {
mCapabilities |= CAPABILITY_IDLE;
}
}
/**
@ -273,6 +303,12 @@ class ImapConnection {
*/
String sendCommand(String command, boolean sensitive)
throws MessagingException, IOException {
// Don't allow any command other than DONE when idling
if (mIdling && !command.equals(ImapConstants.DONE)) {
return null;
}
mIdling = command.equals(ImapConstants.IDLE);
LogUtils.d(Logging.LOG_TAG, "sendCommand %s", (sensitive ? IMAP_REDACTED_LOG : command));
open();
return sendCommandInternal(command, sensitive);
@ -284,7 +320,13 @@ class ImapConnection {
throw new IOException("Null transport");
}
String tag = Integer.toString(mNextCommandTag.incrementAndGet());
String commandToSend = tag + " " + command;
final String commandToSend;
if (command.equals(ImapConstants.DONE)) {
// Do not send a tag for DONE command
commandToSend = command;
} else {
commandToSend = tag + " " + command;
}
mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
return tag;
@ -327,6 +369,11 @@ class ImapConnection {
return executeSimpleCommand(command, false);
}
List<ImapResponse> executeIdleCommand() throws IOException, MessagingException {
mParser.expectIdlingResponse();
return executeSimpleCommand(ImapConstants.IDLE, false);
}
/**
* Read and return all of the responses from the most recent command sent to the server
*
@ -336,13 +383,35 @@ class ImapConnection {
*/
List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
final List<ImapResponse> responses = new ArrayList<ImapResponse>();
ImapResponse response;
do {
response = mParser.readResponse();
responses.add(response);
} while (!response.isTagged());
ImapResponse response = null;
boolean idling = false;
boolean throwSocketTimeoutEx = true;
int lastSocketTimeout = getReadTimeout();
try {
do {
response = mParser.readResponse();
if (idling) {
setReadTimeout(IDLE_OP_READ_TIMEOUT);
throwSocketTimeoutEx = false;
}
responses.add(response);
if (response.isIdling()) {
idling = true;
}
} while (idling || !response.isTagged());
} catch (SocketTimeoutException ex) {
if (throwSocketTimeoutEx) {
throw ex;
}
} finally {
mParser.resetIdlingStatus();
if (lastSocketTimeout != getReadTimeout()) {
setReadTimeout(lastSocketTimeout);
}
}
if (!response.isOk()) {
// When idling, any response is valid; otherwise it must be OK
if (!response.isOk() && !idling) {
final String toString = response.toString();
final String status = response.getStatusOrEmpty().getString();
final String alert = response.getAlertTextOrEmpty().getString();

View File

@ -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) {

View File

@ -501,6 +501,14 @@ public class ImapStore extends Store {
connection.destroyResponses();
}
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
// Shared capabilities (check EmailProxyServices for available shared capabilities)
int capabilities = 0;
if (connection.isCapable(ImapConnection.CAPABILITY_IDLE)) {
capabilities |= EmailServiceProxy.CAPABILITY_PUSH;
}
bundle.putInt(EmailServiceProxy.SETTINGS_BUNDLE_CAPABILITIES, capabilities);
return bundle;
}
@ -556,6 +564,7 @@ public class ImapStore extends Store {
while ((connection = mConnectionPool.poll()) != null) {
try {
connection.setStore(this);
connection.setReadTimeout(MailTransport.SOCKET_READ_TIMEOUT);
connection.executeSimpleCommand(ImapConstants.NOOP);
break;
} catch (MessagingException e) {

View File

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

View File

@ -46,6 +46,7 @@ public final class ImapConstants {
public static final String COPYUID = "COPYUID";
public static final String CREATE = "CREATE";
public static final String DELETE = "DELETE";
public static final String DONE = "DONE";
public static final String EXAMINE = "EXAMINE";
public static final String EXISTS = "EXISTS";
public static final String EXPUNGE = "EXPUNGE";
@ -58,6 +59,8 @@ public final class ImapConstants {
public static final String FLAGS = "FLAGS";
public static final String FLAGS_SILENT = "FLAGS.SILENT";
public static final String ID = "ID";
public static final String IDLE = "IDLE";
public static final String IDLING = "idling";
public static final String INBOX = "INBOX";
public static final String INTERNALDATE = "INTERNALDATE";
public static final String LIST = "LIST";
@ -73,6 +76,7 @@ public final class ImapConstants {
public static final String PREAUTH = "PREAUTH";
public static final String READ_ONLY = "READ-ONLY";
public static final String READ_WRITE = "READ-WRITE";
public static final String RECENT = "RECENT";
public static final String RENAME = "RENAME";
public static final String RFC822_SIZE = "RFC822.SIZE";
public static final String SEARCH = "SEARCH";

View File

@ -180,7 +180,7 @@ public class ImapList extends ImapElement {
@Override
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 whether it's an IDLE response.
*/
public boolean isIdling() {
return is(0, ImapConstants.IDLING);
}
/**
* @return whether it's an {@code responseType} data response. (i.e. not tagged).
* @param index where {@code responseType} should appear. e.g. 1 for "FETCH"

View File

@ -66,6 +66,9 @@ public class ImapResponseParser {
*/
private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
private boolean mIdling;
private boolean mExpectIdlingResponse;
/**
* Exception thrown when we receive BYE. It derives from IOException, so it'll be treated
* in the same way EOF does.
@ -168,10 +171,17 @@ public class ImapResponseParser {
} catch (RuntimeException e) {
// Parser crash -- log network activities.
onParseError(e);
mIdling = false;
throw e;
} catch (IOException e) {
// Network error, or received an unexpected char.
onParseError(e);
// If we are idling don't parse the error, just let the upper layers
// handle the exception
if (!mIdling) {
onParseError(e);
} else {
mIdling = false;
}
throw e;
}
@ -242,6 +252,14 @@ public class ImapResponseParser {
return ret;
}
public void resetIdlingStatus() {
mIdling = false;
}
public void expectIdlingResponse() {
mExpectIdlingResponse = true;
}
/**
* Parse and return the response line.
*/
@ -263,11 +281,26 @@ public class ImapResponseParser {
responseToDestroy = new ImapResponse(null, true);
// If it's continuation request, we don't really care what's in it.
responseToDestroy.add(new ImapSimpleString(readUntilEol()));
// NOTE: specs say the server is supposed to respond to the IDLE command
// with a continuation request response. To simplify internal handling,
// we'll always construct same response (ignoring the server text response).
// Our implementation always returns "+ idling".
if (mExpectIdlingResponse) {
// Discard the server message and put what we expected
readUntilEol();
responseToDestroy.add(new ImapSimpleString(ImapConstants.IDLING));
} else {
responseToDestroy.add(new ImapSimpleString(readUntilEol()));
}
// Response has successfully been built. Let's return it.
responseToReturn = responseToDestroy;
responseToDestroy = null;
mIdling = responseToReturn.isIdling();
if (mIdling) {
mExpectIdlingResponse = true;
}
} else {
// Status response or response data
final String tag;

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
* 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.QuickResponse;
import com.android.emailcommon.provider.SuggestedContact;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.LegacyPolicySet;
import com.android.emailcommon.service.SyncWindow;
import com.android.mail.providers.UIProvider;
@ -187,7 +188,8 @@ public final class DBHelper {
// Version 127: Force mFlags to contain the correct flags for EAS accounts given a protocol
// version above 12.0
// Version 129: Update all IMAP INBOX mailboxes to force synchronization
public static final int DATABASE_VERSION = 129;
// Version 130: Account capabilities (check EmailServiceProxy#CAPABILITY_*)
public static final int DATABASE_VERSION = 130;
// Any changes to the database format *must* include update-in-place code.
// Original version: 2
@ -525,7 +527,8 @@ public final class DBHelper {
+ AccountColumns.POLICY_KEY + " integer, "
+ AccountColumns.MAX_ATTACHMENT_SIZE + " integer, "
+ AccountColumns.PING_DURATION + " integer, "
+ AccountColumns.AUTO_FETCH_ATTACHMENTS + " integer"
+ AccountColumns.AUTO_FETCH_ATTACHMENTS + " integer, "
+ AccountColumns.CAPABILITIES + " integer default 0"
+ ");";
db.execSQL("create table " + Account.TABLE_NAME + s);
// Deleting an account deletes associated Mailboxes and HostAuth's
@ -1562,6 +1565,52 @@ public final class DBHelper {
+ HostAuth.TABLE_NAME + "." + HostAuthColumns.PROTOCOL + "='imap'));");
}
if (oldVersion <= 130) {
//Account capabilities (check EmailServiceProxy#CAPABILITY_*)
try {
// Create capabilities field
db.execSQL("alter table " + Account.TABLE_NAME
+ " add column " + AccountColumns.CAPABILITIES
+ " integer" + " default 0;");
// Update all accounts with the appropriate capabilities
Cursor c = db.rawQuery("select " + Account.TABLE_NAME + "."
+ AccountColumns._ID + ", " + HostAuth.TABLE_NAME + "."
+ HostAuthColumns.PROTOCOL + " from " + Account.TABLE_NAME + ", "
+ HostAuth.TABLE_NAME + " where " + Account.TABLE_NAME + "."
+ AccountColumns.HOST_AUTH_KEY_RECV + " = " + HostAuth.TABLE_NAME
+ "." + HostAuthColumns._ID + ";", null);
if (c != null) {
try {
while(c.moveToNext()) {
long id = c.getLong(c.getColumnIndexOrThrow(AccountColumns._ID));
String protocol = c.getString(c.getColumnIndexOrThrow(
HostAuthColumns.PROTOCOL));
int capabilities = 0;
if (protocol.equals(LEGACY_SCHEME_IMAP)
|| protocol.equals(LEGACY_SCHEME_EAS)) {
// Don't know yet if the imap server supports the IDLE
// capability, but since this is upgrading the account,
// just assume that all imap servers supports the push
// capability and let disable it by the IMAP service
capabilities |= EmailServiceProxy.CAPABILITY_PUSH;
}
final ContentValues cv = new ContentValues(1);
cv.put(AccountColumns.CAPABILITIES, capabilities);
db.update(Account.TABLE_NAME, cv, AccountColumns._ID + " = ?",
new String[]{String.valueOf(id)});
}
} finally {
c.close();
}
}
} catch (final SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
LogUtils.w(TAG, "Exception upgrading EmailProvider.db from v129 to v130", e);
}
}
// Due to a bug in commit 44a064e5f16ddaac25f2acfc03c118f65bc48aec,
// AUTO_FETCH_ATTACHMENTS column could not be available in the Account table.
// Since cm12 and up doesn't use this column, we are leave as is it. In case

View File

@ -189,11 +189,11 @@ public class EmailProvider extends ContentProvider
"vnd.android.cursor.item/email-attachment";
/** Appended to the notification URI for delete operations */
private static final String NOTIFICATION_OP_DELETE = "delete";
public static final String NOTIFICATION_OP_DELETE = "delete";
/** Appended to the notification URI for insert operations */
private static final String NOTIFICATION_OP_INSERT = "insert";
public static final String NOTIFICATION_OP_INSERT = "insert";
/** Appended to the notification URI for update operations */
private static final String NOTIFICATION_OP_UPDATE = "update";
public static final String NOTIFICATION_OP_UPDATE = "update";
/** The query string to trigger a folder refresh. */
protected static String QUERY_UIREFRESH = "uirefresh";
@ -833,6 +833,7 @@ public class EmailProvider extends ContentProvider
// Notify all notifier cursors
sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_DELETE, id);
sendSyncSettingChanged(getBaseSyncSettingChangedUri(match), NOTIFICATION_OP_DELETE, id);
// Notify all email content cursors
notifyUI(EmailContent.CONTENT_URI, null);
@ -1075,6 +1076,7 @@ public class EmailProvider extends ContentProvider
// Notify all notifier cursors
sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_INSERT, id);
sendSyncSettingChanged(getBaseSyncSettingChangedUri(match), NOTIFICATION_OP_INSERT, id);
// Notify all existing cursors.
notifyUI(EmailContent.CONTENT_URI, null);
@ -1924,7 +1926,7 @@ public class EmailProvider extends ContentProvider
private static final int INDEX_SYNC_KEY = 2;
/**
* Restart push if we need it (currently only for Exchange accounts).
* Restart push if we need it.
* @param context A {@link Context}.
* @param db The {@link SQLiteDatabase}.
* @param id The id of the thing we're looking for.
@ -1937,9 +1939,13 @@ public class EmailProvider extends ContentProvider
try {
if (c.moveToFirst()) {
final String protocol = c.getString(INDEX_PROTOCOL);
// Only restart push for EAS accounts that have completed initial sync.
if (context.getString(R.string.protocol_eas).equals(protocol) &&
!EmailContent.isInitialSyncKey(c.getString(INDEX_SYNC_KEY))) {
final String syncKey = c.getString(INDEX_SYNC_KEY);
final boolean supportsPush =
context.getString(R.string.protocol_eas).equals(protocol) ||
context.getString(R.string.protocol_legacy_imap).equals(protocol);
// Only restart push for EAS or IMAP accounts that have completed initial sync.
if (supportsPush && !EmailContent.isInitialSyncKey(syncKey)) {
final String emailAddress = c.getString(INDEX_EMAIL_ADDRESS);
final android.accounts.Account account =
getAccountManagerAccount(context, emailAddress, protocol);
@ -2010,6 +2016,7 @@ public class EmailProvider extends ContentProvider
final SQLiteDatabase db = getDatabase(context);
final int table = match >> BASE_SHIFT;
int result;
boolean syncSettingChanged = false;
// We do NOT allow setting of unreadCount/messageCount via the provider
// These columns are maintained via triggers
@ -2159,6 +2166,14 @@ public class EmailProvider extends ContentProvider
}
} else if (match == MESSAGE_ID) {
db.execSQL(UPDATED_MESSAGE_DELETE + id);
} else if (match == MAILBOX_ID) {
if (values.containsKey(MailboxColumns.SYNC_INTERVAL)) {
syncSettingChanged = true;
}
} else if (match == ACCOUNT_ID) {
if (values.containsKey(AccountColumns.SYNC_INTERVAL)) {
syncSettingChanged = true;
}
}
result = db.update(tableName, values, whereWithId(id, selection),
selectionArgs);
@ -2293,6 +2308,10 @@ public class EmailProvider extends ContentProvider
TextUtils.isEmpty(values.getAsString(AttachmentColumns.LOCATION))) {
LogUtils.w(TAG, new Throwable(), "attachment with blank location");
}
} else if (match == MAILBOX) {
if (values.containsKey(MailboxColumns.SYNC_INTERVAL)) {
syncSettingChanged = true;
}
}
result = db.update(tableName, values, selection, selectionArgs);
break;
@ -2314,6 +2333,10 @@ public class EmailProvider extends ContentProvider
// Notify all notifier cursors if some records where changed in the database
if (result > 0) {
sendNotifierChange(getBaseNotificationUri(match), NOTIFICATION_OP_UPDATE, id);
if (syncSettingChanged) {
sendSyncSettingChanged(getBaseSyncSettingChangedUri(match),
NOTIFICATION_OP_UPDATE, id);
}
notifyUI(notificationUri, null);
}
return result;
@ -2544,6 +2567,21 @@ public class EmailProvider extends ContentProvider
return baseUri;
}
private static Uri getBaseSyncSettingChangedUri(int match) {
Uri baseUri = null;
switch (match) {
case ACCOUNT:
case ACCOUNT_ID:
baseUri = Account.SYNC_SETTING_CHANGED_URI;
break;
case MAILBOX:
case MAILBOX_ID:
baseUri = Mailbox.SYNC_SETTING_CHANGED_URI;
break;
}
return baseUri;
}
/**
* Sends a change notification to any cursors observers of the given base URI. The final
* notification URI is dynamically built to contain the specified information. It will be
@ -2582,6 +2620,25 @@ public class EmailProvider extends ContentProvider
}
}
private void sendSyncSettingChanged(Uri baseUri, String op, String id) {
if (baseUri == null) return;
// Append the operation, if specified
if (op != null) {
baseUri = baseUri.buildUpon().appendEncodedPath(op).build();
}
long longId = 0L;
try {
longId = Long.valueOf(id);
} catch (NumberFormatException ignore) {}
if (longId > 0) {
notifyUI(baseUri, id);
} else {
notifyUI(baseUri, null);
}
}
private void sendMessageListDataChangedNotification() {
final Context context = getContext();
final Intent intent = new Intent(ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED);

View File

@ -40,6 +40,7 @@ import com.android.emailcommon.utility.ConversionUtilities;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.Utils;
import java.io.InputStream;
import java.io.IOException;
import java.util.ArrayList;
@ -118,8 +119,9 @@ public class Utilities {
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
// Don't close the viewables attachment InputStream yet
final ConversionUtilities.BodyFieldData data =
ConversionUtilities.parseBodyFields(viewables);
ConversionUtilities.parseBodyFields(viewables, false);
// set body and local message values
localMessage.setFlags(data.isQuotedReply, data.isQuotedForward);
@ -166,6 +168,21 @@ public class Utilities {
localMessage.mFlagAttachment = true;
}
// Close any parts that may still be open
for (final Part part : viewables) {
if (part.getBody() == null) {
continue;
}
try {
InputStream is = part.getBody().getInputStream();
if (is != null) {
is.close();
}
} catch (IOException io) {
// Ignore
}
}
// One last update of message with two updated flags
localMessage.mFlagLoaded = loadStatus;

View File

@ -293,6 +293,10 @@ public class EmailBroadcastProcessorService extends IntentService {
private void onBootCompleted() {
performOneTimeInitialization();
reconcileAndStartServices();
// This is an special case to start IMAP PUSH via its adapter
Intent imap = new Intent(this, LegacyImapSyncAdapterService.class);
startService(imap);
}
private void reconcileAndStartServices() {

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -16,5 +16,139 @@
package com.android.email.service;
import static com.android.emailcommon.Logging.LOG_TAG;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SyncResult;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager;
import android.text.format.DateUtils;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.IEmailService;
import com.android.mail.utils.LogUtils;
public class LegacyImapSyncAdapterService extends PopImapSyncAdapterService {
}
// 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 {
private static final String TAG = "PopImapSyncService";
private SyncAdapterImpl mSyncAdapter = null;
private AbstractThreadedSyncAdapter mSyncAdapter = null;
private static String sPop3Protocol;
private static String sLegacyImapProtocol;
@ -58,7 +58,7 @@ public class PopImapSyncAdapterService extends Service {
super();
}
private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
public SyncAdapterImpl(Context context) {
super(context, true /* autoInitialize */);
}
@ -71,10 +71,14 @@ public class PopImapSyncAdapterService extends Service {
}
}
public AbstractThreadedSyncAdapter getSyncAdapter() {
return new SyncAdapterImpl(getApplicationContext());
}
@Override
public void onCreate() {
super.onCreate();
mSyncAdapter = new SyncAdapterImpl(getApplicationContext());
mSyncAdapter = getSyncAdapter();
}
@Override
@ -101,14 +105,14 @@ public class PopImapSyncAdapterService extends Service {
return false;
}
private static void sync(final Context context, final long mailboxId,
private static boolean sync(final Context context, final long mailboxId,
final Bundle extras, final SyncResult syncResult, final boolean uiRefresh,
final int deltaMessageCount) {
TempDirectory.setTempDirectory(context);
Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
if (mailbox == null) return;
if (mailbox == null) return false;
Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey);
if (account == null) return;
if (account == null) return false;
ContentResolver resolver = context.getContentResolver();
if ((mailbox.mType != Mailbox.TYPE_OUTBOX) &&
!loadsFromServer(context, mailbox, account)) {
@ -116,7 +120,7 @@ public class PopImapSyncAdapterService extends Service {
// updates table and return
resolver.delete(Message.UPDATED_CONTENT_URI, MessageColumns.MAILBOX_KEY + "=?",
new String[] {Long.toString(mailbox.mId)});
return;
return true;
}
LogUtils.d(TAG, "About to sync mailbox: " + mailbox.mDisplayName);
@ -147,6 +151,7 @@ public class PopImapSyncAdapterService extends Service {
}
EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, status, 0,
lastSyncResult);
return true;
}
} catch (MessagingException e) {
final int type = e.getExceptionType();
@ -186,6 +191,7 @@ public class PopImapSyncAdapterService extends Service {
values.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
resolver.update(mailboxUri, values, null, null);
}
return false;
}
/**
@ -247,7 +253,8 @@ public class PopImapSyncAdapterService extends Service {
// from the account settings. Otherwise just sync the inbox.
if (info.offerLookback) {
mailboxIds = getLoopBackMailboxIdsForSync(context, acct);
} else {
}
if (mailboxIds.length == 0) {
final long inboxId = Mailbox.findMailboxOfType(context, acct.mId,
Mailbox.TYPE_INBOX);
if (inboxId != Mailbox.NO_MAILBOX) {
@ -262,9 +269,20 @@ public class PopImapSyncAdapterService extends Service {
extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
int deltaMessageCount =
extras.getInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, 0);
boolean success = mailboxIds.length > 0;
for (long mailboxId : mailboxIds) {
sync(context, mailboxId, extras, syncResult, uiRefresh,
deltaMessageCount);
boolean result = sync(context, mailboxId, extras, syncResult,
uiRefresh, deltaMessageCount);
if (!result) {
success = false;
}
}
// Initial sync performed?
if (success) {
// All mailboxes (that need a sync) are now synced. Assume we
// have a valid sync key, in case this account has push support
markAsInitialSyncKey(context, acct.mId);
}
}
}
@ -278,6 +296,14 @@ public class PopImapSyncAdapterService extends Service {
}
}
private static void markAsInitialSyncKey(Context context, long accountId) {
ContentResolver resolver = context.getContentResolver();
Uri accountUri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
ContentValues values = new ContentValues();
values.put(AccountColumns.SYNC_KEY, "1");
resolver.update(accountUri, values, null, null);
}
private static boolean isLegacyImapProtocol(Context ctx, Account acct) {
if (sLegacyImapProtocol == null) {
sLegacyImapProtocol = ctx.getString(R.string.protocol_legacy_imap);

View File

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

View File

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

View File

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

View File

@ -69,6 +69,7 @@ import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.mail.preferences.AccountPreferences;
import com.android.mail.preferences.FolderPreferences;
import com.android.mail.preferences.FolderPreferences.NotificationLight;
@ -84,7 +85,9 @@ import com.android.mail.utils.LogUtils;
import com.android.mail.utils.NotificationUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -243,10 +246,7 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
final CharSequence [] syncIntervals =
savedInstanceState.getCharSequenceArray(SAVESTATE_SYNC_INTERVALS);
mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY);
if (mCheckFrequency != null) {
mCheckFrequency.setEntries(syncIntervalStrings);
mCheckFrequency.setEntryValues(syncIntervals);
}
fillCheckFrecuency(syncIntervalStrings, syncIntervals);
}
}
@ -382,16 +382,15 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
final android.accounts.Account androidAcct = new android.accounts.Account(
mAccount.mEmailAddress, mServiceInfo.accountType);
if (Integer.parseInt(summary) == Account.CHECK_INTERVAL_NEVER) {
// Disable syncing from the account manager. Leave the current sync frequency
// in the database.
// Disable syncing from the account manager.
ContentResolver.setSyncAutomatically(androidAcct, EmailContent.AUTHORITY,
false);
} else {
// Enable syncing from the account manager.
ContentResolver.setSyncAutomatically(androidAcct, EmailContent.AUTHORITY,
true);
cv.put(AccountColumns.SYNC_INTERVAL, Integer.parseInt(summary));
}
cv.put(AccountColumns.SYNC_INTERVAL, Integer.parseInt(summary));
}
} else if (key.equals(PREFERENCE_SYNC_WINDOW)) {
final String summary = newValue.toString();
@ -749,8 +748,7 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
R.string.preferences_signature_summary_not_set);
mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY);
mCheckFrequency.setEntries(mServiceInfo.syncIntervalStrings);
mCheckFrequency.setEntryValues(mServiceInfo.syncIntervals);
fillCheckFrecuency(mServiceInfo.syncIntervalStrings, mServiceInfo.syncIntervals);
if (mServiceInfo.syncContacts || mServiceInfo.syncCalendar) {
// This account allows syncing of contacts and/or calendar, so we will always have
// separate preferences to enable or disable syncing of email, contacts, and calendar.
@ -1182,4 +1180,28 @@ public class AccountSettingsFragment extends MailAccountPrefsFragment
}
mInboxLights.setOn(notificationLight.mOn);
}
private void fillCheckFrecuency(CharSequence[] labels, CharSequence[] values) {
if (mCheckFrequency == null) {
return;
}
// Check push capability prior to include as an option
if (mAccount != null) {
boolean hasPushCapability = mAccount.hasCapability(EmailServiceProxy.CAPABILITY_PUSH);
List<CharSequence> valuesList = new ArrayList<>(Arrays.asList(values));
int checkIntervalPushPos = valuesList.indexOf(
String.valueOf(Account.CHECK_INTERVAL_PUSH));
if (!hasPushCapability && checkIntervalPushPos != -1) {
List<CharSequence> labelsList = new ArrayList<>(Arrays.asList(labels));
labelsList.remove(checkIntervalPushPos);
valuesList.remove(checkIntervalPushPos);
labels = labelsList.toArray(new CharSequence[labelsList.size()]);
values = valuesList.toArray(new CharSequence[valuesList.size()]);
}
}
mCheckFrequency.setEntries(labels);
mCheckFrequency.setEntryValues(values);
mCheckFrequency.setDefaultValue(values);
}
}

View File

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

View File

@ -29,8 +29,13 @@ import com.android.email.activity.UiUtilities;
import com.android.email.service.EmailServiceUtils;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.SyncWindow;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AccountSetupOptionsFragment extends AccountSetupFragment {
private Spinner mCheckFrequencyView;
private Spinner mSyncWindowView;
@ -90,11 +95,24 @@ public class AccountSetupOptionsFragment extends AccountSetupFragment {
final CharSequence[] frequencyEntries = serviceInfo.syncIntervalStrings;
// Now create the array used by the sync interval Spinner
final SpinnerOption[] checkFrequencies = new SpinnerOption[frequencyEntries.length];
int checkIntervalPushPos = -1;
SpinnerOption[] checkFrequencies = new SpinnerOption[frequencyEntries.length];
for (int i = 0; i < frequencyEntries.length; i++) {
checkFrequencies[i] = new SpinnerOption(
Integer.valueOf(frequencyValues[i].toString()), frequencyEntries[i].toString());
Integer value = Integer.valueOf(frequencyValues[i].toString());
if (value.intValue() == Account.CHECK_INTERVAL_PUSH) {
checkIntervalPushPos = i;
}
checkFrequencies[i] = new SpinnerOption(value, frequencyEntries[i].toString());
}
// Ensure that push capability is supported by the server
boolean hasPushCapability = account.hasCapability(EmailServiceProxy.CAPABILITY_PUSH);
if (!hasPushCapability && checkIntervalPushPos != -1) {
List<SpinnerOption> options = new ArrayList<>(Arrays.asList(checkFrequencies));
options.remove(checkIntervalPushPos);
checkFrequencies = options.toArray(new SpinnerOption[options.size()]);
}
final ArrayAdapter<SpinnerOption> checkFrequenciesAdapter =
new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_item,
checkFrequencies);