Converted DefaultMessageNotifier to Kotlin because it needs adjustment & that Java is nasty

This commit is contained in:
alansley 2024-08-20 19:11:40 +10:00
parent 6b29e4d8ce
commit 2338bb47ca
2 changed files with 722 additions and 735 deletions

View File

@ -1,735 +0,0 @@
/*
* Copyright (C) 2011 Whisper Systems
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.notifications;
import static org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Build;
import android.service.notification.StatusBarNotification;
import android.text.SpannableString;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.annimon.stream.Optional;
import com.annimon.stream.Stream;
import com.goterl.lazysodium.utils.KeyPair;
import com.squareup.phrase.Phrase;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import me.leolin.shortcutbadger.ShortcutBadger;
import network.loki.messenger.R;
import org.session.libsession.messaging.open_groups.OpenGroup;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.utilities.AccountId;
import org.session.libsession.messaging.utilities.SodiumUtilities;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Contact;
import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.IdPrefix;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.Util;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.contacts.ContactUtil;
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
import org.thoughtcrime.securesms.util.SpanUtil;
/**
* Handles posting system notifications for new messages.
*
*
* @author Moxie Marlinspike
*/
public class DefaultMessageNotifier implements MessageNotifier {
private static final String TAG = DefaultMessageNotifier.class.getSimpleName();
public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply";
public static final String LATEST_MESSAGE_ID_TAG = "extra_latest_message_id";
private static final int FOREGROUND_ID = 313399;
private static final int SUMMARY_NOTIFICATION_ID = 1338;
private static final int PENDING_MESSAGES_ID = 1111;
private static final String NOTIFICATION_GROUP = "messages";
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(5);
private static final long DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1);
private volatile static long visibleThread = -1;
private volatile static boolean homeScreenVisible = false;
private volatile static long lastDesktopActivityTimestamp = -1;
private volatile static long lastAudibleNotification = -1;
private static final CancelableExecutor executor = new CancelableExecutor();
@Override
public void setVisibleThread(long threadId) {
visibleThread = threadId;
}
@Override
public void setHomeScreenVisible(boolean isVisible) {
homeScreenVisible = isVisible;
}
@Override
public void setLastDesktopActivityTimestamp(long timestamp) {
lastDesktopActivityTimestamp = timestamp;
}
@Override
public void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) {
// We do not provide notifications for message delivery failure.
}
@Override
public void cancelDelayedNotifications() {
executor.cancel();
}
private boolean cancelActiveNotifications(@NonNull Context context) {
NotificationManager notifications = ServiceUtil.getNotificationManager(context);
boolean hasNotifications = notifications.getActiveNotifications().length > 0;
notifications.cancel(SUMMARY_NOTIFICATION_ID);
try {
StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
for (StatusBarNotification activeNotification : activeNotifications) {
notifications.cancel(activeNotification.getId());
}
} catch (Throwable e) {
// XXX Appears to be a ROM bug, see #6043
Log.w(TAG, e);
notifications.cancelAll();
}
return hasNotifications;
}
private void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) {
try {
NotificationManager notifications = ServiceUtil.getNotificationManager(context);
StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
for (StatusBarNotification notification : activeNotifications) {
boolean validNotification = false;
if (notification.getId() != SUMMARY_NOTIFICATION_ID &&
notification.getId() != KeyCachingService.SERVICE_RUNNING_ID &&
notification.getId() != FOREGROUND_ID &&
notification.getId() != PENDING_MESSAGES_ID)
{
for (NotificationItem item : notificationState.getNotifications()) {
if (notification.getId() == (SUMMARY_NOTIFICATION_ID + item.getThreadId())) {
validNotification = true;
break;
}
}
if (!validNotification) { notifications.cancel(notification.getId()); }
}
}
} catch (Throwable e) {
// XXX Android ROM Bug, see #6043
Log.w(TAG, e);
}
}
@Override
public void updateNotification(@NonNull Context context) {
if (!TextSecurePreferences.isNotificationsEnabled(context)) {
return;
}
updateNotification(context, false, 0);
}
@Override
public void updateNotification(@NonNull Context context, long threadId)
{
if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) {
Log.i(TAG, "Scheduling delayed notification...");
executor.execute(new DelayedNotification(context, threadId));
} else {
updateNotification(context, threadId, true);
}
}
@Override
public void updateNotification(@NonNull Context context, long threadId, boolean signal)
{
boolean isVisible = visibleThread == threadId;
ThreadDatabase threads = DatabaseComponent.get(context).threadDatabase();
Recipient recipient = threads.getRecipientForThreadId(threadId);
if (recipient != null && !recipient.isGroupRecipient() && threads.getMessageCount(threadId) == 1 &&
!(recipient.isApproved() || threads.getLastSeenAndHasSent(threadId).second())) {
TextSecurePreferences.removeHasHiddenMessageRequests(context);
}
if (!TextSecurePreferences.isNotificationsEnabled(context) ||
(recipient != null && recipient.isMuted()))
{
return;
}
if ((!isVisible && !homeScreenVisible) || hasExistingNotifications(context)) {
updateNotification(context, signal, 0);
}
}
private boolean hasExistingNotifications(Context context) {
NotificationManager notifications = ServiceUtil.getNotificationManager(context);
try {
StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
return activeNotifications.length > 0;
} catch (Exception e) {
return false;
}
}
@Override
public void updateNotification(@NonNull Context context, boolean signal, int reminderCount)
{
Cursor telcoCursor = null;
Cursor pushCursor = null;
try {
telcoCursor = DatabaseComponent.get(context).mmsSmsDatabase().getUnread(); // TODO: add a notification specific lighter query here
if ((telcoCursor == null || telcoCursor.isAfterLast()) || TextSecurePreferences.getLocalNumber(context) == null)
{
updateBadge(context, 0);
cancelActiveNotifications(context);
clearReminder(context);
return;
}
NotificationState notificationState = constructNotificationState(context, telcoCursor);
if (signal && (System.currentTimeMillis() - lastAudibleNotification) < MIN_AUDIBLE_PERIOD_MILLIS) {
signal = false;
} else if (signal) {
lastAudibleNotification = System.currentTimeMillis();
}
try {
if (notificationState.hasMultipleThreads()) {
for (long threadId : notificationState.getThreads()) {
sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true);
}
sendMultipleThreadNotification(context, notificationState, signal);
} else if (notificationState.getMessageCount() > 0) {
sendSingleThreadNotification(context, notificationState, signal, false);
} else {
cancelActiveNotifications(context);
}
} catch (Exception e) {
Log.e(TAG, "Error creating notification", e);
}
cancelOrphanedNotifications(context, notificationState);
updateBadge(context, notificationState.getMessageCount());
if (signal) {
scheduleReminder(context, reminderCount);
}
} finally {
if (telcoCursor != null) telcoCursor.close();
}
}
private void sendSingleThreadNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal, boolean bundled)
{
Log.i(TAG, "sendSingleThreadNotification() signal: " + signal + " bundled: " + bundled);
if (notificationState.getNotifications().isEmpty()) {
if (!bundled) cancelActiveNotifications(context);
Log.i(TAG, "Empty notification state. Skipping.");
return;
}
SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context));
List<NotificationItem> notifications = notificationState.getNotifications();
Recipient recipient = notifications.get(0).getRecipient();
int notificationId = (int) (SUMMARY_NOTIFICATION_ID + (bundled ? notifications.get(0).getThreadId() : 0));
String messageIdTag = String.valueOf(notifications.get(0).getTimestamp());
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
for (StatusBarNotification notification: notificationManager.getActiveNotifications()) {
if ( (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && notification.isAppGroup() == bundled)
&& messageIdTag.equals(notification.getNotification().extras.getString(LATEST_MESSAGE_ID_TAG))) {
return;
}
}
long timestamp = notifications.get(0).getTimestamp();
if (timestamp != 0) builder.setWhen(timestamp);
builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag);
CharSequence text = notifications.get(0).getText();
builder.setThread(notifications.get(0).getRecipient());
builder.setMessageCount(notificationState.getMessageCount());
CharSequence builderCS = text == null ? "" : text;
SpannableString ss = MentionUtilities.highlightMentions(
builderCS,
false,
false,
true,
bundled ? notifications.get(0).getThreadId() : 0,
context
);
builder.setPrimaryMessageBody(recipient,
notifications.get(0).getIndividualRecipient(),
ss,
notifications.get(0).getSlideDeck());
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
builder.setOnlyAlertOnce(!signal);
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
builder.setAutoCancel(true);
ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient);
boolean canReply = SessionMetaProtocol.canUserReplyToNotification(recipient);
PendingIntent quickReplyIntent = canReply ? notificationState.getQuickReplyIntent(context, recipient) : null;
PendingIntent remoteReplyIntent = canReply ? notificationState.getRemoteReplyIntent(context, recipient, replyMethod) : null;
builder.addActions(notificationState.getMarkAsReadIntent(context, notificationId),
quickReplyIntent,
remoteReplyIntent,
replyMethod);
if (canReply) {
builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, recipient),
notificationState.getAndroidAutoHeardIntent(context, notificationId),
notifications.get(0).getTimestamp());
}
ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
while(iterator.hasPrevious()) {
NotificationItem item = iterator.previous();
builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText());
}
if (signal) {
builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate());
builder.setTicker(notifications.get(0).getIndividualRecipient(),
notifications.get(0).getText());
}
if (bundled) {
builder.setGroup(NOTIFICATION_GROUP);
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
}
Notification notification = builder.build();
// ACL FIX THIS PROPERLY
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
NotificationManagerCompat.from(context).notify(notificationId, notification);
Log.i(TAG, "Posted notification. " + notification.toString());
}
private void sendMultipleThreadNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal)
{
Log.i(TAG, "sendMultiThreadNotification() signal: " + signal);
MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context));
List<NotificationItem> notifications = notificationState.getNotifications();
builder.setMessageCount(notificationState.getMessageCount(), notificationState.getThreadCount());
builder.setMostRecentSender(notifications.get(0).getIndividualRecipient(), notifications.get(0).getRecipient());
builder.setGroup(NOTIFICATION_GROUP);
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
builder.setOnlyAlertOnce(!signal);
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
builder.setAutoCancel(true);
String messageIdTag = String.valueOf(notifications.get(0).getTimestamp());
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
for (StatusBarNotification notification: notificationManager.getActiveNotifications()) {
if (notification.getId() == SUMMARY_NOTIFICATION_ID
&& messageIdTag.equals(notification.getNotification().extras.getString(LATEST_MESSAGE_ID_TAG))) {
return;
}
}
long timestamp = notifications.get(0).getTimestamp();
if (timestamp != 0) builder.setWhen(timestamp);
builder.addActions(notificationState.getMarkAsReadIntent(context, SUMMARY_NOTIFICATION_ID));
ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
while(iterator.hasPrevious()) {
NotificationItem item = iterator.previous();
builder.addMessageBody(item.getIndividualRecipient(), item.getRecipient(),
MentionUtilities.highlightMentions(
item.getText() != null ? item.getText() : "",
false,
false,
true, // no styling here, only text formatting
item.getThreadId(),
context
)
);
}
if (signal) {
builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate());
CharSequence text = notifications.get(0).getText();
builder.setTicker(notifications.get(0).getIndividualRecipient(),
MentionUtilities.highlightMentions(
text != null ? text : "",
false,
false,
true, // no styling here, only text formatting
notifications.get(0).getThreadId(),
context
)
);
}
builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag);
// ACL FIX THIS PROPERLY
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
Notification notification = builder.build();
NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, notification);
Log.i(TAG, "Posted notification. " + notification);
}
private NotificationState constructNotificationState(@NonNull Context context,
@NonNull Cursor cursor)
{
NotificationState notificationState = new NotificationState();
MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor);
ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase();
MessageRecord record;
Map<Long, String> cache = new HashMap<Long, String>();
while ((record = reader.getNext()) != null) {
long id = record.getId();
boolean mms = record.isMms() || record.isMmsNotification();
Recipient recipient = record.getIndividualRecipient();
Recipient conversationRecipient = record.getRecipient();
long threadId = record.getThreadId();
CharSequence body = record.getDisplayBody(context);
Recipient threadRecipients = null;
SlideDeck slideDeck = null;
long timestamp = record.getTimestamp();
boolean messageRequest = false;
if (threadId != -1) {
threadRecipients = threadDatabase.getRecipientForThreadId(threadId);
messageRequest = threadRecipients != null && !threadRecipients.isGroupRecipient() &&
!threadRecipients.isApproved() && !threadDatabase.getLastSeenAndHasSent(threadId).second();
if (messageRequest && (threadDatabase.getMessageCount(threadId) > 1 || !TextSecurePreferences.hasHiddenMessageRequests(context))) {
continue;
}
}
// If this is a message request from an unknown user..
if (messageRequest) {
body = SpanUtil.italic(context.getString(R.string.messageRequestsNew));
// If we received some manner of notification but Session is locked..
} else if (KeyCachingService.isLocked(context)) {
// Note: We provide 1 because `messageNewYouveGot` is now a plurals string and we don't have a count yet, so just
// giving it 1 will result in "You got a new message".
body = SpanUtil.italic(context.getResources().getQuantityString(R.plurals.messageNewYouveGot, 1, 1));
// ----- All further cases assume we know the contact and that Session isn't locked -----
// If this is a notification about a multimedia message from a contact we know about..
} else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) {
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
body = ContactUtil.getStringSummary(context, contact);
// If this is a notification about a multimedia message which contains no text but DOES contain a slide deck with at least one slide..
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
body = SpanUtil.italic(slideDeck.getBody());
// If this is a notification about a multimedia message, but it's not ITSELF a multimedia notification AND it contains a slide deck with at least one slide..
} else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
String message = slideDeck.getBody() + ": " + record.getBody();
int italicLength = message.length() - body.length();
body = SpanUtil.italic(message, italicLength);
// If this is a notification about an invitation to a community..
} else if (record.isOpenGroupInvitation()) {
body = SpanUtil.italic(context.getString(R.string.communityInvitation));
}
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
String blindedPublicKey = cache.get(threadId);
if (blindedPublicKey == null) {
blindedPublicKey = generateBlindedId(threadId, context);
cache.put(threadId, blindedPublicKey);
}
if (threadRecipients == null || !threadRecipients.isMuted()) {
if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) {
// check if mentioned here
boolean isQuoteMentioned = false;
if (record instanceof MmsMessageRecord) {
Quote quote = ((MmsMessageRecord) record).getQuote();
Address quoteAddress = quote != null ? quote.getAuthor() : null;
String serializedAddress = quoteAddress != null ? quoteAddress.serialize() : null;
isQuoteMentioned = (serializedAddress!= null && Objects.equals(userPublicKey, serializedAddress)) ||
(blindedPublicKey != null && Objects.equals(userPublicKey, blindedPublicKey));
}
if (body.toString().contains("@"+userPublicKey) || body.toString().contains("@"+blindedPublicKey) || isQuoteMentioned) {
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
}
} else if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
// do nothing, no notifications
} else {
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
}
String userBlindedPublicKey = blindedPublicKey;
Optional<ReactionRecord> lastReact = Stream.of(record.getReactions())
.filter(r -> !(r.getAuthor().equals(userPublicKey) || r.getAuthor().equals(userBlindedPublicKey)))
.findLast();
if (lastReact.isPresent()) {
if (threadRecipients != null && !threadRecipients.isGroupRecipient()) {
ReactionRecord reaction = lastReact.get();
Recipient reactor = Recipient.from(context, Address.fromSerialized(reaction.getAuthor()), false);
String emoji = Phrase.from(context, R.string.emojiReactsNotification).put(EMOJI_KEY, reaction.getEmoji()).format().toString();
notificationState.addNotification(new NotificationItem(id, mms, reactor, reactor, threadRecipients, threadId, emoji, reaction.getDateSent(), slideDeck));
}
}
}
}
reader.close();
return notificationState;
}
private @Nullable String generateBlindedId(long threadId, Context context) {
LokiThreadDatabase lokiThreadDatabase = DatabaseComponent.get(context).lokiThreadDatabase();
OpenGroup openGroup = lokiThreadDatabase.getOpenGroupChat(threadId);
KeyPair edKeyPair = KeyPairUtilities.INSTANCE.getUserED25519KeyPair(context);
if (openGroup != null && edKeyPair != null) {
KeyPair blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup.getPublicKey(), edKeyPair);
if (blindedKeyPair != null) {
return new AccountId(IdPrefix.BLINDED, blindedKeyPair.getPublicKey().getAsBytes()).getHexString();
}
}
return null;
}
private void updateBadge(Context context, int count) {
try {
if (count == 0) ShortcutBadger.removeCount(context);
else ShortcutBadger.applyCount(context, count);
} catch (Throwable t) {
// NOTE :: I don't totally trust this thing, so I'm catching
// everything.
Log.w("MessageNotifier", t);
}
}
private void scheduleReminder(Context context, int count) {
if (count >= TextSecurePreferences.getRepeatAlertsCount(context)) {
return;
}
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
alarmIntent.putExtra("reminder_count", count);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
long timeout = TimeUnit.MINUTES.toMillis(2);
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent);
}
@Override
public void clearReminder(Context context) {
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(pendingIntent);
}
public static class ReminderReceiver extends BroadcastReceiver {
public static final String REMINDER_ACTION = "network.loki.securesms.MessageNotifier.REMINDER_ACTION";
@SuppressLint("StaticFieldLeak")
@Override
public void onReceive(final Context context, final Intent intent) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
int reminderCount = intent.getIntExtra("reminder_count", 0);
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, true, reminderCount + 1);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
private static class DelayedNotification implements Runnable {
private static final long DELAY = TimeUnit.SECONDS.toMillis(5);
private final AtomicBoolean canceled = new AtomicBoolean(false);
private final Context context;
private final long threadId;
private final long delayUntil;
private DelayedNotification(Context context, long threadId) {
this.context = context;
this.threadId = threadId;
this.delayUntil = System.currentTimeMillis() + DELAY;
}
@Override
public void run() {
long delayMillis = delayUntil - System.currentTimeMillis();
Log.i(TAG, "Waiting to notify: " + delayMillis);
if (delayMillis > 0) {
Util.sleep(delayMillis);
}
if (!canceled.get()) {
Log.i(TAG, "Not canceled, notifying...");
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, threadId, true);
ApplicationContext.getInstance(context).messageNotifier.cancelDelayedNotifications();
} else {
Log.w(TAG, "Canceled, not notifying...");
}
}
public void cancel() {
canceled.set(true);
}
}
private static class CancelableExecutor {
private final Executor executor = Executors.newSingleThreadExecutor();
private final Set<DelayedNotification> tasks = new HashSet<>();
public void execute(final DelayedNotification runnable) {
synchronized (tasks) {
tasks.add(runnable);
}
Runnable wrapper = new Runnable() {
@Override
public void run() {
runnable.run();
synchronized (tasks) {
tasks.remove(runnable);
}
}
};
executor.execute(wrapper);
}
public void cancel() {
synchronized (tasks) {
for (DelayedNotification task : tasks) {
task.cancel();
}
}
}
}
}

