mirror of
https://github.com/deltachat/deltachat-android.git
synced 2025-10-03 09:49:21 +02:00
849 lines
39 KiB
Java
849 lines
39 KiB
Java
package org.thoughtcrime.securesms.notifications;
|
|
|
|
import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PRIVATE_TAG;
|
|
|
|
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.graphics.Bitmap;
|
|
import android.graphics.Color;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.media.AudioAttributes;
|
|
import android.media.RingtoneManager;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.WorkerThread;
|
|
import androidx.core.app.NotificationCompat;
|
|
import androidx.core.app.NotificationManagerCompat;
|
|
import androidx.core.app.RemoteInput;
|
|
import androidx.core.app.TaskStackBuilder;
|
|
|
|
import com.b44t.messenger.DcChat;
|
|
import com.b44t.messenger.DcContact;
|
|
import com.b44t.messenger.DcContext;
|
|
import com.b44t.messenger.DcMsg;
|
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
|
|
import org.json.JSONObject;
|
|
import org.thoughtcrime.securesms.ApplicationContext;
|
|
import org.thoughtcrime.securesms.ConversationActivity;
|
|
import org.thoughtcrime.securesms.ConversationListActivity;
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
|
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
|
import org.thoughtcrime.securesms.util.IntentUtils;
|
|
import org.thoughtcrime.securesms.util.JsonUtils;
|
|
import org.thoughtcrime.securesms.util.Pair;
|
|
import org.thoughtcrime.securesms.util.Prefs;
|
|
import org.thoughtcrime.securesms.util.Util;
|
|
import org.thoughtcrime.securesms.videochat.VideochatActivity;
|
|
|
|
import java.math.BigInteger;
|
|
import java.security.MessageDigest;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
public class NotificationCenter {
|
|
private static final String TAG = NotificationCenter.class.getSimpleName();
|
|
@NonNull private final ApplicationContext context;
|
|
private volatile ChatData visibleChat = null;
|
|
private volatile Pair<Integer, Integer> visibleWebxdc = null;
|
|
private volatile long lastAudibleNotification = 0;
|
|
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
|
|
|
|
// Map<accountId, Map<chatId, lines>, contains the last lines of each chat for each account
|
|
private final HashMap<Integer, HashMap<Integer, ArrayList<String>>> inboxes = new HashMap<>();
|
|
|
|
public NotificationCenter(Context context) {
|
|
this.context = ApplicationContext.getInstance(context);
|
|
}
|
|
|
|
private @Nullable Uri effectiveSound(ChatData chatData) { // chatData=null: return app-global setting
|
|
if (chatData == null) {
|
|
chatData = new ChatData(0, 0);
|
|
}
|
|
@Nullable Uri chatRingtone = Prefs.getChatRingtone(context, chatData.accountId, chatData.chatId);
|
|
if (chatRingtone!=null) {
|
|
return chatRingtone;
|
|
} else {
|
|
@NonNull Uri appDefaultRingtone = Prefs.getNotificationRingtone(context);
|
|
if (!TextUtils.isEmpty(appDefaultRingtone.toString())) {
|
|
return appDefaultRingtone;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private boolean effectiveVibrate(ChatData chatData) { // chatData=null: return app-global setting
|
|
if (chatData == null) {
|
|
chatData = new ChatData(0, 0);
|
|
}
|
|
Prefs.VibrateState vibrate = Prefs.getChatVibrate(context, chatData.accountId, chatData.chatId);
|
|
if (vibrate == Prefs.VibrateState.ENABLED) {
|
|
return true;
|
|
} else if (vibrate == Prefs.VibrateState.DISABLED) {
|
|
return false;
|
|
}
|
|
return Prefs.isNotificationVibrateEnabled(context);
|
|
}
|
|
|
|
private boolean requiresIndependentChannel(ChatData chatData) {
|
|
if (chatData == null) {
|
|
chatData = new ChatData(0, 0);
|
|
}
|
|
return Prefs.getChatRingtone(context, chatData.accountId, chatData.chatId) != null
|
|
|| Prefs.getChatVibrate(context, chatData.accountId, chatData.chatId) != Prefs.VibrateState.DEFAULT;
|
|
}
|
|
|
|
private int getLedArgb(String ledColor) {
|
|
int argb;
|
|
try {
|
|
argb = Color.parseColor(ledColor);
|
|
}
|
|
catch (Exception e) {
|
|
argb = Color.rgb(0xFF, 0xFF, 0xFF);
|
|
}
|
|
return argb;
|
|
}
|
|
|
|
private PendingIntent getOpenChatlistIntent(int accountId) {
|
|
Intent intent = new Intent(context, ConversationListActivity.class);
|
|
intent.putExtra(ConversationListActivity.ACCOUNT_ID_EXTRA, accountId);
|
|
intent.putExtra(ConversationListActivity.CLEAR_NOTIFICATIONS, true);
|
|
intent.setData(Uri.parse("custom://"+accountId));
|
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
|
|
}
|
|
|
|
private PendingIntent getOpenChatIntent(ChatData chatData) {
|
|
Intent intent = new Intent(context, ConversationActivity.class);
|
|
intent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, chatData.accountId);
|
|
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatData.chatId);
|
|
intent.setData(Uri.parse("custom://"+chatData.accountId+"."+chatData.chatId));
|
|
return TaskStackBuilder.create(context)
|
|
.addNextIntentWithParentStack(intent)
|
|
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
|
|
}
|
|
|
|
private PendingIntent getRemoteReplyIntent(ChatData chatData, int msgId) {
|
|
Intent intent = new Intent(RemoteReplyReceiver.REPLY_ACTION);
|
|
intent.setClass(context, RemoteReplyReceiver.class);
|
|
intent.setData(Uri.parse("custom://"+chatData.accountId+"."+chatData.chatId));
|
|
intent.putExtra(RemoteReplyReceiver.ACCOUNT_ID_EXTRA, chatData.accountId);
|
|
intent.putExtra(RemoteReplyReceiver.CHAT_ID_EXTRA, chatData.chatId);
|
|
intent.putExtra(RemoteReplyReceiver.MSG_ID_EXTRA, msgId);
|
|
intent.setPackage(context.getPackageName());
|
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
|
|
}
|
|
|
|
private PendingIntent getMarkAsReadIntent(ChatData chatData, int msgId, boolean markNoticed) {
|
|
Intent intent = new Intent(markNoticed? MarkReadReceiver.MARK_NOTICED_ACTION : MarkReadReceiver.CANCEL_ACTION);
|
|
intent.setClass(context, MarkReadReceiver.class);
|
|
intent.setData(Uri.parse("custom://"+chatData.accountId+"."+chatData.chatId));
|
|
intent.putExtra(MarkReadReceiver.ACCOUNT_ID_EXTRA, chatData.accountId);
|
|
intent.putExtra(MarkReadReceiver.CHAT_ID_EXTRA, chatData.chatId);
|
|
intent.putExtra(MarkReadReceiver.MSG_ID_EXTRA, msgId);
|
|
intent.setPackage(context.getPackageName());
|
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
|
|
}
|
|
|
|
private PendingIntent getAnswerIntent(ChatData chatData, int callId, String payload) {
|
|
String hash = "#offer=" + payload;
|
|
Intent intent = new Intent(context, VideochatActivity.class);
|
|
intent.setAction(Intent.ACTION_VIEW);
|
|
intent.putExtra(VideochatActivity.EXTRA_ACCOUNT_ID, chatData.accountId);
|
|
intent.putExtra(VideochatActivity.EXTRA_CHAT_ID, chatData.chatId);
|
|
intent.putExtra(VideochatActivity.EXTRA_CALL_ID, callId);
|
|
intent.putExtra(VideochatActivity.EXTRA_HASH, hash);
|
|
intent.setPackage(context.getPackageName());
|
|
return TaskStackBuilder.create(context)
|
|
.addNextIntentWithParentStack(intent)
|
|
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
|
|
}
|
|
|
|
private PendingIntent getDeclineCallIntent(ChatData chatData, int callId) {
|
|
Intent intent = new Intent(DeclineCallReceiver.DECLINE_ACTION);
|
|
intent.setClass(context, DeclineCallReceiver.class);
|
|
intent.putExtra(DeclineCallReceiver.ACCOUNT_ID_EXTRA, chatData.accountId);
|
|
intent.putExtra(DeclineCallReceiver.CALL_ID_EXTRA, callId);
|
|
intent.setPackage(context.getPackageName());
|
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
|
|
}
|
|
|
|
// Groups and Notification channel groups
|
|
// --------------------------------------------------------------------------------------------
|
|
|
|
// this is just to further organize the appearance of channels in the settings UI
|
|
private static final String CH_GRP_MSG = "chgrp_msg";
|
|
|
|
// this is to group together notifications as such, maybe including a summary,
|
|
// see https://developer.android.com/training/notify-user/group.html
|
|
private static final String GRP_MSG = "grp_msg";
|
|
|
|
|
|
// Notification IDs
|
|
// --------------------------------------------------------------------------------------------
|
|
|
|
public static final int ID_PERMANENT = 1;
|
|
public static final int ID_MSG_SUMMARY = 2;
|
|
public static final int ID_GENERIC = 3;
|
|
public static final int ID_FETCH = 4;
|
|
public static final int ID_MSG_OFFSET = 0; // msgId is added - as msgId start at 10, there are no conflicts with lower numbers
|
|
|
|
|
|
// Notification channels
|
|
// --------------------------------------------------------------------------------------------
|
|
|
|
// Overview:
|
|
// - since SDK 26 (Oreo), a NotificationChannel is a MUST for notifications
|
|
// - NotificationChannels are defined by a channelId
|
|
// and its user-editable settings have a higher precedence as the Notification.Builder setting
|
|
// - once created, NotificationChannels cannot be modified programmatically
|
|
// - NotificationChannels can be deleted, however, on re-creation with the same id,
|
|
// it becomes un-deleted with the old user-defined settings
|
|
//
|
|
// How we use Notification channel:
|
|
// - We include the delta-chat-notifications settings into the name of the channelId
|
|
// - The chatId is included only, if there are separate sound- or vibration-settings for a chat
|
|
// - This way, we have stable and few channelIds and the user
|
|
// can edit the notifications in Delta Chat as well as in the system
|
|
|
|
// channelIds: CH_MSG_* are used here, the other ones from outside (defined here to have some overview)
|
|
public static final String CH_MSG_PREFIX = "ch_msg";
|
|
public static final String CH_MSG_VERSION = "5";
|
|
public static final String CH_PERMANENT = "dc_foreground_notification_ch";
|
|
public static final String CH_GENERIC = "ch_generic";
|
|
public static final String CH_CALLS_PREFIX = "call_chan";
|
|
|
|
private boolean notificationChannelsSupported() {
|
|
return Build.VERSION.SDK_INT >= 26;
|
|
}
|
|
|
|
// full name is "ch_msgV_HASH" or "ch_msgV_HASH.ACCOUNTID.CHATID"
|
|
private String computeChannelId(String ledColor, boolean vibrate, @Nullable Uri ringtone, ChatData chatData) {
|
|
String channelId = CH_MSG_PREFIX;
|
|
try {
|
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
md.update(ledColor.getBytes());
|
|
md.update(vibrate ? (byte) 1 : (byte) 0);
|
|
md.update((ringtone != null ? ringtone.toString() : "").getBytes());
|
|
String hash = String.format("%X", new BigInteger(1, md.digest())).substring(0, 16);
|
|
|
|
channelId = CH_MSG_PREFIX + CH_MSG_VERSION + "_" + hash;
|
|
if (chatData != null) {
|
|
channelId += String.format(".%d.%d", chatData.accountId, chatData.chatId);
|
|
}
|
|
|
|
} catch(Exception e) {
|
|
Log.e(TAG, e.toString());
|
|
}
|
|
return channelId;
|
|
}
|
|
|
|
// return ChatData(ACCOUNTID, CHATID) from "ch_msgV_HASH.ACCOUNTID.CHATID" or null
|
|
private ChatData parseNotificationChannelChat(String channelId) {
|
|
try {
|
|
int point = channelId.lastIndexOf(".");
|
|
if (point>0) {
|
|
int chatId = Integer.parseInt(channelId.substring(point + 1));
|
|
channelId = channelId.substring(0, point);
|
|
point = channelId.lastIndexOf(".");
|
|
if (point>0) {
|
|
int accountId = Integer.parseInt(channelId.substring(point + 1));
|
|
return new ChatData(accountId, chatId);
|
|
}
|
|
}
|
|
} catch(Exception ignored) { }
|
|
return null;
|
|
}
|
|
|
|
private String getNotificationChannelGroup(NotificationManagerCompat notificationManager) {
|
|
if (notificationChannelsSupported() && notificationManager.getNotificationChannelGroup(CH_GRP_MSG) == null) {
|
|
NotificationChannelGroup chGrp = new NotificationChannelGroup(CH_GRP_MSG, context.getString(R.string.pref_chats));
|
|
notificationManager.createNotificationChannelGroup(chGrp);
|
|
}
|
|
return CH_GRP_MSG;
|
|
}
|
|
|
|
private String getNotificationChannel(NotificationManagerCompat notificationManager, ChatData chatData, DcChat dcChat) {
|
|
String channelId = CH_MSG_PREFIX;
|
|
|
|
if (notificationChannelsSupported()) {
|
|
try {
|
|
// get all values we'll use as settings for the NotificationChannel
|
|
String ledColor = Prefs.getNotificationLedColor(context);
|
|
boolean defaultVibrate = effectiveVibrate(chatData);
|
|
@Nullable Uri ringtone = effectiveSound(chatData);
|
|
boolean isIndependent = requiresIndependentChannel(chatData);
|
|
|
|
// get channel id from these settings
|
|
channelId = computeChannelId(ledColor, defaultVibrate, ringtone, isIndependent? chatData : null);
|
|
|
|
// user-visible name of the channel -
|
|
// we just use the name of the chat or "Default"
|
|
// (the name is shown in the context of the group "Chats" - that should be enough context)
|
|
String name = context.getString(R.string.def);
|
|
if (isIndependent) {
|
|
name = dcChat.getName();
|
|
}
|
|
|
|
// check if there is already a channel with the given name
|
|
List<NotificationChannel> channels = notificationManager.getNotificationChannels();
|
|
boolean channelExists = false;
|
|
for (int i = 0; i < channels.size(); i++) {
|
|
String currChannelId = channels.get(i).getId();
|
|
if (currChannelId.startsWith(CH_MSG_PREFIX)) {
|
|
// this is one of the message channels handled here ...
|
|
if (currChannelId.equals(channelId)) {
|
|
// ... this is the actually required channel, fine :)
|
|
// update the name to reflect localize changes and chat renames
|
|
channelExists = true;
|
|
channels.get(i).setName(name);
|
|
} else {
|
|
// ... another message channel, delete if it is not in use.
|
|
ChatData currChat = parseNotificationChannelChat(currChannelId);
|
|
if (!currChannelId.equals(computeChannelId(ledColor, effectiveVibrate(currChat), effectiveSound(currChat), currChat))) {
|
|
notificationManager.deleteNotificationChannel(currChannelId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// create a channel with the given settings;
|
|
// we cannot change the settings, however, this is handled by using different values for chId
|
|
if(!channelExists) {
|
|
NotificationChannel channel = new NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_HIGH);
|
|
channel.setDescription("Informs about new messages.");
|
|
channel.setGroup(getNotificationChannelGroup(notificationManager));
|
|
channel.enableVibration(defaultVibrate);
|
|
channel.setShowBadge(true);
|
|
|
|
if (!ledColor.equals("none")) {
|
|
channel.enableLights(true);
|
|
channel.setLightColor(getLedArgb(ledColor));
|
|
} else {
|
|
channel.enableLights(false);
|
|
}
|
|
|
|
if (ringtone != null && !TextUtils.isEmpty(ringtone.toString())) {
|
|
channel.setSound(ringtone,
|
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
|
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
|
|
.build());
|
|
} else {
|
|
channel.setSound(null, null);
|
|
}
|
|
|
|
notificationManager.createNotificationChannel(channel);
|
|
}
|
|
}
|
|
catch(Exception e) {
|
|
Log.e(TAG, "Error in getNotificationChannel()", e);
|
|
}
|
|
}
|
|
|
|
return channelId;
|
|
}
|
|
|
|
private String getCallNotificationChannel(NotificationManagerCompat notificationManager, ChatData chatData, String name) {
|
|
String channelId = CH_CALLS_PREFIX + "-" + chatData.accountId + "-"+ chatData.chatId;
|
|
|
|
if (notificationChannelsSupported()) {
|
|
try {
|
|
name = "(calls) " + name;
|
|
|
|
// check if there is already a channel with the given name
|
|
List<NotificationChannel> channels = notificationManager.getNotificationChannels();
|
|
boolean channelExists = false;
|
|
for (int i = 0; i < channels.size(); i++) {
|
|
String currChannelId = channels.get(i).getId();
|
|
if (currChannelId.startsWith(CH_CALLS_PREFIX)) {
|
|
// this is one of the calls channels handled here ...
|
|
if (currChannelId.equals(channelId)) {
|
|
// ... this is the actually required channel, fine :)
|
|
// update the name to reflect localize changes and chat renames
|
|
channelExists = true;
|
|
channels.get(i).setName(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// create a the channel
|
|
if(!channelExists) {
|
|
NotificationChannel channel = new NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_HIGH);
|
|
channel.setDescription("Informs about incoming calls.");
|
|
channel.setShowBadge(true);
|
|
|
|
Uri ringtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
|
|
channel.setSound(ringtone,
|
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
|
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
.build());
|
|
notificationManager.createNotificationChannel(channel);
|
|
}
|
|
} catch(Exception e) {
|
|
Log.e(TAG, "Error in getCallNotificationChannel()", e);
|
|
}
|
|
}
|
|
|
|
return channelId;
|
|
}
|
|
|
|
|
|
// add notifications & co.
|
|
// --------------------------------------------------------------------------------------------
|
|
|
|
public void notifyCall(int accId, int callId, String payload) {
|
|
Util.runOnAnyBackgroundThread(() -> {
|
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
|
DcContext dcContext = context.dcAccounts.getAccount(accId);
|
|
int chatId = dcContext.getMsg(callId).getChatId();
|
|
DcChat dcChat = dcContext.getChat(chatId);
|
|
String name = dcChat.getName();
|
|
ChatData chatData = new ChatData(accId, chatId);
|
|
String notificationChannel = getCallNotificationChannel(notificationManager, chatData, name);
|
|
|
|
PendingIntent declineCallIntent = getDeclineCallIntent(chatData, callId);
|
|
|
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannel)
|
|
.setSmallIcon(R.drawable.icon_notification)
|
|
.setColor(context.getResources().getColor(R.color.delta_primary))
|
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
.setOngoing(true)
|
|
.setOnlyAlertOnce(false)
|
|
.setTicker(name)
|
|
.setContentTitle(name)
|
|
.setContentText("Incoming Call")
|
|
.setDeleteIntent(declineCallIntent);
|
|
|
|
builder.addAction(
|
|
new NotificationCompat.Action.Builder(
|
|
R.drawable.baseline_call_end_24,
|
|
context.getString(R.string.end_call),
|
|
declineCallIntent).build());
|
|
|
|
builder.addAction(
|
|
new NotificationCompat.Action.Builder(
|
|
R.drawable.baseline_call_24,
|
|
context.getString(R.string.answer_call),
|
|
getAnswerIntent(chatData, callId, payload)).build());
|
|
|
|
Bitmap bitmap = getAvatar(dcChat);
|
|
if (bitmap != null) {
|
|
builder.setLargeIcon(bitmap);
|
|
}
|
|
|
|
Notification notif = builder.build();
|
|
notif.flags = notif.flags | Notification.FLAG_INSISTENT;
|
|
// add notification, we use one notification per chat,
|
|
// esp. older android are not that great at grouping
|
|
try {
|
|
notificationManager.notify("call-" + accId, callId, notif);
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "cannot add notification", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
public void notifyMessage(int accountId, int chatId, int msgId) {
|
|
Util.runOnAnyBackgroundThread(() -> {
|
|
DcContext dcContext = context.dcAccounts.getAccount(accountId);
|
|
DcChat dcChat = dcContext.getChat(chatId);
|
|
|
|
DcMsg dcMsg = dcContext.getMsg(msgId);
|
|
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
|
|
|
|
String shortLine = privacy.isDisplayMessage()? dcMsg.getSummarytext(2000) : context.getString(R.string.notify_new_message);
|
|
if (dcChat.isMultiUser() && privacy.isDisplayContact()) {
|
|
shortLine = dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + shortLine;
|
|
}
|
|
String tickerLine = shortLine;
|
|
if (!dcChat.isMultiUser() && privacy.isDisplayContact()) {
|
|
tickerLine = dcMsg.getSenderName(dcContext.getContact(dcMsg.getFromId())) + ": " + tickerLine;
|
|
|
|
if (dcMsg.getOverrideSenderName() != null) {
|
|
// There is an "overridden" display name on the message, so, we need to prepend the display name to the message,
|
|
// i.e. set the shortLine to be the same as the tickerLine.
|
|
shortLine = tickerLine;
|
|
}
|
|
}
|
|
|
|
DcMsg quotedMsg = dcMsg.getQuotedMsg();
|
|
boolean isMention = dcChat.isMultiUser() && quotedMsg != null && quotedMsg.isOutgoing();
|
|
|
|
maybeAddNotification(accountId, dcChat, msgId, shortLine, tickerLine, true, isMention);
|
|
});
|
|
}
|
|
|
|
public void notifyReaction(int accountId, int contactId, int msgId, String reaction) {
|
|
Util.runOnAnyBackgroundThread(() -> {
|
|
DcContext dcContext = context.dcAccounts.getAccount(accountId);
|
|
DcMsg dcMsg = dcContext.getMsg(msgId);
|
|
|
|
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
|
|
if (!privacy.isDisplayContact() || !privacy.isDisplayMessage()) {
|
|
return; // showing "New Message" is wrong and showing "New Reaction" is already content. just do nothing.
|
|
}
|
|
|
|
DcContact sender = dcContext.getContact(contactId);
|
|
String shortLine = context.getString(R.string.reaction_by_other, sender.getDisplayName(), reaction, dcMsg.getSummarytext(2000));
|
|
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
|
|
maybeAddNotification(accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser());
|
|
});
|
|
}
|
|
|
|
public void notifyWebxdc(int accountId, int contactId, int msgId, String text) {
|
|
Util.runOnAnyBackgroundThread(() -> {
|
|
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
|
|
if (!privacy.isDisplayContact() || !privacy.isDisplayMessage()) {
|
|
return; // showing "New Message" is wrong, just do nothing.
|
|
}
|
|
|
|
DcContext dcContext = context.dcAccounts.getAccount(accountId);
|
|
DcMsg dcMsg = dcContext.getMsg(msgId);
|
|
DcMsg parentMsg;
|
|
if(dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) {
|
|
parentMsg = dcMsg;
|
|
} else { // info message, get parent xdc
|
|
parentMsg = dcMsg.getParent() != null? dcMsg.getParent() : dcMsg;
|
|
}
|
|
|
|
if (Util.equals(visibleWebxdc, new Pair<>(accountId, parentMsg.getId()))) {
|
|
return; // do not notify if the app is already open
|
|
}
|
|
|
|
JSONObject info = parentMsg.getWebxdcInfo();
|
|
final String name = JsonUtils.optString(info, "name");
|
|
String shortLine = name.isEmpty()? text : (name + ": " + text);
|
|
DcChat dcChat = dcContext.getChat(dcMsg.getChatId());
|
|
maybeAddNotification(accountId, dcChat, msgId, shortLine, shortLine, false, dcChat.isMultiUser());
|
|
});
|
|
}
|
|
|
|
@WorkerThread
|
|
private void maybeAddNotification(int accountId, DcChat dcChat, int msgId, String shortLine, String tickerLine, boolean playInChatSound, boolean isMention) {
|
|
|
|
DcContext dcContext = context.dcAccounts.getAccount(accountId);
|
|
int chatId = dcChat.getId();
|
|
ChatData chatData = new ChatData(accountId, chatId);
|
|
isMention = isMention && dcContext.isMentionsEnabled();
|
|
|
|
if (dcContext.isMuted() || (!isMention && dcChat.isMuted())) {
|
|
return;
|
|
}
|
|
|
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationManager.areNotificationsEnabled()) {
|
|
return;
|
|
}
|
|
|
|
if (Util.equals(visibleChat, chatData)) {
|
|
if (playInChatSound && Prefs.isInChatNotifications(context)) {
|
|
InChatSounds.getInstance(context).playIncomingSound();
|
|
}
|
|
return;
|
|
}
|
|
|
|
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
|
|
long now = System.currentTimeMillis();
|
|
boolean signal = (now - lastAudibleNotification) > MIN_AUDIBLE_PERIOD_MILLIS;
|
|
if (signal) {
|
|
lastAudibleNotification = now;
|
|
}
|
|
|
|
// create a basic notification
|
|
// even without a name or message displayed,
|
|
// it makes sense to use separate notification channels and to open the respective chat directly -
|
|
// the user may eg. have chosen a different sound
|
|
String notificationChannel = getNotificationChannel(notificationManager, chatData, dcChat);
|
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, notificationChannel)
|
|
.setSmallIcon(R.drawable.icon_notification)
|
|
.setColor(context.getResources().getColor(R.color.delta_primary))
|
|
.setPriority(Prefs.getNotificationPriority(context))
|
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
|
.setOnlyAlertOnce(!signal)
|
|
.setContentText(shortLine)
|
|
.setDeleteIntent(getMarkAsReadIntent(chatData, msgId, false))
|
|
.setContentIntent(getOpenChatIntent(chatData));
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
builder.setGroup(GRP_MSG + "." + accountId);
|
|
}
|
|
|
|
String accountTag = dcContext.getConfig(CONFIG_PRIVATE_TAG);
|
|
if (accountTag.isEmpty() && context.dcAccounts.getAll().length > 1) {
|
|
accountTag = dcContext.getName();
|
|
}
|
|
|
|
if (privacy.isDisplayContact()) {
|
|
builder.setContentTitle(dcChat.getName());
|
|
if (!TextUtils.isEmpty(accountTag)) {
|
|
builder.setSubText(accountTag);
|
|
}
|
|
}
|
|
|
|
builder.setTicker(tickerLine);
|
|
|
|
// set sound, vibrate, led for systems that do not have notification channels
|
|
if (!notificationChannelsSupported()) {
|
|
if (signal) {
|
|
Uri sound = effectiveSound(chatData);
|
|
if (sound != null && !TextUtils.isEmpty(sound.toString())) {
|
|
builder.setSound(sound);
|
|
}
|
|
boolean vibrate = effectiveVibrate(chatData);
|
|
if (vibrate) {
|
|
builder.setDefaults(Notification.DEFAULT_VIBRATE);
|
|
}
|
|
}
|
|
String ledColor = Prefs.getNotificationLedColor(context);
|
|
if (!ledColor.equals("none")) {
|
|
builder.setLights(getLedArgb(ledColor),500, 2000);
|
|
}
|
|
}
|
|
|
|
// set avatar
|
|
if (privacy.isDisplayContact()) {
|
|
Bitmap bitmap = getAvatar(dcChat);
|
|
if (bitmap != null) {
|
|
builder.setLargeIcon(bitmap);
|
|
}
|
|
}
|
|
|
|
// add buttons that allow some actions without opening Delta Chat.
|
|
// if privacy options are enabled, the buttons are not added.
|
|
if (privacy.isDisplayContact() && privacy.isDisplayMessage()) {
|
|
try {
|
|
PendingIntent inNotificationReplyIntent = getRemoteReplyIntent(chatData, msgId);
|
|
PendingIntent markReadIntent = getMarkAsReadIntent(chatData, msgId, true);
|
|
|
|
NotificationCompat.Action markAsReadAction = new NotificationCompat.Action(R.drawable.check,
|
|
context.getString(R.string.mark_as_read_short),
|
|
markReadIntent);
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(R.drawable.ic_reply_white_36dp,
|
|
context.getString(R.string.notify_reply_button),
|
|
inNotificationReplyIntent)
|
|
.addRemoteInput(new RemoteInput.Builder(RemoteReplyReceiver.EXTRA_REMOTE_REPLY)
|
|
.setLabel(context.getString(R.string.notify_reply_button)).build())
|
|
.build();
|
|
builder.addAction(replyAction);
|
|
}
|
|
|
|
NotificationCompat.Action wearableReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_reply,
|
|
context.getString(R.string.notify_reply_button),
|
|
inNotificationReplyIntent)
|
|
.addRemoteInput(new RemoteInput.Builder(RemoteReplyReceiver.EXTRA_REMOTE_REPLY)
|
|
.setLabel(context.getString(R.string.notify_reply_button)).build())
|
|
.build();
|
|
builder.addAction(markAsReadAction);
|
|
builder.extend(new NotificationCompat.WearableExtender().addAction(markAsReadAction).addAction(wearableReplyAction));
|
|
} catch(Exception e) { Log.w(TAG, e); }
|
|
}
|
|
|
|
// create a tiny inbox (gets visible if the notification is expanded)
|
|
if (privacy.isDisplayContact() && privacy.isDisplayMessage()) {
|
|
try {
|
|
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
|
|
synchronized (inboxes) {
|
|
HashMap<Integer, ArrayList<String>> accountInbox = inboxes.get(accountId);
|
|
if (accountInbox == null) {
|
|
accountInbox = new HashMap<>();
|
|
inboxes.put(accountId, accountInbox);
|
|
}
|
|
ArrayList<String> lines = accountInbox.get(chatId);
|
|
if (lines == null) {
|
|
lines = new ArrayList<>();
|
|
accountInbox.put(chatId, lines);
|
|
}
|
|
lines.add(shortLine);
|
|
|
|
for (int l = 0; l < lines.size(); l++) {
|
|
inboxStyle.addLine(lines.get(l));
|
|
}
|
|
}
|
|
builder.setStyle(inboxStyle);
|
|
} catch(Exception e) { Log.w(TAG, e); }
|
|
}
|
|
|
|
// messages count, some os make some use of that
|
|
// - do not use setSubText() as this is displayed together with setContentInfo() eg. on Lollipop
|
|
// - setNumber() may overwrite setContentInfo(), should be called last
|
|
// weird stuff.
|
|
int cnt = dcContext.getFreshMsgCount(chatId);
|
|
builder.setContentInfo(String.valueOf(cnt));
|
|
builder.setNumber(cnt);
|
|
|
|
// add notification, we use one notification per chat,
|
|
// esp. older android are not that great at grouping
|
|
try {
|
|
notificationManager.notify(String.valueOf(accountId), ID_MSG_OFFSET + chatId, builder.build());
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "cannot add notification", e);
|
|
}
|
|
|
|
// group notifications together in a summary, this is possible since SDK 24 (Android 7)
|
|
// https://developer.android.com/training/notify-user/group.html
|
|
// in theory, this won't be needed due to setGroup(), however, in practise, it is needed up to at least Android 10.
|
|
if (Build.VERSION.SDK_INT >= 24) {
|
|
try {
|
|
NotificationCompat.Builder summary = new NotificationCompat.Builder(context, notificationChannel)
|
|
.setGroup(GRP_MSG + "." + accountId)
|
|
.setGroupSummary(true)
|
|
.setSmallIcon(R.drawable.icon_notification)
|
|
.setColor(context.getResources().getColor(R.color.delta_primary, null))
|
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
|
.setContentTitle("Delta Chat") // content title would only be used on SDK <24
|
|
.setContentText("New messages") // content text would only be used on SDK <24
|
|
.setContentIntent(getOpenChatlistIntent(accountId));
|
|
if (privacy.isDisplayContact() && !TextUtils.isEmpty(accountTag)) {
|
|
summary.setSubText(accountTag);
|
|
}
|
|
notificationManager.notify(String.valueOf(accountId), ID_MSG_SUMMARY, summary.build());
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "cannot add notification summary", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
public Bitmap getAvatar(DcChat dcChat) {
|
|
Recipient recipient = new Recipient(context, dcChat);
|
|
try {
|
|
Drawable drawable;
|
|
ContactPhoto contactPhoto = recipient.getContactPhoto(context);
|
|
if (contactPhoto != null) {
|
|
drawable = GlideApp.with(context.getApplicationContext())
|
|
.load(contactPhoto)
|
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
.circleCrop()
|
|
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
|
|
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
|
|
.get();
|
|
|
|
} else {
|
|
drawable = recipient.getFallbackContactPhoto().asDrawable(context, recipient.getFallbackAvatarColor());
|
|
}
|
|
if (drawable != null) {
|
|
int wh = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
|
|
return BitmapUtil.createFromDrawable(drawable, wh, wh);
|
|
}
|
|
} catch (Exception e) { Log.w(TAG, e); }
|
|
|
|
return null;
|
|
}
|
|
|
|
public void removeCallNotification(int accountId, int callId) {
|
|
try {
|
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
|
String tag = "call-" + accountId;
|
|
notificationManager.cancel(tag, callId);
|
|
} catch (Exception e) { Log.w(TAG, e); }
|
|
}
|
|
|
|
public void removeNotifications(int accountId, int chatId) {
|
|
boolean removeSummary;
|
|
synchronized (inboxes) {
|
|
HashMap<Integer, ArrayList<String>> accountInbox = inboxes.get(accountId);
|
|
if (accountInbox == null) {
|
|
accountInbox = new HashMap<>();
|
|
}
|
|
accountInbox.remove(chatId);
|
|
removeSummary = accountInbox.isEmpty();
|
|
}
|
|
|
|
// cancel notification independently of inboxes array,
|
|
// due to restarts, the app may have notification even when inboxes is empty.
|
|
try {
|
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
|
String tag = String.valueOf(accountId);
|
|
notificationManager.cancel(tag, ID_MSG_OFFSET + chatId);
|
|
if (removeSummary) {
|
|
notificationManager.cancel(tag, ID_MSG_SUMMARY);
|
|
}
|
|
} catch (Exception e) { Log.w(TAG, e); }
|
|
}
|
|
|
|
public void removeAllNotifications(int accountId) {
|
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
|
String tag = String.valueOf(accountId);
|
|
synchronized (inboxes) {
|
|
HashMap<Integer, ArrayList<String>> accountInbox = inboxes.get(accountId);
|
|
notificationManager.cancel(tag, ID_MSG_SUMMARY);
|
|
if (accountInbox != null) {
|
|
for (Integer chatId : accountInbox.keySet()) {
|
|
notificationManager.cancel(tag, chatId);
|
|
}
|
|
accountInbox.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void updateVisibleChat(int accountId, int chatId) {
|
|
Util.runOnAnyBackgroundThread(() -> {
|
|
|
|
if (accountId != 0 && chatId != 0) {
|
|
visibleChat = new ChatData(accountId, chatId);
|
|
removeNotifications(accountId, chatId);
|
|
} else {
|
|
visibleChat = null;
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
public void clearVisibleChat() {
|
|
visibleChat = null;
|
|
}
|
|
|
|
public void updateVisibleWebxdc(int accountId, int msgId) {
|
|
if (accountId != 0 && msgId != 0) {
|
|
visibleWebxdc = new Pair<>(accountId, msgId);
|
|
} else {
|
|
visibleWebxdc = null;
|
|
}
|
|
}
|
|
|
|
public void clearVisibleWebxdc() {
|
|
visibleWebxdc = null;
|
|
}
|
|
|
|
public void maybePlaySendSound(DcChat dcChat) {
|
|
if (Prefs.isInChatNotifications(context) && !dcChat.isMuted()) {
|
|
InChatSounds.getInstance(context).playSendSound();
|
|
}
|
|
}
|
|
|
|
private static class ChatData {
|
|
public final int accountId;
|
|
public final int chatId;
|
|
|
|
public ChatData(int accountId, int chatId) {
|
|
this.accountId = accountId;
|
|
this.chatId = chatId;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
|
|
ChatData chatData = (ChatData) o;
|
|
return accountId == chatData.accountId && chatId == chatData.chatId;
|
|
}
|
|
}
|
|
}
|