diff --git a/app/src/main/java/eu/faircode/email/Core.java b/app/src/main/java/eu/faircode/email/Core.java index 84799e66b5..46ea606e26 100644 --- a/app/src/main/java/eu/faircode/email/Core.java +++ b/app/src/main/java/eu/faircode/email/Core.java @@ -35,6 +35,7 @@ import android.util.Pair; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; +import androidx.core.app.RemoteInput; import androidx.preference.PreferenceManager; import com.sun.mail.iap.BadCommandException; @@ -2080,6 +2081,7 @@ class Core { boolean notify_trash = (prefs.getBoolean("notify_trash", true) || !pro); boolean notify_archive = (prefs.getBoolean("notify_archive", true) || !pro); boolean notify_reply = (prefs.getBoolean("notify_reply", false) && pro); + boolean notify_reply_direct = (prefs.getBoolean("notify_reply_direct", false) && pro); boolean notify_flag = (prefs.getBoolean("notify_flag", false) && flags && pro); boolean notify_seen = (prefs.getBoolean("notify_seen", true) || !pro); boolean light = prefs.getBoolean("light", false); @@ -2279,6 +2281,25 @@ class Core { mbuilder.addAction(actionReply.build()); } + if (notify_reply_direct && + message.content && + message.identity != null && + message.from != null && message.from.length > 0 && + db.folder().getOutbox() != null) { + Intent reply = new Intent(context, ServiceUI.class) + .setAction("reply:" + message.id) + .putExtra("group", group); + PendingIntent piReply = PendingIntent.getService(context, ServiceUI.PI_REPLY_DIRECT, reply, PendingIntent.FLAG_UPDATE_CURRENT); + NotificationCompat.Action.Builder actionReply = new NotificationCompat.Action.Builder( + R.drawable.baseline_reply_24, + context.getString(R.string.title_advanced_notify_action_reply_direct), + piReply); + RemoteInput.Builder input = new RemoteInput.Builder("text") + .setLabel(context.getString(R.string.title_advanced_notify_action_reply)); + actionReply.addRemoteInput(input.build()).setAllowGeneratedReplies(false); + mbuilder.addAction(actionReply.build()); + } + if (notify_flag) { Intent flag = new Intent(context, ServiceUI.class) .setAction("flag:" + message.id) diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsNotifications.java b/app/src/main/java/eu/faircode/email/FragmentOptionsNotifications.java index 8e96f71234..35aeb5afbd 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsNotifications.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsNotifications.java @@ -56,6 +56,7 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared private CheckBox cbNotifyActionTrash; private CheckBox cbNotifyActionArchive; private CheckBox cbNotifyActionReply; + private CheckBox cbNotifyActionReplyDirect; private CheckBox cbNotifyActionFlag; private CheckBox cbNotifyActionSeen; private TextView tvNotifyActionsPro; @@ -70,7 +71,7 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared private final static String[] RESET_OPTIONS = new String[]{ "badge", "unseen_ignored", - "notify_preview", "notify_trash", "notify_archive", "notify_reply", "notify_flag", "notify_seen", "biometrics_notify", + "notify_preview", "notify_trash", "notify_archive", "notify_reply", "notify_reply_direct", "notify_flag", "notify_seen", "biometrics_notify", "light", "sound" }; @@ -90,6 +91,7 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared cbNotifyActionTrash = view.findViewById(R.id.cbNotifyActionTrash); cbNotifyActionArchive = view.findViewById(R.id.cbNotifyActionArchive); cbNotifyActionReply = view.findViewById(R.id.cbNotifyActionReply); + cbNotifyActionReplyDirect = view.findViewById(R.id.cbNotifyActionReplyDirect); cbNotifyActionFlag = view.findViewById(R.id.cbNotifyActionFlag); cbNotifyActionSeen = view.findViewById(R.id.cbNotifyActionSeen); tvNotifyActionsPro = view.findViewById(R.id.tvNotifyActionsPro); @@ -153,6 +155,13 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared } }); + cbNotifyActionReplyDirect.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean checked) { + prefs.edit().putBoolean("notify_reply_direct", checked).apply(); + } + }); + cbNotifyActionFlag.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean checked) { @@ -273,6 +282,7 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared cbNotifyActionTrash.setChecked(prefs.getBoolean("notify_trash", true) || !pro); cbNotifyActionArchive.setChecked(prefs.getBoolean("notify_archive", true) || !pro); cbNotifyActionReply.setChecked(prefs.getBoolean("notify_reply", false) && pro); + cbNotifyActionReplyDirect.setChecked(prefs.getBoolean("notify_reply_direct", false) && pro); cbNotifyActionFlag.setChecked(prefs.getBoolean("notify_flag", false) && pro); cbNotifyActionSeen.setChecked(prefs.getBoolean("notify_seen", true) || !pro); diff --git a/app/src/main/java/eu/faircode/email/ServiceUI.java b/app/src/main/java/eu/faircode/email/ServiceUI.java index cf58c7e491..9b0fe3fbd4 100644 --- a/app/src/main/java/eu/faircode/email/ServiceUI.java +++ b/app/src/main/java/eu/faircode/email/ServiceUI.java @@ -24,20 +24,30 @@ import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.os.Bundle; +import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.core.app.RemoteInput; import androidx.preference.PreferenceManager; +import java.io.IOException; +import java.util.Date; import java.util.List; +import java.util.regex.Pattern; + +import javax.mail.Address; +import javax.mail.internet.InternetAddress; public class ServiceUI extends IntentService { static final int PI_CLEAR = 1; static final int PI_TRASH = 2; static final int PI_ARCHIVE = 3; - static final int PI_FLAG = 4; - static final int PI_SEEN = 5; - static final int PI_IGNORED = 6; - static final int PI_SNOOZED = 7; + static final int PI_REPLY_DIRECT = 4; + static final int PI_FLAG = 5; + static final int PI_SEEN = 6; + static final int PI_IGNORED = 7; + static final int PI_SNOOZED = 8; public ServiceUI() { this(ServiceUI.class.getName()); @@ -74,47 +84,58 @@ public class ServiceUI extends IntentService { if (action == null) return; - String[] parts = action.split(":"); - long id = (parts.length > 1 ? Long.parseLong(parts[1]) : -1); + try { + String[] parts = action.split(":"); + long id = (parts.length > 1 ? Long.parseLong(parts[1]) : -1); + String group = intent.getStringExtra("group"); - switch (parts[0]) { - case "clear": - onClear(); - break; + switch (parts[0]) { + case "clear": + onClear(); + break; - case "trash": - cancel(intent.getStringExtra("group"), id); - onTrash(id); - break; + case "trash": + cancel(group, id); + onTrash(id); + break; - case "archive": - cancel(intent.getStringExtra("group"), id); - onArchive(id); - break; + case "archive": + cancel(group, id); + onArchive(id); + break; - case "flag": - cancel(intent.getStringExtra("group"), id); - onFlag(id); - break; + case "reply": + onReplyDirect(id, intent); + cancel(group, id); + onSeen(id); + break; - case "seen": - cancel(intent.getStringExtra("group"), id); - onSeen(id); - break; + case "flag": + cancel(group, id); + onFlag(id); + break; - case "ignore": - onIgnore(id); - break; + case "seen": + cancel(group, id); + onSeen(id); + break; - case "snooze": - // AlarmManager.RTC_WAKEUP - // When the alarm is dispatched, the app will also be added to the system's temporary whitelist - // for approximately 10 seconds to allow that application to acquire further wake locks in which to complete its work. - // https://developer.android.com/reference/android/app/AlarmManager - onSnooze(id); - break; - default: - Log.w("Unknown action: " + parts[0]); + case "ignore": + onIgnore(id); + break; + + case "snooze": + // AlarmManager.RTC_WAKEUP + // When the alarm is dispatched, the app will also be added to the system's temporary whitelist + // for approximately 10 seconds to allow that application to acquire further wake locks in which to complete its work. + // https://developer.android.com/reference/android/app/AlarmManager + onSnooze(id); + break; + default: + throw new IllegalArgumentException("Unknown UI action: " + parts[0]); + } + } catch (Throwable ex) { + Log.e(ex); } } @@ -167,6 +188,67 @@ public class ServiceUI extends IntentService { } } + private void onReplyDirect(long id, Intent intent) throws IOException { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean prefix_once = prefs.getBoolean("prefix_once", true); + boolean plain_only = prefs.getBoolean("plain_only", false); + + Bundle results = RemoteInput.getResultsFromIntent(intent); + String text = results.getString("text"); + + DB db = DB.getInstance(this); + try { + db.beginTransaction(); + + EntityMessage ref = db.message().getMessage(id); + if (ref == null) + throw new IllegalArgumentException("message not found"); + + EntityIdentity identity = db.identity().getIdentity(ref.identity); + if (identity == null) + throw new IllegalArgumentException("identity not found"); + + EntityFolder outbox = db.folder().getOutbox(); + if (outbox == null) + throw new IllegalArgumentException("outbox not found"); + + String subject = (ref.subject == null ? "" : ref.subject); + if (prefix_once) { + String re = getString(R.string.title_subject_reply, ""); + subject = subject.replaceAll("(?i)" + Pattern.quote(re.trim()), "").trim(); + } + + EntityMessage reply = new EntityMessage(); + reply.account = identity.account; + reply.folder = outbox.id; + reply.identity = identity.id; + reply.msgid = EntityMessage.generateMessageId(); + reply.inreplyto = ref.msgid; + reply.thread = ref.thread; + reply.to = ref.from; + reply.from = new Address[]{new InternetAddress(identity.email, identity.name)}; + reply.subject = getString(R.string.title_subject_reply, subject); + reply.received = new Date().getTime(); + reply.seen = true; + reply.ui_seen = true; + reply.id = db.message().insertMessage(reply); + Helper.writeText(reply.getFile(this), text); + db.message().setMessageContent(reply.id, + true, + plain_only || ref.plain_only, + HtmlHelper.getPreview(text), + null); + + EntityOperation.queue(this, reply, EntityOperation.SEND); + + db.setTransactionSuccessful(); + + ToastEx.makeText(this, R.string.title_queued, Toast.LENGTH_LONG).show(); + } finally { + db.endTransaction(); + } + } + private void onFlag(long id) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); boolean threading = prefs.getBoolean("threading", true); diff --git a/app/src/main/res/layout/fragment_options_notifications.xml b/app/src/main/res/layout/fragment_options_notifications.xml index 3c02c1b25d..0f7eca67dd 100644 --- a/app/src/main/res/layout/fragment_options_notifications.xml +++ b/app/src/main/res/layout/fragment_options_notifications.xml @@ -117,6 +117,15 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/cbNotifyActionArchive" /> + + + app:layout_constraintTop_toBottomOf="@id/cbNotifyActionReplyDirect" /> Trash Archive Reply + Direct reply Star Read Show notification content when using biometric authentication