Change the way we replace drafts on the server
Don't overwrite the previous draft in the database. Create a new message entry and convert the previous one to a 'deleted' placeholder. This also introduces a new pending command 'replace'. It is implemented as upload + delete.
This commit is contained in:
parent
1ef1af8b0f
commit
54e5d8af9c
7 changed files with 191 additions and 21 deletions
|
@ -1,10 +1,16 @@
|
|||
package com.fsck.k9.controller
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.backend.api.Backend
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingAppend
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace
|
||||
import com.fsck.k9.mail.FetchProfile
|
||||
import com.fsck.k9.mail.Flag
|
||||
import com.fsck.k9.mail.Message
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import com.fsck.k9.mailstore.LocalFolder
|
||||
import com.fsck.k9.mailstore.LocalMessage
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DraftOperations(private val messagingController: MessagingController) {
|
||||
|
@ -22,31 +28,129 @@ internal class DraftOperations(private val messagingController: MessagingControl
|
|||
val localFolder = localStore.getFolder(draftsFolderId)
|
||||
localFolder.open()
|
||||
|
||||
if (existingDraftId != null) {
|
||||
val uid = localFolder.getMessageUidById(existingDraftId)
|
||||
message.uid = uid
|
||||
val localMessage = if (messagingController.supportsUpload(account)) {
|
||||
saveAndUploadDraft(account, message, localFolder, existingDraftId)
|
||||
} else {
|
||||
saveDraftLocally(message, localFolder, existingDraftId)
|
||||
}
|
||||
|
||||
// Save the message to the store.
|
||||
localFolder.appendMessages(listOf(message))
|
||||
|
||||
// Fetch the message back from the store. This is the Message that's returned to the caller.
|
||||
val localMessage = localFolder.getMessage(message.uid)
|
||||
localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true)
|
||||
if (plaintextSubject != null) {
|
||||
localMessage.setCachedDecryptedSubject(plaintextSubject)
|
||||
}
|
||||
|
||||
if (messagingController.supportsUpload(account)) {
|
||||
val command = PendingAppend.create(localFolder.databaseId, localMessage.uid)
|
||||
messagingController.queuePendingCommand(account, command)
|
||||
messagingController.processPendingCommands(account)
|
||||
}
|
||||
|
||||
localMessage
|
||||
} catch (e: MessagingException) {
|
||||
Timber.e(e, "Unable to save message as draft.")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAndUploadDraft(
|
||||
account: Account,
|
||||
message: Message,
|
||||
localFolder: LocalFolder,
|
||||
existingDraftId: Long?
|
||||
): LocalMessage {
|
||||
localFolder.appendMessages(listOf(message))
|
||||
|
||||
val localMessage = localFolder.getMessage(message.uid)
|
||||
val previousDraftMessage = if (existingDraftId != null) localFolder.getMessage(existingDraftId) else null
|
||||
|
||||
val folderId = localFolder.databaseId
|
||||
if (previousDraftMessage != null) {
|
||||
previousDraftMessage.delete()
|
||||
|
||||
val uploadMessageId = localMessage.databaseId
|
||||
val deleteMessageId = previousDraftMessage.databaseId
|
||||
val command = PendingReplace.create(folderId, uploadMessageId, deleteMessageId)
|
||||
messagingController.queuePendingCommand(account, command)
|
||||
} else {
|
||||
val command = PendingAppend.create(folderId, localMessage.uid)
|
||||
messagingController.queuePendingCommand(account, command)
|
||||
}
|
||||
|
||||
messagingController.processPendingCommands(account)
|
||||
|
||||
return localMessage
|
||||
}
|
||||
|
||||
private fun saveDraftLocally(message: Message, localFolder: LocalFolder, existingDraftId: Long?): LocalMessage {
|
||||
if (existingDraftId != null) {
|
||||
// Setting the UID will cause LocalFolder.appendMessages() to replace the existing draft.
|
||||
message.uid = localFolder.getMessageUidById(existingDraftId)
|
||||
}
|
||||
|
||||
localFolder.appendMessages(listOf(message))
|
||||
|
||||
return localFolder.getMessage(message.uid)
|
||||
}
|
||||
|
||||
fun processPendingReplace(command: PendingReplace, account: Account) {
|
||||
val localStore = messagingController.getLocalStoreOrThrow(account)
|
||||
val localFolder = localStore.getFolder(command.folderId)
|
||||
localFolder.open()
|
||||
|
||||
val backend = messagingController.getBackend(account)
|
||||
|
||||
val uploadMessageId = command.uploadMessageId
|
||||
val localMessage = localFolder.getMessage(uploadMessageId)
|
||||
if (localMessage == null) {
|
||||
Timber.w("Couldn't find local copy of message to upload [ID: %d]", uploadMessageId)
|
||||
return
|
||||
} else if (!localMessage.uid.startsWith(K9.LOCAL_UID_PREFIX)) {
|
||||
Timber.i("Message [ID: %d] to be uploaded already has a server ID set. Skipping upload.", uploadMessageId)
|
||||
} else {
|
||||
uploadMessage(backend, account, localFolder, localMessage)
|
||||
}
|
||||
|
||||
deleteMessage(backend, localFolder, command.deleteMessageId)
|
||||
}
|
||||
|
||||
private fun uploadMessage(
|
||||
backend: Backend,
|
||||
account: Account,
|
||||
localFolder: LocalFolder,
|
||||
localMessage: LocalMessage
|
||||
) {
|
||||
val folderServerId = localFolder.serverId
|
||||
Timber.d("Uploading message [ID: %d] to remote folder '%s'", localMessage.databaseId, folderServerId)
|
||||
|
||||
val fetchProfile = FetchProfile().apply {
|
||||
add(FetchProfile.Item.BODY)
|
||||
}
|
||||
localFolder.fetch(listOf(localMessage), fetchProfile, null)
|
||||
|
||||
val messageServerId = backend.uploadMessage(folderServerId, localMessage)
|
||||
|
||||
if (messageServerId == null) {
|
||||
Timber.w(
|
||||
"Failed to get a server ID for the uploaded message. Removing local copy [ID: %d]",
|
||||
localMessage.databaseId
|
||||
)
|
||||
localMessage.destroy()
|
||||
} else {
|
||||
val oldUid = localMessage.uid
|
||||
|
||||
localMessage.uid = messageServerId
|
||||
localFolder.changeUid(localMessage)
|
||||
|
||||
for (listener in messagingController.listeners) {
|
||||
listener.messageUidChanged(account, localFolder.databaseId, oldUid, localMessage.uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMessage(backend: Backend, localFolder: LocalFolder, messageId: Long) {
|
||||
val messageServerId = localFolder.getMessageUidById(messageId) ?: run {
|
||||
Timber.i("Couldn't find local copy of message [ID: %d] to be deleted. Skipping delete.", messageId)
|
||||
return
|
||||
}
|
||||
|
||||
val messageServerIds = listOf(messageServerId)
|
||||
val folderServerId = localFolder.serverId
|
||||
backend.deleteMessages(folderServerId, messageServerIds)
|
||||
|
||||
messagingController.destroyPlaceholderMessages(localFolder, messageServerIds)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ import com.fsck.k9.controller.MessagingControllerCommands.PendingExpunge;
|
|||
import com.fsck.k9.controller.MessagingControllerCommands.PendingMarkAllAsRead;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveAndMarkAsRead;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveOrCopy;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingSetFlag;
|
||||
import com.fsck.k9.controller.ProgressBodyFactory.ProgressListener;
|
||||
import com.fsck.k9.helper.MutableBoolean;
|
||||
|
@ -268,7 +269,7 @@ public class MessagingController {
|
|||
throw new Error(e);
|
||||
}
|
||||
|
||||
private Backend getBackend(Account account) {
|
||||
Backend getBackend(Account account) {
|
||||
return backendManager.getBackend(account);
|
||||
}
|
||||
|
||||
|
@ -846,6 +847,10 @@ public class MessagingController {
|
|||
}
|
||||
}
|
||||
|
||||
void processPendingReplace(PendingReplace pendingReplace, Account account) {
|
||||
draftOperations.processPendingReplace(pendingReplace, account);
|
||||
}
|
||||
|
||||
private void queueMoveOrCopy(Account account, long srcFolderId, long destFolderId, MoveOrCopyFlavor operation,
|
||||
Map<String, String> uidMap) {
|
||||
PendingCommand command;
|
||||
|
@ -957,7 +962,7 @@ public class MessagingController {
|
|||
}
|
||||
}
|
||||
|
||||
private void destroyPlaceholderMessages(LocalFolder localFolder, List<String> uids) throws MessagingException {
|
||||
void destroyPlaceholderMessages(LocalFolder localFolder, List<String> uids) throws MessagingException {
|
||||
for (String uid : uids) {
|
||||
LocalMessage placeholderMessage = localFolder.getMessage(uid);
|
||||
if (placeholderMessage == null) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import static com.fsck.k9.helper.Preconditions.checkNotNull;
|
|||
|
||||
public class MessagingControllerCommands {
|
||||
static final String COMMAND_APPEND = "append";
|
||||
static final String COMMAND_REPLACE = "replace";
|
||||
static final String COMMAND_MARK_ALL_AS_READ = "mark_all_as_read";
|
||||
static final String COMMAND_SET_FLAG = "set_flag";
|
||||
static final String COMMAND_DELETE = "delete";
|
||||
|
@ -167,6 +168,33 @@ public class MessagingControllerCommands {
|
|||
}
|
||||
}
|
||||
|
||||
public static class PendingReplace extends PendingCommand {
|
||||
public final long folderId;
|
||||
public final long uploadMessageId;
|
||||
public final long deleteMessageId;
|
||||
|
||||
|
||||
public static PendingReplace create(long folderId, long uploadMessageId, long deleteMessageId) {
|
||||
return new PendingReplace(folderId, uploadMessageId, deleteMessageId);
|
||||
}
|
||||
|
||||
private PendingReplace(long folderId, long uploadMessageId, long deleteMessageId) {
|
||||
this.folderId = folderId;
|
||||
this.uploadMessageId = uploadMessageId;
|
||||
this.deleteMessageId = deleteMessageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return COMMAND_REPLACE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(MessagingController controller, Account account) throws MessagingException {
|
||||
controller.processPendingReplace(this, account);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PendingMarkAllAsRead extends PendingCommand {
|
||||
public final long folderId;
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import com.fsck.k9.controller.MessagingControllerCommands.PendingExpunge;
|
|||
import com.fsck.k9.controller.MessagingControllerCommands.PendingMarkAllAsRead;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveAndMarkAsRead;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingMoveOrCopy;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingReplace;
|
||||
import com.fsck.k9.controller.MessagingControllerCommands.PendingSetFlag;
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
|
@ -35,6 +36,7 @@ public class PendingCommandSerializer {
|
|||
adapters.put(MessagingControllerCommands.COMMAND_MOVE_AND_MARK_AS_READ,
|
||||
moshi.adapter(PendingMoveAndMarkAsRead.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_APPEND, moshi.adapter(PendingAppend.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_REPLACE, moshi.adapter(PendingReplace.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_EMPTY_TRASH, moshi.adapter(PendingEmptyTrash.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_EXPUNGE, moshi.adapter(PendingExpunge.class));
|
||||
adapters.put(MessagingControllerCommands.COMMAND_MARK_ALL_AS_READ, moshi.adapter(PendingMarkAllAsRead.class));
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteDatabase;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.DI;
|
||||
import com.fsck.k9.K9;
|
||||
|
@ -730,6 +731,34 @@ public class LocalFolder {
|
|||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public LocalMessage getMessage(long messageId) throws MessagingException {
|
||||
return localStore.getDatabase().execute(false, db -> {
|
||||
open();
|
||||
LocalMessage message = new LocalMessage(localStore, messageId, LocalFolder.this);
|
||||
|
||||
Cursor cursor = db.rawQuery(
|
||||
"SELECT " +
|
||||
LocalStore.GET_MESSAGES_COLS +
|
||||
"FROM messages " +
|
||||
"LEFT JOIN message_parts ON (message_parts.id = messages.message_part_id) " +
|
||||
"LEFT JOIN threads ON (threads.message_id = messages.id) " +
|
||||
"WHERE messages.id = ? AND folder_id = ?",
|
||||
new String[] { Long.toString(messageId), Long.toString(databaseId) });
|
||||
try {
|
||||
if (cursor.moveToNext()) {
|
||||
message.populateFromGetMessageCursor(cursor);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} finally {
|
||||
Utility.closeQuietly(cursor);
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
public List<LocalMessage> getMessages(MessageRetrievalListener<LocalMessage> listener) throws MessagingException {
|
||||
return getMessages(listener, true);
|
||||
}
|
||||
|
|
|
@ -51,6 +51,12 @@ public class LocalMessage extends MimeMessage {
|
|||
this.mFolder = folder;
|
||||
}
|
||||
|
||||
LocalMessage(LocalStore localStore, long databaseId, LocalFolder folder) {
|
||||
this.localStore = localStore;
|
||||
this.databaseId = databaseId;
|
||||
this.mFolder = folder;
|
||||
}
|
||||
|
||||
|
||||
void populateFromGetMessageCursor(Cursor cursor) throws MessagingException {
|
||||
final String subject = cursor.getString(LocalStore.MSG_INDEX_SUBJECT);
|
||||
|
@ -300,7 +306,7 @@ public class LocalMessage extends MimeMessage {
|
|||
* If a message is being marked as deleted we want to clear out its content. Delete will not actually remove the
|
||||
* row since we need to retain the UID for synchronization purposes.
|
||||
*/
|
||||
private void delete() throws MessagingException {
|
||||
public void delete() throws MessagingException {
|
||||
try {
|
||||
localStore.getDatabase().execute(true, new DbCallback<Void>() {
|
||||
@Override
|
||||
|
|
|
@ -1535,10 +1535,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
|
|||
changesMadeSinceLastSave = false;
|
||||
currentMessageBuilder = null;
|
||||
|
||||
if (action == Action.EDIT_DRAFT && relatedMessageReference != null) {
|
||||
message.setUid(relatedMessageReference.getUid());
|
||||
}
|
||||
|
||||
new SaveMessageTask(getApplicationContext(), account, contacts, internalMessageHandler,
|
||||
message, draftMessageId, plaintextSubject).execute();
|
||||
if (finishAfterDraftSaved) {
|
||||
|
|
Loading…
Reference in a new issue