mirror of https://github.com/M66B/FairEmail.git
1392 lines
66 KiB
Java
1392 lines
66 KiB
Java
package eu.faircode.email;
|
|
|
|
/*
|
|
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/>.
|
|
|
|
Copyright 2018-2024 by Marcel Bokhorst (M66B)
|
|
*/
|
|
|
|
import static androidx.core.app.NotificationCompat.DEFAULT_LIGHTS;
|
|
import static androidx.core.app.NotificationCompat.DEFAULT_SOUND;
|
|
|
|
import android.app.Notification;
|
|
import android.app.NotificationChannel;
|
|
import android.app.NotificationChannelGroup;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.graphics.Color;
|
|
import android.media.AudioAttributes;
|
|
import android.media.Ringtone;
|
|
import android.media.RingtoneManager;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.PowerManager;
|
|
import android.service.notification.StatusBarNotification;
|
|
import android.text.Html;
|
|
import android.text.TextUtils;
|
|
|
|
import androidx.annotation.RequiresApi;
|
|
import androidx.core.app.NotificationCompat;
|
|
import androidx.core.app.Person;
|
|
import androidx.core.app.RemoteInput;
|
|
import androidx.core.graphics.drawable.IconCompat;
|
|
import androidx.preference.PreferenceManager;
|
|
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import java.io.File;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.text.DateFormat;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
|
|
import javax.mail.Address;
|
|
import javax.mail.internet.InternetAddress;
|
|
|
|
import me.leolin.shortcutbadger.ShortcutBadgerAlt;
|
|
|
|
class NotificationHelper {
|
|
static final int NOTIFICATION_SYNCHRONIZE = 100;
|
|
static final int NOTIFICATION_SEND = 200;
|
|
static final int NOTIFICATION_EXTERNAL = 300;
|
|
static final int NOTIFICATION_UPDATE = 400;
|
|
static final int NOTIFICATION_TAGGED = 500;
|
|
|
|
private static final int MAX_NOTIFICATION_DISPLAY = 10; // per group
|
|
private static final int MAX_NOTIFICATION_COUNT = 100; // per group
|
|
private static final long SCREEN_ON_DURATION = 3000L; // milliseconds
|
|
|
|
// Android applies a rate limit when updating a notification.
|
|
// If you post updates to a notification too frequently—many in less than one second—
|
|
// the system might drop updates.
|
|
private static final int DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5; // NotificationManagerService.java
|
|
private static final int MAX_PREVIEW = 5000; // characters
|
|
private static final long NOTIFY_DELAY = 1250L / DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE; // milliseconds
|
|
|
|
private static final List<String> PERSISTENT_IDS = Collections.unmodifiableList(Arrays.asList(
|
|
"service",
|
|
"send",
|
|
"notification",
|
|
"progress",
|
|
"update",
|
|
"announcements",
|
|
"warning",
|
|
"error",
|
|
"alerts",
|
|
"LEAKCANARY_LOW",
|
|
"LEAKCANARY_MAX"
|
|
));
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
static void createNotificationChannels(Context context) {
|
|
// https://issuetracker.google.com/issues/65108694
|
|
NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
|
|
|
|
// Sync
|
|
NotificationChannel service = new NotificationChannel(
|
|
"service", context.getString(R.string.channel_service),
|
|
NotificationManager.IMPORTANCE_MIN);
|
|
service.setDescription(context.getString(R.string.channel_service_description));
|
|
service.setSound(null, null);
|
|
service.enableVibration(false);
|
|
service.enableLights(false);
|
|
service.setShowBadge(false);
|
|
service.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
|
|
createNotificationChannel(nm, service);
|
|
|
|
// Send
|
|
NotificationChannel send = new NotificationChannel(
|
|
"send", context.getString(R.string.channel_send),
|
|
NotificationManager.IMPORTANCE_DEFAULT);
|
|
send.setDescription(context.getString(R.string.channel_send_description));
|
|
send.setSound(null, null);
|
|
send.enableVibration(false);
|
|
send.enableLights(false);
|
|
send.setShowBadge(false);
|
|
send.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
|
createNotificationChannel(nm, send);
|
|
|
|
// Notify
|
|
NotificationChannel notification = new NotificationChannel(
|
|
"notification", context.getString(R.string.channel_notification),
|
|
NotificationManager.IMPORTANCE_HIGH);
|
|
notification.setDescription(context.getString(R.string.channel_notification_description));
|
|
notification.enableLights(true);
|
|
notification.setLightColor(Color.YELLOW);
|
|
notification.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
|
//notification.setBypassDnd(true);
|
|
createNotificationChannel(nm, notification);
|
|
|
|
NotificationChannel progress = new NotificationChannel(
|
|
"progress", context.getString(R.string.channel_progress),
|
|
NotificationManager.IMPORTANCE_DEFAULT);
|
|
notification.setDescription(context.getString(R.string.channel_progress_description));
|
|
progress.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT);
|
|
progress.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
|
createNotificationChannel(nm, progress);
|
|
|
|
if (!Helper.isPlayStoreInstall()) {
|
|
// Update
|
|
NotificationChannel update = new NotificationChannel(
|
|
"update", context.getString(R.string.channel_update),
|
|
NotificationManager.IMPORTANCE_HIGH);
|
|
update.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT);
|
|
update.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
|
createNotificationChannel(nm, update);
|
|
|
|
// Announcements
|
|
NotificationChannel announcements = new NotificationChannel(
|
|
"announcements", context.getString(R.string.channel_announcements),
|
|
NotificationManager.IMPORTANCE_HIGH);
|
|
announcements.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT);
|
|
announcements.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
|
createNotificationChannel(nm, announcements);
|
|
}
|
|
|
|
// Warnings
|
|
NotificationChannel warning = new NotificationChannel(
|
|
"warning", context.getString(R.string.channel_warning),
|
|
NotificationManager.IMPORTANCE_HIGH);
|
|
warning.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
|
warning.setBypassDnd(true);
|
|
createNotificationChannel(nm, warning);
|
|
|
|
// Errors
|
|
NotificationChannel error = new NotificationChannel(
|
|
"error",
|
|
context.getString(R.string.channel_error),
|
|
NotificationManager.IMPORTANCE_HIGH);
|
|
error.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
|
error.setBypassDnd(true);
|
|
createNotificationChannel(nm, error);
|
|
|
|
// Server alerts
|
|
NotificationChannel alerts = new NotificationChannel(
|
|
"alerts",
|
|
context.getString(R.string.channel_alert),
|
|
NotificationManager.IMPORTANCE_HIGH);
|
|
alerts.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
|
alerts.setBypassDnd(true);
|
|
createNotificationChannel(nm, alerts);
|
|
|
|
// Contacts grouping
|
|
NotificationChannelGroup group = new NotificationChannelGroup(
|
|
"contacts",
|
|
context.getString(R.string.channel_group_contacts));
|
|
createNotificationChannelGroup(nm, group);
|
|
}
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
private static void createNotificationChannel(NotificationManager nm, NotificationChannel channel) {
|
|
try {
|
|
nm.createNotificationChannel(channel);
|
|
} catch (Throwable ex) {
|
|
Log.e(ex);
|
|
/*
|
|
Caused by: java.lang.NullPointerException: Attempt to read from field 'android.os.IInterface com.android.server.notification.ManagedServices$ManagedServiceInfo.service' on a null object reference in method 'com.android.server.notification.ManagedServices$ManagedServiceInfo com.android.server.notification.ManagedServices.getServiceFromTokenLocked(android.os.IInterface)'
|
|
at android.os.Parcel.createExceptionOrNull(Parcel.java:3017)
|
|
at android.os.Parcel.createException(Parcel.java:2995)
|
|
at android.os.Parcel.readException(Parcel.java:2978)
|
|
at android.os.Parcel.readException(Parcel.java:2920)
|
|
at android.app.INotificationManager$Stub$Proxy.createNotificationChannels(INotificationManager.java:3583)
|
|
at android.app.NotificationManager.createNotificationChannels(NotificationManager.java:929)
|
|
at android.app.NotificationManager.createNotificationChannel(NotificationManager.java:917)
|
|
at eu.faircode.email.a0.a(Unknown Source:0)
|
|
at eu.faircode.email.NotificationHelper.createNotificationChannels(SourceFile:54)
|
|
at eu.faircode.email.ApplicationEx.onCreate(SourceFile:137)
|
|
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1278)
|
|
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7083)
|
|
... 9 more
|
|
Caused by: android.os.RemoteException: Remote stack trace:
|
|
at com.android.server.notification.ManagedServices.getServiceFromTokenLocked(ManagedServices.java:1056)
|
|
at com.android.server.notification.ManagedServices.isServiceTokenValidLocked(ManagedServices.java:1065)
|
|
at com.android.server.notification.NotificationManagerService.isInteractionVisibleToListener(NotificationManagerService.java:10237)
|
|
at com.android.server.notification.NotificationManagerService.-$$Nest$misInteractionVisibleToListener(Unknown Source:0)
|
|
at com.android.server.notification.NotificationManagerService$NotificationListeners.notifyNotificationChannelChanged(NotificationManagerService.java:11498)
|
|
*/
|
|
}
|
|
}
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
private static void createNotificationChannelGroup(NotificationManager nm, NotificationChannelGroup group) {
|
|
try {
|
|
nm.createNotificationChannelGroup(group);
|
|
} catch (Throwable ex) {
|
|
Log.e(ex);
|
|
}
|
|
}
|
|
|
|
static boolean areNotificationsEnabled(NotificationManager nm) {
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
|
|
return true;
|
|
else
|
|
return nm.areNotificationsEnabled();
|
|
}
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
static String[] getChannelIds(Context context) {
|
|
List<String> result = new ArrayList();
|
|
|
|
NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
|
|
for (NotificationChannel channel : nm.getNotificationChannels()) {
|
|
String id = channel.getId();
|
|
if (!PERSISTENT_IDS.contains(id))
|
|
result.add(id);
|
|
}
|
|
|
|
Collections.sort(result);
|
|
|
|
return result.toArray(new String[0]);
|
|
}
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
static void deleteChannel(Context context, String id) {
|
|
NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
|
|
nm.deleteNotificationChannel(id);
|
|
}
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
static JSONObject channelToJSON(NotificationChannel channel) throws JSONException {
|
|
JSONObject jchannel = new JSONObject();
|
|
|
|
jchannel.put("id", channel.getId());
|
|
jchannel.put("group", channel.getGroup());
|
|
jchannel.put("name", channel.getName());
|
|
jchannel.put("description", channel.getDescription());
|
|
|
|
jchannel.put("importance", channel.getImportance());
|
|
jchannel.put("dnd", channel.canBypassDnd());
|
|
jchannel.put("visibility", channel.getLockscreenVisibility());
|
|
jchannel.put("badge", channel.canShowBadge());
|
|
|
|
Uri sound = channel.getSound();
|
|
if (sound != null) {
|
|
jchannel.put("sound", sound.toString());
|
|
AudioAttributes attr = channel.getAudioAttributes();
|
|
try {
|
|
jchannel.put("sound_content_type", attr.getContentType());
|
|
jchannel.put("sound_usage", attr.getUsage());
|
|
} catch (Throwable ex) {
|
|
Log.e(ex);
|
|
}
|
|
}
|
|
|
|
jchannel.put("light", channel.shouldShowLights());
|
|
// color
|
|
|
|
jchannel.put("vibrate", channel.shouldVibrate());
|
|
// pattern
|
|
|
|
return jchannel;
|
|
}
|
|
|
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
static NotificationChannel channelFromJSON(Context context, JSONObject jchannel) throws JSONException {
|
|
int importance = jchannel.getInt("importance");
|
|
if (importance < NotificationManager.IMPORTANCE_MIN ||
|
|
importance > NotificationManager.IMPORTANCE_MAX)
|
|
importance = NotificationManager.IMPORTANCE_DEFAULT;
|
|
|
|
NotificationChannel channel = new NotificationChannel(
|
|
jchannel.getString("id"),
|
|
jchannel.getString("name"),
|
|
importance);
|
|
|
|
String group = jchannel.optString("group");
|
|
if (!TextUtils.isEmpty(group))
|
|
channel.setGroup(group);
|
|
|
|
if (jchannel.has("description") && !jchannel.isNull("description"))
|
|
channel.setDescription(jchannel.getString("description"));
|
|
|
|
channel.setBypassDnd(jchannel.getBoolean("dnd"));
|
|
|
|
int visibility = jchannel.getInt("visibility");
|
|
if (visibility == Notification.VISIBILITY_PRIVATE ||
|
|
visibility == Notification.VISIBILITY_PUBLIC ||
|
|
visibility == Notification.VISIBILITY_SECRET)
|
|
channel.setLockscreenVisibility(visibility);
|
|
|
|
channel.setShowBadge(jchannel.getBoolean("badge"));
|
|
|
|
if (jchannel.has("sound") && !jchannel.isNull("sound"))
|
|
try {
|
|
Uri uri = Uri.parse(jchannel.getString("sound"));
|
|
AudioAttributes attr;
|
|
try {
|
|
AudioAttributes.Builder builder = new AudioAttributes.Builder();
|
|
if (jchannel.has("sound_content_type"))
|
|
builder.setContentType(jchannel.getInt("sound_content_type"));
|
|
else
|
|
builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION);
|
|
if (jchannel.has("sound_usage"))
|
|
builder.setUsage(jchannel.getInt("sound_usage"));
|
|
else
|
|
builder.setUsage(AudioAttributes.USAGE_NOTIFICATION);
|
|
attr = builder.build();
|
|
} catch (Throwable ex) {
|
|
Log.e(ex);
|
|
attr = Notification.AUDIO_ATTRIBUTES_DEFAULT;
|
|
}
|
|
Ringtone ringtone = RingtoneManager.getRingtone(context, uri);
|
|
if (ringtone != null)
|
|
channel.setSound(uri, attr);
|
|
} catch (Throwable ex) {
|
|
Log.e(ex);
|
|
}
|
|
|
|
channel.enableLights(jchannel.getBoolean("light"));
|
|
channel.enableVibration(jchannel.getBoolean("vibrate"));
|
|
|
|
return channel;
|
|
}
|
|
|
|
static void notifyMessages(Context context, List<TupleMessageEx> messages, NotificationHelper.NotificationData data, boolean foreground) {
|
|
if (messages == null)
|
|
messages = new ArrayList<>();
|
|
|
|
NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
|
|
if (nm == null)
|
|
return;
|
|
|
|
DB db = DB.getInstance(context);
|
|
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
boolean badge = prefs.getBoolean("badge", true);
|
|
boolean notify_background_only = prefs.getBoolean("notify_background_only", false);
|
|
boolean notify_summary = prefs.getBoolean("notify_summary", false);
|
|
boolean notify_preview = prefs.getBoolean("notify_preview", true);
|
|
boolean notify_preview_only = prefs.getBoolean("notify_preview_only", false);
|
|
boolean notify_screen_on = prefs.getBoolean("notify_screen_on", false);
|
|
boolean wearable_preview = prefs.getBoolean("wearable_preview", false);
|
|
boolean biometrics = prefs.getBoolean("biometrics", false);
|
|
String pin = prefs.getString("pin", null);
|
|
boolean biometric_notify = prefs.getBoolean("biometrics_notify", true);
|
|
boolean pro = ActivityBilling.isPro(context);
|
|
|
|
boolean redacted = ((biometrics || !TextUtils.isEmpty(pin)) && !biometric_notify);
|
|
if (redacted)
|
|
notify_summary = true;
|
|
if (notify_screen_on &&
|
|
!(BuildConfig.DEBUG ||
|
|
Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU ||
|
|
Helper.hasPermission(context, "android.permission.TURN_SCREEN_ON")))
|
|
notify_screen_on = false;
|
|
|
|
Log.i("Notify messages=" + messages.size() +
|
|
" biometrics=" + biometrics + "/" + biometric_notify +
|
|
" summary=" + notify_summary);
|
|
|
|
Map<Long, Integer> newMessages = new HashMap<>();
|
|
|
|
Map<Long, List<TupleMessageEx>> groupMessages = new HashMap<>();
|
|
for (long group : data.groupNotifying.keySet())
|
|
groupMessages.put(group, new ArrayList<>());
|
|
|
|
Map<String, Boolean> channelIdDisabled = new HashMap<>();
|
|
|
|
// Current
|
|
for (TupleMessageEx message : messages) {
|
|
if (message.notifying == EntityMessage.NOTIFYING_IGNORE) {
|
|
Log.e("Notify ignore");
|
|
continue;
|
|
}
|
|
|
|
// Check if notification channel enabled
|
|
if (message.notifying == 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && pro) {
|
|
// Disabling a channel for a sender or folder doesn't disable notifications
|
|
// because the (account) summary notification isn't disabled
|
|
// So, suppress notifications here
|
|
|
|
String mChannelId = message.getNotificationChannelId();
|
|
if (mChannelId != null && !channelIdDisabled.containsKey(mChannelId)) {
|
|
NotificationChannel channel = nm.getNotificationChannel(mChannelId);
|
|
channelIdDisabled.put(mChannelId,
|
|
channel != null && channel.getImportance() == NotificationManager.IMPORTANCE_NONE);
|
|
}
|
|
|
|
String fChannelId = EntityFolder.getNotificationChannelId(message.folder);
|
|
if (!channelIdDisabled.containsKey(fChannelId)) {
|
|
NotificationChannel channel = nm.getNotificationChannel(fChannelId);
|
|
channelIdDisabled.put(fChannelId,
|
|
channel != null && channel.getImportance() == NotificationManager.IMPORTANCE_NONE);
|
|
}
|
|
|
|
if (Boolean.TRUE.equals(channelIdDisabled.get(fChannelId)) ||
|
|
(mChannelId != null && Boolean.TRUE.equals(channelIdDisabled.get(mChannelId)))) {
|
|
db.message().setMessageUiIgnored(message.id, true);
|
|
Log.i("Notify disabled=" + message.id +
|
|
" " + mChannelId + "=" + channelIdDisabled.get(mChannelId) +
|
|
" " + fChannelId + "=" + channelIdDisabled.get(fChannelId));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (notify_preview && notify_preview_only && !message.content)
|
|
continue;
|
|
|
|
if (foreground && notify_background_only && message.notifying == 0) {
|
|
Log.i("Notify foreground=" + message.id);
|
|
if (!message.ui_ignored)
|
|
db.message().setMessageUiIgnored(message.id, true);
|
|
continue;
|
|
}
|
|
|
|
long group = (pro && message.accountNotify ? message.account : 0);
|
|
if (!message.folderUnified)
|
|
group = -message.folder;
|
|
if (!data.groupNotifying.containsKey(group))
|
|
data.groupNotifying.put(group, new ArrayList<>());
|
|
if (!groupMessages.containsKey(group))
|
|
groupMessages.put(group, new ArrayList<>());
|
|
|
|
if (message.notifying == 0) {
|
|
// Handle clear notifying on boot/update
|
|
EntityMessage msg = db.message().getMessage(message.id);
|
|
if (msg != null && msg.notifying == 0) {
|
|
Log.i("Notify boot=" + msg.id);
|
|
data.groupNotifying.get(group).remove(msg.id);
|
|
data.groupNotifying.get(group).remove(-msg.id);
|
|
}
|
|
} else {
|
|
long id = message.id * message.notifying;
|
|
if (!data.groupNotifying.get(group).contains(id) &&
|
|
!data.groupNotifying.get(group).contains(-id)) {
|
|
Log.i("Notify database=" + id);
|
|
data.groupNotifying.get(group).add(id);
|
|
}
|
|
}
|
|
|
|
if (message.ui_seen || message.ui_ignored || message.ui_hide)
|
|
Log.i("Notify id=" + message.id +
|
|
" seen=" + message.ui_seen +
|
|
" ignored=" + message.ui_ignored +
|
|
" hide=" + message.ui_hide);
|
|
else {
|
|
// Prevent reappearing notifications
|
|
EntityMessage msg = db.message().getMessage(message.id);
|
|
if (msg == null || msg.ui_ignored) {
|
|
Log.i("Notify skip id=" + message.id + " msg=" + (msg != null));
|
|
continue;
|
|
}
|
|
|
|
Integer current = newMessages.get(group);
|
|
newMessages.put(group, current == null ? 1 : current + 1);
|
|
|
|
// This assumes the messages are properly ordered
|
|
if (groupMessages.get(group).size() < MAX_NOTIFICATION_COUNT)
|
|
groupMessages.get(group).add(message);
|
|
else {
|
|
EntityLog.log(context, "Notify max group=" + group +
|
|
" count=" + groupMessages.get(group).size() + "/" + MAX_NOTIFICATION_COUNT);
|
|
db.message().setMessageUiIgnored(message.id, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Difference
|
|
boolean flash = false;
|
|
for (long group : groupMessages.keySet()) {
|
|
List<Long> add = new ArrayList<>();
|
|
List<Long> update = new ArrayList<>();
|
|
List<Long> remove = new ArrayList<>(data.groupNotifying.get(group));
|
|
for (int m = 0; m < groupMessages.get(group).size(); m++) {
|
|
TupleMessageEx message = groupMessages.get(group).get(m);
|
|
if (m >= MAX_NOTIFICATION_DISPLAY) {
|
|
// This is to prevent notification sounds when shifting messages up
|
|
if (!message.ui_silent) {
|
|
Log.i("Notify silence=" + message.id);
|
|
db.message().setMessageUiSilent(message.id, true);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
long id = (message.content ? message.id : -message.id);
|
|
if (remove.contains(id)) {
|
|
remove.remove(id);
|
|
Log.i("Notify existing=" + id);
|
|
} else {
|
|
boolean existing = remove.contains(-id);
|
|
if (existing) {
|
|
if (message.content && notify_preview) {
|
|
Log.i("Notify preview=" + id);
|
|
add.add(id);
|
|
update.add(id);
|
|
}
|
|
remove.remove(-id);
|
|
} else {
|
|
flash = true;
|
|
add.add(id);
|
|
}
|
|
Log.i("Notify adding=" + id + " existing=" + existing);
|
|
}
|
|
}
|
|
|
|
Integer prev = prefs.getInt("new_messages." + group, 0);
|
|
Integer current = newMessages.get(group);
|
|
if (current == null)
|
|
current = 0;
|
|
prefs.edit().putInt("new_messages." + group, current).apply();
|
|
|
|
if (prev.equals(current) &&
|
|
remove.size() + add.size() == 0) {
|
|
Log.i("Notify unchanged");
|
|
continue;
|
|
}
|
|
|
|
boolean summary = (notify_summary ||
|
|
(group != 0 &&
|
|
groupMessages.get(group).size() > 0 &&
|
|
groupMessages.get(group).get(0).accountSummary));
|
|
|
|
// Build notifications
|
|
List<NotificationCompat.Builder> notifications = getNotificationUnseen(context,
|
|
group, groupMessages.get(group),
|
|
summary, current - prev, current,
|
|
redacted);
|
|
|
|
Log.i("Notify group=" + group +
|
|
" new=" + prev + "/" + current +
|
|
" count=" + notifications.size() +
|
|
" add=" + add.size() +
|
|
" update=" + update.size() +
|
|
" remove=" + remove.size());
|
|
|
|
for (Long id : remove) {
|
|
String tag = "unseen." + group + "." + Math.abs(id);
|
|
EntityLog.log(context, EntityLog.Type.Notification,
|
|
null, null, id == 0 ? null : Math.abs(id),
|
|
"Notify cancel tag=" + tag + " id=" + id);
|
|
nm.cancel(tag, NotificationHelper.NOTIFICATION_TAGGED);
|
|
|
|
data.groupNotifying.get(group).remove(id);
|
|
db.message().setMessageNotifying(Math.abs(id), 0);
|
|
}
|
|
|
|
if (notifications.size() == 0) {
|
|
String tag = "unseen." + group + "." + 0;
|
|
EntityLog.log(context, EntityLog.Type.Notification,
|
|
"Notify cancel tag=" + tag);
|
|
nm.cancel(tag, NotificationHelper.NOTIFICATION_TAGGED);
|
|
}
|
|
|
|
for (Long id : add) {
|
|
data.groupNotifying.get(group).add(id);
|
|
data.groupNotifying.get(group).remove(-id);
|
|
db.message().setMessageNotifying(Math.abs(id), (int) Math.signum(id));
|
|
}
|
|
|
|
for (NotificationCompat.Builder builder : notifications) {
|
|
long id = builder.getExtras().getLong("id", 0);
|
|
if ((id == 0 && !prev.equals(current)) || add.contains(id)) {
|
|
// https://developer.android.com/training/wearables/notifications/bridger#non-bridged
|
|
if (id == 0) {
|
|
if (!summary)
|
|
builder.setLocalOnly(true);
|
|
} else {
|
|
if (wearable_preview ? id < 0 : update.contains(id))
|
|
builder.setLocalOnly(true);
|
|
}
|
|
|
|
String tag = "unseen." + group + "." + Math.abs(id);
|
|
Notification notification = builder.build();
|
|
EntityLog.log(context, EntityLog.Type.Notification,
|
|
null, null, id == 0 ? null : Math.abs(id),
|
|
"Notifying tag=" + tag +
|
|
" id=" + id + " group=" + notification.getGroup() +
|
|
(Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
|
? " sdk=" + Build.VERSION.SDK_INT
|
|
: " channel=" + notification.getChannelId()) +
|
|
" sort=" + notification.getSortKey());
|
|
try {
|
|
if (NotificationHelper.areNotificationsEnabled(nm)) {
|
|
nm.notify(tag, NotificationHelper.NOTIFICATION_TAGGED, notification);
|
|
if (update.contains(id))
|
|
try {
|
|
Log.i("Notify delay id=" + id);
|
|
Thread.sleep(NOTIFY_DELAY);
|
|
} catch (InterruptedException ex) {
|
|
Log.w(ex);
|
|
}
|
|
}
|
|
|
|
// https://github.com/leolin310148/ShortcutBadger/wiki/Xiaomi-Device-Support
|
|
if (id == 0 && badge && Helper.isXiaomi())
|
|
ShortcutBadgerAlt.applyNotification(context, notification, current);
|
|
} catch (Throwable ex) {
|
|
Log.w(ex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (notify_screen_on && flash) {
|
|
EntityLog.log(context, EntityLog.Type.Notification, "Notify screen on");
|
|
PowerManager pm = Helper.getSystemService(context, PowerManager.class);
|
|
PowerManager.WakeLock wakeLock = pm.newWakeLock(
|
|
PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
BuildConfig.APPLICATION_ID + ":notification");
|
|
wakeLock.acquire(SCREEN_ON_DURATION);
|
|
}
|
|
}
|
|
|
|
private static List<NotificationCompat.Builder> getNotificationUnseen(
|
|
Context context,
|
|
long group, List<TupleMessageEx> messages,
|
|
boolean notify_summary, int new_messages, int total_messages, boolean redacted) {
|
|
List<NotificationCompat.Builder> notifications = new ArrayList<>();
|
|
|
|
// Android 7+ N https://developer.android.com/training/notify-user/group
|
|
// Android 8+ O https://developer.android.com/training/notify-user/channels
|
|
// Android 7+ N https://android-developers.googleblog.com/2016/06/notifications-in-android-n.html
|
|
|
|
// Group
|
|
// < 0: folder
|
|
// = 0: unified
|
|
// > 0: account
|
|
|
|
NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
|
|
if (messages == null || messages.size() == 0 || nm == null)
|
|
return notifications;
|
|
|
|
boolean pro = ActivityBilling.isPro(context);
|
|
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
boolean notify_grouping = prefs.getBoolean("notify_grouping", true);
|
|
boolean notify_private = prefs.getBoolean("notify_private", true);
|
|
boolean notify_newest_first = prefs.getBoolean("notify_newest_first", false);
|
|
MessageHelper.AddressFormat email_format = MessageHelper.getAddressFormat(context);
|
|
boolean prefer_contact = prefs.getBoolean("prefer_contact", false);
|
|
boolean flags = prefs.getBoolean("flags", true);
|
|
boolean notify_messaging = prefs.getBoolean("notify_messaging", false);
|
|
boolean notify_subtext = prefs.getBoolean("notify_subtext", true);
|
|
boolean notify_preview = prefs.getBoolean("notify_preview", true);
|
|
boolean notify_preview_all = prefs.getBoolean("notify_preview_all", false);
|
|
boolean wearable_preview = prefs.getBoolean("wearable_preview", false);
|
|
boolean notify_trash = (prefs.getBoolean("notify_trash", true) || !pro);
|
|
boolean notify_junk = (prefs.getBoolean("notify_junk", false) && pro);
|
|
boolean notify_archive = (prefs.getBoolean("notify_archive", true) || !pro);
|
|
boolean notify_move = (prefs.getBoolean("notify_move", false) && 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 notify_hide = (prefs.getBoolean("notify_hide", false) && pro);
|
|
boolean notify_snooze = (prefs.getBoolean("notify_snooze", false) && pro);
|
|
boolean notify_remove = prefs.getBoolean("notify_remove", true);
|
|
boolean light = prefs.getBoolean("light", false);
|
|
String sound = prefs.getString("sound", null);
|
|
boolean alert_once = prefs.getBoolean("alert_once", true);
|
|
boolean perform_expunge = prefs.getBoolean("perform_expunge", true);
|
|
boolean delete_notification = prefs.getBoolean("delete_notification", false);
|
|
|
|
// Get contact info
|
|
Map<Long, Address[]> messageFrom = new HashMap<>();
|
|
Map<Long, ContactInfo[]> messageInfo = new HashMap<>();
|
|
for (int m = 0; m < messages.size() && m < MAX_NOTIFICATION_DISPLAY; m++) {
|
|
TupleMessageEx message = messages.get(m);
|
|
ContactInfo[] info = ContactInfo.get(context,
|
|
message.account, message.folderType, message.bimi_selector,
|
|
message.isForwarder() ? message.submitter : message.from);
|
|
|
|
Address[] modified = (message.from == null
|
|
? new InternetAddress[0]
|
|
: Arrays.copyOf(message.from, message.from.length));
|
|
for (int i = 0; i < modified.length; i++) {
|
|
String displayName = info[i].getDisplayName();
|
|
if (!TextUtils.isEmpty(displayName)) {
|
|
String email = ((InternetAddress) modified[i]).getAddress();
|
|
String personal = ((InternetAddress) modified[i]).getPersonal();
|
|
if (TextUtils.isEmpty(personal) || prefer_contact)
|
|
try {
|
|
modified[i] = new InternetAddress(email, displayName, StandardCharsets.UTF_8.name());
|
|
} catch (UnsupportedEncodingException ex) {
|
|
Log.w(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
messageInfo.put(message.id, info);
|
|
messageFrom.put(message.id, modified);
|
|
}
|
|
|
|
// Summary notification
|
|
if (notify_summary ||
|
|
(notify_grouping && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)) {
|
|
// Build pending intents
|
|
Intent content;
|
|
if (group < 0) {
|
|
content = new Intent(context, ActivityView.class)
|
|
.setAction("folder:" + (-group) + (notify_remove ? ":" + group : ""));
|
|
if (messages.size() > 0)
|
|
content.putExtra("type", messages.get(0).folderType);
|
|
} else
|
|
content = new Intent(context, ActivityView.class)
|
|
.setAction("unified" + (notify_remove ? ":" + group : ""));
|
|
content.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
PendingIntent piContent = PendingIntentCompat.getActivity(
|
|
context, ActivityView.PI_UNIFIED, content, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
|
|
Intent clear = new Intent(context, ServiceUI.class).setAction("clear:" + group);
|
|
PendingIntent piClear = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_CLEAR, clear, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
|
|
// Build title
|
|
String title = context.getResources().getQuantityString(
|
|
R.plurals.title_notification_unseen, total_messages, total_messages);
|
|
|
|
long cgroup = (group >= 0
|
|
? group
|
|
: (pro && messages.size() > 0 && messages.get(0).accountNotify ? messages.get(0).account : 0));
|
|
|
|
// Build notification
|
|
NotificationCompat.Builder builder =
|
|
new NotificationCompat.Builder(context, EntityAccount.getNotificationChannelId(cgroup))
|
|
.setSmallIcon(messages.size() > 1
|
|
? R.drawable.baseline_mail_more_white_24
|
|
: R.drawable.baseline_mail_white_24)
|
|
.setContentTitle(title)
|
|
.setContentIntent(piContent)
|
|
.setNumber(total_messages)
|
|
.setDeleteIntent(piClear)
|
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
.setCategory(notify_summary
|
|
? NotificationCompat.CATEGORY_EMAIL : NotificationCompat.CATEGORY_STATUS)
|
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
.setAllowSystemGeneratedContextualActions(false);
|
|
|
|
if (group != 0 && messages.size() > 0)
|
|
builder.setSubText(messages.get(0).accountName);
|
|
|
|
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) {
|
|
Integer color = null;
|
|
for (TupleMessageEx message : messages) {
|
|
Integer mcolor = getColor(message);
|
|
if (mcolor == null) {
|
|
color = null;
|
|
break;
|
|
} else if (color == null)
|
|
color = mcolor;
|
|
else if (!color.equals(mcolor)) {
|
|
color = null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (color != null) {
|
|
builder.setColor(color);
|
|
builder.setColorized(true);
|
|
}
|
|
}
|
|
|
|
// Subtext should not be set, to show number of new messages
|
|
|
|
if (notify_private) {
|
|
Notification pub = builder.build();
|
|
builder
|
|
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
|
|
.setPublicVersion(pub);
|
|
}
|
|
|
|
if (notify_preview)
|
|
if (redacted)
|
|
builder.setContentText(context.getString(R.string.title_notification_redacted));
|
|
else {
|
|
DateFormat DTF = Helper.getDateTimeInstance(context, SimpleDateFormat.SHORT, SimpleDateFormat.SHORT);
|
|
StringBuilder sb = new StringBuilder();
|
|
for (EntityMessage message : messages) {
|
|
Address[] afrom = messageFrom.get(message.id);
|
|
String from = MessageHelper.formatAddresses(afrom, email_format, false);
|
|
sb.append("<strong>").append(Html.escapeHtml(from)).append("</strong>");
|
|
if (!TextUtils.isEmpty(message.subject))
|
|
sb.append(": ").append(Html.escapeHtml(message.subject));
|
|
sb.append(" ").append(Html.escapeHtml(DTF.format(message.received)));
|
|
sb.append("<br>");
|
|
}
|
|
|
|
// Wearables
|
|
builder.setContentText(title);
|
|
|
|
// Device
|
|
builder.setStyle(new NotificationCompat.BigTextStyle()
|
|
.bigText(HtmlHelper.fromHtml(sb.toString(), context))
|
|
.setSummaryText(title));
|
|
}
|
|
|
|
//builder.extend(new NotificationCompat.WearableExtender()
|
|
// .setDismissalId(BuildConfig.APPLICATION_ID));
|
|
|
|
notifications.add(builder);
|
|
}
|
|
|
|
if (notify_summary)
|
|
return notifications;
|
|
|
|
// Message notifications
|
|
for (int m = 0; m < messages.size() && m < MAX_NOTIFICATION_DISPLAY; m++) {
|
|
TupleMessageEx message = messages.get(m);
|
|
ContactInfo[] info = messageInfo.get(message.id);
|
|
|
|
// Build arguments
|
|
long id = (message.content ? message.id : -message.id);
|
|
Bundle args = new Bundle();
|
|
args.putLong("id", id);
|
|
|
|
// Build pending intents
|
|
Intent thread = new Intent(context, ActivityView.class);
|
|
thread.setAction("thread:" + message.id);
|
|
thread.putExtra("group", group);
|
|
thread.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
thread.putExtra("account", message.account);
|
|
thread.putExtra("folder", message.folder);
|
|
thread.putExtra("type", message.folderType);
|
|
thread.putExtra("thread", message.thread);
|
|
thread.putExtra("filter_archive", !EntityFolder.ARCHIVE.equals(message.folderType));
|
|
thread.putExtra("ignore", notify_remove);
|
|
PendingIntent piContent = PendingIntentCompat.getActivity(
|
|
context, ActivityView.PI_THREAD, thread, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
|
|
Intent ignore = new Intent(context, ServiceUI.class).setAction("ignore:" + message.id);
|
|
PendingIntent piIgnore = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_IGNORED, ignore, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
|
|
// Get channel name
|
|
String channelName = EntityAccount.getNotificationChannelId(0);
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && pro) {
|
|
NotificationChannel channel = null;
|
|
|
|
String channelId = message.getNotificationChannelId();
|
|
if (channelId != null)
|
|
channel = nm.getNotificationChannel(channelId);
|
|
|
|
if (channel == null)
|
|
channel = nm.getNotificationChannel(EntityFolder.getNotificationChannelId(message.folder));
|
|
|
|
if (channel == null) {
|
|
if (message.accountNotify)
|
|
channelName = EntityAccount.getNotificationChannelId(message.account);
|
|
} else
|
|
channelName = channel.getId();
|
|
}
|
|
|
|
String sortKey = String.format(Locale.ROOT, "%13d",
|
|
notify_newest_first ? (10000000000000L - message.received) : message.received);
|
|
|
|
NotificationCompat.Builder mbuilder =
|
|
new NotificationCompat.Builder(context, channelName)
|
|
.addExtras(args)
|
|
.setSmallIcon(R.drawable.baseline_mail_white_24)
|
|
.setContentIntent(piContent)
|
|
.setWhen(message.received)
|
|
.setShowWhen(true)
|
|
.setSortKey(sortKey)
|
|
.setDeleteIntent(piIgnore)
|
|
.setPriority(EntityMessage.PRIORITIY_HIGH.equals(message.importance)
|
|
? NotificationCompat.PRIORITY_HIGH
|
|
: NotificationCompat.PRIORITY_DEFAULT)
|
|
.setCategory(NotificationCompat.CATEGORY_EMAIL)
|
|
.setVisibility(notify_private
|
|
? NotificationCompat.VISIBILITY_PRIVATE
|
|
: NotificationCompat.VISIBILITY_PUBLIC)
|
|
.setOnlyAlertOnce(alert_once)
|
|
.setAllowSystemGeneratedContextualActions(false);
|
|
|
|
if (message.ui_silent) {
|
|
mbuilder.setSilent(true);
|
|
Log.i("Notify silent=" + message.id);
|
|
}
|
|
if (message.ui_local_only) {
|
|
mbuilder.setLocalOnly(true);
|
|
Log.i("Notify local=" + message.id);
|
|
}
|
|
|
|
if (notify_messaging) {
|
|
// https://developer.android.com/training/cars/messaging
|
|
String meName = MessageHelper.formatAddresses(message.to, email_format, false);
|
|
String youName = MessageHelper.formatAddresses(message.from, email_format, false);
|
|
|
|
// Names cannot be empty
|
|
if (TextUtils.isEmpty(meName))
|
|
meName = "-";
|
|
if (TextUtils.isEmpty(youName))
|
|
youName = "-";
|
|
|
|
Person.Builder me = new Person.Builder().setName(meName);
|
|
Person.Builder you = new Person.Builder().setName(youName);
|
|
|
|
if (info[0].hasPhoto())
|
|
you.setIcon(IconCompat.createWithBitmap(info[0].getPhotoBitmap()));
|
|
|
|
if (info[0].hasLookupUri())
|
|
you.setUri(info[0].getLookupUri().toString());
|
|
|
|
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(me.build());
|
|
|
|
if (!TextUtils.isEmpty(message.subject))
|
|
messagingStyle.setConversationTitle(message.subject);
|
|
|
|
messagingStyle.addMessage(
|
|
notify_preview && message.preview != null ? message.preview : "",
|
|
message.received,
|
|
you.build());
|
|
|
|
mbuilder.setStyle(messagingStyle);
|
|
}
|
|
|
|
if (notify_grouping && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
|
mbuilder
|
|
.setGroup(Long.toString(group))
|
|
.setGroupSummary(false)
|
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
|
setLightAndSound(mbuilder, light, sound);
|
|
|
|
Address[] afrom = messageFrom.get(message.id);
|
|
String from = MessageHelper.formatAddresses(afrom, email_format, false);
|
|
mbuilder.setContentTitle(from);
|
|
if (notify_subtext)
|
|
if (message.folderUnified && EntityFolder.INBOX.equals(message.folderType))
|
|
mbuilder.setSubText(message.accountName);
|
|
else
|
|
mbuilder.setSubText(message.accountName + " - " + message.getFolderName(context));
|
|
|
|
DB db = DB.getInstance(context);
|
|
|
|
if (message.content && notify_preview) {
|
|
// Android will truncate the text
|
|
String preview = message.preview;
|
|
if (notify_preview_all)
|
|
try {
|
|
File file = message.getFile(context);
|
|
preview = HtmlHelper.getFullText(file);
|
|
if (preview != null && preview.length() > MAX_PREVIEW)
|
|
preview = preview.substring(0, MAX_PREVIEW);
|
|
} catch (Throwable ex) {
|
|
Log.e(ex);
|
|
}
|
|
|
|
// Wearables
|
|
StringBuilder sb = new StringBuilder();
|
|
if (!TextUtils.isEmpty(message.subject))
|
|
sb.append(TextHelper.normalizeNotification(context, message.subject));
|
|
if (wearable_preview && !TextUtils.isEmpty(preview)) {
|
|
if (sb.length() > 0)
|
|
sb.append(" - ");
|
|
sb.append(TextHelper.normalizeNotification(context, preview));
|
|
}
|
|
if (sb.length() > 0)
|
|
mbuilder.setContentText(sb.toString());
|
|
|
|
// Device
|
|
if (!notify_messaging) {
|
|
StringBuilder sbm = new StringBuilder();
|
|
|
|
if (message.keywords != null && BuildConfig.DEBUG)
|
|
for (String keyword : message.keywords)
|
|
if (keyword.startsWith("!"))
|
|
sbm.append(Html.escapeHtml(keyword)).append(": ");
|
|
|
|
if (!TextUtils.isEmpty(message.subject))
|
|
sbm.append("<em>").append(Html.escapeHtml(message.subject)).append("</em>").append("<br>");
|
|
|
|
if (!TextUtils.isEmpty(preview))
|
|
sbm.append(Html.escapeHtml(preview));
|
|
|
|
if (sbm.length() > 0) {
|
|
NotificationCompat.BigTextStyle bigText = new NotificationCompat.BigTextStyle()
|
|
.bigText(HtmlHelper.fromHtml(sbm.toString(), context));
|
|
if (!TextUtils.isEmpty(message.subject))
|
|
bigText.setSummaryText(message.subject);
|
|
|
|
mbuilder.setStyle(bigText);
|
|
}
|
|
}
|
|
} else {
|
|
if (!TextUtils.isEmpty(message.subject))
|
|
mbuilder.setContentText(TextHelper.normalizeNotification(context, message.subject));
|
|
}
|
|
|
|
if (info[0].hasPhoto())
|
|
mbuilder.setLargeIcon(info[0].getPhotoBitmap());
|
|
|
|
if (info[0].hasLookupUri()) {
|
|
Person.Builder you = new Person.Builder()
|
|
.setUri(info[0].getLookupUri().toString());
|
|
mbuilder.addPerson(you.build());
|
|
}
|
|
|
|
if (pro) {
|
|
Integer color = getColor(message);
|
|
if (color != null) {
|
|
mbuilder.setColor(color);
|
|
mbuilder.setColorized(true);
|
|
}
|
|
}
|
|
|
|
// Notification actions
|
|
|
|
List<NotificationCompat.Action> wactions = new ArrayList<>();
|
|
|
|
if (notify_trash &&
|
|
!delete_notification &&
|
|
message.accountProtocol == EntityAccount.TYPE_IMAP && perform_expunge) {
|
|
EntityFolder folder = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
|
|
if (folder != null && !folder.id.equals(message.folder)) {
|
|
Intent trash = new Intent(context, ServiceUI.class)
|
|
.setAction("trash:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piTrash = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_TRASH, trash, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionTrash = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_delete_24,
|
|
context.getString(R.string.title_advanced_notify_action_trash),
|
|
piTrash)
|
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_DELETE)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
mbuilder.addAction(actionTrash.build());
|
|
|
|
wactions.add(actionTrash.build());
|
|
}
|
|
} else if (notify_trash &&
|
|
(delete_notification ||
|
|
(message.accountProtocol == EntityAccount.TYPE_POP && message.accountLeaveDeleted) ||
|
|
(message.accountProtocol == EntityAccount.TYPE_IMAP && !perform_expunge))) {
|
|
Intent delete = new Intent(context, ServiceUI.class)
|
|
.setAction("delete:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piDelete = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_DELETE, delete, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionDelete = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_delete_forever_24,
|
|
context.getString(R.string.title_delete_permanently),
|
|
piDelete)
|
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_DELETE)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
mbuilder.addAction(actionDelete.build());
|
|
|
|
wactions.add(actionDelete.build());
|
|
}
|
|
|
|
if (notify_junk &&
|
|
message.accountProtocol == EntityAccount.TYPE_IMAP) {
|
|
EntityFolder folder = db.folder().getFolderByType(message.account, EntityFolder.JUNK);
|
|
if (folder != null && !folder.id.equals(message.folder)) {
|
|
Intent junk = new Intent(context, ServiceUI.class)
|
|
.setAction("junk:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piJunk = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_JUNK, junk, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionJunk = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_report_24,
|
|
context.getString(R.string.title_advanced_notify_action_junk),
|
|
piJunk)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
mbuilder.addAction(actionJunk.build());
|
|
|
|
wactions.add(actionJunk.build());
|
|
}
|
|
}
|
|
|
|
if (notify_archive &&
|
|
message.accountProtocol == EntityAccount.TYPE_IMAP) {
|
|
EntityFolder folder = db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE);
|
|
if (folder != null && !folder.id.equals(message.folder)) {
|
|
Intent archive = new Intent(context, ServiceUI.class)
|
|
.setAction("archive:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piArchive = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_ARCHIVE, archive, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionArchive = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_archive_24,
|
|
context.getString(R.string.title_advanced_notify_action_archive),
|
|
piArchive)
|
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_ARCHIVE)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
mbuilder.addAction(actionArchive.build());
|
|
|
|
wactions.add(actionArchive.build());
|
|
}
|
|
}
|
|
|
|
if (notify_move &&
|
|
message.accountProtocol == EntityAccount.TYPE_IMAP) {
|
|
EntityAccount account = db.account().getAccount(message.account);
|
|
if (account != null && account.move_to != null) {
|
|
EntityFolder folder = db.folder().getFolder(account.move_to);
|
|
if (folder != null && !folder.id.equals(message.folder)) {
|
|
Intent move = new Intent(context, ServiceUI.class)
|
|
.setAction("move:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piMove = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_MOVE, move, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionMove = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_folder_24,
|
|
folder.getDisplayName(context),
|
|
piMove)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
mbuilder.addAction(actionMove.build());
|
|
|
|
wactions.add(actionMove.build());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (notify_reply && message.content) {
|
|
List<TupleIdentityEx> identities = db.identity().getComposableIdentities(message.account);
|
|
if (identities != null && identities.size() > 0) {
|
|
Intent reply = new Intent(context, ActivityCompose.class)
|
|
.setAction("reply:" + message.id)
|
|
.putExtra("action", "reply")
|
|
.putExtra("reference", message.id)
|
|
.putExtra("group", group);
|
|
reply.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
PendingIntent piReply = PendingIntentCompat.getActivity(
|
|
context, ActivityCompose.PI_REPLY, reply, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionReply = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_reply_24,
|
|
context.getString(R.string.title_advanced_notify_action_reply),
|
|
piReply)
|
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
|
.setShowsUserInterface(true)
|
|
.setAllowGeneratedReplies(false);
|
|
mbuilder.addAction(actionReply.build());
|
|
}
|
|
}
|
|
|
|
if (message.content &&
|
|
message.identity != null &&
|
|
message.from != null && message.from.length > 0 &&
|
|
db.folder().getOutbox() != null) {
|
|
Intent reply = new Intent(context, ServiceUI.class)
|
|
.setPackage(BuildConfig.APPLICATION_ID)
|
|
.setAction("reply:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piReply = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_REPLY_DIRECT, reply, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
|
NotificationCompat.Action.Builder actionReply = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_reply_24,
|
|
context.getString(R.string.title_advanced_notify_action_reply_direct),
|
|
piReply)
|
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
RemoteInput.Builder input = new RemoteInput.Builder("text")
|
|
.setLabel(context.getString(R.string.title_advanced_notify_action_reply));
|
|
actionReply.addRemoteInput(input.build())
|
|
.setAllowGeneratedReplies(false);
|
|
if (notify_reply_direct) {
|
|
mbuilder.addAction(actionReply.build());
|
|
wactions.add(actionReply.build());
|
|
} else
|
|
mbuilder.addInvisibleAction(actionReply.build());
|
|
}
|
|
|
|
if (notify_flag) {
|
|
Intent flag = new Intent(context, ServiceUI.class)
|
|
.setAction("flag:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piFlag = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_FLAG, flag, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionFlag = new NotificationCompat.Action.Builder(
|
|
R.drawable.baseline_star_24,
|
|
context.getString(R.string.title_advanced_notify_action_flag),
|
|
piFlag)
|
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_THUMBS_UP)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
mbuilder.addAction(actionFlag.build());
|
|
|
|
wactions.add(actionFlag.build());
|
|
}
|
|
|
|
if (true) {
|
|
Intent seen = new Intent(context, ServiceUI.class)
|
|
.setAction("seen:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piSeen = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_SEEN, seen, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionSeen = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_visibility_24,
|
|
context.getString(R.string.title_advanced_notify_action_seen),
|
|
piSeen)
|
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
if (notify_seen) {
|
|
mbuilder.addAction(actionSeen.build());
|
|
wactions.add(actionSeen.build());
|
|
} else
|
|
mbuilder.addInvisibleAction(actionSeen.build());
|
|
}
|
|
|
|
if (notify_hide) {
|
|
Intent hide = new Intent(context, ServiceUI.class)
|
|
.setAction("hide:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piHide = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_HIDE, hide, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionHide = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_visibility_off_24,
|
|
context.getString(R.string.title_advanced_notify_action_hide),
|
|
piHide)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
mbuilder.addAction(actionHide.build());
|
|
|
|
wactions.add(actionHide.build());
|
|
}
|
|
|
|
if (notify_snooze) {
|
|
Intent snooze = new Intent(context, ServiceUI.class)
|
|
.setAction("snooze:" + message.id)
|
|
.putExtra("group", group);
|
|
PendingIntent piSnooze = PendingIntentCompat.getService(
|
|
context, ServiceUI.PI_SNOOZE, snooze, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
NotificationCompat.Action.Builder actionSnooze = new NotificationCompat.Action.Builder(
|
|
R.drawable.twotone_timelapse_24,
|
|
context.getString(R.string.title_advanced_notify_action_snooze),
|
|
piSnooze)
|
|
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MUTE)
|
|
.setShowsUserInterface(false)
|
|
.setAllowGeneratedReplies(false);
|
|
mbuilder.addAction(actionSnooze.build());
|
|
|
|
wactions.add(actionSnooze.build());
|
|
}
|
|
|
|
// https://developer.android.com/training/wearables/notifications
|
|
// https://developer.android.com/reference/androidx/core/app/NotificationCompat.Action.WearableExtender
|
|
mbuilder.extend(new NotificationCompat.WearableExtender()
|
|
.addActions(wactions)
|
|
.setDismissalId(BuildConfig.APPLICATION_ID + ":" + id)
|
|
/* .setBridgeTag(id < 0 ? "header" : "body") */);
|
|
|
|
// https://developer.android.com/reference/androidx/core/app/NotificationCompat.CarExtender
|
|
mbuilder.extend(new NotificationCompat.CarExtender());
|
|
|
|
notifications.add(mbuilder);
|
|
}
|
|
|
|
return notifications;
|
|
}
|
|
|
|
private static Integer getColor(TupleMessageEx message) {
|
|
if (!message.folderUnified && message.folderColor != null)
|
|
return message.folderColor;
|
|
return message.accountColor;
|
|
}
|
|
|
|
private static void setLightAndSound(NotificationCompat.Builder builder, boolean light, String sound) {
|
|
int def = 0;
|
|
|
|
if (light) {
|
|
def |= DEFAULT_LIGHTS;
|
|
Log.i("Notify light enabled");
|
|
}
|
|
|
|
if (!"".equals(sound)) {
|
|
// Not silent sound
|
|
Uri uri = (sound == null ? null : Uri.parse(sound));
|
|
if (uri != null && !"content".equals(uri.getScheme()))
|
|
uri = null;
|
|
Log.i("Notify sound=" + uri);
|
|
|
|
if (uri == null)
|
|
def |= DEFAULT_SOUND;
|
|
else
|
|
builder.setSound(uri);
|
|
}
|
|
|
|
builder.setDefaults(def);
|
|
}
|
|
|
|
static class NotificationData {
|
|
private Map<Long, List<Long>> groupNotifying = new HashMap<>();
|
|
|
|
NotificationData(Context context) {
|
|
// Get existing notifications
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
|
try {
|
|
NotificationManager nm = Helper.getSystemService(context, NotificationManager.class);
|
|
for (StatusBarNotification sbn : nm.getActiveNotifications()) {
|
|
String tag = sbn.getTag();
|
|
if (tag != null && tag.startsWith("unseen.")) {
|
|
String[] p = tag.split(("\\."));
|
|
long group = Long.parseLong(p[1]);
|
|
long id = sbn.getNotification().extras.getLong("id", 0);
|
|
|
|
if (!groupNotifying.containsKey(group))
|
|
groupNotifying.put(group, new ArrayList<>());
|
|
|
|
if (id > 0) {
|
|
EntityLog.log(context, EntityLog.Type.Notification, null, null, id,
|
|
"Notify restore " + tag + " id=" + id);
|
|
groupNotifying.get(group).add(id);
|
|
}
|
|
}
|
|
}
|
|
} catch (Throwable ex) {
|
|
Log.w(ex);
|
|
/*
|
|
java.lang.RuntimeException: Unable to create service eu.faircode.email.ServiceSynchronize: java.lang.NullPointerException: Attempt to invoke virtual method 'java.util.List android.content.pm.ParceledListSlice.getList()' on a null object reference
|
|
at android.app.ActivityThread.handleCreateService(ActivityThread.java:2944)
|
|
at android.app.ActivityThread.access$1900(ActivityThread.java:154)
|
|
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1474)
|
|
at android.os.Handler.dispatchMessage(Handler.java:102)
|
|
at android.os.Looper.loop(Looper.java:234)
|
|
at android.app.ActivityThread.main(ActivityThread.java:5526)
|
|
*/
|
|
}
|
|
}
|
|
}
|
|
}
|