diff --git a/app/src/main/java/eu/faircode/email/Core.java b/app/src/main/java/eu/faircode/email/Core.java index aeff951b59..ed3c12c68c 100644 --- a/app/src/main/java/eu/faircode/email/Core.java +++ b/app/src/main/java/eu/faircode/email/Core.java @@ -6,7 +6,11 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Color; +import android.media.RingtoneManager; import android.net.Uri; +import android.os.Build; +import android.os.Bundle; import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.TextUtils; @@ -25,6 +29,7 @@ import com.sun.mail.util.MailConnectException; import org.json.JSONArray; import org.json.JSONException; +import org.jsoup.Jsoup; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -39,12 +44,16 @@ import java.io.OutputStream; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -1378,6 +1387,229 @@ class Core { } } + static List getNotificationUnseen(Context context, long account, String accountName, List messages) { + List notifications = new ArrayList<>(); + + if (messages.size() == 0) + return notifications; + + boolean pro = Helper.isPro(context); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + // https://developer.android.com/training/notify-user/group + String group = Long.toString(account); + + String title = context.getResources().getQuantityString( + R.plurals.title_notification_unseen, messages.size(), messages.size()); + + // Get contact info + Map messageContact = new HashMap<>(); + for (TupleMessageEx message : messages) + messageContact.put(message, ContactInfo.get(context, message.from, false)); + + // Build pending intent + Intent summary = new Intent(context, ServiceUI.class); + summary.setAction("summary"); + PendingIntent piSummary = PendingIntent.getService(context, ServiceUI.PI_SUMMARY, summary, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent clear = new Intent(context, ServiceUI.class); + clear.setAction("clear"); + PendingIntent piClear = PendingIntent.getService(context, ServiceUI.PI_CLEAR, clear, PendingIntent.FLAG_UPDATE_CURRENT); + + String channelName = (account == 0 ? "notification" : EntityAccount.getNotificationChannelName(account)); + + // Build public notification + NotificationCompat.Builder pbuilder = new NotificationCompat.Builder(context, channelName); + + pbuilder + .setSmallIcon(R.drawable.baseline_email_white_24) + .setContentTitle(title) + .setContentIntent(piSummary) + .setNumber(messages.size()) + .setShowWhen(false) + .setDeleteIntent(piClear) + .setPriority(Notification.PRIORITY_DEFAULT) + .setCategory(Notification.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + + if (!TextUtils.isEmpty(accountName)) + pbuilder.setSubText(accountName); + + // Build notification + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelName); + + builder + .setSmallIcon(R.drawable.baseline_email_white_24) + .setContentTitle(context.getResources().getQuantityString(R.plurals.title_notification_unseen, messages.size(), messages.size())) + .setContentIntent(piSummary) + .setNumber(messages.size()) + .setShowWhen(false) + .setDeleteIntent(piClear) + .setPriority(Notification.PRIORITY_DEFAULT) + .setCategory(Notification.CATEGORY_STATUS) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setPublicVersion(pbuilder.build()) + .setGroup(group) + .setGroupSummary(true); + + if (!TextUtils.isEmpty(accountName)) + builder.setSubText(accountName); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + boolean light = prefs.getBoolean("light", false); + String sound = prefs.getString("sound", null); + + if (light) + builder.setLights(Color.GREEN, 1000, 1000); + + if (sound == null) { + Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + builder.setSound(uri); + } else + builder.setSound(Uri.parse(sound)); + + builder.setOnlyAlertOnce(true); + } else + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + + if (pro) { + DateFormat df = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT); + StringBuilder sb = new StringBuilder(); + for (EntityMessage message : messages) { + sb.append("").append(messageContact.get(message).getDisplayName(true)).append(""); + if (!TextUtils.isEmpty(message.subject)) + sb.append(": ").append(message.subject); + sb.append(" ").append(df.format(message.received)); + sb.append("
"); + } + + builder.setStyle(new NotificationCompat.BigTextStyle() + .bigText(HtmlHelper.fromHtml(sb.toString())) + .setSummaryText(title)); + } + + notifications.add(builder.build()); + + boolean preview = prefs.getBoolean("notify_preview", true); + for (TupleMessageEx message : messages) { + ContactInfo info = messageContact.get(message); + + Bundle args = new Bundle(); + args.putLong("id", message.content ? message.id : -message.id); + + Intent thread = new Intent(context, ActivityView.class); + thread.setAction("thread:" + message.thread); + thread.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + thread.putExtra("account", message.account); + thread.putExtra("id", message.id); + PendingIntent piContent = PendingIntent.getActivity( + context, ActivityView.REQUEST_THREAD, thread, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent ignored = new Intent(context, ServiceUI.class); + ignored.setAction("ignore:" + message.id); + PendingIntent piDelete = PendingIntent.getService(context, ServiceUI.PI_IGNORED, ignored, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent seen = new Intent(context, ServiceUI.class); + seen.setAction("seen:" + message.id); + PendingIntent piSeen = PendingIntent.getService(context, ServiceUI.PI_SEEN, seen, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent archive = new Intent(context, ServiceUI.class); + archive.setAction("archive:" + message.id); + PendingIntent piArchive = PendingIntent.getService(context, ServiceUI.PI_ARCHIVE, archive, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent trash = new Intent(context, ServiceUI.class); + trash.setAction("trash:" + message.id); + PendingIntent piTrash = PendingIntent.getService(context, ServiceUI.PI_TRASH, trash, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Action.Builder actionSeen = new NotificationCompat.Action.Builder( + R.drawable.baseline_visibility_24, + context.getString(R.string.title_action_seen), + piSeen); + + NotificationCompat.Action.Builder actionArchive = new NotificationCompat.Action.Builder( + R.drawable.baseline_archive_24, + context.getString(R.string.title_action_archive), + piArchive); + + NotificationCompat.Action.Builder actionTrash = new NotificationCompat.Action.Builder( + R.drawable.baseline_delete_24, + context.getString(R.string.title_action_trash), + piTrash); + + NotificationCompat.Builder mbuilder; + mbuilder = new NotificationCompat.Builder(context, channelName); + + String folderName = message.folderDisplay == null + ? Helper.localizeFolderName(context, message.folderName) + : message.folderDisplay; + + mbuilder + .addExtras(args) + .setSmallIcon(R.drawable.baseline_email_white_24) + .setContentTitle(info.getDisplayName(true)) + .setSubText(message.accountName + " · " + folderName) + .setContentIntent(piContent) + .setWhen(message.received) + .setDeleteIntent(piDelete) + .setPriority(Notification.PRIORITY_DEFAULT) + .setOnlyAlertOnce(true) + .setCategory(Notification.CATEGORY_MESSAGE) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setGroup(group) + .setGroupSummary(false) + .addAction(actionSeen.build()) + .addAction(actionArchive.build()) + .addAction(actionTrash.build()); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + mbuilder.setSound(null); + + if (pro) { + if (!TextUtils.isEmpty(message.subject)) + mbuilder.setContentText(message.subject); + + if (message.content && preview) + try { + String body = Helper.readText(EntityMessage.getFile(context, message.id)); + StringBuilder sb = new StringBuilder(); + if (!TextUtils.isEmpty(message.subject)) + sb.append(message.subject).append("
"); + String text = Jsoup.parse(body).text(); + if (!TextUtils.isEmpty(text)) { + sb.append(""); + if (text.length() > HtmlHelper.PREVIEW_SIZE) { + sb.append(text.substring(0, HtmlHelper.PREVIEW_SIZE)); + sb.append("…"); + } else + sb.append(text); + sb.append(""); + } + mbuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(HtmlHelper.fromHtml(sb.toString()))); + } catch (IOException ex) { + Log.e(ex); + mbuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(ex.toString())); + } + + if (info.hasPhoto()) + mbuilder.setLargeIcon(info.getPhotoBitmap()); + + if (info.hasLookupUri()) + mbuilder.addPerson(info.getLookupUri().toString()); + + if (message.accountColor != null) { + mbuilder.setColor(message.accountColor); + mbuilder.setColorized(true); + } + } + + mbuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + + notifications.add(mbuilder.build()); + } + + return notifications; + } + static void reportError(Context context, EntityAccount account, EntityFolder folder, Throwable ex) { // FolderClosedException: can happen when no connectivity diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 9eeaf1970b..88a587d56e 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -28,27 +28,20 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.graphics.Color; -import android.media.RingtoneManager; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; -import android.net.Uri; import android.os.Build; -import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; import android.preference.PreferenceManager; -import android.text.TextUtils; import android.util.LongSparseArray; import com.sun.mail.imap.IMAPFolder; import com.sun.mail.imap.IMAPMessage; import com.sun.mail.imap.IMAPStore; -import org.jsoup.Jsoup; - import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -159,8 +152,8 @@ public class ServiceSynchronize extends LifecycleService { for (int i = 0; i < accountMessages.size(); i++) { long account = accountMessages.keyAt(i); - List notifications = getNotificationUnseen( - account, accountName.get(account), accountMessages.get(account)); + List notifications = Core.getNotificationUnseen( + ServiceSynchronize.this, account, accountName.get(account), accountMessages.get(account)); List all = new ArrayList<>(); List added = new ArrayList<>(); @@ -307,229 +300,6 @@ public class ServiceSynchronize extends LifecycleService { return builder; } - private List getNotificationUnseen(long account, String accountName, List messages) { - List notifications = new ArrayList<>(); - - if (messages.size() == 0) - return notifications; - - boolean pro = Helper.isPro(this); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - - // https://developer.android.com/training/notify-user/group - String group = Long.toString(account); - - String title = getResources().getQuantityString( - R.plurals.title_notification_unseen, messages.size(), messages.size()); - - // Get contact info - Map messageContact = new HashMap<>(); - for (TupleMessageEx message : messages) - messageContact.put(message, ContactInfo.get(this, message.from, false)); - - // Build pending intent - Intent summary = new Intent(this, ServiceUI.class); - summary.setAction("summary"); - PendingIntent piSummary = PendingIntent.getService(this, ServiceUI.PI_SUMMARY, summary, PendingIntent.FLAG_UPDATE_CURRENT); - - Intent clear = new Intent(this, ServiceUI.class); - clear.setAction("clear"); - PendingIntent piClear = PendingIntent.getService(this, ServiceUI.PI_CLEAR, clear, PendingIntent.FLAG_UPDATE_CURRENT); - - String channelName = (account == 0 ? "notification" : EntityAccount.getNotificationChannelName(account)); - - // Build public notification - NotificationCompat.Builder pbuilder = new NotificationCompat.Builder(this, channelName); - - pbuilder - .setSmallIcon(R.drawable.baseline_email_white_24) - .setContentTitle(title) - .setContentIntent(piSummary) - .setNumber(messages.size()) - .setShowWhen(false) - .setDeleteIntent(piClear) - .setPriority(Notification.PRIORITY_DEFAULT) - .setCategory(Notification.CATEGORY_STATUS) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - - if (!TextUtils.isEmpty(accountName)) - pbuilder.setSubText(accountName); - - // Build notification - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelName); - - builder - .setSmallIcon(R.drawable.baseline_email_white_24) - .setContentTitle(getResources().getQuantityString(R.plurals.title_notification_unseen, messages.size(), messages.size())) - .setContentIntent(piSummary) - .setNumber(messages.size()) - .setShowWhen(false) - .setDeleteIntent(piClear) - .setPriority(Notification.PRIORITY_DEFAULT) - .setCategory(Notification.CATEGORY_STATUS) - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setPublicVersion(pbuilder.build()) - .setGroup(group) - .setGroupSummary(true); - - if (!TextUtils.isEmpty(accountName)) - builder.setSubText(accountName); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - boolean light = prefs.getBoolean("light", false); - String sound = prefs.getString("sound", null); - - if (light) - builder.setLights(Color.GREEN, 1000, 1000); - - if (sound == null) { - Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); - builder.setSound(uri); - } else - builder.setSound(Uri.parse(sound)); - - builder.setOnlyAlertOnce(true); - } else - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - - if (pro) { - DateFormat df = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT); - StringBuilder sb = new StringBuilder(); - for (EntityMessage message : messages) { - sb.append("").append(messageContact.get(message).getDisplayName(true)).append(""); - if (!TextUtils.isEmpty(message.subject)) - sb.append(": ").append(message.subject); - sb.append(" ").append(df.format(message.received)); - sb.append("
"); - } - - builder.setStyle(new NotificationCompat.BigTextStyle() - .bigText(HtmlHelper.fromHtml(sb.toString())) - .setSummaryText(title)); - } - - notifications.add(builder.build()); - - boolean preview = prefs.getBoolean("notify_preview", true); - for (TupleMessageEx message : messages) { - ContactInfo info = messageContact.get(message); - - Bundle args = new Bundle(); - args.putLong("id", message.content ? message.id : -message.id); - - Intent thread = new Intent(this, ActivityView.class); - thread.setAction("thread:" + message.thread); - thread.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - thread.putExtra("account", message.account); - thread.putExtra("id", message.id); - PendingIntent piContent = PendingIntent.getActivity( - this, ActivityView.REQUEST_THREAD, thread, PendingIntent.FLAG_UPDATE_CURRENT); - - Intent ignored = new Intent(this, ServiceUI.class); - ignored.setAction("ignore:" + message.id); - PendingIntent piDelete = PendingIntent.getService(this, ServiceUI.PI_IGNORED, ignored, PendingIntent.FLAG_UPDATE_CURRENT); - - Intent seen = new Intent(this, ServiceUI.class); - seen.setAction("seen:" + message.id); - PendingIntent piSeen = PendingIntent.getService(this, ServiceUI.PI_SEEN, seen, PendingIntent.FLAG_UPDATE_CURRENT); - - Intent archive = new Intent(this, ServiceUI.class); - archive.setAction("archive:" + message.id); - PendingIntent piArchive = PendingIntent.getService(this, ServiceUI.PI_ARCHIVE, archive, PendingIntent.FLAG_UPDATE_CURRENT); - - Intent trash = new Intent(this, ServiceUI.class); - trash.setAction("trash:" + message.id); - PendingIntent piTrash = PendingIntent.getService(this, ServiceUI.PI_TRASH, trash, PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationCompat.Action.Builder actionSeen = new NotificationCompat.Action.Builder( - R.drawable.baseline_visibility_24, - getString(R.string.title_action_seen), - piSeen); - - NotificationCompat.Action.Builder actionArchive = new NotificationCompat.Action.Builder( - R.drawable.baseline_archive_24, - getString(R.string.title_action_archive), - piArchive); - - NotificationCompat.Action.Builder actionTrash = new NotificationCompat.Action.Builder( - R.drawable.baseline_delete_24, - getString(R.string.title_action_trash), - piTrash); - - NotificationCompat.Builder mbuilder; - mbuilder = new NotificationCompat.Builder(this, channelName); - - String folderName = message.folderDisplay == null - ? Helper.localizeFolderName(this, message.folderName) - : message.folderDisplay; - - mbuilder - .addExtras(args) - .setSmallIcon(R.drawable.baseline_email_white_24) - .setContentTitle(info.getDisplayName(true)) - .setSubText(message.accountName + " · " + folderName) - .setContentIntent(piContent) - .setWhen(message.received) - .setDeleteIntent(piDelete) - .setPriority(Notification.PRIORITY_DEFAULT) - .setOnlyAlertOnce(true) - .setCategory(Notification.CATEGORY_MESSAGE) - .setVisibility(Notification.VISIBILITY_PRIVATE) - .setGroup(group) - .setGroupSummary(false) - .addAction(actionSeen.build()) - .addAction(actionArchive.build()) - .addAction(actionTrash.build()); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - mbuilder.setSound(null); - - if (pro) { - if (!TextUtils.isEmpty(message.subject)) - mbuilder.setContentText(message.subject); - - if (message.content && preview) - try { - String body = Helper.readText(EntityMessage.getFile(this, message.id)); - StringBuilder sb = new StringBuilder(); - if (!TextUtils.isEmpty(message.subject)) - sb.append(message.subject).append("
"); - String text = Jsoup.parse(body).text(); - if (!TextUtils.isEmpty(text)) { - sb.append(""); - if (text.length() > HtmlHelper.PREVIEW_SIZE) { - sb.append(text.substring(0, HtmlHelper.PREVIEW_SIZE)); - sb.append("…"); - } else - sb.append(text); - sb.append(""); - } - mbuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(HtmlHelper.fromHtml(sb.toString()))); - } catch (IOException ex) { - Log.e(ex); - mbuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(ex.toString())); - } - - if (info.hasPhoto()) - mbuilder.setLargeIcon(info.getPhotoBitmap()); - - if (info.hasLookupUri()) - mbuilder.addPerson(info.getLookupUri().toString()); - - if (message.accountColor != null) { - mbuilder.setColor(message.accountColor); - mbuilder.setColorized(true); - } - } - - mbuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - - notifications.add(mbuilder.build()); - } - - return notifications; - } - private void monitorAccount(final EntityAccount account, final Core.State state) throws NoSuchProviderException { final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); final PowerManager.WakeLock wlAccount = pm.newWakeLock(