diff --git a/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java new file mode 100644 index 0000000000..c5b789eff6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -0,0 +1,621 @@ +/* + * 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 . + */ +package org.thoughtcrime.securesms.notifications; + +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.database.Cursor; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.service.notification.StatusBarNotification; +import android.support.annotation.NonNull; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.text.TextUtils; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.contactshare.Contact; +import org.thoughtcrime.securesms.contactshare.ContactUtil; +import org.thoughtcrime.securesms.conversation.ConversationActivity; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; +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.logging.Log; +import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; +import org.thoughtcrime.securesms.mms.SlideDeck; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.service.IncomingMessageObserver; +import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.util.ServiceUtil; +import org.thoughtcrime.securesms.util.SpanUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; +import org.whispersystems.signalservice.internal.util.Util; + +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +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; + +/** + * 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"; + + 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(2); + private static final long DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1); + + private volatile static long visibleThread = -1; + 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 setLastDesktopActivityTimestamp(long timestamp) { + lastDesktopActivityTimestamp = timestamp; + } + + @Override + public void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) { + if (visibleThread == threadId) { + sendInThreadNotification(context, recipient); + } else { + Intent intent = new Intent(context, ConversationActivity.class); + intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress()); + intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); + intent.setData((Uri.parse("custom://" + System.currentTimeMillis()))); + + FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent); + ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify((int)threadId, builder.build()); + } + } + + public void notifyMessagesPending(Context context) { + if (!TextSecurePreferences.isNotificationsEnabled(context)) { + return; + } + + PendingMessageNotificationBuilder builder = new PendingMessageNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); + ServiceUtil.getNotificationManager(context).notify(PENDING_MESSAGES_ID, builder.build()); + } + + @Override + public void cancelDelayedNotifications() { + executor.cancel(); + } + + private void cancelActiveNotifications(@NonNull Context context) { + NotificationManager notifications = ServiceUtil.getNotificationManager(context); + notifications.cancel(SUMMARY_NOTIFICATION_ID); + + if (Build.VERSION.SDK_INT >= 23) { + try { + StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); + + for (StatusBarNotification activeNotification : activeNotifications) { + if (activeNotification.getId() != CallNotificationBuilder.WEBRTC_NOTIFICATION) { + notifications.cancel(activeNotification.getId()); + } + } + } catch (Throwable e) { + // XXX Appears to be a ROM bug, see #6043 + Log.w(TAG, e); + notifications.cancelAll(); + } + } + } + + private void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) { + if (Build.VERSION.SDK_INT >= 23) { + 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() != CallNotificationBuilder.WEBRTC_NOTIFICATION && + notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && + notification.getId() != IncomingMessageObserver.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 = DatabaseFactory.getThreadDatabase(context); + Recipient recipients = DatabaseFactory.getThreadDatabase(context) + .getRecipientForThreadId(threadId); + + if (isVisible) { + List messageIds = threads.setRead(threadId, false); + MarkReadReceiver.process(context, messageIds); + } + + if (!TextSecurePreferences.isNotificationsEnabled(context) || + (recipients != null && recipients.isMuted())) + { + return; + } + + if (isVisible) { + sendInThreadNotification(context, threads.getRecipientForThreadId(threadId)); + } else { + updateNotification(context, signal, 0); + } + } + + @Override + public void updateNotification(@NonNull Context context, boolean signal, int reminderCount) + { + Cursor telcoCursor = null; + Cursor pushCursor = null; + + try { + telcoCursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread(); + pushCursor = DatabaseFactory.getPushDatabase(context).getPending(); + + if ((telcoCursor == null || telcoCursor.isAfterLast()) && + (pushCursor == null || pushCursor.isAfterLast())) + { + cancelActiveNotifications(context); + updateBadge(context, 0); + 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(); + } + + if (notificationState.hasMultipleThreads()) { + if (Build.VERSION.SDK_INT >= 23) { + for (long threadId : notificationState.getThreads()) { + sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true); + } + } + + sendMultipleThreadNotification(context, notificationState, signal); + } else { + sendSingleThreadNotification(context, notificationState, signal, false); + } + + cancelOrphanedNotifications(context, notificationState); + updateBadge(context, notificationState.getMessageCount()); + + if (signal) { + scheduleReminder(context, reminderCount); + } + } finally { + if (telcoCursor != null) telcoCursor.close(); + if (pushCursor != null) pushCursor.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 notifications = notificationState.getNotifications(); + Recipient recipient = notifications.get(0).getRecipient(); + int notificationId = (int) (SUMMARY_NOTIFICATION_ID + (bundled ? notifications.get(0).getThreadId() : 0)); + + + builder.setThread(notifications.get(0).getRecipient()); + builder.setMessageCount(notificationState.getMessageCount()); + builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(), + notifications.get(0).getText(), 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); + + long timestamp = notifications.get(0).getTimestamp(); + if (timestamp != 0) builder.setWhen(timestamp); + + long threadID = notifications.get(0).getThreadId(); + + ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient); + + boolean canReply = SessionMetaProtocol.canUserReplyToNotification(recipient, context); + + 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 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(); + 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 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); + + long timestamp = notifications.get(0).getTimestamp(); + if (timestamp != 0) builder.setWhen(timestamp); + + builder.addActions(notificationState.getMarkAsReadIntent(context, SUMMARY_NOTIFICATION_ID)); + + ListIterator iterator = notifications.listIterator(notifications.size()); + + while(iterator.hasPrevious()) { + NotificationItem item = iterator.previous(); + builder.addMessageBody(item.getIndividualRecipient(), item.getRecipient(), item.getText()); + } + + if (signal) { + builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate()); + builder.setTicker(notifications.get(0).getIndividualRecipient(), + notifications.get(0).getText()); + } + + Notification notification = builder.build(); + NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, builder.build()); + Log.i(TAG, "Posted notification. " + notification.toString()); + } + + private void sendInThreadNotification(Context context, Recipient recipient) { + if (!TextSecurePreferences.isInThreadNotifications(context) || + ServiceUtil.getAudioManager(context).getRingerMode() != AudioManager.RINGER_MODE_NORMAL) + { + return; + } + + Uri uri = null; + if (recipient != null) { + uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient) : recipient.getMessageRingtone(); + } + + if (uri == null) { + uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context) : TextSecurePreferences.getNotificationRingtone(context); + } + + if (uri.toString().isEmpty()) { + Log.d(TAG, "ringtone uri is empty"); + return; + } + + Ringtone ringtone = RingtoneManager.getRingtone(context, uri); + + if (ringtone == null) { + Log.w(TAG, "ringtone is null"); + return; + } + + if (Build.VERSION.SDK_INT >= 21) { + ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) + .build()); + } else { + ringtone.setStreamType(AudioManager.STREAM_NOTIFICATION); + } + + ringtone.play(); + } + + private NotificationState constructNotificationState(@NonNull Context context, + @NonNull Cursor cursor) + { + NotificationState notificationState = new NotificationState(); + MmsSmsDatabase.Reader reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor); + + MessageRecord record; + + 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(); + + + if (threadId != -1) { + threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); + } + + if (KeyCachingService.isLocked(context)) { + body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); + } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) { + Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); + body = ContactUtil.getStringSummary(context, contact); + } else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) { + body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker)); + slideDeck = ((MmsMessageRecord) record).getSlideDeck(); + } else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { + body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message)); + slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck(); + } else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { + String message = context.getString(R.string.MessageNotifier_media_message_with_text, body); + int italicLength = message.length() - body.length(); + body = SpanUtil.italic(message, italicLength); + slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck(); + } + + if (threadRecipients == null || !threadRecipients.isMuted()) { + notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck)); + } + } + + reader.close(); + return notificationState; + } + + 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); + 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); + 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() { + @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 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(); + } + } + } + } +} diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index c3fd7ad1e3..16f4343bea 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -1,615 +1,19 @@ -/* - * 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 . - */ package org.thoughtcrime.securesms.notifications; -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.database.Cursor; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.service.notification.StatusBarNotification; -import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; -import android.text.TextUtils; -import org.thoughtcrime.securesms.contactshare.Contact; -import org.thoughtcrime.securesms.contactshare.ContactUtil; -import org.thoughtcrime.securesms.conversation.ConversationActivity; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; -import org.thoughtcrime.securesms.database.MmsSmsDatabase; -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.logging.Log; -import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; -import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.service.IncomingMessageObserver; -import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.util.ServiceUtil; -import org.thoughtcrime.securesms.util.SpanUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; -import org.whispersystems.signalservice.internal.util.Util; -import java.util.HashSet; -import java.util.List; -import java.util.ListIterator; -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; - -/** - * Handles posting system notifications for new messages. - * - * - * @author Moxie Marlinspike - */ - -public class MessageNotifier { - - private static final String TAG = MessageNotifier.class.getSimpleName(); - - public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply"; - - 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(2); - private static final long DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1); - - private volatile static long visibleThread = -1; - private volatile static long lastDesktopActivityTimestamp = -1; - private volatile static long lastAudibleNotification = -1; - private static final CancelableExecutor executor = new CancelableExecutor(); - - public static void setVisibleThread(long threadId) { - visibleThread = threadId; - } - - public static void setLastDesktopActivityTimestamp(long timestamp) { - lastDesktopActivityTimestamp = timestamp; - } - - public static void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) { - if (visibleThread == threadId) { - sendInThreadNotification(context, recipient); - } else { - Intent intent = new Intent(context, ConversationActivity.class); - intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress()); - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); - intent.setData((Uri.parse("custom://" + System.currentTimeMillis()))); - - FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent); - ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) - .notify((int)threadId, builder.build()); - } - } - - public static void notifyMessagesPending(Context context) { - if (!TextSecurePreferences.isNotificationsEnabled(context)) { - return; - } - - PendingMessageNotificationBuilder builder = new PendingMessageNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); - ServiceUtil.getNotificationManager(context).notify(PENDING_MESSAGES_ID, builder.build()); - } - - public static void cancelDelayedNotifications() { - executor.cancel(); - } - - private static void cancelActiveNotifications(@NonNull Context context) { - NotificationManager notifications = ServiceUtil.getNotificationManager(context); - notifications.cancel(SUMMARY_NOTIFICATION_ID); - - if (Build.VERSION.SDK_INT >= 23) { - try { - StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); - - for (StatusBarNotification activeNotification : activeNotifications) { - if (activeNotification.getId() != CallNotificationBuilder.WEBRTC_NOTIFICATION) { - notifications.cancel(activeNotification.getId()); - } - } - } catch (Throwable e) { - // XXX Appears to be a ROM bug, see #6043 - Log.w(TAG, e); - notifications.cancelAll(); - } - } - } - - private static void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) { - if (Build.VERSION.SDK_INT >= 23) { - 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() != CallNotificationBuilder.WEBRTC_NOTIFICATION && - notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && - notification.getId() != IncomingMessageObserver.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); - } - } - } - - public static void updateNotification(@NonNull Context context) { - if (!TextSecurePreferences.isNotificationsEnabled(context)) { - return; - } - - updateNotification(context, false, 0); - } - - public static 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); - } - } - - public static void updateNotification(@NonNull Context context, - long threadId, - boolean signal) - { - boolean isVisible = visibleThread == threadId; - - ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context); - Recipient recipients = DatabaseFactory.getThreadDatabase(context) - .getRecipientForThreadId(threadId); - - if (isVisible) { - List messageIds = threads.setRead(threadId, false); - MarkReadReceiver.process(context, messageIds); - } - - if (!TextSecurePreferences.isNotificationsEnabled(context) || - (recipients != null && recipients.isMuted())) - { - return; - } - - if (isVisible) { - sendInThreadNotification(context, threads.getRecipientForThreadId(threadId)); - } else { - updateNotification(context, signal, 0); - } - } - - private static void updateNotification(@NonNull Context context, - boolean signal, - int reminderCount) - { - Cursor telcoCursor = null; - Cursor pushCursor = null; - - try { - telcoCursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread(); - pushCursor = DatabaseFactory.getPushDatabase(context).getPending(); - - if ((telcoCursor == null || telcoCursor.isAfterLast()) && - (pushCursor == null || pushCursor.isAfterLast())) - { - cancelActiveNotifications(context); - updateBadge(context, 0); - 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(); - } - - if (notificationState.hasMultipleThreads()) { - if (Build.VERSION.SDK_INT >= 23) { - for (long threadId : notificationState.getThreads()) { - sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true); - } - } - - sendMultipleThreadNotification(context, notificationState, signal); - } else { - sendSingleThreadNotification(context, notificationState, signal, false); - } - - cancelOrphanedNotifications(context, notificationState); - updateBadge(context, notificationState.getMessageCount()); - - if (signal) { - scheduleReminder(context, reminderCount); - } - } finally { - if (telcoCursor != null) telcoCursor.close(); - if (pushCursor != null) pushCursor.close(); - } - } - - private static 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 notifications = notificationState.getNotifications(); - Recipient recipient = notifications.get(0).getRecipient(); - int notificationId = (int) (SUMMARY_NOTIFICATION_ID + (bundled ? notifications.get(0).getThreadId() : 0)); - - - builder.setThread(notifications.get(0).getRecipient()); - builder.setMessageCount(notificationState.getMessageCount()); - builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(), - notifications.get(0).getText(), 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); - - long timestamp = notifications.get(0).getTimestamp(); - if (timestamp != 0) builder.setWhen(timestamp); - - long threadID = notifications.get(0).getThreadId(); - - ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient); - - boolean canReply = SessionMetaProtocol.canUserReplyToNotification(recipient, context); - - 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 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(); - NotificationManagerCompat.from(context).notify(notificationId, notification); - Log.i(TAG, "Posted notification. " + notification.toString()); - } - - private static 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 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); - - long timestamp = notifications.get(0).getTimestamp(); - if (timestamp != 0) builder.setWhen(timestamp); - - builder.addActions(notificationState.getMarkAsReadIntent(context, SUMMARY_NOTIFICATION_ID)); - - ListIterator iterator = notifications.listIterator(notifications.size()); - - while(iterator.hasPrevious()) { - NotificationItem item = iterator.previous(); - builder.addMessageBody(item.getIndividualRecipient(), item.getRecipient(), item.getText()); - } - - if (signal) { - builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate()); - builder.setTicker(notifications.get(0).getIndividualRecipient(), - notifications.get(0).getText()); - } - - Notification notification = builder.build(); - NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, builder.build()); - Log.i(TAG, "Posted notification. " + notification.toString()); - } - - private static void sendInThreadNotification(Context context, Recipient recipient) { - if (!TextSecurePreferences.isInThreadNotifications(context) || - ServiceUtil.getAudioManager(context).getRingerMode() != AudioManager.RINGER_MODE_NORMAL) - { - return; - } - - Uri uri = null; - if (recipient != null) { - uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient) : recipient.getMessageRingtone(); - } - - if (uri == null) { - uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context) : TextSecurePreferences.getNotificationRingtone(context); - } - - if (uri.toString().isEmpty()) { - Log.d(TAG, "ringtone uri is empty"); - return; - } - - Ringtone ringtone = RingtoneManager.getRingtone(context, uri); - - if (ringtone == null) { - Log.w(TAG, "ringtone is null"); - return; - } - - if (Build.VERSION.SDK_INT >= 21) { - ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); - } else { - ringtone.setStreamType(AudioManager.STREAM_NOTIFICATION); - } - - ringtone.play(); - } - - private static NotificationState constructNotificationState(@NonNull Context context, - @NonNull Cursor cursor) - { - NotificationState notificationState = new NotificationState(); - MmsSmsDatabase.Reader reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor); - - MessageRecord record; - - 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(); - - - if (threadId != -1) { - threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); - } - - if (KeyCachingService.isLocked(context)) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); - } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) { - Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); - body = ContactUtil.getStringSummary(context, contact); - } else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker)); - slideDeck = ((MmsMessageRecord) record).getSlideDeck(); - } else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message)); - slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck(); - } else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { - String message = context.getString(R.string.MessageNotifier_media_message_with_text, body); - int italicLength = message.length() - body.length(); - body = SpanUtil.italic(message, italicLength); - slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck(); - } - - if (threadRecipients == null || !threadRecipients.isMuted()) { - notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck)); - } - } - - reader.close(); - return notificationState; - } - - private static 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 static 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); - long timeout = TimeUnit.MINUTES.toMillis(2); - - alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent); - } - - public static void clearReminder(Context context) { - Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT); - 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() { - @Override - protected Void doInBackground(Void... params) { - int reminderCount = intent.getIntExtra("reminder_count", 0); - 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..."); - MessageNotifier.updateNotification(context, threadId, true); - 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 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(); - } - } - } - } +import androidx.annotation.NonNull; + +public interface MessageNotifier { + void setVisibleThread(long threadId); + void setLastDesktopActivityTimestamp(long timestamp); + void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId); + void cancelDelayedNotifications(); + void updateNotification(@NonNull Context context); + void updateNotification(@NonNull Context context, long threadId); + void updateNotification(@NonNull Context context, long threadId, boolean signal); + void updateNotification(@android.support.annotation.NonNull Context context, boolean signal, int reminderCount); + void clearReminder(@NonNull Context context); } diff --git a/src/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java new file mode 100644 index 0000000000..ed812d5a87 --- /dev/null +++ b/src/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -0,0 +1,108 @@ +package org.thoughtcrime.securesms.notifications; + +import android.content.Context; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.loki.api.LokiPublicChatManager; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.Debouncer; +import org.thoughtcrime.securesms.util.Throttler; +import org.whispersystems.signalservice.loki.api.LokiPoller; + +import java.util.concurrent.TimeUnit; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; + +public class OptimizedMessageNotifier implements MessageNotifier { + private final MessageNotifier wrapped; + private final Debouncer debouncer; + + @MainThread + public OptimizedMessageNotifier(@NonNull MessageNotifier wrapped) { + this.wrapped = wrapped; + this.debouncer = new Debouncer(TimeUnit.SECONDS.toMillis(1)); + } + + @Override + public void setVisibleThread(long threadId) { wrapped.setVisibleThread(threadId); } + + @Override + public void setLastDesktopActivityTimestamp(long timestamp) { wrapped.setLastDesktopActivityTimestamp(timestamp);} + + @Override + public void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) { + wrapped.notifyMessageDeliveryFailed(context, recipient, threadId); + } + + @Override + public void cancelDelayedNotifications() { wrapped.cancelDelayedNotifications(); } + + @Override + public void updateNotification(@NonNull Context context) { + LokiPoller lokiPoller = ApplicationContext.getInstance(context).lokiPoller; + LokiPublicChatManager lokiPublicChatManager = ApplicationContext.getInstance(context).lokiPublicChatManager; + Boolean isCatchUp = false; + if (lokiPoller != null && lokiPublicChatManager != null) { + isCatchUp = lokiPoller.isCatchUp() && lokiPublicChatManager.isAllCatchUp(); + } + + if (isCatchUp) { + wrapped.updateNotification(context); + } else { + debouncer.publish(() -> wrapped.updateNotification(context)); + } + } + + @Override + public void updateNotification(@NonNull Context context, long threadId) { + LokiPoller lokiPoller = ApplicationContext.getInstance(context).lokiPoller; + LokiPublicChatManager lokiPublicChatManager = ApplicationContext.getInstance(context).lokiPublicChatManager; + Boolean isCatchUp = false; + if (lokiPoller != null && lokiPublicChatManager != null) { + isCatchUp = lokiPoller.isCatchUp() && lokiPublicChatManager.isAllCatchUp(); + } + + if (isCatchUp) { + wrapped.updateNotification(context, threadId); + } else { + debouncer.publish(() -> wrapped.updateNotification(context, threadId)); + } + } + + @Override + public void updateNotification(@NonNull Context context, long threadId, boolean signal) { + LokiPoller lokiPoller = ApplicationContext.getInstance(context).lokiPoller; + LokiPublicChatManager lokiPublicChatManager = ApplicationContext.getInstance(context).lokiPublicChatManager; + Boolean isCatchUp = false; + if (lokiPoller != null && lokiPublicChatManager != null) { + isCatchUp = lokiPoller.isCatchUp() && lokiPublicChatManager.isAllCatchUp(); + } + + if (isCatchUp) { + wrapped.updateNotification(context, threadId, signal); + } else { + debouncer.publish(() -> wrapped.updateNotification(context, threadId, signal)); + } + } + + @Override + public void updateNotification(@android.support.annotation.NonNull Context context, boolean signal, int reminderCount) { + LokiPoller lokiPoller = ApplicationContext.getInstance(context).lokiPoller; + LokiPublicChatManager lokiPublicChatManager = ApplicationContext.getInstance(context).lokiPublicChatManager; + Boolean isCatchUp = false; + if (lokiPoller != null && lokiPublicChatManager != null) { + isCatchUp = lokiPoller.isCatchUp() && lokiPublicChatManager.isAllCatchUp(); + } + + if (isCatchUp) { + wrapped.updateNotification(context, signal, reminderCount); + } else { + debouncer.publish(() -> wrapped.updateNotification(context, signal, reminderCount)); + } + } + + + @Override + public void clearReminder(@NonNull Context context) { wrapped.clearReminder(context); } +}