mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Converted DefaultMessageNotifier to Kotlin because it needs adjustment & that Java is nasty
This commit is contained in:
parent
6b29e4d8ce
commit
2338bb47ca
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user