Files
FairEmail/app/src/main/java/eu/faircode/email/ServiceUI.java

615 lines
21 KiB
Java
Raw Normal View History

2019-02-27 15:05:15 +00:00
package eu.faircode.email;
2019-05-04 22:49:22 +02:00
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
2020-01-05 18:32:53 +01:00
Copyright 2018-2020 by Marcel Bokhorst (M66B)
2019-05-04 22:49:22 +02:00
*/
import android.app.AlarmManager;
2019-02-27 15:05:15 +00:00
import android.app.IntentService;
2019-05-21 15:55:10 +02:00
import android.app.NotificationManager;
import android.app.PendingIntent;
2019-05-21 15:55:10 +02:00
import android.content.Context;
2019-02-27 15:05:15 +00:00
import android.content.Intent;
2019-05-06 10:33:29 +02:00
import android.content.SharedPreferences;
2019-08-24 15:00:14 +02:00
import android.os.Bundle;
import android.widget.Toast;
2019-02-27 15:05:15 +00:00
import androidx.annotation.Nullable;
import androidx.core.app.AlarmManagerCompat;
2019-08-24 15:00:14 +02:00
import androidx.core.app.RemoteInput;
2019-05-06 10:33:29 +02:00
import androidx.preference.PreferenceManager;
import org.json.JSONException;
2020-02-20 10:35:01 +01:00
import java.io.File;
2019-08-24 15:00:14 +02:00
import java.io.IOException;
import java.util.Collections;
2019-08-24 15:00:14 +02:00
import java.util.Date;
2019-05-06 10:33:29 +02:00
import java.util.List;
2019-08-24 15:00:14 +02:00
import java.util.regex.Pattern;
import javax.mail.Address;
import javax.mail.internet.InternetAddress;
2019-02-27 15:05:15 +00:00
public class ServiceUI extends IntentService {
2019-03-14 14:57:24 +00:00
static final int PI_CLEAR = 1;
2019-05-05 22:28:07 +02:00
static final int PI_TRASH = 2;
2019-09-26 21:28:05 +02:00
static final int PI_JUNK = 3;
static final int PI_ARCHIVE = 4;
2019-10-23 12:51:20 +02:00
static final int PI_MOVE = 5;
static final int PI_REPLY_DIRECT = 6;
static final int PI_FLAG = 7;
static final int PI_SEEN = 8;
static final int PI_SNOOZE = 9;
static final int PI_IGNORED = 10;
static final int PI_THREAD = 11;
static final int PI_WAKEUP = 12;
static final int PI_SYNC = 13;
static final int PI_BANNER = 14;
2019-02-27 15:05:15 +00:00
static final int HIDE_BANNER = 3; // weeks
2020-06-07 08:08:17 +02:00
2019-02-27 15:05:15 +00:00
public ServiceUI() {
this(ServiceUI.class.getName());
}
public ServiceUI(String name) {
super(name);
}
2019-02-27 16:48:03 +00:00
@Override
public void onCreate() {
Log.i("Service UI create");
super.onCreate();
}
@Override
public void onDestroy() {
Log.i("Service UI destroy");
super.onDestroy();
}
2019-02-27 15:05:15 +00:00
@Override
protected void onHandleIntent(@Nullable Intent intent) {
2019-03-02 08:16:27 +00:00
// Under certain circumstances, a background app is placed on a temporary whitelist for several minutes
// - Executing a PendingIntent from a notification.
// https://developer.android.com/about/versions/oreo/background#services
2019-02-27 16:48:03 +00:00
Log.i("Service UI intent=" + intent);
2019-07-25 10:43:07 +02:00
Log.logExtras(intent);
2019-02-27 16:48:03 +00:00
2019-02-27 15:05:15 +00:00
if (intent == null)
return;
String action = intent.getAction();
if (action == null)
return;
2019-08-24 15:00:14 +02:00
try {
String[] parts = action.split(":");
long id = (parts.length > 1 ? Long.parseLong(parts[1]) : -1);
2020-04-24 17:43:10 +02:00
long group = intent.getLongExtra("group", -1);
2019-08-24 15:00:14 +02:00
switch (parts[0]) {
case "clear":
2019-09-01 11:32:18 +02:00
onClear(id);
2019-08-24 15:00:14 +02:00
break;
case "trash":
cancel(group, id);
2019-09-26 21:28:05 +02:00
onMove(id, EntityFolder.TRASH);
break;
case "junk":
cancel(group, id);
onJunk(id);
2019-08-24 15:00:14 +02:00
break;
case "archive":
cancel(group, id);
2019-09-26 21:28:05 +02:00
onMove(id, EntityFolder.ARCHIVE);
2019-08-24 15:00:14 +02:00
break;
2019-10-23 12:51:20 +02:00
case "move":
cancel(group, id);
onMove(id);
break;
2019-08-24 15:00:14 +02:00
case "reply":
cancel(group, id);
onSeen(id);
2020-04-24 10:36:14 +02:00
onReplyDirect(id, intent);
2019-08-24 15:00:14 +02:00
break;
case "flag":
cancel(group, id);
onFlag(id);
break;
case "seen":
cancel(group, id);
onSeen(id);
break;
2019-09-24 09:34:43 +02:00
case "snooze":
cancel(group, id);
onSnooze(id);
break;
2019-08-24 15:00:14 +02:00
case "ignore":
boolean view = intent.getBooleanExtra("view", false);
onIgnore(id, view);
2019-08-24 15:00:14 +02:00
break;
2019-09-24 09:34:43 +02:00
case "wakeup":
2019-08-24 15:00:14 +02:00
// 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
2019-09-24 09:34:43 +02:00
onWakeup(id);
2019-08-24 15:00:14 +02:00
break;
2019-11-23 11:39:34 +01:00
case "sync":
boolean reschedule = intent.getBooleanExtra("reschedule", false);
onSync(id, reschedule);
break;
2019-12-30 08:39:35 +01:00
case "daily":
2019-12-29 12:09:20 +01:00
case "banner":
onBanner();
2019-11-23 11:39:34 +01:00
break;
2019-08-24 15:00:14 +02:00
default:
throw new IllegalArgumentException("Unknown UI action: " + parts[0]);
}
} catch (Throwable ex) {
Log.e(ex);
2019-02-27 15:05:15 +00:00
}
2020-05-24 12:36:07 +02:00
ServiceSynchronize.eval(this, "ui/" + action);
2019-02-27 15:05:15 +00:00
}
2019-09-01 11:32:18 +02:00
private void onClear(long group) {
DB db = DB.getInstance(this);
2020-06-15 12:34:25 +02:00
int cleared;
if (group < 0)
cleared = db.message().ignoreAll(null, -group);
else
cleared = db.message().ignoreAll(group == 0 ? null : group, null);
2019-09-01 11:32:18 +02:00
Log.i("Cleared=" + cleared);
2019-02-27 15:05:15 +00:00
}
2020-04-24 17:43:10 +02:00
private void cancel(long group, long id) {
2019-05-21 15:55:10 +02:00
String tag = "unseen." + group + ":" + id;
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(tag, 1);
}
2019-09-26 21:28:05 +02:00
private void onMove(long id, String folderType) {
2019-02-27 15:05:15 +00:00
DB db = DB.getInstance(this);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
2019-09-24 09:34:43 +02:00
if (message == null)
return;
2019-10-23 12:51:20 +02:00
EntityFolder folder = db.folder().getFolderByType(message.account, folderType);
if (folder == null)
return;
EntityOperation.queue(this, message, EntityOperation.MOVE, folder.id);
2019-10-23 12:51:20 +02:00
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
private void onMove(long id) {
DB db = DB.getInstance(this);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
if (message == null)
return;
EntityAccount account = db.account().getAccount(message.account);
if (account == null || account.move_to == null)
return;
EntityOperation.queue(this, message, EntityOperation.MOVE, account.move_to);
2019-02-27 15:05:15 +00:00
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
private void onJunk(long id) throws JSONException {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean block_sender = prefs.getBoolean("notify_block_sender", false);
2020-04-19 16:57:40 +02:00
List<String> whitelist = EmailProvider.getDomainNames(this);
DB db = DB.getInstance(this);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
if (message == null)
return;
EntityFolder junk = db.folder().getFolderByType(message.account, EntityFolder.JUNK);
if (junk == null)
return;
EntityOperation.queue(this, message, EntityOperation.MOVE, junk.id);
if (block_sender) {
2020-04-19 16:57:40 +02:00
EntityRule rule = EntityRule.blockSender(this, message, junk, false, whitelist);
rule.id = db.rule().insertRule(rule);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
2019-02-27 15:05:15 +00:00
}
2019-08-24 15:00:14 +02:00
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);
2020-03-26 15:25:44 +01:00
String body = results.getString("text");
if (body != null)
body = "<p>" + body.replaceAll("\\r?\\n", "<br>") + "</p>";
2019-08-24 15:00:14 +02:00
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);
2020-02-20 10:35:01 +01:00
File file = reply.getFile(this);
2020-03-26 15:25:44 +01:00
Helper.writeText(file, body);
2020-02-20 10:35:01 +01:00
2019-08-24 15:00:14 +02:00
db.message().setMessageContent(reply.id,
true,
2020-03-26 15:25:44 +01:00
HtmlHelper.getLanguage(this, body),
2019-08-24 15:00:14 +02:00
plain_only || ref.plain_only,
2020-03-26 15:25:44 +01:00
HtmlHelper.getPreview(body),
2019-08-24 15:00:14 +02:00
null);
EntityOperation.queue(this, reply, EntityOperation.SEND);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
2019-12-07 20:32:58 +01:00
ServiceSend.start(this);
2019-12-07 20:32:58 +01:00
ToastEx.makeText(this, R.string.title_queued, Toast.LENGTH_LONG).show();
2019-08-24 15:00:14 +02:00
}
2019-05-06 10:33:29 +02:00
private void onFlag(long id) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean threading = prefs.getBoolean("threading", true);
DB db = DB.getInstance(this);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
2019-09-24 09:34:43 +02:00
if (message == null)
return;
List<EntityMessage> messages = db.message().getMessagesByThread(
2020-02-24 18:33:22 +01:00
message.account, message.thread, threading ? null : id, message.folder);
2019-09-24 09:34:43 +02:00
for (EntityMessage threaded : messages) {
EntityOperation.queue(this, threaded, EntityOperation.FLAG, true);
EntityOperation.queue(this, threaded, EntityOperation.SEEN, true);
2019-05-06 10:33:29 +02:00
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
2019-05-05 22:28:07 +02:00
private void onSeen(long id) {
2019-02-27 15:05:15 +00:00
DB db = DB.getInstance(this);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
2019-09-24 09:34:43 +02:00
if (message == null)
return;
EntityOperation.queue(this, message, EntityOperation.SEEN, true);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
private void onSnooze(long id) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
2019-10-06 21:01:01 +02:00
int notify_snooze_duration = prefs.getInt("default_snooze", 1);
2019-09-24 09:34:43 +02:00
2019-10-06 21:01:01 +02:00
long wakeup = new Date().getTime() + notify_snooze_duration * 3600 * 1000L;
2019-09-24 09:34:43 +02:00
DB db = DB.getInstance(this);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
if (message == null)
return;
db.message().setMessageSnoozed(id, wakeup);
2019-11-04 09:00:14 +01:00
db.message().setMessageUiIgnored(message.id, true);
2019-09-24 09:34:43 +02:00
EntityMessage.snooze(this, id, wakeup);
2019-02-27 15:05:15 +00:00
db.setTransactionSuccessful();
2019-09-24 09:34:43 +02:00
2019-02-27 15:05:15 +00:00
} finally {
db.endTransaction();
}
}
private void onIgnore(long id, boolean open) {
EntityMessage message;
2020-04-03 09:39:48 +02:00
EntityFolder folder;
2019-02-27 15:05:15 +00:00
DB db = DB.getInstance(this);
try {
db.beginTransaction();
message = db.message().getMessage(id);
2019-09-24 09:34:43 +02:00
if (message == null)
return;
2020-04-03 09:39:48 +02:00
folder = db.folder().getFolder(message.folder);
if (folder == null)
return;
2019-09-24 09:34:43 +02:00
db.message().setMessageUiIgnored(message.id, true);
2019-02-27 15:05:15 +00:00
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (open) {
Intent thread = new Intent(this, ActivityView.class);
thread.setAction("thread:" + message.thread);
thread.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
thread.putExtra("account", message.account);
2020-06-21 08:25:41 +02:00
thread.putExtra("folder", message.folder);
thread.putExtra("id", message.id);
2020-04-03 09:39:48 +02:00
thread.putExtra("filter_archive", !EntityFolder.ARCHIVE.equals(folder.type));
startActivity(thread);
}
2019-02-27 15:05:15 +00:00
}
2019-09-24 09:34:43 +02:00
private void onWakeup(long id) {
2019-12-07 20:32:58 +01:00
EntityFolder folder;
2019-02-27 15:05:15 +00:00
DB db = DB.getInstance(this);
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
2019-09-26 15:28:21 +02:00
if (message == null)
return;
2019-12-07 20:32:58 +01:00
folder = db.folder().getFolder(message.folder);
if (folder == null)
return;
2019-09-26 15:28:21 +02:00
if (EntityFolder.OUTBOX.equals(folder.type)) {
Log.i("Delayed send id=" + message.id);
2020-04-09 21:36:33 +02:00
if (message.ui_snoozed != null) {
db.message().setMessageSnoozed(message.id, null);
EntityOperation.queue(this, message, EntityOperation.SEND);
}
2019-09-26 15:28:21 +02:00
} else {
2019-11-08 08:25:42 +01:00
if (folder.notify) {
2020-06-29 08:13:57 +02:00
List<EntityAttachment> attachments = db.attachment().getAttachments(id);
2019-11-09 19:39:24 +01:00
// A new message ID is needed for a new (wearable) notification
db.message().deleteMessage(id);
2020-06-29 08:13:57 +02:00
2019-11-08 08:25:42 +01:00
message.id = null;
2020-01-15 12:07:45 +01:00
message.fts = false;
2019-11-08 08:25:42 +01:00
message.id = db.message().insertMessage(message);
2020-06-29 08:13:57 +02:00
if (message.content) {
File source = EntityMessage.getFile(this, id);
File target = message.getFile(this);
try {
Helper.copy(source, target);
} catch (IOException ex) {
Log.e(ex);
db.message().resetMessageContent(message.id);
}
}
for (EntityAttachment attachment : attachments) {
File source = attachment.getFile(this);
attachment.id = null;
attachment.message = message.id;
attachment.progress = null;
attachment.id = db.attachment().insertAttachment(attachment);
if (attachment.available) {
File target = attachment.getFile(this);
try {
Helper.copy(source, target);
} catch (IOException ex) {
Log.e(ex);
db.attachment().setError(attachment.id, Log.formatThrowable(ex, false));
}
}
}
2019-11-08 08:25:42 +01:00
}
2019-11-29 18:12:11 +01:00
db.message().setMessageSnoozed(message.id, null);
2020-05-19 12:46:35 +02:00
db.message().setMessageUnsnoozed(message.id, true);
2019-11-29 18:12:11 +01:00
EntityOperation.queue(this, message, EntityOperation.SEEN, false, false);
2019-02-27 15:05:15 +00:00
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
2019-12-07 20:32:58 +01:00
if (EntityFolder.OUTBOX.equals(folder.type))
ServiceSend.start(this);
2019-02-27 15:05:15 +00:00
}
2019-11-23 11:39:34 +01:00
private void onSync(long aid, boolean reschedule) {
DB db = DB.getInstance(this);
try {
db.beginTransaction();
2020-01-22 21:39:38 +01:00
List<EntityAccount> accounts = db.account().getPollAccounts(aid < 0 ? null : aid);
for (EntityAccount account : accounts) {
List<EntityFolder> folders = db.folder().getSynchronizingFolders(account.id);
if (folders.size() > 0)
Collections.sort(folders, folders.get(0).getComparator(this));
for (EntityFolder folder : folders)
EntityOperation.sync(this, folder.id, false);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
2020-01-14 09:02:37 +01:00
if (reschedule) {
long now = new Date().getTime();
long[] schedule = ServiceSynchronize.getSchedule(this);
2020-03-21 09:39:46 +01:00
boolean poll = (schedule == null || (now >= schedule[0] && now < schedule[1]));
schedule(this, poll);
2020-01-14 09:02:37 +01:00
}
}
2019-12-29 12:09:20 +01:00
private void onBanner() {
2019-11-23 11:39:34 +01:00
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
2020-03-19 16:20:47 +01:00
prefs.edit().remove("banner_hidden").apply();
2019-11-23 11:39:34 +01:00
}
static void sync(Context context, Long account) {
context.startService(new Intent(context, ServiceUI.class)
.setAction(account == null ? "sync" : "sync:" + account));
}
2020-03-21 09:39:46 +01:00
static void schedule(Context context, boolean poll) {
Intent intent = new Intent(context, ServiceUI.class);
intent.setAction("sync");
intent.putExtra("reschedule", true);
PendingIntent piSync = PendingIntent.getService(
context, ServiceUI.PI_SYNC, intent, PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
am.cancel(piSync);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
2020-03-21 09:39:46 +01:00
boolean enabled = prefs.getBoolean("enabled", true);
2020-02-23 18:39:42 +01:00
int pollInterval = prefs.getInt("poll_interval", ServiceSynchronize.DEFAULT_POLL_INTERVAL);
2020-03-21 09:39:46 +01:00
if (poll && enabled && pollInterval > 0) {
long now = new Date().getTime();
long interval = pollInterval * 60 * 1000L;
long next = now + interval - now % interval + 30 * 1000L;
2020-01-12 15:54:30 +01:00
EntityLog.log(context, "Poll next=" + new Date(next));
AlarmManagerCompat.setAndAllowWhileIdle(am, AlarmManager.RTC_WAKEUP, next, piSync);
}
}
2020-03-19 13:37:08 +01:00
2020-03-19 16:20:47 +01:00
private static PendingIntent getBannerIntent(Context context) {
2020-03-19 13:37:08 +01:00
Intent banner = new Intent(context, ServiceUI.class);
banner.setAction("banner");
2020-03-19 16:20:47 +01:00
return PendingIntent.getService(context, ServiceUI.PI_BANNER, banner, PendingIntent.FLAG_UPDATE_CURRENT);
}
2020-03-19 13:37:08 +01:00
2020-03-19 16:20:47 +01:00
static void scheduleBanner(Context context, boolean set) {
2020-03-19 13:37:08 +01:00
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
2020-03-19 16:20:47 +01:00
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
2020-03-19 13:37:08 +01:00
if (set) {
long now = new Date().getTime();
2020-06-07 08:08:17 +02:00
long interval = AlarmManager.INTERVAL_DAY * HIDE_BANNER * 7;
2020-03-19 13:37:08 +01:00
long due = interval - (now % interval);
long trigger = now + due;
Log.i("Set banner alarm at " + new Date(trigger) + " due=" + due);
2020-03-19 16:20:47 +01:00
am.set(AlarmManager.RTC, trigger, getBannerIntent(context));
prefs.edit().putLong("banner_hidden", trigger).apply();
2020-03-19 13:37:08 +01:00
} else {
Log.i("Cancel banner alarm");
2020-03-19 16:20:47 +01:00
am.cancel(getBannerIntent(context));
prefs.edit().remove("banner_hidden").apply();
}
}
static void boot(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
long banner_hidden = prefs.getLong("banner_hidden", 0);
if (banner_hidden > 0) {
Log.i("Restore banner alarm at " + new Date(banner_hidden));
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
am.set(AlarmManager.RTC, banner_hidden, getBannerIntent(context));
2020-03-19 13:37:08 +01:00
}
}
2019-02-27 15:05:15 +00:00
}