View File

@ -0,0 +1,722 @@
/*
* Copyright (C) 2011 Whisper Systems
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.notifications
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.os.AsyncTask
import android.os.Build
import android.text.TextUtils
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.annimon.stream.Stream
import com.squareup.phrase.Phrase
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.Volatile
import me.leolin.shortcutbadger.ShortcutBadger
import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
import org.session.libsession.messaging.utilities.AccountId
import org.session.libsession.messaging.utilities.SodiumUtilities.blindedKeyPair
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.ServiceUtil
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.TextSecurePreferences.Companion.getNotificationPrivacy
import org.session.libsession.utilities.TextSecurePreferences.Companion.getRepeatAlertsCount
import org.session.libsession.utilities.TextSecurePreferences.Companion.hasHiddenMessageRequests
import org.session.libsession.utilities.TextSecurePreferences.Companion.isNotificationsEnabled
import org.session.libsession.utilities.TextSecurePreferences.Companion.removeHasHiddenMessageRequests
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Util
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.contacts.ContactUtil
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
import org.thoughtcrime.securesms.crypto.KeyPairUtilities.getUserED25519KeyPair
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.SessionMetaProtocol.canUserReplyToNotification
import org.thoughtcrime.securesms.util.SpanUtil
/**
* Handles posting system notifications for new messages.
*
*
* @author Moxie Marlinspike
*/
class DefaultMessageNotifier : MessageNotifier {
override fun setVisibleThread(threadId: Long) {
visibleThread = threadId
}
override fun setHomeScreenVisible(isVisible: Boolean) {
homeScreenVisible = isVisible
}
override fun setLastDesktopActivityTimestamp(timestamp: Long) {
lastDesktopActivityTimestamp = timestamp
}
override fun notifyMessageDeliveryFailed(context: Context?, recipient: Recipient?, threadId: Long) {
// We do not provide notifications for message delivery failure.
}
override fun cancelDelayedNotifications() {
executor.cancel()
}
private fun cancelActiveNotifications(context: Context): Boolean {
val notifications = ServiceUtil.getNotificationManager(context)
val hasNotifications = notifications.activeNotifications.size > 0
notifications.cancel(SUMMARY_NOTIFICATION_ID)
try {
val activeNotifications = notifications.activeNotifications
for (activeNotification in activeNotifications) {
notifications.cancel(activeNotification.id)
}
} catch (e: Throwable) {
// XXX Appears to be a ROM bug, see #6043
Log.w(TAG, e)
notifications.cancelAll()
}
return hasNotifications
}
private fun cancelOrphanedNotifications(context: Context, notificationState: NotificationState) {
try {
val notifications = ServiceUtil.getNotificationManager(context)
val activeNotifications = notifications.activeNotifications
for (notification in activeNotifications) {
var validNotification = false
if (notification.id != SUMMARY_NOTIFICATION_ID && notification.id != KeyCachingService.SERVICE_RUNNING_ID && notification.id != FOREGROUND_ID && notification.id != PENDING_MESSAGES_ID) {
for (item in notificationState.notifications) {
if (notification.id.toLong() == (SUMMARY_NOTIFICATION_ID + item.threadId)) {
validNotification = true
break
}
}
if (!validNotification) {
notifications.cancel(notification.id)
}
}
}
} catch (e: Throwable) {
// XXX Android ROM Bug, see #6043
Log.w(TAG, e)
}
}
override fun updateNotification(context: Context) {
if (!isNotificationsEnabled(context)) {
return
}
updateNotification(context, false, 0)
}
override fun updateNotification(context: Context, threadId: Long) {
if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) {
Log.i(TAG, "Scheduling delayed notification...")
executor.execute(DelayedNotification(context, threadId))
} else {
updateNotification(context, threadId, true)
}
}
override fun updateNotification(context: Context, threadId: Long, signal: Boolean) {
val isVisible = visibleThread == threadId
val threads = get(context).threadDatabase()
val recipient = threads.getRecipientForThreadId(threadId)
if (recipient != null && !recipient.isGroupRecipient && threads.getMessageCount(threadId) == 1 &&
!(recipient.isApproved || threads.getLastSeenAndHasSent(threadId).second())
) {
removeHasHiddenMessageRequests(context)
}
if (!isNotificationsEnabled(context) ||
(recipient != null && recipient.isMuted)
) {
return
}
if ((!isVisible && !homeScreenVisible) || hasExistingNotifications(context)) {
updateNotification(context, signal, 0)
}
}
private fun hasExistingNotifications(context: Context): Boolean {
val notifications = ServiceUtil.getNotificationManager(context)
try {
val activeNotifications = notifications.activeNotifications
return activeNotifications.isNotEmpty()
} catch (e: Exception) {
return false
}
}
override fun updateNotification(context: Context, signal: Boolean, reminderCount: Int) {
var playNotificationAudio = signal // Local copy of the argument so we can modify it
var telcoCursor: Cursor? = null
val pushCursor: Cursor? = null
try {
telcoCursor = get(context).mmsSmsDatabase().unread // TODO: add a notification specific lighter query here
if ((telcoCursor == null || telcoCursor.isAfterLast) || getLocalNumber(context) == null) {
updateBadge(context, 0)
cancelActiveNotifications(context)
clearReminder(context)
return
}
//var notificationState: NotificationState
try {
val notificationState = constructNotificationState(context, telcoCursor)
if (playNotificationAudio && (System.currentTimeMillis() - lastAudibleNotification) < MIN_AUDIBLE_PERIOD_MILLIS) {
playNotificationAudio = false
} else if (playNotificationAudio) {
lastAudibleNotification = System.currentTimeMillis()
}
if (notificationState.hasMultipleThreads()) {
for (threadId in notificationState.threads) {
sendSingleThreadNotification(context, NotificationState(notificationState.getNotificationsForThread(threadId)), false, true)
}
sendMultipleThreadNotification(context, notificationState, playNotificationAudio)
} else if (notificationState.messageCount > 0) {
sendSingleThreadNotification(context, notificationState, playNotificationAudio, false)
} else {
cancelActiveNotifications(context)
}
cancelOrphanedNotifications(context, notificationState)
updateBadge(context, notificationState.messageCount)
if (playNotificationAudio) {
scheduleReminder(context, reminderCount)
}
}
catch (e: Exception) {
Log.e(TAG, "Error creating notification", e)
}
} finally {
telcoCursor?.close()
}
}
// Note: The `signal` parameter means "play an audio signal for the notification".
private fun sendSingleThreadNotification(
context: Context,
notificationState: NotificationState,
signal: Boolean,
bundled: Boolean
) {
Log.i(TAG, "sendSingleThreadNotification() signal: $signal bundled: $bundled")
if (notificationState.notifications.isEmpty()) {
if (!bundled) cancelActiveNotifications(context)
Log.i(TAG, "Empty notification state. Skipping.")
return
}
val builder = SingleRecipientNotificationBuilder(context, getNotificationPrivacy(context))
val notifications = notificationState.notifications
val recipient = notifications[0].recipient
val notificationId = (SUMMARY_NOTIFICATION_ID + (if (bundled) notifications[0].threadId else 0)).toInt()
val messageIdTag = notifications[0].timestamp.toString()
val notificationManager = ServiceUtil.getNotificationManager(context)
for (notification in notificationManager.activeNotifications) {
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && notification.isAppGroup == bundled)
&& (messageIdTag == notification.notification.extras.getString(LATEST_MESSAGE_ID_TAG))
) {
return
}
}
val timestamp = notifications[0].timestamp
if (timestamp != 0L) builder.setWhen(timestamp)
builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag)
val text = notifications[0].text
builder.setThread(notifications[0].recipient)
builder.setMessageCount(notificationState.messageCount)
val builderCS = text ?: ""
val ss = highlightMentions(
builderCS,
false,
false,
true,
if (bundled) notifications[0].threadId else 0,
context
)
builder.setPrimaryMessageBody(
recipient,
notifications[0].individualRecipient,
ss,
notifications[0].slideDeck
)
builder.setContentIntent(notifications[0].getPendingIntent(context))
builder.setDeleteIntent(notificationState.getDeleteIntent(context))
builder.setOnlyAlertOnce(!signal)
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
builder.setAutoCancel(true)
val replyMethod = ReplyMethod.forRecipient(context, recipient)
val canReply = canUserReplyToNotification(recipient)
val quickReplyIntent = if (canReply) notificationState.getQuickReplyIntent(context, recipient) else null
val remoteReplyIntent = if (canReply) notificationState.getRemoteReplyIntent(context, recipient, replyMethod) else null
builder.addActions(
notificationState.getMarkAsReadIntent(context, notificationId),
quickReplyIntent,
remoteReplyIntent,
replyMethod
)
if (canReply) {
builder.addAndroidAutoAction(
notificationState.getAndroidAutoReplyIntent(context, recipient),
notificationState.getAndroidAutoHeardIntent(context, notificationId),
notifications[0].timestamp
)
}
val iterator: ListIterator<NotificationItem> = notifications.listIterator(notifications.size)
while (iterator.hasPrevious()) {
val item = iterator.previous()
builder.addMessageBody(item.recipient, item.individualRecipient, item.text)
}
if (signal) {
builder.setAlarms(notificationState.getRingtone(context), notificationState.vibrate)
builder.setTicker(
notifications[0].individualRecipient,
notifications[0].text
)
}
if (bundled) {
builder.setGroup(NOTIFICATION_GROUP)
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
}
val notification = builder.build()
// ACL FIX THIS PROPERLY
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return
}
NotificationManagerCompat.from(context).notify(notificationId, notification)
Log.i(TAG, "Posted notification. $notification")
}
// Note: The `signal` parameter means "play an audio signal for the notification".
private fun sendMultipleThreadNotification(
context: Context,
notificationState: NotificationState,
signal: Boolean
) {
Log.i(TAG, "sendMultiThreadNotification() signal: $signal")
val builder = MultipleRecipientNotificationBuilder(context, getNotificationPrivacy(context))
val notifications = notificationState.notifications
builder.setMessageCount(notificationState.messageCount, notificationState.threadCount)
builder.setMostRecentSender(notifications[0].individualRecipient, notifications[0].recipient)
builder.setGroup(NOTIFICATION_GROUP)
builder.setDeleteIntent(notificationState.getDeleteIntent(context))
builder.setOnlyAlertOnce(!signal)
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
builder.setAutoCancel(true)
val messageIdTag = notifications[0].timestamp.toString()
val notificationManager = ServiceUtil.getNotificationManager(context)
for (notification in notificationManager.activeNotifications) {
if (notification.id == SUMMARY_NOTIFICATION_ID && messageIdTag == notification.notification.extras.getString(LATEST_MESSAGE_ID_TAG)) {
return
}
}
val timestamp = notifications[0].timestamp
if (timestamp != 0L) builder.setWhen(timestamp)
builder.addActions(notificationState.getMarkAsReadIntent(context, SUMMARY_NOTIFICATION_ID))
val iterator: ListIterator<NotificationItem> = notifications.listIterator(notifications.size)
while (iterator.hasPrevious()) {
val item = iterator.previous()
builder.addMessageBody(
item.individualRecipient, item.recipient,
highlightMentions(
(if (item.text != null) item.text else "")!!,
false,
false,
true, // no styling here, only text formatting
item.threadId,
context
)
)
}
if (signal) {
builder.setAlarms(notificationState.getRingtone(context), notificationState.vibrate)
val text = notifications[0].text
builder.setTicker(
notifications[0].individualRecipient,
highlightMentions(
text ?: "",
false,
false,
true, // no styling here, only text formatting
notifications[0].threadId,
context
)
)
}
builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag)
// ACL FIX THIS PROPERLY
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return
}
val notification = builder.build()
NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, notification)
Log.i(TAG, "Posted notification. $notification")
}
private fun constructNotificationState(context: Context, cursor: Cursor): NotificationState {
val notificationState = NotificationState()
val reader = get(context).mmsSmsDatabase().readerFor(cursor)
if (reader == null) {
Log.e(TAG, "No reader for cursor - aborting constructNotificationState")
return NotificationState()
}
val threadDatabase = get(context).threadDatabase()
val cache: MutableMap<Long, String?> = HashMap()
// CAREFUL: Do not put this loop back as `while ((reader.next.also { record = it }) != null) {` because it breaks with a Null Pointer Exception - you have been warned.
var record: MessageRecord? = null
do {
record = reader.next
if (record == null) break // Bail if there are no more MessageRecords
val id = record.getId()
val mms = record.isMms || record.isMmsNotification
val recipient = record.individualRecipient
val conversationRecipient = record.recipient
val threadId = record.threadId
var body: CharSequence = record.getDisplayBody(context)
var threadRecipients: Recipient? = null
var slideDeck: SlideDeck? = null
val timestamp = record.timestamp
var messageRequest = false
if (threadId != -1L) {
threadRecipients = threadDatabase.getRecipientForThreadId(threadId)
messageRequest = threadRecipients != null && !threadRecipients.isGroupRecipient &&
!threadRecipients.isApproved && !threadDatabase.getLastSeenAndHasSent(threadId).second()
if (messageRequest && (threadDatabase.getMessageCount(threadId) > 1 || !hasHiddenMessageRequests(context))) {
continue
}
}
// If this is a message request from an unknown user..
if (messageRequest) {
body = SpanUtil.italic(context.getString(R.string.messageRequestsNew))
// If we received some manner of notification but Session is locked..
} else if (KeyCachingService.isLocked(context)) {
// Note: We provide 1 because `messageNewYouveGot` is now a plurals string and we don't have a count yet, so just
// giving it 1 will result in "You got a new message".
body = SpanUtil.italic(context.resources.getQuantityString(R.plurals.messageNewYouveGot, 1, 1))
// ----- Note: All further cases assume we know the contact and that Session isn't locked -----
// If this is a notification about a multimedia message from a contact we know about..
} else if (record.isMms && !(record as MmsMessageRecord).sharedContacts.isEmpty()) {
val contact = (record as MmsMessageRecord).sharedContacts[0]
body = ContactUtil.getStringSummary(context, contact)
// If this is a notification about a multimedia message which contains no text but DOES contain a slide deck with at least one slide..
} else if (record.isMms && TextUtils.isEmpty(body) && !(record as MmsMessageRecord).slideDeck.slides.isEmpty()) {
slideDeck = (record as MediaMmsMessageRecord).slideDeck
body = SpanUtil.italic(slideDeck.body)
// If this is a notification about a multimedia message, but it's not ITSELF a multimedia notification AND it contains a slide deck with at least one slide..
} else if (record.isMms && !record.isMmsNotification && !(record as MmsMessageRecord).slideDeck.slides.isEmpty()) {
slideDeck = (record as MediaMmsMessageRecord).slideDeck
val message = slideDeck.body + ": " + record.body
val italicLength = message.length - body.length
body = SpanUtil.italic(message, italicLength)
// If this is a notification about an invitation to a community..
} else if (record.isOpenGroupInvitation) {
body = SpanUtil.italic(context.getString(R.string.communityInvitation))
}
val userPublicKey = getLocalNumber(context)
var blindedPublicKey = cache[threadId]
if (blindedPublicKey == null) {
blindedPublicKey = generateBlindedId(threadId, context)
cache[threadId] = blindedPublicKey
}
if (threadRecipients == null || !threadRecipients.isMuted) {
if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) {
// check if mentioned here
var isQuoteMentioned = false
if (record is MmsMessageRecord) {
val quote = (record as MmsMessageRecord).quote
val quoteAddress = quote?.author
val serializedAddress = quoteAddress?.serialize()
isQuoteMentioned = (serializedAddress != null && userPublicKey == serializedAddress) ||
(blindedPublicKey != null && userPublicKey == blindedPublicKey)
}
if (body.toString().contains("@$userPublicKey") || body.toString().contains("@$blindedPublicKey") || isQuoteMentioned) {
notificationState.addNotification(NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck))
}
} else if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
// do nothing, no notifications
} else {
notificationState.addNotification(NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck))
}
val userBlindedPublicKey = blindedPublicKey
val lastReact = Stream.of(record.reactions)
.filter { r: ReactionRecord -> !(r.author == userPublicKey || r.author == userBlindedPublicKey) }
.findLast()
if (lastReact.isPresent) {
if (threadRecipients != null && !threadRecipients.isGroupRecipient) {
val reaction = lastReact.get()
val reactor = Recipient.from(context, fromSerialized(reaction.author), false)
val emoji = Phrase.from(context, R.string.emojiReactsNotification).put(EMOJI_KEY, reaction.emoji).format().toString()
notificationState.addNotification(NotificationItem(id, mms, reactor, reactor, threadRecipients, threadId, emoji, reaction.dateSent, slideDeck))
}
}
}
} while (record != null) // This will never hit because we break early if we get a null record at the start of the do..while loop
reader.close()
return notificationState
}
private fun generateBlindedId(threadId: Long, context: Context): String? {
val lokiThreadDatabase = get(context).lokiThreadDatabase()
val openGroup = lokiThreadDatabase.getOpenGroupChat(threadId)
val edKeyPair = getUserED25519KeyPair(context)
if (openGroup != null && edKeyPair != null) {
val blindedKeyPair = blindedKeyPair(openGroup.publicKey, edKeyPair)
if (blindedKeyPair != null) {
return AccountId(IdPrefix.BLINDED, blindedKeyPair.publicKey.asBytes).hexString
}
}
return null
}
private fun updateBadge(context: Context, count: Int) {
try {
if (count == 0) ShortcutBadger.removeCount(context)
else ShortcutBadger.applyCount(context, count)
} catch (t: Throwable) {
// NOTE :: I don't totally trust this thing, so I'm catching everything.
Log.w("MessageNotifier", t)
}
}
private fun scheduleReminder(context: Context, count: Int) {
if (count >= getRepeatAlertsCount(context)) {
return
}
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val alarmIntent = Intent(ReminderReceiver.REMINDER_ACTION)
alarmIntent.putExtra("reminder_count", count)
val pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val timeout = TimeUnit.MINUTES.toMillis(2)
alarmManager[AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout] = pendingIntent
}
override fun clearReminder(context: Context) {
val alarmIntent = Intent(ReminderReceiver.REMINDER_ACTION)
val pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.cancel(pendingIntent)
}
class ReminderReceiver : BroadcastReceiver() {
@SuppressLint("StaticFieldLeak")
override fun onReceive(context: Context, intent: Intent) {
object : AsyncTask<Void?, Void?, Void?>() {
override fun doInBackground(vararg params: Void?): Void? {
val reminderCount = intent.getIntExtra("reminder_count", 0)
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, true, reminderCount + 1)
return null
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
}
companion object {
const val REMINDER_ACTION: String = "network.loki.securesms.MessageNotifier.REMINDER_ACTION"
}
}
// ACL: What is the concept behind delayed notifications? Why would we ever want this? To batch them up so
// that we get a bunch of notifications once per minute or something rather than a constant stream of them
// if that's what was incoming?!?
private class DelayedNotification(private val context: Context, private val threadId: Long) : Runnable {
private val canceled = AtomicBoolean(false)
private val delayUntil: Long
init {
this.delayUntil = System.currentTimeMillis() + DELAY
}
override fun run() {
val delayMillis = delayUntil - System.currentTimeMillis()
Log.i(TAG, "Waiting to notify: $delayMillis")
if (delayMillis > 0) { Util.sleep(delayMillis) }
if (!canceled.get()) {
Log.i(TAG, "Not canceled, notifying...")
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, threadId, true)
ApplicationContext.getInstance(context).messageNotifier.cancelDelayedNotifications()
} else {
Log.w(TAG, "Canceled, not notifying...")
}
}
fun cancel() {
canceled.set(true)
}
companion object {
private val DELAY = TimeUnit.SECONDS.toMillis(5)
}
}
private class CancelableExecutor {
private val executor: Executor = Executors.newSingleThreadExecutor()
private val tasks: MutableSet<DelayedNotification> = HashSet()
fun execute(runnable: DelayedNotification) {
synchronized(tasks) { tasks.add(runnable) }
val wrapper = Runnable {
runnable.run()
synchronized(tasks) {
tasks.remove(runnable)
}
}
executor.execute(wrapper)
}
fun cancel() {
synchronized(tasks) {
for (task in tasks) { task.cancel() }
}
}
}
companion object {
private val TAG: String = DefaultMessageNotifier::class.java.simpleName
const val EXTRA_REMOTE_REPLY: String = "extra_remote_reply"
const val LATEST_MESSAGE_ID_TAG: String = "extra_latest_message_id"
private const val FOREGROUND_ID = 313399
private const val SUMMARY_NOTIFICATION_ID = 1338
private const val PENDING_MESSAGES_ID = 1111
private const val NOTIFICATION_GROUP = "messages"
private val MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(5)
private val DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1)
@Volatile
private var visibleThread: Long = -1
@Volatile
private var homeScreenVisible = false
@Volatile
private var lastDesktopActivityTimestamp: Long = -1
@Volatile
private var lastAudibleNotification: Long = -1
private val executor = CancelableExecutor()
}
}