From 4401d134e9964428d3fe87c131b730c20d044749 Mon Sep 17 00:00:00 2001 From: M66B Date: Mon, 7 Jan 2019 17:50:23 +0000 Subject: [PATCH] Added delayed send --- FAQ.md | 1 - README.md | 3 +- .../eu/faircode/email/AdapterMessage.java | 158 +++++++++--------- .../java/eu/faircode/email/DaoMessage.java | 2 +- .../java/eu/faircode/email/EntityMessage.java | 2 +- .../eu/faircode/email/FragmentCompose.java | 109 +++++++++--- .../eu/faircode/email/ServiceSynchronize.java | 12 +- app/src/main/res/menu/menu_compose.xml | 6 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 189 insertions(+), 105 deletions(-) diff --git a/FAQ.md b/FAQ.md index 80565e1c72..3e2abd62fa 100644 --- a/FAQ.md +++ b/FAQ.md @@ -41,7 +41,6 @@ None at this moment. * Resize images: this is not a feature directly related to email and there are plenty of apps that can do this for you. * Calendar events: opening the attached calendar file should open the related calendar app. * Executing filter rules: filter rules should be executed on the server because a battery powered device with possibly an unstable internet connection is not suitable for this. -* Send timer: basically the same as executing filter rules. Delayed sending is not supported by [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol). You could move messages to a "to do" folder instead. * Badge count: there is no standard Android API for this and third party solutions might stop working anytime. For example *ShortcutBadger* [has lots of problems](https://github.com/leolin310148/ShortcutBadger/issues). You can use the provided widget instead. * Switch language: although it is possible to change the language of an app, Android is not designed for this. Better fix the translation in your language if needed, see [this FAQ](#user-content-faq26) about how to. * Select identities to show in unified inbox: this would add complexity for something which would hardly be used. diff --git a/README.md b/README.md index dfd5f60508..796d3bb55c 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ This app starts a foreground service with a low priority status bar notification * Account/identity colors * Notifications per account * Notifications with message preview (requires Android 7 Nougat or later) -* Snoozing messages +* Snooze messages +* Send messages after selected time * Reply templates * Search on server * Keyword management diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index e99680b475..6b289b1bb5 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -702,7 +702,7 @@ public class AdapterMessage extends RecyclerView.Adapter messages = db.message().getMessageByThread( + message.account, message.thread, threading && thread ? null : id, message.folder); + for (EntityMessage threaded : messages) + EntityOperation.queue(context, db, threaded, EntityOperation.FLAG, flagged); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + return null; + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Helper.unexpectedError(context, owner, ex); + } + }.execute(context, owner, args, "message:flag"); + } + private void onAddContact(TupleMessageEx message) { for (Address address : message.from) { InternetAddress ia = (InternetAddress) address; @@ -1139,26 +1186,26 @@ public class AdapterMessage extends RecyclerView.Adapter>() { @Override protected List onExecute(Context context, Bundle args) { @@ -1313,7 +1360,7 @@ public class AdapterMessage extends RecyclerView.Adapter messages = db.message().getMessageByThread( - message.account, message.thread, threading && thread ? null : id, message.folder); - for (EntityMessage threaded : messages) - EntityOperation.queue(context, db, threaded, EntityOperation.FLAG, flagged); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - return null; - } - - @Override - protected void onException(Bundle args, Throwable ex) { - Helper.unexpectedError(context, owner, ex); - } - }.execute(context, owner, args, "message:flag"); - } - - private void onShare(ActionData data) { + private void onMenuShare(ActionData data) { Bundle args = new Bundle(); args.putLong("id", data.message.id); @@ -1452,7 +1452,7 @@ public class AdapterMessage extends RecyclerView.Adapter 0" + diff --git a/app/src/main/java/eu/faircode/email/EntityMessage.java b/app/src/main/java/eu/faircode/email/EntityMessage.java index debffdac01..933569f8cd 100644 --- a/app/src/main/java/eu/faircode/email/EntityMessage.java +++ b/app/src/main/java/eu/faircode/email/EntityMessage.java @@ -101,7 +101,7 @@ public class EntityMessage implements Serializable { public String extra; // plus public Long replying; public Long forwarding; - public Long uid; // compose = null + public Long uid; // compose/moved = null public String msgid; public String references; public String deliveredto; diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index af0c0b6315..6e35421578 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -72,6 +72,7 @@ import android.widget.EditText; import android.widget.FilterQueryProvider; import android.widget.ImageView; import android.widget.MultiAutoCompleteTextView; +import android.widget.NumberPicker; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -288,8 +289,9 @@ public class FragmentCompose extends FragmentEx { switch (action) { case R.id.action_delete: - onDelete(); + onActionDelete(); break; + case R.id.action_send: final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); boolean autosend = prefs.getBoolean("autosend", false); @@ -327,6 +329,7 @@ public class FragmentCompose extends FragmentEx { onAction(action); } break; + default: onAction(action); } @@ -496,7 +499,7 @@ public class FragmentCompose extends FragmentEx { args.putString("subject", getArguments().getString("subject")); args.putString("body", getArguments().getString("body")); args.putParcelableArrayList("attachments", getArguments().getParcelableArrayList("attachments")); - draftLoader.execute(this, args, "draft:new"); + draftLoader.execute(this, args, "compose:new"); } else { Bundle args = new Bundle(); args.putString("action", "edit"); @@ -504,7 +507,7 @@ public class FragmentCompose extends FragmentEx { args.putLong("account", -1); args.putLong("reference", -1); args.putLong("answer", -1); - draftLoader.execute(this, args, "draft:edit"); + draftLoader.execute(this, args, "compose:edit"); } } else { working = savedInstanceState.getLong("working"); @@ -514,7 +517,7 @@ public class FragmentCompose extends FragmentEx { args.putLong("account", -1); args.putLong("reference", -1); args.putLong("answer", -1); - draftLoader.execute(this, args, "draft:instance"); + draftLoader.execute(this, args, "compose:instance"); } } @@ -614,6 +617,9 @@ public class FragmentCompose extends FragmentEx { case R.id.menu_encrypt: onAction(R.id.menu_encrypt); return true; + case R.id.menu_send_after: + onMenuSendAfter(); + return true; default: return super.onOptionsItemSelected(item); } @@ -689,6 +695,62 @@ public class FragmentCompose extends FragmentEx { } } + private void onMenuSendAfter() { + final View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_duration, null); + final NumberPicker npHours = dview.findViewById(R.id.npHours); + final NumberPicker npDays = dview.findViewById(R.id.npDays); + + npHours.setMinValue(0); + npHours.setMaxValue(24); + + npDays.setMinValue(0); + npDays.setMaxValue(90); + + new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner()) + .setTitle(R.string.title_send_after) + .setView(dview) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + int hours = npHours.getValue(); + int days = npDays.getValue(); + long duration = (hours + days * 24) * 3600L * 1000L; + + Bundle args = new Bundle(); + args.putLong("id", working); + args.putLong("wakeup", new Date().getTime() + duration); + + new SimpleTask() { + @Override + protected Void onExecute(Context context, Bundle args) { + long id = args.getLong("id"); + Long wakeup = args.getLong("wakeup"); + + DB db = DB.getInstance(context); + db.message().setMessageSnoozed(id, wakeup); + + return null; + } + + @Override + protected void onExecuted(Bundle args, Void data) { + onAction(R.id.action_send); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); + } + }.execute(FragmentCompose.this, args, "compose:send:after"); + } catch (Throwable ex) { + Log.e(ex); + } + } + }) + .show(); + } + private void onMenuImage() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); @@ -716,7 +778,7 @@ public class FragmentCompose extends FragmentEx { grpAddresses.setVisibility(grpAddresses.getVisibility() == View.GONE ? View.VISIBLE : View.GONE); } - private void onDelete() { + private void onActionDelete() { new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner()) .setMessage(R.string.title_ask_discard) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @@ -915,7 +977,7 @@ public class FragmentCompose extends FragmentEx { else Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); } - }.execute(this, args, "encrypt"); + }.execute(this, args, "compose:encrypt"); } @Override @@ -1043,7 +1105,7 @@ public class FragmentCompose extends FragmentEx { else Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); } - }.execute(this, args, "add:attachment"); + }.execute(this, args, "compose:attachment:add"); } private void handleExit() { @@ -1101,7 +1163,7 @@ public class FragmentCompose extends FragmentEx { dirty = false; Log.i("Run execute id=" + working); - actionLoader.execute(this, args, "action:" + action); + actionLoader.execute(this, args, "compose:action:" + action); } private boolean isEmpty() { @@ -1658,7 +1720,7 @@ public class FragmentCompose extends FragmentEx { protected void onException(Bundle args, Throwable ex) { Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); } - }.execute(this, args, "draft:check"); + }.execute(this, args, "compose:check"); } private void showDraft(EntityMessage draft) { @@ -1729,7 +1791,7 @@ public class FragmentCompose extends FragmentEx { protected void onException(Bundle args, Throwable ex) { Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); } - }.execute(FragmentCompose.this, args, "draft:show"); + }.execute(FragmentCompose.this, args, "compose:show"); } private SimpleTask actionLoader = new SimpleTask() { @@ -1926,19 +1988,17 @@ public class FragmentCompose extends FragmentEx { Helper.copy(file, EntityAttachment.getFile(context, attachment.id)); } - EntityOperation.queue(context, db, draft, EntityOperation.SEND); + if (draft.ui_snoozed == null) + EntityOperation.queue(context, db, draft, EntityOperation.SEND); - if (draft.replying != null) { - EntityMessage replying = db.message().getMessage(draft.replying); - EntityOperation.queue(context, db, replying, EntityOperation.ANSWERED, true); + if (draft.ui_snoozed == null) { + Handler handler = new Handler(context.getMainLooper()); + handler.post(new Runnable() { + public void run() { + Toast.makeText(context, R.string.title_queued, Toast.LENGTH_LONG).show(); + } + }); } - - Handler handler = new Handler(context.getMainLooper()); - handler.post(new Runnable() { - public void run() { - Toast.makeText(context, R.string.title_queued, Toast.LENGTH_LONG).show(); - } - }); } db.setTransactionSuccessful(); @@ -1946,6 +2006,11 @@ public class FragmentCompose extends FragmentEx { db.endTransaction(); } + if (action == R.id.action_send && draft.ui_snoozed != null) { + Log.i("Delayed send id=" + draft.id + " at " + new Date(draft.ui_snoozed)); + EntityMessage.snooze(getContext(), draft.id, draft.ui_snoozed); + } + return draft; } @@ -2046,7 +2111,7 @@ public class FragmentCompose extends FragmentEx { protected void onException(Bundle args, Throwable ex) { Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); } - }.execute(FragmentCompose.this, args, source); + }.execute(FragmentCompose.this, args, "compose:cid:" + source); } }); } else diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 1d15014381..78ac886a10 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -391,7 +391,12 @@ public class ServiceSynchronize extends LifecycleService { break; case "snooze": - db.message().setMessageSnoozed(message.id, null); + EntityFolder folder = db.folder().getFolder(message.folder); + if (EntityFolder.OUTBOX.equals(folder.type)) { + Log.i("Delayed send id=" + message.id); + EntityOperation.queue(ServiceSynchronize.this, db, message, EntityOperation.SEND); + } else + db.message().setMessageSnoozed(message.id, null); break; default: @@ -1826,6 +1831,11 @@ public class ServiceSynchronize extends LifecycleService { } } + if (message.replying != null) { + EntityMessage replying = db.message().getMessage(message.replying); + EntityOperation.queue(this, db, replying, EntityOperation.ANSWERED, true); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/res/menu/menu_compose.xml b/app/src/main/res/menu/menu_compose.xml index f5d467e331..2c621d7374 100644 --- a/app/src/main/res/menu/menu_compose.xml +++ b/app/src/main/res/menu/menu_compose.xml @@ -18,4 +18,10 @@ android:icon="@drawable/baseline_lock_24" android:title="@string/title_encrypt" app:showAsAction="ifRoom" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 94ba1b658e..cef4252efc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -315,6 +315,7 @@ Save Send View + Send after … Nothing selected Bold