From b0c1747bbe063c09a8f949640064a506c19321a2 Mon Sep 17 00:00:00 2001 From: M66B Date: Wed, 23 Jan 2019 18:52:52 +0000 Subject: [PATCH] Added cross account move --- FAQ.md | 9 - .../java/eu/faircode/email/EntityAccount.java | 4 +- .../eu/faircode/email/EntityOperation.java | 16 +- .../eu/faircode/email/FragmentMessages.java | 305 +++++++++--------- .../eu/faircode/email/ServiceSynchronize.java | 8 +- app/src/main/res/layout/fragment_messages.xml | 7 - app/src/main/res/values/strings.xml | 2 +- 7 files changed, 176 insertions(+), 175 deletions(-) diff --git a/FAQ.md b/FAQ.md index 9349df6256..a0cc3765cc 100644 --- a/FAQ.md +++ b/FAQ.md @@ -133,7 +133,6 @@ FairEmail follows all the best practices for an email client as decribed in [thi * [(69) Can you add auto scroll up on new message?](#user-content-faq69) * [(70) When will messages be auto expanded?](#user-content-faq70) * [(71) How do I use filter rules?](#user-content-faq71) -* [(72) Is moving messages between accounts supported?](#user-content-faq72) [I have another question.](#support) @@ -1136,14 +1135,6 @@ Using filter rules is a pro feature.
- -**(72) Is moving messages between accounts supported?** - -Moving messages between accounts is not supported because this can result in lost or duplicate messages. -A message structure or message content supported by one provider might not be supported by another provider. - -
- ## Support diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java index b84b6f1f1a..bc3a9f9238 100644 --- a/app/src/main/java/eu/faircode/email/EntityAccount.java +++ b/app/src/main/java/eu/faircode/email/EntityAccount.java @@ -28,6 +28,8 @@ import android.os.Build; import org.json.JSONException; import org.json.JSONObject; +import java.io.Serializable; + import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.room.Entity; @@ -38,7 +40,7 @@ import androidx.room.PrimaryKey; indices = { } ) -public class EntityAccount { +public class EntityAccount implements Serializable { static final String TABLE_NAME = "account"; @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java index 31089b6eaa..b224c5d002 100644 --- a/app/src/main/java/eu/faircode/email/EntityOperation.java +++ b/app/src/main/java/eu/faircode/email/EntityOperation.java @@ -127,6 +127,7 @@ public class EntityOperation { } private static void queue(Context context, DB db, EntityMessage message, String name, JSONArray jargs) { + long folder = message.folder; try { if (SEEN.equals(name)) { for (EntityMessage similar : db.message().getMessageByMsgId(message.account, message.msgid)) { @@ -166,12 +167,14 @@ public class EntityOperation { long id = message.id; long uid = message.uid; message.id = null; - message.uid = null; + message.account = target.account; message.folder = target.id; + message.uid = null; long newid = db.message().insertMessage(message); message.id = id; - message.uid = uid; + message.account = source.account; message.folder = source.id; + message.uid = uid; if (message.content) try { @@ -186,6 +189,13 @@ public class EntityOperation { EntityAttachment.copy(context, db, message.id, newid); } + // Cross account move + if (!source.account.equals(target.account)) { + name = ADD; + folder = target.id; + jargs.remove(0); + } + } else if (DELETE.equals(name)) db.message().setMessageUiHide(message.id, true); } catch (JSONException ex) { @@ -193,7 +203,7 @@ public class EntityOperation { } EntityOperation operation = new EntityOperation(); - operation.folder = message.folder; + operation.folder = folder; operation.message = message.id; operation.name = name; operation.args = jargs.toString(); diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index efa71290a7..8b876a7237 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -44,6 +44,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; @@ -57,6 +58,7 @@ import com.google.android.material.snackbar.Snackbar; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -88,7 +90,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; public class FragmentMessages extends FragmentBase { private ViewGroup view; private SwipeRefreshLayout swipeRefresh; - private View popupAnchor; private TextView tvSupport; private ImageButton ibHintSupport; private ImageButton ibHintSwipe; @@ -199,7 +200,6 @@ public class FragmentMessages extends FragmentBase { // Get controls swipeRefresh = view.findViewById(R.id.swipeRefresh); - popupAnchor = view.findViewById(R.id.popupAnchor); tvSupport = view.findViewById(R.id.tvSupport); ibHintSupport = view.findViewById(R.id.ibHintSupport); ibHintSwipe = view.findViewById(R.id.ibHintSwipe); @@ -530,15 +530,17 @@ public class FragmentMessages extends FragmentBase { EntityMessage message = db.message().getMessage(id); - EntityFolder folder = null; + EntityFolder target = null; if (message != null) if (type) - folder = db.folder().getFolderByType(message.account, name); + target = db.folder().getFolderByType(message.account, name); else - folder = db.folder().getFolderByName(message.account, name); + target = db.folder().getFolderByName(message.account, name); - if (folder != null) - result.add(new MessageTarget(id, folder)); + if (target != null) { + EntityAccount account = db.account().getAccount(target.account); + result.add(new MessageTarget(id, account, target)); + } db.setTransactionSuccessful(); } finally { @@ -650,7 +652,7 @@ public class FragmentMessages extends FragmentBase { protected ArrayList onExecute(Context context, Bundle args) { long id = args.getLong("id"); boolean thread = args.getBoolean("thread"); - long target = args.getLong("target"); + long tid = args.getLong("target"); ArrayList result = new ArrayList<>(); @@ -659,14 +661,15 @@ public class FragmentMessages extends FragmentBase { try { db.beginTransaction(); - EntityFolder folder = db.folder().getFolder(target); - if (folder != null) { + EntityFolder target = db.folder().getFolder(tid); + if (target != null) { + EntityAccount account = db.account().getAccount(target.account); EntityMessage message = db.message().getMessage(id); if (message != null) { List messages = db.message().getMessageByThread( message.account, message.thread, threading && thread ? null : id, message.folder); for (EntityMessage threaded : messages) { - result.add(new MessageTarget(threaded.id, folder)); + result.add(new MessageTarget(threaded.id, account, target)); db.message().setMessageUiHide(threaded.id, true); // Prevent new message notification on undo db.message().setMessageUiIgnored(threaded.id, true); @@ -721,15 +724,15 @@ public class FragmentMessages extends FragmentBase { args.putLong("account", account); args.putString("thread", thread); args.putLong("id", id); - args.putString("folderType", folderType); + args.putString("type", folderType); new SimpleTask>() { @Override protected ArrayList onExecute(Context context, Bundle args) { - long account = args.getLong("account"); + long aid = args.getLong("account"); String thread = args.getString("thread"); long id = args.getLong("id"); - String folderType = args.getString("folderType"); + String type = args.getString("type"); ArrayList result = new ArrayList<>(); @@ -737,10 +740,11 @@ public class FragmentMessages extends FragmentBase { try { db.beginTransaction(); - EntityFolder target = db.folder().getFolderByType(account, folderType); + EntityFolder target = db.folder().getFolderByType(aid, type); if (target != null) { + EntityAccount account = db.account().getAccount(target.account); List messages = db.message().getMessageByThread( - account, thread, threading ? null : id, null); + aid, thread, threading ? null : id, null); for (EntityMessage threaded : messages) { EntityFolder folder = db.folder().getFolder(threaded.folder); if (!target.id.equals(threaded.folder) && @@ -749,7 +753,7 @@ public class FragmentMessages extends FragmentBase { (!EntityFolder.SENT.equals(folder.type) || EntityFolder.TRASH.equals(target.type)) && !EntityFolder.TRASH.equals(folder.type) && !EntityFolder.JUNK.equals(folder.type)) - result.add(new MessageTarget(threaded.id, target)); + result.add(new MessageTarget(threaded.id, account, target)); } } @@ -778,79 +782,118 @@ public class FragmentMessages extends FragmentBase { args.putLong("folder", folder); args.putLongArray("ids", getSelection()); - new SimpleTask() { + new SimpleTask() { + @Override - protected Boolean[] onExecute(Context context, Bundle args) { + protected MoreResult onExecute(Context context, Bundle args) { long fid = args.getLong("folder"); long[] ids = args.getLongArray("ids"); - Boolean[] result = new Boolean[12]; - for (int i = 0; i < result.length; i++) - result[i] = false; + MoreResult result = new MoreResult(); DB db = DB.getInstance(context); - List accounts = new ArrayList<>(); for (long id : ids) { EntityMessage message = db.message().getMessage(id); if (message != null) { - if (!accounts.contains(message.account)) - accounts.add(message.account); - result[message.ui_seen ? 1 : 0] = true; - result[message.flagged ? 3 : 2] = true; - - if (db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE) != null) - result[4] = true; - if (db.folder().getFolderByType(message.account, EntityFolder.TRASH) != null) - result[5] = true; - if (db.folder().getFolderByType(message.account, EntityFolder.JUNK) != null) - result[6] = true; + if (message.ui_seen) + result.seen = true; + else + result.unseen = true; + + if (message.ui_flagged) + result.flagged = true; + else + result.unflagged = true; + + result.hasArchive = (result.hasArchive && + db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE) != null); + result.hasTrash = (result.hasTrash && + db.folder().getFolderByType(message.account, EntityFolder.TRASH) != null); + result.hasJunk = (result.hasJunk && + db.folder().getFolderByType(message.account, EntityFolder.JUNK) != null); } } EntityFolder folder = db.folder().getFolder(fid); if (folder != null) { - result[7] = EntityFolder.ARCHIVE.equals(folder.type); - result[8] = EntityFolder.TRASH.equals(folder.type); - result[9] = EntityFolder.JUNK.equals(folder.type); - result[10] = EntityFolder.DRAFTS.equals(folder.type); + result.isArchive = EntityFolder.ARCHIVE.equals(folder.type); + result.isTrash = EntityFolder.TRASH.equals(folder.type); + result.isJunk = EntityFolder.JUNK.equals(folder.type); + result.isDrafts = EntityFolder.DRAFTS.equals(folder.type); } - result[11] = (accounts.size() == 1); + result.accounts = db.account().getAccounts(true); + + final Collator collator = Collator.getInstance(Locale.getDefault()); + collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc + Collections.sort(result.accounts, new Comparator() { + @Override + public int compare(EntityAccount a1, EntityAccount a2) { + int p = -a1.primary.compareTo(a2.primary); + if (p != 0) + return p; + return collator.compare(a1.name, a2.name); + } + }); + + for (EntityAccount account : result.accounts) { + List targets = new ArrayList<>(); + List folders = db.folder().getFolders(account.id); + for (EntityFolder target : folders) + if (!target.hide && + !EntityFolder.ARCHIVE.equals(target.type) && + !EntityFolder.TRASH.equals(target.type) && + !EntityFolder.JUNK.equals(target.type) && + !target.id.equals(fid)) + targets.add(target); + EntityFolder.sort(context, targets); + result.targets.put(account, targets); + } return result; } @Override - protected void onExecuted(Bundle args, final Boolean[] result) { + protected void onExecuted(Bundle args, final MoreResult result) { PopupMenu popupMenu = new PopupMenu(getContext(), fabMore); - if (result[0] && !result[10]) // Unseen, not draft + if (result.unseen && !result.isDrafts) // Unseen, not draft popupMenu.getMenu().add(Menu.NONE, action_seen, 1, R.string.title_seen); - if (result[1] && !result[10]) // Seen, not draft + if (result.seen && !result.isDrafts) // Seen, not draft popupMenu.getMenu().add(Menu.NONE, action_unseen, 2, R.string.title_unseen); popupMenu.getMenu().add(Menu.NONE, action_snooze, 3, R.string.title_snooze); - if (result[2]) + if (result.unflagged) popupMenu.getMenu().add(Menu.NONE, action_flag, 4, R.string.title_flag); - if (result[3]) + if (result.flagged) popupMenu.getMenu().add(Menu.NONE, action_unflag, 5, R.string.title_unflag); - if (result[4] && !result[7] && !result[10]) // has archive and not is archive/drafts + if (result.hasArchive && !result.isArchive && !result.isDrafts) // has archive and not is archive/drafts popupMenu.getMenu().add(Menu.NONE, action_archive, 6, R.string.title_archive); - if (result[8]) // is trash + if (result.isTrash) // is trash popupMenu.getMenu().add(Menu.NONE, action_delete, 7, R.string.title_delete); - if (!result[8] && result[5]) // not trash and has trash + if (!result.isTrash && result.hasTrash) // not trash and has trash popupMenu.getMenu().add(Menu.NONE, action_trash, 8, R.string.title_trash); - if (result[6] && !result[9] && !result[10]) // has junk and not junk/drafts + if (result.hasJunk && !result.isJunk && !result.isDrafts) // has junk and not junk/drafts popupMenu.getMenu().add(Menu.NONE, action_junk, 9, R.string.title_spam); - if (!result[10]) // not drafts - popupMenu.getMenu().add(Menu.NONE, action_move, 10, R.string.title_move); + if (!result.isDrafts) { // not drafts + int order = 11; + for (EntityAccount account : result.accounts) { + SubMenu smenu = popupMenu.getMenu() + .addSubMenu(Menu.NONE, 0, order++, getString(R.string.title_move_to, account.name)); + int sorder = 1; + for (EntityFolder target : result.targets.get(account)) { + MenuItem item = smenu.add(Menu.NONE, action_move, sorder++, target.getDisplayName(getContext())); + item.setIntent(new Intent().putExtra("target", target.id)); + } + } + } popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override @@ -884,10 +927,7 @@ public class FragmentMessages extends FragmentBase { onActionJunkSelection(); return true; case action_move: - if (result[11]) // single account - onActionMoveSelection(); - else - Snackbar.make(view, R.string.title_no_cross_account, Snackbar.LENGTH_LONG).show(); + onActionMoveSelection(target.getIntent().getLongExtra("target", -1)); return true; default: return false; @@ -1142,8 +1182,8 @@ public class FragmentMessages extends FragmentBase { message.account, message.thread, threading ? null : id, message.folder); for (EntityMessage threaded : messages) { EntityFolder target = db.folder().getFolderByType(message.account, type); - if (target != null) - result.add(new MessageTarget(threaded.id, target)); + EntityAccount account = db.account().getAccount(target.account); + result.add(new MessageTarget(threaded.id, account, target)); } } } @@ -1168,107 +1208,50 @@ public class FragmentMessages extends FragmentBase { }.execute(FragmentMessages.this, args, "messages:move"); } - private void onActionMoveSelection() { + private void onActionMoveSelection(long target) { Bundle args = new Bundle(); - args.putLong("folder", folder); args.putLongArray("ids", getSelection()); + args.putLong("target", target); - new SimpleTask>() { + selectionTracker.clearSelection(); + + new SimpleTask>() { @Override - protected List onExecute(Context context, Bundle args) { - long fid = args.getLong("folder"); + protected ArrayList onExecute(Context context, Bundle args) { long[] ids = args.getLongArray("ids"); + long tid = args.getLong("target"); + + ArrayList result = new ArrayList<>(); DB db = DB.getInstance(context); + try { + db.beginTransaction(); - long account = -1; - for (long id : ids) { - EntityMessage message = db.message().getMessage(id); - if (message != null) { - account = message.account; - break; + EntityFolder target = db.folder().getFolder(tid); + if (target != null) { + EntityAccount account = db.account().getAccount(target.account); + for (long id : ids) { + EntityMessage message = db.message().getMessage(id); + if (message != null) { + List messages = db.message().getMessageByThread( + message.account, message.thread, threading ? null : id, message.folder); + for (EntityMessage threaded : messages) + result.add(new MessageTarget(threaded.id, account, target)); + } + } } - } - List folders = db.folder().getFolders(account); - - List targets = new ArrayList<>(); - for (EntityFolder folder : folders) - if (!folder.hide && - !EntityFolder.ARCHIVE.equals(folder.type) && - !EntityFolder.TRASH.equals(folder.type) && - !EntityFolder.JUNK.equals(folder.type) && - (fid < 0 ? !folder.unified : !folder.id.equals(fid))) - targets.add(folder); - - EntityFolder.sort(context, targets); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } - return targets; + return result; } @Override - protected void onExecuted(final Bundle args, List folders) { - PopupMenu popupMenu = new PopupMenu(getContext(), popupAnchor); - - int order = 0; - for (EntityFolder folder : folders) - popupMenu.getMenu().add(Menu.NONE, folder.id.intValue(), order++, folder.getDisplayName(getContext())); - - popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(final MenuItem target) { - args.putLong("target", target.getItemId()); - - selectionTracker.clearSelection(); - - new SimpleTask>() { - @Override - protected ArrayList onExecute(Context context, Bundle args) { - long[] ids = args.getLongArray("ids"); - long target = args.getLong("target"); - - ArrayList result = new ArrayList<>(); - - DB db = DB.getInstance(context); - try { - db.beginTransaction(); - - EntityFolder folder = db.folder().getFolder(target); - if (folder != null) - for (long id : ids) { - EntityMessage message = db.message().getMessage(id); - if (message != null) { - List messages = db.message().getMessageByThread( - message.account, message.thread, threading ? null : id, message.folder); - for (EntityMessage threaded : messages) - result.add(new MessageTarget(threaded.id, folder)); - } - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - return result; - } - - @Override - protected void onExecuted(Bundle args, ArrayList result) { - moveAsk(result); - } - - @Override - protected void onException(Bundle args, Throwable ex) { - Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); - } - }.execute(FragmentMessages.this, args, "messages:move"); - - return true; - } - }); - - popupMenu.show(); + protected void onExecuted(Bundle args, ArrayList result) { + moveAsk(result); } @Override @@ -2180,14 +2163,12 @@ public class FragmentMessages extends FragmentBase { } private String getDisplay(ArrayList result) { - List folders = new ArrayList<>(); - for (MessageTarget target : result) - if (!folders.contains(target.folder)) - folders.add(target.folder); - List displays = new ArrayList<>(); - for (EntityFolder folder : folders) - displays.add(folder.getDisplayName(getContext())); + for (MessageTarget target : result) { + String display = target.account.name + "/" + target.folder.getDisplayName(getContext()); + if (!displays.contains(display)) + displays.add(display); + } Collator collator = Collator.getInstance(Locale.getDefault()); collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc @@ -2216,23 +2197,43 @@ public class FragmentMessages extends FragmentBase { } }; - static class MessageTarget implements Parcelable { + private class MoreResult { + boolean seen; + boolean unseen; + boolean flagged; + boolean unflagged; + boolean hasArchive = true; + boolean hasTrash = true; + boolean hasJunk = true; + boolean isArchive; + boolean isTrash; + boolean isJunk; + boolean isDrafts; + List accounts; + Map> targets = new HashMap<>(); + } + + private static class MessageTarget implements Parcelable { long id; + EntityAccount account; EntityFolder folder; - MessageTarget(long id, EntityFolder folder) { + MessageTarget(long id, EntityAccount account, EntityFolder folder) { this.id = id; + this.account = account; this.folder = folder; } protected MessageTarget(Parcel in) { id = in.readLong(); + account = (EntityAccount) in.readSerializable(); folder = (EntityFolder) in.readSerializable(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); + dest.writeSerializable(account); dest.writeSerializable(folder); } diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index cae975971c..2787fdea62 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -1483,7 +1483,7 @@ public class ServiceSynchronize extends LifecycleService { doKeyword(folder, ifolder, message, jargs, db); else if (EntityOperation.ADD.equals(op.name)) - doAdd(folder, isession, istore, ifolder, message, jargs, db); + doAdd(folder, isession, ifolder, message, jargs, db); else if (EntityOperation.MOVE.equals(op.name)) doMove(folder, isession, istore, ifolder, message, jargs, db); @@ -1663,7 +1663,7 @@ public class ServiceSynchronize extends LifecycleService { } } - private void doAdd(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException, IOException { + private void doAdd(EntityFolder folder, Session isession, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException, IOException { // Append message MimeMessage imessage = MessageHelper.from(this, message, isession); @@ -1673,6 +1673,10 @@ public class ServiceSynchronize extends LifecycleService { } ifolder.appendMessages(new Message[]{imessage}); + + // Cross account move + if (!folder.id.equals(message.folder)) + EntityOperation.queue(this, db, message, EntityOperation.DELETE); } private void doMove(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException, IOException { diff --git a/app/src/main/res/layout/fragment_messages.xml b/app/src/main/res/layout/fragment_messages.xml index 2bbfc12c7c..d14aa17123 100644 --- a/app/src/main/res/layout/fragment_messages.xml +++ b/app/src/main/res/layout/fragment_messages.xml @@ -15,13 +15,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - More Spam Move + Move to %1$s Snooze … Archive Reply @@ -305,7 +306,6 @@ An outdated app sent a file path instead of a file stream Contact picker not available No internet connection - Moving across accounts is not supported Raw message saved Attachment saved Attachments saved