Merge "Simplify mailbox synchronization logic"

This commit is contained in:
Todd Kennedy 2011-04-21 12:29:18 -07:00 committed by Android (Google) Code Review
commit 7f40e8290b
10 changed files with 230 additions and 127 deletions

View File

@ -16,6 +16,7 @@
package com.android.emailcommon.provider;
import com.android.emailcommon.Logging;
import com.android.emailcommon.utility.TextUtilities;
import com.android.emailcommon.utility.Utility;
@ -33,6 +34,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.net.URI;
@ -2250,6 +2252,10 @@ public abstract class EmailContent {
MailboxColumns.TYPE + " =?";
private static final String MAILBOX_TYPE_SELECTION =
MailboxColumns.TYPE + " =?";
/** Selection by display name for a given account */
private static final String NAME_AND_ACCOUNT_SELECTION =
MailboxColumns.DISPLAY_NAME + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
private static final String[] MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION = new String [] {
"sum(" + MailboxColumns.UNREAD_COUNT + ")"
};
@ -2387,6 +2393,38 @@ public abstract class EmailContent {
}
}
/**
* Returns a Mailbox from the database, given its pathname and account id. All mailbox
* paths for a particular account must be unique.
* @param context
* @param accountId the ID of the account
* @param path the fully qualified, remote pathname
*/
public static Mailbox restoreMailboxForPath(Context context, long accountId, String path) {
Cursor c = context.getContentResolver().query(
Mailbox.CONTENT_URI,
Mailbox.CONTENT_PROJECTION,
Mailbox.NAME_AND_ACCOUNT_SELECTION,
new String[] { path, Long.toString(accountId) },
null);
// TODO for mblank; uncomment when you submit CL Iab059f9a68eecd797914a6229f1ff9c03d0f0800
// if (c == null) throw new ProviderUnavailableException();
try {
Mailbox mailbox = null;
if (c.moveToFirst()) {
mailbox = getContent(c, Mailbox.class);
if (c.moveToNext()) {
Log.w(Logging.LOG_TAG, "Multiple mailboxes named \"" + path + "\"");
}
} else {
Log.i(Logging.LOG_TAG, "Could not find mailbox at \"" + path + "\"");
}
return mailbox;
} finally {
c.close();
}
}
@Override
public void restore(Cursor cursor) {
mBaseUri = CONTENT_URI;

View File

@ -36,6 +36,7 @@ import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Account;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
import com.android.emailcommon.provider.EmailContent.Mailbox;
@ -197,38 +198,23 @@ public class MessagingController implements Runnable {
return mListeners.isActiveListener(listener);
}
/**
* Lightweight class for capturing local mailboxes in an account. Just the columns
* necessary for a sync.
*/
private static class LocalMailboxInfo {
private static final int COLUMN_ID = 0;
private static final int COLUMN_DISPLAY_NAME = 1;
private static final int COLUMN_ACCOUNT_KEY = 2;
private static final int COLUMN_TYPE = 3;
private static final int MAILBOX_COLUMN_ID = 0;
private static final int MAILBOX_COLUMN_DISPLAY_NAME = 1;
private static final int MAILBOX_COLUMN_TYPE = 2;
private static final String[] PROJECTION = new String[] {
EmailContent.RECORD_ID,
MailboxColumns.DISPLAY_NAME, MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE,
};
final long mId;
final String mDisplayName;
final long mAccountKey;
final int mType;
public LocalMailboxInfo(Cursor c) {
mId = c.getLong(COLUMN_ID);
mDisplayName = c.getString(COLUMN_DISPLAY_NAME);
mAccountKey = c.getLong(COLUMN_ACCOUNT_KEY);
mType = c.getInt(COLUMN_TYPE);
}
}
/** Small projection for just the columns required for a sync. */
private static final String[] MAILBOX_PROJECTION = new String[] {
MailboxColumns.ID,
MailboxColumns.DISPLAY_NAME,
MailboxColumns.TYPE,
};
/**
* Asynchronously synchronize the folder list. If the specified {@link MessagingListener}
* is not {@code null}, it must have been previously added to the set of listeners using the
* {@link #addListener(MessagingListener)}. Otherwise, no actions will be performed.
* Synchronize the folder list with the remote server. Synchronization occurs in the
* background and results are passed through the {@link MessagingListener}. If the
* given listener is not {@code null}, it must have been previously added to the set
* of listeners using the {@link #addListener(MessagingListener)}. Otherwise, no
* actions will be performed.
*
* TODO this needs to cache the remote folder list
* TODO break out an inner listFoldersSynchronized which could simplify checkMail
@ -237,101 +223,71 @@ public class MessagingController implements Runnable {
* @param listener A listener to notify
*/
void listFolders(final long accountId, MessagingListener listener) {
final EmailContent.Account account =
EmailContent.Account.restoreAccountWithId(mContext, accountId);
final Account account = Account.restoreAccountWithId(mContext, accountId);
if (account == null) {
Log.i(Logging.LOG_TAG, "Could not load account id " + accountId
+ ". Has it been removed?");
return;
}
mListeners.listFoldersStarted(accountId);
put("listFolders", listener, new Runnable() {
// TODO For now, mailbox addition occurs in the server-dependent store implementation,
// but, mailbox removal occurs here. Instead, each store should be responsible for
// content synchronization (addition AND removal) since each store will likely need
// to implement it's own, unique synchronization methodology.
public void run() {
Cursor localFolderCursor = null;
try {
// Step 1: Get remote folders, make a list, and add any local folders
// that don't already exist.
// Step 1: Get remote mailboxes
Store store = Store.getInstance(account, mContext, null);
Folder[] remoteFolders = store.getAllFolders();
Folder[] remoteFolders = store.updateFolders();
HashSet<String> remoteFolderNames = new HashSet<String>();
for (int i = 0, count = remoteFolders.length; i < count; i++) {
remoteFolderNames.add(remoteFolders[i].getName());
}
HashMap<String, LocalMailboxInfo> localFolders =
new HashMap<String, LocalMailboxInfo>();
HashSet<String> localFolderNames = new HashSet<String>();
// Step 2: Get local mailboxes
localFolderCursor = mContext.getContentResolver().query(
EmailContent.Mailbox.CONTENT_URI,
LocalMailboxInfo.PROJECTION,
MAILBOX_PROJECTION,
EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
new String[] { String.valueOf(account.mId) },
null);
// Step 3: Remove any local mailbox not on the remote list
while (localFolderCursor.moveToNext()) {
LocalMailboxInfo info = new LocalMailboxInfo(localFolderCursor);
localFolders.put(info.mDisplayName, info);
localFolderNames.add(info.mDisplayName);
}
// Short circuit the rest if the sets are the same (the usual case)
if (!remoteFolderNames.equals(localFolderNames)) {
// They are different, so we have to do some adds and drops
// Drops first, to make things smaller rather than larger
HashSet<String> localsToDrop = new HashSet<String>(localFolderNames);
localsToDrop.removeAll(remoteFolderNames);
for (String localNameToDrop : localsToDrop) {
LocalMailboxInfo localInfo = localFolders.get(localNameToDrop);
// Exclusion list - never delete local special folders, irrespective
// of server-side existence.
switch (localInfo.mType) {
case Mailbox.TYPE_INBOX:
case Mailbox.TYPE_DRAFTS:
case Mailbox.TYPE_OUTBOX:
case Mailbox.TYPE_SENT:
case Mailbox.TYPE_TRASH:
break;
default:
// Drop all attachment files related to this mailbox
AttachmentUtilities.deleteAllMailboxAttachmentFiles(
mContext, accountId, localInfo.mId);
// Delete the mailbox. Triggers will take care of
// related Message, Body and Attachment records.
Uri uri = ContentUris.withAppendedId(
EmailContent.Mailbox.CONTENT_URI, localInfo.mId);
mContext.getContentResolver().delete(uri, null, null);
break;
}
String mailboxPath
= localFolderCursor.getString(MAILBOX_COLUMN_DISPLAY_NAME);
// Short circuit if we have a remote mailbox with the same name
if (remoteFolderNames.contains(mailboxPath)) {
continue;
}
// Now do the adds
remoteFolderNames.removeAll(localFolderNames);
for (String remoteNameToAdd : remoteFolderNames) {
EmailContent.Mailbox box = new EmailContent.Mailbox();
box.mDisplayName = remoteNameToAdd;
// box.mServerId;
// box.mParentServerId;
// box.mParentKey;
box.mAccountKey = account.mId;
box.mType = LegacyConversions.inferMailboxTypeFromName(
mContext, remoteNameToAdd);
// box.mDelimiter;
// box.mSyncKey;
// box.mSyncLookback;
// box.mSyncFrequency;
// box.mSyncTime;
// box.mUnreadCount;
box.mFlagVisible = true;
// box.mFlags;
box.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
box.save(mContext);
int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE);
long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID);
switch (mailboxType) {
case Mailbox.TYPE_INBOX:
case Mailbox.TYPE_DRAFTS:
case Mailbox.TYPE_OUTBOX:
case Mailbox.TYPE_SENT:
case Mailbox.TYPE_TRASH:
// Never, ever delete special mailboxes
break;
default:
// Drop all attachment files related to this mailbox
AttachmentUtilities.deleteAllMailboxAttachmentFiles(
mContext, accountId, mailboxId);
// Delete the mailbox; database triggers take care of related
// Message, Body and Attachment records
Uri uri = ContentUris.withAppendedId(
Mailbox.CONTENT_URI, mailboxId);
mContext.getContentResolver().delete(uri, null, null);
break;
}
}
mListeners.listFoldersFinished(accountId);
} catch (Exception e) {
mListeners.listFoldersFailed(accountId, "");
mListeners.listFoldersFailed(accountId, e.toString());
} finally {
if (localFolderCursor != null) {
localFolderCursor.close();

View File

@ -17,13 +17,14 @@
package com.android.email.mail;
import com.android.email.Email;
import com.android.email.LegacyConversions;
import com.android.email.R;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.EmailContent.Account;
import com.android.emailcommon.provider.EmailContent.HostAuth;
import com.android.emailcommon.utility.Utility;
import com.android.emailcommon.provider.EmailContent.Mailbox;
import com.google.common.annotations.VisibleForTesting;
import org.xmlpull.v1.XmlPullParserException;
@ -35,6 +36,7 @@ import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
/**
@ -274,7 +276,14 @@ public abstract class Store {
public abstract Folder getFolder(String name) throws MessagingException;
public abstract Folder[] getAllFolders() throws MessagingException;
/**
* Updates the local list of mailboxes according to what is located on the remote server.
* <em>Note: This does not perform folder synchronization and it will not remove mailboxes
* that are stored locally but not remotely.</em>
* @return The set of remote folders
* @throws MessagingException If there was a problem connecting to the remote server
*/
public abstract Folder[] updateFolders() throws MessagingException;
public abstract Bundle checkSettings() throws MessagingException;
@ -328,4 +337,59 @@ public abstract class Store {
throws MessagingException {
return null;
}
/**
* Returns a {@link Mailbox} for the given path. If the path is not in the database, a new
* mailbox will be created.
*/
private static Mailbox getMailboxForPath(Context context, long accountId, String path) {
Mailbox mailbox = Mailbox.restoreMailboxForPath(context, accountId, path);
if (mailbox == null) {
mailbox = new Mailbox();
}
return mailbox;
}
/**
* Adds the mailbox with the given path to the folder list and to the database. If the folder
* already exists on the server (e.g. the path is identical), the database row will be updated.
* Otherwise, a new database row will be inserted.
* @param folders the list of folders
* @param mailboxPath The path of the mailbox to add
* @param delimiter A path delimiter. May be {@code null} if there is no delimiter.
*/
protected void addMailbox(Context context, long accountId, String mailboxPath, String delimiter,
ArrayList<Folder> folders) throws MessagingException {
char delimiterChar = 0;
if (!TextUtils.isEmpty(delimiter)) {
delimiterChar = delimiter.charAt(0);
}
folders.add(getFolder(mailboxPath));
Mailbox mailbox = getMailboxForPath(context, accountId, mailboxPath);
mailbox.mAccountKey = accountId;
mailbox.mDelimiter = delimiterChar;
mailbox.mDisplayName = mailboxPath;
//mailbox.mFlags;
mailbox.mFlagVisible = true;
//mailbox.mParentKey;
//mailbox.mParentServerId;
mailbox.mServerId = mailboxPath;
//mailbox.mServerId;
//mailbox.mSyncFrequency;
//mailbox.mSyncKey;
//mailbox.mSyncLookback;
//mailbox.mSyncTime;
mailbox.mType = LegacyConversions.inferMailboxTypeFromName(context, mailboxPath);
//box.mUnreadCount;
mailbox.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
// TODO This is horribly inefficient. Only update db if the mailbox has really changed
if (mailbox.isSaved()) {
mailbox.update(context, mailbox.toContentValues());
} else {
mailbox.save(context);
}
// TODO ?? Add mailbox to Folder object ??
}
}

View File

@ -80,7 +80,7 @@ public class ExchangeStore extends Store {
}
@Override
public Folder[] getAllFolders() {
public Folder[] updateFolders() {
return null;
}

View File

@ -93,6 +93,7 @@ public class ImapStore extends Store {
static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED };
private final Context mContext;
private final Account mAccount;
private Transport mRootTransport;
private String mUsername;
private String mPassword;
@ -140,6 +141,7 @@ public class ImapStore extends Store {
*/
private ImapStore(Context context, Account account) throws MessagingException {
mContext = context;
mAccount = account;
HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
if (recvAuth == null || !STORE_SCHEME_IMAP.equalsIgnoreCase(recvAuth.mProtocol)) {
@ -359,7 +361,7 @@ public class ImapStore extends Store {
}
@Override
public Folder[] getAllFolders() throws MessagingException {
public Folder[] updateFolders() throws MessagingException {
ImapConnection connection = getConnection();
try {
ArrayList<Folder> folders = new ArrayList<Folder>();
@ -379,8 +381,8 @@ public class ImapStore extends Store {
// Get folder name.
ImapString encodedFolder = response.getStringOrEmpty(3);
if (encodedFolder.isEmpty()) continue;
String folder = decodeFolderName(encodedFolder.getString(), mPathPrefix);
if (ImapConstants.INBOX.equalsIgnoreCase(folder)) {
String folderName = decodeFolderName(encodedFolder.getString(), mPathPrefix);
if (ImapConstants.INBOX.equalsIgnoreCase(folderName)) {
continue;
}
@ -389,11 +391,12 @@ public class ImapStore extends Store {
includeFolder = false;
}
if (includeFolder) {
folders.add(getFolder(folder));
String delimiter = response.getStringOrEmpty(2).toString();
addMailbox(mContext, mAccount.mId, folderName, delimiter, folders);
}
}
}
folders.add(getFolder(ImapConstants.INBOX));
addMailbox(mContext, mAccount.mId, ImapConstants.INBOX, null, folders);
return folders.toArray(new Folder[] {});
} catch (IOException ioe) {
connection.close();

View File

@ -19,6 +19,7 @@ package com.android.email.mail.store;
import com.android.email.Email;
import com.android.email.mail.Store;
import com.android.email.mail.Transport;
import com.android.email.mail.store.imap.ImapConstants;
import com.android.email.mail.transport.MailTransport;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.MimeMessage;
@ -53,6 +54,8 @@ public class Pop3Store extends Store {
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
private final Context mContext;
private final Account mAccount;
private Transport mTransport;
private String mUsername;
private String mPassword;
@ -95,6 +98,9 @@ public class Pop3Store extends Store {
* Creates a new store for the given account.
*/
private Pop3Store(Context context, Account account) throws MessagingException {
mContext = context;
mAccount = account;
HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
if (recvAuth == null || !STORE_SCHEME_POP3.equalsIgnoreCase(recvAuth.mProtocol)) {
throw new MessagingException("Unsupported protocol");
@ -148,10 +154,10 @@ public class Pop3Store extends Store {
}
@Override
public Folder[] getAllFolders() {
return new Folder[] {
getFolder("INBOX"),
};
public Folder[] updateFolders() throws MessagingException {
ArrayList<Folder> folders = new ArrayList<Folder>();
addMailbox(mContext, mAccount.mId, "INBOX", null, folders);
return folders.toArray(new Folder[] {});
}
/**

View File

@ -1212,7 +1212,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
"* lIST (\\HAsNoChildren) \"/\" \"&ZeVnLIqe-\"", // Japanese folder name
getNextTag(true) + " oK SUCCESS"
});
Folder[] folders = mStore.getAllFolders();
Folder[] folders = mStore.updateFolders();
ArrayList<String> list = new ArrayList<String>();
for (Folder f : folders) {
@ -2087,7 +2087,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
"* LIST () \"/\" \"" + FOLDER_2 + "\"",
getNextTag(true) + " OK SUCCESS"
});
final Folder[] folders = mStore.getAllFolders();
final Folder[] folders = mStore.updateFolders();
ArrayList<String> list = new ArrayList<String>();
for (Folder f : folders) {

View File

@ -246,10 +246,10 @@ public class Pop3StoreUnitTests extends InstrumentationTestCase {
/**
* Test small Store & Folder functions that manage folders & namespace
*/
public void testStoreFoldersFunctions() {
public void testStoreFoldersFunctions() throws MessagingException {
// getPersonalNamespaces() always returns INBOX folder
Folder[] folders = mStore.getAllFolders();
Folder[] folders = mStore.updateFolders();
assertEquals(1, folders.length);
assertSame(mFolder, folders[0]);
@ -326,8 +326,8 @@ public class Pop3StoreUnitTests extends InstrumentationTestCase {
/**
* Lightweight test to confirm that POP3 hasn't implemented any folder roles yet.
*/
public void testNoFolderRolesYet() {
Folder[] remoteFolders = mStore.getAllFolders();
public void testNoFolderRolesYet() throws MessagingException {
Folder[] remoteFolders = mStore.updateFolders();
for (Folder folder : remoteFolders) {
assertEquals(Folder.FolderRole.UNKNOWN, folder.getRole());
}

View File

@ -117,9 +117,12 @@ public class ProviderTestUtils extends Assert {
Context context) {
return setupMailbox(name, accountId, saveIt, context, Mailbox.TYPE_MAIL);
}
public static Mailbox setupMailbox(String name, long accountId, boolean saveIt,
Context context, int type) {
return setupMailbox(name, accountId, saveIt, context, type, '/');
}
public static Mailbox setupMailbox(String name, long accountId, boolean saveIt,
Context context, int type, char delimiter) {
Mailbox box = new Mailbox();
box.mDisplayName = name;
@ -128,7 +131,7 @@ public class ProviderTestUtils extends Assert {
box.mParentKey = 4;
box.mAccountKey = accountId;
box.mType = type;
box.mDelimiter = 1;
box.mDelimiter = delimiter;
box.mSyncKey = "sync-key-" + name;
box.mSyncLookback = 2;
box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;

View File

@ -2093,22 +2093,55 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
assertEquals(-1, Account.getAccountIdForMessageId(c, 12345));
}
public void testGetAccountMailboxFromMessageId() {
public void testGetMailboxForMessageId() {
final Context c = mMockContext;
Account a = ProviderTestUtils.setupAccount("acct", true, c);
Mailbox b1 = ProviderTestUtils.setupMailbox("box1", a.mId, true, c, Mailbox.TYPE_MAIL);
Mailbox b2 = ProviderTestUtils.setupMailbox("box2", a.mId, true, c, Mailbox.TYPE_MAIL);
Mailbox b1 = ProviderTestUtils.setupMailbox("box1", 1, true, c, Mailbox.TYPE_MAIL);
Mailbox b2 = ProviderTestUtils.setupMailbox("box2", 1, true, c, Mailbox.TYPE_MAIL);
Message m1 = createMessage(c, b1, false, false);
Message m2 = createMessage(c, b2, false, false);
ProviderTestUtils.assertAccountEqual("x", a, Account.getAccountForMessageId(c, m1.mId));
ProviderTestUtils.assertAccountEqual("x", a, Account.getAccountForMessageId(c, m2.mId));
// Restore the mailboxes, since the unread & total counts will have changed
b1 = Mailbox.restoreMailboxWithId(c, b1.mId);
b2 = Mailbox.restoreMailboxWithId(c, b2.mId);
ProviderTestUtils.assertMailboxEqual("x", b1, Mailbox.getMailboxForMessageId(c, m1.mId));
ProviderTestUtils.assertMailboxEqual("x", b2, Mailbox.getMailboxForMessageId(c, m2.mId));
}
public void testRestoreMailboxWithId() {
final Context c = mMockContext;
Mailbox testMailbox;
testMailbox = ProviderTestUtils.setupMailbox("box1", 1, true, c, Mailbox.TYPE_MAIL);
ProviderTestUtils.assertMailboxEqual(
"x", testMailbox, Mailbox.restoreMailboxWithId(c, testMailbox.mId));
testMailbox = ProviderTestUtils.setupMailbox("box2", 1, true, c, Mailbox.TYPE_MAIL);
ProviderTestUtils.assertMailboxEqual(
"x", testMailbox, Mailbox.restoreMailboxWithId(c, testMailbox.mId));
// Unknown IDs
assertNull(Mailbox.restoreMailboxWithId(c, 8));
assertNull(Mailbox.restoreMailboxWithId(c, -1));
assertNull(Mailbox.restoreMailboxWithId(c, Long.MAX_VALUE));
}
public void testRestoreMailboxForPath() {
final Context c = mMockContext;
Mailbox testMailbox;
testMailbox = ProviderTestUtils.setupMailbox("a/b/c/box", 1, true, c, Mailbox.TYPE_MAIL);
ProviderTestUtils.assertMailboxEqual(
"x", testMailbox, Mailbox.restoreMailboxForPath(c, 1, "a/b/c/box"));
// Same name, different account; no match
assertNull(Mailbox.restoreMailboxForPath(c, 2, "a/b/c/box"));
// Substring; no match
assertNull(Mailbox.restoreMailboxForPath(c, 1, "a/b/c"));
// Wild cards not supported; no match
assertNull(Mailbox.restoreMailboxForPath(c, 1, "a/b/c/%"));
}
public void testGetAccountForMessageId() {
final Context c = mMockContext;
Account a = ProviderTestUtils.setupAccount("acct", true, c);
Message m1 = ProviderTestUtils.setupMessage("1", a.mId, 1, true, true, c, false, false);
Message m2 = ProviderTestUtils.setupMessage("1", a.mId, 2, true, true, c, false, false);
ProviderTestUtils.assertAccountEqual("x", a, Account.getAccountForMessageId(c, m1.mId));
ProviderTestUtils.assertAccountEqual("x", a, Account.getAccountForMessageId(c, m2.mId));
}
public void testGetAccountGetInboxIdTest() {
final Context c = mMockContext;