Implement move-to-trash for IMAP and POP3.

* Define new message-loaded state "FLAG_LOADED_DELETED" (used only for
   POP3, which needs to write sentinel messages that are not displayed.)
* Also renamed the other flags to make the naming more consistent.
* Tweak MessageList query generation to inhibit display of deleted
    message sentinels, and MessagingController won't try to resync them.
* Clean up implementation of Controller.deleteMessage()
* Add support for move to trash to MessagingController.  This operates
    in three primary modes:
  * POP3 local delete (no server-side interaction)
  * POP3 server delete
  * IMAP server delete (and copy to IMAP trash mailbox)
* Add missing implementation in provider to delete all of the attachments
    for a given message
* Fix progress reporting in activities (the test for error vs. progress
    was inverted, which caused progress indicators to keep spinning
    after errors.)
* Fix broken account settings UI (POP3 delete policy was not persisting)

Addresses bug 2097409

TODO delete from trash / empty trash

Change-Id: I00188e6dc2093823106e009f35b68c760227c9e6
This commit is contained in:
Andrew Stadler 2009-09-10 11:52:36 -07:00
parent c522cfa770
commit 6c21942ec4
15 changed files with 322 additions and 149 deletions

View File

@ -254,7 +254,7 @@ public class Controller {
// and get out of here.
Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId);
ContentValues cv = new ContentValues();
cv.put(MessageColumns.FLAG_LOADED, Message.LOADED);
cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE);
mContext.getContentResolver().update(uri, cv, null, null);
Log.d(Email.LOG_TAG, "Unexpected loadMessageForView() for service-based message.");
synchronized (mListeners) {
@ -565,24 +565,20 @@ public class Controller {
Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
resolver.update(uri, cv, null, null);
// 5. Drop non-essential data for the message (e.g. attachments)
// TODO: find the actual files (if any, if loaded) & delete them
c = null;
try {
c = resolver.query(EmailContent.Attachment.CONTENT_URI,
EmailContent.Attachment.CONTENT_PROJECTION,
EmailContent.AttachmentColumns.MESSAGE_KEY + "=?",
new String[] { Long.toString(messageId) }, null);
while (c.moveToNext()) {
// delete any associated storage
// delete row?
}
} finally {
if (c != null) c.close();
}
// 5. Drop non-essential data for the message (e.g. attachment files)
AttachmentProvider.deleteAllAttachmentFiles(mContext, accountId, messageId);
// 6. For IMAP/POP3 we may need to kick off an immediate delete (depends on acct settings)
// TODO write this
// 6. Service runs automatically, MessagingController needs a kick
final Message message = Message.restoreMessageWithId(mContext, messageId);
Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
if (isMessagingController(account)) {
new Thread() {
@Override
public void run() {
mLegacyController.processPendingCommands(message.mAccountKey);
}
}.start();
}
}
/**

View File

@ -74,11 +74,11 @@ public class LegacyConversions {
// Keep the message in the "unloaded" state until it has (at least) a display name.
// This prevents early flickering of empty messages in POP download.
if (localMessage.mFlagLoaded != EmailContent.Message.LOADED) {
if (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE) {
if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) {
localMessage.mFlagLoaded = EmailContent.Message.NOT_LOADED;
localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_UNLOADED;
} else {
localMessage.mFlagLoaded = EmailContent.Message.PARTIALLY_LOADED;
localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
}
}
localMessage.mFlagFavorite = message.isSet(Flag.FLAGGED);

View File

@ -41,6 +41,7 @@ import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.AttachmentColumns;
import com.android.email.provider.EmailContent.Body;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.MessageColumns;
@ -280,6 +281,7 @@ public class MessagingController implements Runnable {
// Drops first, to make things smaller rather than larger
HashSet<String> localsToDrop = new HashSet<String>(localFolderNames);
localsToDrop.removeAll(remoteFolderNames);
// TODO drop all attachment files too
for (String localNameToDrop : localsToDrop) {
LocalMailboxInfo localInfo = localFolders.get(localNameToDrop);
Uri uri = ContentUris.withAppendedId(
@ -563,6 +565,7 @@ public class MessagingController implements Runnable {
throws MessagingException {
Log.d(Email.LOG_TAG, "*** synchronizeMailboxGeneric ***");
ContentResolver resolver = mContext.getContentResolver();
// 1. Get the message list from the local store and create an index of the uids
@ -570,7 +573,7 @@ public class MessagingController implements Runnable {
HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
try {
localUidCursor = mContext.getContentResolver().query(
localUidCursor = resolver.query(
EmailContent.Message.CONTENT_URI,
LocalMessageInfo.PROJECTION,
EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
@ -660,14 +663,18 @@ public class MessagingController implements Runnable {
* Get a list of the messages that are in the remote list but not on the
* local store, or messages that are in the local store but failed to download
* on the last sync. These are the new messages that we will download.
* Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
* because they are locally deleted and we don't need or want the old message from
* the server.
*/
for (Message message : remoteMessages) {
LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
if (localMessage == null) {
newMessageCount++;
}
if (localMessage == null ||
localMessage.mFlagLoaded != EmailContent.Message.LOADED) {
if (localMessage == null
|| (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)
|| (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_PARTIAL)) {
unsyncedMessages.add(message);
}
}
@ -770,7 +777,7 @@ public class MessagingController implements Runnable {
ContentValues updateValues = new ContentValues();
updateValues.put(EmailContent.Message.FLAG_READ, remoteSeen);
updateValues.put(EmailContent.Message.FLAG_FAVORITE, remoteFlagged);
mContext.getContentResolver().update(uri, updateValues, null, null);
resolver.update(uri, updateValues, null, null);
}
}
}
@ -805,7 +812,7 @@ public class MessagingController implements Runnable {
// Uri uri = ContentUris.withAppendedId(EmailContent.Mailbox.CONTENT_URI, folder.mId);
// ContentValues updateValues = new ContentValues();
// updateValues.put(EmailContent.Mailbox.UNREAD_COUNT, remoteUnreadMessageCount);
// mContext.getContentResolver().update(uri, updateValues, null, null);
// resolver.update(uri, updateValues, null, null);
// 11. Remove any messages that are in the local store but no longer on the remote store.
@ -814,9 +821,22 @@ public class MessagingController implements Runnable {
for (String uidToDelete : localUidsToDelete) {
LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
// Delete associated data (attachment files)
// Attachment & Body records are auto-deleted when we delete the Message record
AttachmentProvider.deleteAllAttachmentFiles(mContext, account.mId, infoToDelete.mId);
// Delete the message itself
Uri uriToDelete = ContentUris.withAppendedId(
EmailContent.Message.CONTENT_URI, infoToDelete.mId);
mContext.getContentResolver().delete(uriToDelete, null, null);
resolver.delete(uriToDelete, null, null);
// Delete extra rows (e.g. synced or deleted)
Uri syncRowToDelete = ContentUris.withAppendedId(
EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
resolver.delete(syncRowToDelete, null, null);
Uri deletERowToDelete = ContentUris.withAppendedId(
EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
resolver.delete(deletERowToDelete, null, null);
}
// 12. Divide the unsynced messages into small & large (by size)
@ -851,7 +871,7 @@ public class MessagingController implements Runnable {
public void messageFinished(Message message, int number, int ofTotal) {
// Store the updated message locally and mark it fully loaded
copyOneMessageToProvider(message, account, folder,
EmailContent.Message.LOADED);
EmailContent.Message.FLAG_LOADED_COMPLETE);
}
public void messageStarted(String uid, int number, int ofTotal) {
@ -877,7 +897,7 @@ public class MessagingController implements Runnable {
// Store the partially-loaded message and mark it partially loaded
copyOneMessageToProvider(message, account, folder,
EmailContent.Message.PARTIALLY_LOADED);
EmailContent.Message.FLAG_LOADED_PARTIAL);
} else {
// We have a structure to deal with, from which
// we can pull down the parts we want to actually store.
@ -895,7 +915,8 @@ public class MessagingController implements Runnable {
remoteFolder.fetch(new Message[] { message }, fp, null);
}
// Store the updated message locally and mark it fully loaded
copyOneMessageToProvider(message, account, folder, EmailContent.Message.LOADED);
copyOneMessageToProvider(message, account, folder,
EmailContent.Message.FLAG_LOADED_COMPLETE);
}
}
@ -1135,14 +1156,15 @@ public class MessagingController implements Runnable {
* Handles:
* Read/Unread
* Flagged
* Move To Trash
* TODO:
* Delete
* Empty trash
* Append
* Move
*
* TODO: tighter projections
*
* @param account the account to update
* @param account the account to scan for pending actions
* @throws MessagingException
*/
private void processPendingCommandsSynchronous(EmailContent.Account account)
@ -1157,10 +1179,11 @@ public class MessagingController implements Runnable {
try {
// Defer setting up the store until we know we need to access it
Store remoteStore = null;
// Demand load mailbox (note order-by to reduce thrashing here
// Demand load mailbox (note order-by to reduce thrashing here)
Mailbox mailbox = null;
// loop through messages marked as needing updates
while (updates.moveToNext()) {
boolean changeMoveToTrash = false;
boolean changeRead = false;
boolean changeFlagged = false;
@ -1170,20 +1193,27 @@ public class MessagingController implements Runnable {
EmailContent.Message newMessage =
EmailContent.Message.restoreMessageWithId(mContext, oldMessage.mId);
if (newMessage != null) {
if (mailbox == null || mailbox.mId != newMessage.mMailboxKey) {
mailbox = Mailbox.restoreMailboxWithId(mContext, newMessage.mMailboxKey);
}
changeMoveToTrash = (oldMessage.mMailboxKey != newMessage.mMailboxKey)
&& (mailbox.mType == Mailbox.TYPE_TRASH);
changeRead = oldMessage.mFlagRead != newMessage.mFlagRead;
changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite;
}
// Handle simple flag changes
if (changeRead || changeFlagged) {
if (remoteStore == null) {
remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
null);
}
if (mailbox == null || mailbox.mId != newMessage.mMailboxKey) {
mailbox =
Mailbox.restoreMailboxWithId(mContext, newMessage.mMailboxKey);
}
// Load the remote store if it will be needed
if (remoteStore == null && (changeMoveToTrash || changeRead || changeFlagged)) {
remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext, null);
}
// Dispatch here for specific change types
if (changeMoveToTrash) {
// Move message to trash
processPendingMoveToTrash(remoteStore, account, mailbox,
oldMessage, newMessage);
} else if (changeRead || changeFlagged) {
// Upsync changes to read or flagged
Folder remoteFolder = remoteStore.getFolder(mailbox.mDisplayName);
if (remoteFolder.exists()) {
remoteFolder.open(OpenMode.READ_WRITE, null);
@ -1331,44 +1361,68 @@ public class MessagingController implements Runnable {
/**
* Process a pending trash message command.
*
* @param command arguments = (String folder, String uid)
* @param account
* @throws MessagingException
* @param remoteStore the remote store we're working in
* @param account The account in which we are working
* @param newMailbox The local trash mailbox
* @param oldMessage The message copy that was saved in the updates shadow table
* @param newMessage The message that was moved to the mailbox
*/
private void processPendingTrash(PendingCommand command, final EmailContent.Account account)
throws MessagingException {
String folder = command.arguments[0];
String uid = command.arguments[1];
private void processPendingMoveToTrash(Store remoteStore,
EmailContent.Account account, Mailbox newMailbox, EmailContent.Message oldMessage,
final EmailContent.Message newMessage) throws MessagingException {
final LocalStore localStore = (LocalStore) Store.getInstance(
account.getLocalStoreUri(mContext), mContext, null);
LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder);
// 1. Escape early if we can't find the local mailbox
// TODO smaller projection here
Mailbox oldMailbox = Mailbox.restoreMailboxWithId(mContext, oldMessage.mMailboxKey);
if (oldMailbox == null) {
// can't find old mailbox, it may have been deleted. just return.
return;
}
// 2. We don't support delete-from-trash here
if (oldMailbox.mType == Mailbox.TYPE_TRASH) {
return;
}
Store remoteStore = Store.getInstance(account.getStoreUri(mContext), mContext,
localStore.getPersistentCallbacks());
Folder remoteFolder = remoteStore.getFolder(folder);
// 3. If DELETE_POLICY_NEVER, simply write back the deleted sentinel and return
//
// This sentinel takes the place of the server-side message, and locally "deletes" it
// by inhibiting future sync or display of the message. It will eventually go out of
// scope when it becomes old, or is deleted on the server, and the regular sync code
// will clean it up for us.
if (account.getDeletePolicy() == Account.DELETE_POLICY_NEVER) {
EmailContent.Message sentinel = new EmailContent.Message();
sentinel.mAccountKey = oldMessage.mAccountKey;
sentinel.mMailboxKey = oldMessage.mMailboxKey;
sentinel.mFlagLoaded = EmailContent.Message.FLAG_LOADED_DELETED;
sentinel.mServerId = oldMessage.mServerId;
sentinel.save(mContext);
return;
}
// The rest of this method handles server-side deletion
// 4. Find the remote mailbox (that we deleted from), and open it
Folder remoteFolder = remoteStore.getFolder(oldMailbox.mDisplayName);
if (!remoteFolder.exists()) {
return;
}
remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks());
remoteFolder.open(OpenMode.READ_WRITE, null);
if (remoteFolder.getMode() != OpenMode.READ_WRITE) {
remoteFolder.close(false);
return;
}
Message remoteMessage = null;
if (!uid.startsWith("Local")) {
remoteMessage = remoteFolder.getMessage(uid);
}
// 5. Find the remote original message
Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId);
if (remoteMessage == null) {
remoteFolder.close(false);
return;
}
Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName(mContext));
/*
* Attempt to copy the remote message to the remote trash folder.
*/
// 6. Find the remote trash folder, and create it if not found
Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mDisplayName);
if (!remoteTrashFolder.exists()) {
/*
* If the remote trash folder doesn't exist we try to create it.
@ -1376,14 +1430,13 @@ public class MessagingController implements Runnable {
remoteTrashFolder.create(FolderType.HOLDS_MESSAGES);
}
// 7. Try to copy the message into the remote trash folder
// Note, this entire section will be skipped for POP3 because there's no remote trash
if (remoteTrashFolder.exists()) {
/*
* Because remoteTrashFolder may be new, we need to explicitly open it
* and pass in the persistence callbacks.
*/
final LocalFolder localTrashFolder =
(LocalFolder) localStore.getFolder(account.getTrashFolderName(mContext));
remoteTrashFolder.open(OpenMode.READ_WRITE, localTrashFolder.getPersistentCallbacks());
remoteTrashFolder.open(OpenMode.READ_WRITE, null);
if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) {
remoteFolder.close(false);
remoteTrashFolder.close(false);
@ -1392,16 +1445,12 @@ public class MessagingController implements Runnable {
remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder,
new Folder.MessageUpdateCallbacks() {
public void onMessageUidChange(Message message, String newUid)
throws MessagingException {
public void onMessageUidChange(Message message, String newUid) {
// update the UID in the local trash folder, because some stores will
// have to change it when copying to remoteTrashFolder
LocalMessage localMessage =
(LocalMessage) localTrashFolder.getMessage(message.getUid());
if(localMessage != null) {
localMessage.setUid(newUid);
localTrashFolder.updateMessage(localMessage);
}
ContentValues cv = new ContentValues();
cv.put(EmailContent.Message.SERVER_ID, newUid);
mContext.getContentResolver().update(newMessage.getUri(), cv, null, null);
}
/**
@ -1409,12 +1458,8 @@ public class MessagingController implements Runnable {
* deleted (e.g. it was already deleted from the server.) In this case,
* attempt to delete the local copy as well.
*/
public void onMessageNotFound(Message message) throws MessagingException {
LocalMessage localMessage =
(LocalMessage) localTrashFolder.getMessage(message.getUid());
if (localMessage != null) {
localMessage.setFlag(Flag.DELETED, true);
}
public void onMessageNotFound(Message message) {
mContext.getContentResolver().delete(newMessage.getUri(), null, null);
}
}
@ -1422,6 +1467,7 @@ public class MessagingController implements Runnable {
remoteTrashFolder.close(false);
}
// 8. Delete the message from the remote source folder
remoteMessage.setFlag(Flag.DELETED, true);
remoteFolder.expunge();
remoteFolder.close(false);
@ -1446,7 +1492,7 @@ public class MessagingController implements Runnable {
mListeners.loadMessageForViewFailed(messageId, "Unknown message");
return;
}
if (message.mFlagLoaded == EmailContent.Message.LOADED) {
if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) {
mListeners.loadMessageForViewFinished(messageId);
return;
}
@ -1492,7 +1538,7 @@ public class MessagingController implements Runnable {
// 5. Write to provider
copyOneMessageToProvider(remoteMessage, account, mailbox,
EmailContent.Message.LOADED);
EmailContent.Message.FLAG_LOADED_COMPLETE);
// 6. Notify UI
mListeners.loadMessageForViewFinished(messageId);

View File

@ -238,7 +238,9 @@ public class Utility {
public static String buildMailboxIdSelection(ContentResolver resolver, long mailboxId) {
// Setup default selection & args, then add to it as necessary
StringBuilder selection = new StringBuilder(
Message.FLAG_LOADED + "!=" + Message.NOT_LOADED + " AND ");
MessageColumns.FLAG_LOADED + " IN ("
+ Message.FLAG_LOADED_PARTIAL + "," + Message.FLAG_LOADED_COMPLETE
+ ") AND ");
if (mailboxId == Mailbox.QUERY_ALL_INBOXES
|| mailboxId == Mailbox.QUERY_ALL_DRAFTS
|| mailboxId == Mailbox.QUERY_ALL_OUTBOX) {

View File

@ -601,20 +601,12 @@ public class AccountFolderList extends ListActivity
private class ControllerResults implements Controller.Result {
public void updateMailboxListCallback(MessagingException result, long accountKey,
int progress) {
if (progress == 0) {
mHandler.progress(true);
} else if (result != null || progress == 100) {
mHandler.progress(false);
}
updateProgress(result, progress);
}
public void updateMailboxCallback(MessagingException result, long accountKey,
long mailboxKey, int progress, int numNewMessages) {
if (progress == 0) {
mHandler.progress(true);
} else if (result != null || progress == 100) {
mHandler.progress(false);
}
updateProgress(result, progress);
}
public void loadMessageForViewCallback(MessagingException result, long messageId,
@ -627,11 +619,20 @@ public class AccountFolderList extends ListActivity
public void serviceCheckMailCallback(MessagingException result, long accountId,
long mailboxId, int progress, long tag) {
updateProgress(result, progress);
}
public void sendMailCallback(MessagingException result, long accountId, long messageId,
int progress) {
}
private void updateProgress(MessagingException result, int progress) {
if (result != null || progress == 100) {
mHandler.progress(false);
} else if (progress == 0) {
mHandler.progress(true);
}
}
}
/* package */ static class AccountsAdapter extends CursorAdapter {

View File

@ -376,11 +376,7 @@ public class MailboxList extends ListActivity implements OnItemClickListener, On
public void updateMailboxListCallback(MessagingException result, long accountKey,
int progress) {
if (accountKey == mAccountId) {
if (progress == 0) {
mHandler.progress(true);
} else if (result != null || progress == 100) {
mHandler.progress(false);
}
updateProgress(result, progress);
}
}
@ -388,11 +384,7 @@ public class MailboxList extends ListActivity implements OnItemClickListener, On
public void updateMailboxCallback(MessagingException result, long accountKey,
long mailboxKey, int progress, int numNewMessages) {
if (accountKey == mAccountId) {
if (progress == 0) {
mHandler.progress(true);
} else if (result != null || progress == 100) {
mHandler.progress(false);
}
updateProgress(result, progress);
}
}
@ -410,6 +402,17 @@ public class MailboxList extends ListActivity implements OnItemClickListener, On
public void sendMailCallback(MessagingException result, long accountId, long messageId,
int progress) {
if (accountId == mAccountId) {
updateProgress(result, progress);
}
}
private void updateProgress(MessagingException result, int progress) {
if (result != null || progress == 100) {
mHandler.progress(false);
} else if (progress == 0) {
mHandler.progress(true);
}
}
}

View File

@ -667,7 +667,7 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus
message.mText = bodyText;
message.mAccountKey = account.mId;
message.mDisplayName = makeDisplayName(message.mTo, message.mCc, message.mBcc);
message.mFlagLoaded = Message.LOADED;
message.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
message.mFlagAttachment = hasAttachments;
String action = getIntent().getAction();
// Use the Intent to set flags saying this message is a reply or a forward and save the

View File

@ -1118,15 +1118,9 @@ public class MessageList extends ListActivity implements OnItemClickListener, On
// TODO check accountKey and only react to relevant notifications
public void updateMailboxListCallback(MessagingException result, long accountKey,
int progress) {
if (progress == 0) {
mHandler.progress(true);
} else if (result != null || progress == 100) {
mHandler.progress(false);
if (mWaitForMailboxType != -1) {
if (result == null) {
mHandler.lookupMailboxType(accountKey, mWaitForMailboxType);
}
}
updateProgress(result, progress);
if (progress == 100) {
mHandler.lookupMailboxType(accountKey, mWaitForMailboxType);
}
}
@ -1134,11 +1128,7 @@ public class MessageList extends ListActivity implements OnItemClickListener, On
// TODO check accountKey and only react to relevant notifications
public void updateMailboxCallback(MessagingException result, long accountKey,
long mailboxKey, int progress, int numNewMessages) {
if (progress == 0) {
mHandler.progress(true);
} else if (result != null || progress == 100) {
mHandler.progress(false);
}
updateProgress(result, progress);
}
public void loadMessageForViewCallback(MessagingException result, long messageId,
@ -1157,11 +1147,15 @@ public class MessageList extends ListActivity implements OnItemClickListener, On
public void sendMailCallback(MessagingException result, long accountId, long messageId,
int progress) {
if (mListFooterMode == LIST_FOOTER_MODE_SEND) {
if (progress == 0) {
mHandler.progress(true);
} else if (result != null || progress == 100) {
mHandler.progress(false);
}
updateProgress(result, progress);
}
}
private void updateProgress(MessagingException result, int progress) {
if (result != null || progress == 100) {
mHandler.progress(false);
} else if (progress == 0) {
mHandler.progress(true);
}
}
}

View File

@ -1167,7 +1167,7 @@ public class MessageView extends Activity implements OnClickListener {
// 2. If != LOADED, ask controller to load it
// 3. Controller callback (after loaded) should trigger LoadBodyTask & LoadAttachmentsTask
// 4. Else start the loader tasks right away (message already loaded)
if (okToFetch && message.mFlagLoaded != Message.LOADED) {
if (okToFetch && message.mFlagLoaded != Message.FLAG_LOADED_COMPLETE) {
mWaitForLoadMessageId = message.mId;
mController.loadMessageForView(message.mId, mControllerCallback);
} else {

View File

@ -228,8 +228,12 @@ public class AccountSettings extends PreferenceActivity {
this, mAccount.mHostAuthKeyRecv);
mAccount.mHostAuthSend = HostAuth.restoreHostAuthWithId(
this, mAccount.mHostAuthKeySend);
// TODO write me.
// Because "delete policy" UI is on edit incoming settings, we have
// to refresh that as well.
Account refreshedAccount = Account.restoreAccountWithId(this, mAccount.mId);
mAccount.setDeletePolicy(refreshedAccount.getDeletePolicy());
mAccountDirty = false;
}
}

View File

@ -273,8 +273,7 @@ public abstract class EmailContent {
/**
* Returns the bodyId for the given messageId, or -1 if no body is found.
*/
/* package */
static long lookupBodyIdWithMessageId(ContentResolver resolver, long messageId) {
public static long lookupBodyIdWithMessageId(ContentResolver resolver, long messageId) {
Cursor c = resolver.query(Body.CONTENT_URI, ID_PROJECTION,
Body.MESSAGE_KEY + "=?",
new String[] {Long.toString(messageId)}, null);
@ -356,7 +355,7 @@ public abstract class EmailContent {
public static final String SUBJECT = "subject";
// Boolean, unread = 0, read = 1 [INDEX]
public static final String FLAG_READ = "flagRead";
// Three state, unloaded = 0, loaded = 1, partially loaded (optional) = 2 [INDEX]
// Load state, see constants below (unloaded, partial, complete, deleted)
public static final String FLAG_LOADED = "flagLoaded";
// Boolean, unflagged = 0, flagged (favorite) = 1
public static final String FLAG_FAVORITE = "flagFavorite";
@ -471,7 +470,7 @@ public abstract class EmailContent {
public long mTimeStamp;
public String mSubject;
public boolean mFlagRead = false;
public int mFlagLoaded = 0;
public int mFlagLoaded = FLAG_LOADED_UNLOADED;
public boolean mFlagFavorite = false;
public boolean mFlagAttachment = false;
public int mFlags = 0;
@ -502,12 +501,12 @@ public abstract class EmailContent {
// Values used in mFlagRead
public static final int UNREAD = 0;
public static final int READ = 1;
public static final int DELETED = 2;
// Values used in mFlagLoaded
public static final int NOT_LOADED = 0;
public static final int LOADED = 1;
public static final int PARTIALLY_LOADED = 2;
public static final int FLAG_LOADED_UNLOADED = 0;
public static final int FLAG_LOADED_COMPLETE = 1;
public static final int FLAG_LOADED_PARTIAL = 2;
public static final int FLAG_LOADED_DELETED = 3;
// Bits used in mFlags
// These three states are mutually exclusive, and indicate whether the message is an

View File

@ -617,6 +617,8 @@ public class EmailProvider extends ContentProvider {
// 1) Begin a transaction, ensuring that both databases are affected atomically
// 2) Do the requested deletion, with cascading deletions handled in triggers
// 3) End the transaction, committing all changes atomically
//
// Bodies are auto-deleted here; Attachments are auto-deleted via trigger
messageDeletion = true;
if (!mInTransaction) {
@ -645,6 +647,13 @@ public class EmailProvider extends ContentProvider {
result = db.delete(TABLE_NAMES[table], whereWithId(id, selection),
selectionArgs);
break;
case ATTACHMENTS_MESSAGE_ID:
// All attachments for the given message
id = uri.getPathSegments().get(2);
result = db.delete(TABLE_NAMES[table],
whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs);
break;
case BODY:
case MESSAGE:
case DELETED_MESSAGE:
@ -655,6 +664,7 @@ public class EmailProvider extends ContentProvider {
case HOSTAUTH:
result = db.delete(TABLE_NAMES[table], selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -846,18 +856,32 @@ public class EmailProvider extends ContentProvider {
sb.append("_id=");
sb.append(id);
if (selection != null) {
sb.append(" AND ");
sb.append(" AND (");
sb.append(selection);
sb.append(')');
}
return sb.toString();
}
/**
* Combine a locally-generated selection with a user-provided selection
*
* This introduces risk that the local selection might insert incorrect chars
* into the SQL, so use caution.
*
* @param where locally-generated selection, must not be null
* @param selection user-provided selection, may be null
* @return a single selection string
*/
private String whereWith(String where, String selection) {
StringBuilder sb = new StringBuilder(where);
if (selection != null) {
sb.append(" AND ");
sb.append(selection);
if (selection == null) {
return where;
}
StringBuilder sb = new StringBuilder(where);
sb.append(" AND (");
sb.append(selection);
sb.append(')');
return sb.toString();
}

View File

@ -176,7 +176,7 @@ public class EmailSyncAdapter extends AbstractSyncAdapter {
Message msg = new Message();
msg.mAccountKey = mAccount.mId;
msg.mMailboxKey = mMailbox.mId;
msg.mFlagLoaded = Message.LOADED;
msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
while (nextTag(Tags.SYNC_ADD) != END) {
switch (tag) {

View File

@ -103,7 +103,7 @@ public class ProviderTestUtils extends Assert {
message.mTimeStamp = 1;
message.mSubject = "subject " + name;
message.mFlagRead = true;
message.mFlagLoaded = Message.NOT_LOADED;
message.mFlagLoaded = Message.FLAG_LOADED_UNLOADED;
message.mFlagFavorite = true;
message.mFlagAttachment = true;
message.mFlags = 2;
@ -127,7 +127,7 @@ public class ProviderTestUtils extends Assert {
message.mHtml = "body html " + name;
message.mTextReply = "reply text " + name;
message.mHtmlReply = "reply html " + name;
message.mSourceKey = mailboxId;
message.mSourceKey = 1000;
}
if (saveIt) {

View File

@ -19,6 +19,7 @@ package com.android.email.provider;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.AttachmentColumns;
import com.android.email.provider.EmailContent.Body;
import com.android.email.provider.EmailContent.BodyColumns;
import com.android.email.provider.EmailContent.Mailbox;
@ -199,6 +200,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
String html2 = message2.mHtml;
String textReply2 = message2.mTextReply;
String htmlReply2 = message2.mHtmlReply;
long sourceKey2 = message2.mSourceKey;
message2.mText = null;
message2.mHtml = null;
message2.mTextReply = null;
@ -213,7 +215,7 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
assertEquals("body html", html2, body2.mHtmlContent);
assertEquals("reply text", textReply2, body2.mTextReply);
assertEquals("reply html", htmlReply2, body2.mHtmlReply);
assertEquals("source key", message2.mMailboxKey, body2.mSourceKey);
assertEquals("source key", sourceKey2, body2.mSourceKey);
// Message with attachments and body
Message message3 = ProviderTestUtils.setupMessage("message3", account1Id, box1Id, true,
@ -816,10 +818,78 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
}
/**
* TODO: Test cascaded delete message
* TODO: body
* TODO: attachments
* Test cascaded delete message
* Confirms that deleting a message will also delete its body & attachments
*/
public void testCascadeMessageDelete() {
Account account1 = ProviderTestUtils.setupAccount("message-cascade", true, mMockContext);
long account1Id = account1.mId;
Mailbox box1 = ProviderTestUtils.setupMailbox("box1", account1Id, true, mMockContext);
long box1Id = box1.mId;
// Each message has a body, and also give each 2 attachments
Message message1 = ProviderTestUtils.setupMessage("message1", account1Id, box1Id, true,
false, mMockContext);
ArrayList<Attachment> atts = new ArrayList<Attachment>();
for (int i = 0; i < 2; i++) {
atts.add(ProviderTestUtils.setupAttachment(
-1, expectedAttachmentNames[i], expectedAttachmentSizes[i],
false, mMockContext));
}
message1.mAttachments = atts;
message1.save(mMockContext);
long message1Id = message1.mId;
Message message2 = ProviderTestUtils.setupMessage("message2", account1Id, box1Id, true,
false, mMockContext);
atts = new ArrayList<Attachment>();
for (int i = 0; i < 2; i++) {
atts.add(ProviderTestUtils.setupAttachment(
-1, expectedAttachmentNames[i], expectedAttachmentSizes[i],
false, mMockContext));
}
message2.mAttachments = atts;
message2.save(mMockContext);
long message2Id = message2.mId;
// Set up to test total counts of bodies & attachments for our test messages
String bodySelection = BodyColumns.MESSAGE_KEY + " IN (?,?)";
String attachmentSelection = AttachmentColumns.MESSAGE_KEY + " IN (?,?)";
String[] selArgs = new String[] { String.valueOf(message1Id), String.valueOf(message2Id) };
// make sure there are two bodies
int numBodies = EmailContent.count(mMockContext, Body.CONTENT_URI, bodySelection, selArgs);
assertEquals(2, numBodies);
// make sure there are four attachments
int numAttachments = EmailContent.count(mMockContext, Attachment.CONTENT_URI,
attachmentSelection, selArgs);
assertEquals(4, numAttachments);
// now delete one of the messages
Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, message1Id);
mMockContext.getContentResolver().delete(uri, null, null);
// there should be one body and two attachments
numBodies = EmailContent.count(mMockContext, Body.CONTENT_URI, bodySelection, selArgs);
assertEquals(1, numBodies);
numAttachments = EmailContent.count(mMockContext, Attachment.CONTENT_URI,
attachmentSelection, selArgs);
assertEquals(2, numAttachments);
// now delete the other message
uri = ContentUris.withAppendedId(Message.CONTENT_URI, message2Id);
mMockContext.getContentResolver().delete(uri, null, null);
// make sure there are no bodies or attachments
numBodies = EmailContent.count(mMockContext, Body.CONTENT_URI, bodySelection, selArgs);
assertEquals(0, numBodies);
numAttachments = EmailContent.count(mMockContext, Attachment.CONTENT_URI,
attachmentSelection, selArgs);
assertEquals(0, numAttachments);
}
/**
* Test that our unique file name algorithm works as expected. Since this test requires an
@ -895,6 +965,40 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
}
}
/**
* Test deleting attachments by message ID (using EmailContent.Attachment.MESSAGE_ID_URI)
*/
public void testDeleteAttachmentByMessageIdUri() {
ContentResolver mockResolver = mMockContext.getContentResolver();
// Note, we don't strictly need accounts, mailboxes or messages to run this test.
ProviderTestUtils.setupAttachment(1, "a1", 100, true, mMockContext);
ProviderTestUtils.setupAttachment(1, "a2", 200, true, mMockContext);
Attachment a3 = ProviderTestUtils.setupAttachment(2, "a3", 300, true, mMockContext);
Attachment a4 = ProviderTestUtils.setupAttachment(2, "a4", 400, true, mMockContext);
// Delete all attachments for message id=1
Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, 1);
mockResolver.delete(uri, null, null);
// Read back all attachments and confirm that we have the expected remaining attachments
// (the attachments that are set for message id=2). Note order-by size to simplify test.
Cursor c = mockResolver.query(Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION,
null, null, Attachment.SIZE);
assertEquals(2, c.getCount());
try {
c.moveToFirst();
Attachment a3Get = EmailContent.getContent(c, Attachment.class);
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-3", a3, a3Get);
c.moveToNext();
Attachment a4Get = EmailContent.getContent(c, Attachment.class);
ProviderTestUtils.assertAttachmentEqual("getAttachByUri-4", a4, a4Get);
} finally {
c.close();
}
}
/**
* Tests of default account behavior
*