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