diff --git a/app/src/main/java/eu/faircode/email/Core.java b/app/src/main/java/eu/faircode/email/Core.java index 5bab0f0991..2d8b00499e 100644 --- a/app/src/main/java/eu/faircode/email/Core.java +++ b/app/src/main/java/eu/faircode/email/Core.java @@ -2514,14 +2514,24 @@ class Core { static void notifyMessages(Context context, List messages, Map> groupNotifying) { if (messages == null) messages = new ArrayList<>(); - Log.i("Notify messages=" + messages.size()); NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (nm == null) return; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean notify_summary = prefs.getBoolean("notify_summary", false); + boolean biometrics = prefs.getBoolean("biometrics", false); + boolean biometric_notify = prefs.getBoolean("biometrics_notify", false); boolean pro = ActivityBilling.isPro(context); + if (biometrics && !biometric_notify) + notify_summary = true; + + Log.i("Notify messages=" + messages.size() + + " biometrics=" + biometrics + "/" + biometric_notify + + " summary=" + notify_summary); + Map> groupMessages = new HashMap<>(); for (long group : groupNotifying.keySet()) groupMessages.put(group, new ArrayList<>()); @@ -2562,9 +2572,9 @@ class Core { // Difference for (long group : groupMessages.keySet()) { - // Difference - final List add = new ArrayList<>(); - final List remove = new ArrayList<>(groupNotifying.get(group)); + int new_messages = 0; + List add = new ArrayList<>(); + List remove = new ArrayList<>(groupNotifying.get(group)); for (TupleMessageEx message : groupMessages.get(group)) { long id = (message.content ? message.id : -message.id); if (remove.contains(id)) { @@ -2572,18 +2582,27 @@ class Core { Log.i("Notify existing=" + id); } else { add.add(id); - remove.remove(-id); - Log.i("Notify adding=" + id); + boolean existing = remove.contains(-id); + if (existing) + remove.remove(-id); + else + new_messages++; + Log.i("Notify adding=" + id + " existing=" + existing); } } - if (remove.size() + add.size() == 0) { + if (notify_summary + ? remove.size() + new_messages == 0 + : remove.size() + add.size() == 0) { Log.i("Notify unchanged"); continue; } // Build notifications - List notifications = getNotificationUnseen(context, group, groupMessages.get(group)); + List notifications = getNotificationUnseen(context, + group, groupMessages.get(group), + notify_summary, new_messages, + biometrics && !biometric_notify); Log.i("Notify group=" + group + " count=" + notifications.size() + " added=" + add.size() + " removed=" + remove.size()); @@ -2605,6 +2624,12 @@ class Core { db.message().setMessageNotifying(Math.abs(id), 0); } + for (Long id : add) { + groupNotifying.get(group).add(id); + groupNotifying.get(group).remove(-id); + db.message().setMessageNotifying(Math.abs(id), (int) Math.signum(id)); + } + for (Notification notification : notifications) { long id = notification.extras.getLong("id", 0); if ((id == 0 && add.size() + remove.size() > 0) || add.contains(id)) { @@ -2612,18 +2637,15 @@ class Core { Log.i("Notifying tag=" + tag + " id=" + id + (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? "" : " channel=" + notification.getChannelId())); nm.notify(tag, 1, notification); - - if (id != 0) { - groupNotifying.get(group).add(id); - groupNotifying.get(group).remove(-id); - db.message().setMessageNotifying(Math.abs(id), (int) Math.signum(id)); - } } } } } - private static List getNotificationUnseen(Context context, long group, List messages) { + private static List getNotificationUnseen( + Context context, + long group, List messages, + boolean notify_summary, int new_messages, boolean redacted) { List notifications = new ArrayList<>(); // Android 7+ N https://developer.android.com/training/notify-user/group @@ -2636,8 +2658,6 @@ class Core { boolean pro = ActivityBilling.isPro(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - boolean biometrics = prefs.getBoolean("biometrics", false); - boolean biometric_notify = prefs.getBoolean("biometrics_notify", false); boolean name_email = prefs.getBoolean("name_email", false); boolean flags = prefs.getBoolean("flags", true); boolean notify_preview = prefs.getBoolean("notify_preview", true); @@ -2659,7 +2679,7 @@ class Core { messageContact.put(message, ContactInfo.get(context, message.from, false)); // Summary notification - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || notify_summary) { // Build pending intents Intent summary = new Intent(context, ActivityView.class).setAction("unified"); PendingIntent piSummary = PendingIntent.getActivity(context, ActivityView.REQUEST_UNIFIED, summary, PendingIntent.FLAG_UPDATE_CURRENT); @@ -2667,12 +2687,6 @@ class Core { Intent clear = new Intent(context, ServiceUI.class).setAction("clear:" + group); PendingIntent piClear = PendingIntent.getService(context, ServiceUI.PI_CLEAR, clear, PendingIntent.FLAG_UPDATE_CURRENT); - // Wearable action - NotificationCompat.Action.Builder actionDismiss = new NotificationCompat.Action.Builder( - R.drawable.baseline_clear_all_24, - context.getString(R.string.title_dismiss), - piClear); - // Build title String title = context.getResources().getQuantityString( R.plurals.title_notification_unseen, messages.size(), messages.size()); @@ -2688,12 +2702,25 @@ class Core { .setDeleteIntent(piClear) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setCategory(NotificationCompat.CATEGORY_STATUS) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setGroup(Long.toString(group)) - .setGroupSummary(true) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) - .extend(new NotificationCompat.WearableExtender() - .addAction(actionDismiss.build())); + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + + if (notify_summary) { + builder.setOnlyAlertOnce(new_messages == 0); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + if (new_messages > 0) + setLightAndSound(builder, light, sound); + else + builder.setSound(null); + } else { + builder + .setGroup(Long.toString(group)) + .setGroupSummary(true) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + builder.setSound(null); + } if (pro && group != 0 && messages.size() > 0) { TupleMessageEx amessage = messages.get(0); @@ -2704,15 +2731,14 @@ class Core { builder.setSubText(amessage.accountName); } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - builder.setSound(null); - Notification pub = builder.build(); builder .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) .setPublicVersion(pub); - if (!biometrics || biometric_notify) { + if (redacted) + builder.setContentText(context.getString(R.string.title_setup_biometrics)); + else { DateFormat DTF = Helper.getDateTimeInstance(context, SimpleDateFormat.SHORT, SimpleDateFormat.SHORT); StringBuilder sb = new StringBuilder(); for (EntityMessage message : messages) { @@ -2731,6 +2757,9 @@ class Core { notifications.add(builder.build()); } + if (notify_summary) + return notifications; + // Message notifications for (TupleMessageEx message : messages) { ContactInfo info = messageContact.get(message); @@ -2791,40 +2820,15 @@ class Core { .setGroupSummary(false) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - int def = 0; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + setLightAndSound(mbuilder, light, sound); - if (light) { - def |= DEFAULT_LIGHTS; - Log.i("Notify light enabled"); - } + String folderName = message.folderDisplay == null + ? Helper.localizeFolderName(context, message.folderName) + : message.folderDisplay; - Uri uri = (sound == null ? null : Uri.parse(sound)); - if (uri == null || "file".equals(uri.getScheme())) - uri = null; - Log.i("Notify sound=" + uri); - - if (uri == null) - def |= DEFAULT_SOUND; - else - mbuilder.setSound(uri); - - mbuilder.setDefaults(def); - } - - if (biometrics && !biometric_notify) - mbuilder - .setContentTitle(context.getResources().getQuantityString( - R.plurals.title_notification_unseen, 1, 1)) - .setContentText(context.getString(R.string.title_setup_biometrics)); - else { - String folderName = message.folderDisplay == null - ? Helper.localizeFolderName(context, message.folderName) - : message.folderDisplay; - - mbuilder.setContentTitle(info.getDisplayName(name_email)) - .setSubText(message.accountName + " · " + folderName); - } + mbuilder.setContentTitle(info.getDisplayName(name_email)) + .setSubText(message.accountName + " · " + folderName); DB db = DB.getInstance(context); @@ -2936,43 +2940,41 @@ class Core { mbuilder.addAction(actionSnooze.build()); } - if (!biometrics || biometric_notify) { - if (!TextUtils.isEmpty(message.subject)) - mbuilder.setContentText(message.subject); + if (!TextUtils.isEmpty(message.subject)) + mbuilder.setContentText(message.subject); - if (message.content && notify_preview) - try { - String body = Helper.readText(message.getFile(context)); - StringBuilder sbm = new StringBuilder(); - if (!TextUtils.isEmpty(message.subject)) - sbm.append(message.subject).append("
"); - String text = Jsoup.parse(body).text(); - if (!TextUtils.isEmpty(text)) { - sbm.append(""); - if (text.length() > HtmlHelper.PREVIEW_SIZE) { - sbm.append(text.substring(0, HtmlHelper.PREVIEW_SIZE)); - sbm.append("…"); - } else - sbm.append(text); - sbm.append(""); - } - mbuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(HtmlHelper.fromHtml(sbm.toString()))); - } catch (IOException ex) { - Log.e(ex); - mbuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(ex.toString())); - db.message().setMessageContent(message.id, false, null, null, null); + if (message.content && notify_preview) + try { + String body = Helper.readText(message.getFile(context)); + StringBuilder sbm = new StringBuilder(); + if (!TextUtils.isEmpty(message.subject)) + sbm.append(message.subject).append("
"); + String text = Jsoup.parse(body).text(); + if (!TextUtils.isEmpty(text)) { + sbm.append(""); + if (text.length() > HtmlHelper.PREVIEW_SIZE) { + sbm.append(text.substring(0, HtmlHelper.PREVIEW_SIZE)); + sbm.append("…"); + } else + sbm.append(text); + sbm.append(""); } - - if (info.hasPhoto()) - mbuilder.setLargeIcon(info.getPhotoBitmap()); - - if (info.hasLookupUri()) - mbuilder.addPerson(info.getLookupUri().toString()); - - if (pro && message.accountColor != null) { - mbuilder.setColor(message.accountColor); - mbuilder.setColorized(true); + mbuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(HtmlHelper.fromHtml(sbm.toString()))); + } catch (IOException ex) { + Log.e(ex); + mbuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(ex.toString())); + db.message().setMessageContent(message.id, false, null, null, null); } + + if (info.hasPhoto()) + mbuilder.setLargeIcon(info.getPhotoBitmap()); + + if (info.hasLookupUri()) + mbuilder.addPerson(info.getLookupUri().toString()); + + if (pro && message.accountColor != null) { + mbuilder.setColor(message.accountColor); + mbuilder.setColorized(true); } notifications.add(mbuilder.build()); @@ -2981,6 +2983,27 @@ class Core { return notifications; } + private static void setLightAndSound(NotificationCompat.Builder builder, boolean light, String sound) { + int def = 0; + + if (light) { + def |= DEFAULT_LIGHTS; + Log.i("Notify light enabled"); + } + + Uri uri = (sound == null ? null : Uri.parse(sound)); + if (uri == null || "file".equals(uri.getScheme())) + uri = null; + Log.i("Notify sound=" + uri); + + if (uri == null) + def |= DEFAULT_SOUND; + else + builder.setSound(uri); + + builder.setDefaults(def); + } + // FolderClosedException: can happen when no connectivity // IllegalStateException: diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsNotifications.java b/app/src/main/java/eu/faircode/email/FragmentOptionsNotifications.java index 8a9dd99122..0b4dc185e5 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 @@ import static android.app.Activity.RESULT_OK; public class FragmentOptionsNotifications extends FragmentBase implements SharedPreferences.OnSharedPreferenceChangeListener { private SwitchCompat swBadge; private SwitchCompat swUnseenIgnored; + private SwitchCompat swNotifySummary; private SwitchCompat swNotifyPreview; private CheckBox cbNotifyActionTrash; private CheckBox cbNotifyActionJunk; @@ -82,7 +83,7 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared private final static String[] RESET_OPTIONS = new String[]{ "badge", "unseen_ignored", - "notify_preview", "notify_trash", "notify_junk", "notify_archive", "notify_reply", "notify_reply_direct", "notify_flag", + "notify_summary", "notify_preview", "notify_trash", "notify_junk", "notify_archive", "notify_reply", "notify_reply_direct", "notify_flag", "notify_seen", "notify_snooze", "notify_snooze_duration", "notify_remove", "biometrics_notify", "light", "sound", "alert_once" @@ -100,6 +101,7 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared swBadge = view.findViewById(R.id.swBadge); swUnseenIgnored = view.findViewById(R.id.swUnseenIgnored); + swNotifySummary = view.findViewById(R.id.swNotifySummary); swNotifyPreview = view.findViewById(R.id.swNotifyPreview); cbNotifyActionTrash = view.findViewById(R.id.cbNotifyActionTrash); cbNotifyActionJunk = view.findViewById(R.id.cbNotifyActionJunk); @@ -147,6 +149,14 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared } }); + swNotifySummary.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + prefs.edit().putBoolean("notify_summary", checked).apply(); + enableOptions(); + } + }); + swNotifyPreview.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { @@ -354,6 +364,7 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared swBadge.setChecked(prefs.getBoolean("badge", true)); swUnseenIgnored.setChecked(prefs.getBoolean("unseen_ignored", false)); + swNotifySummary.setChecked(prefs.getBoolean("notify_summary", false)); swNotifyPreview.setChecked(prefs.getBoolean("notify_preview", true)); cbNotifyActionTrash.setChecked(prefs.getBoolean("notify_trash", true) || !pro); @@ -366,20 +377,31 @@ public class FragmentOptionsNotifications extends FragmentBase implements Shared cbNotifyActionSnooze.setChecked(prefs.getBoolean("notify_snooze", false) || !pro); etNotifyActionSnooze.setText(Integer.toString(prefs.getInt("notify_snooze_duration", 60))); - cbNotifyActionTrash.setEnabled(pro); - cbNotifyActionJunk.setEnabled(pro); - cbNotifyActionArchive.setEnabled(pro); - cbNotifyActionReply.setEnabled(pro); - cbNotifyActionReplyDirect.setEnabled(pro); - cbNotifyActionFlag.setEnabled(pro); - cbNotifyActionSeen.setEnabled(pro); - cbNotifyActionSnooze.setEnabled(pro); - swNotifyRemove.setChecked(prefs.getBoolean("notify_remove", true)); swBiometricsNotify.setChecked(prefs.getBoolean("biometrics_notify", false)); swLight.setChecked(prefs.getBoolean("light", false)); swAlertOnce.setChecked(!prefs.getBoolean("alert_once", true)); + + enableOptions(); + } + + private void enableOptions() { + boolean pro = ActivityBilling.isPro(getContext()); + boolean checked = swNotifySummary.isChecked(); + + swNotifyPreview.setEnabled(!checked); + cbNotifyActionTrash.setEnabled(pro && !checked); + cbNotifyActionJunk.setEnabled(pro && !checked); + cbNotifyActionArchive.setEnabled(pro && !checked); + cbNotifyActionReply.setEnabled(pro && !checked); + cbNotifyActionReplyDirect.setEnabled(pro && !checked); + cbNotifyActionFlag.setEnabled(pro && !checked); + cbNotifyActionSeen.setEnabled(pro && !checked); + cbNotifyActionSnooze.setEnabled(pro && !checked); + etNotifyActionSnooze.setEnabled(pro && !checked); + swNotifyRemove.setEnabled(pro && !checked); + swBiometricsNotify.setEnabled(!checked); } @Override diff --git a/app/src/main/res/layout/fragment_options_notifications.xml b/app/src/main/res/layout/fragment_options_notifications.xml index 1eba6266d6..8423a26779 100644 --- a/app/src/main/res/layout/fragment_options_notifications.xml +++ b/app/src/main/res/layout/fragment_options_notifications.xml @@ -46,6 +46,17 @@ app:layout_constraintTop_toBottomOf="@id/tvBadgeHint" app:switchPadding="12dp" /> + + Show launcher icon with number of new messages Let the number of new messages match the number of notifications + Show summary notification only Show message preview in notifications Notification actions Trash