Fix various notification display issues and properly support reply.

This commit is contained in:
Cody Henthorne 2021-04-16 17:18:12 -04:00 committed by Greyson Parrelli
parent 5bbc4aea95
commit a843619c5b
17 changed files with 199 additions and 69 deletions

View file

@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MessageDatabase.ThreadUpdate;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
@ -218,6 +219,28 @@ public class MmsSmsDatabase extends Database {
return queryTables(PROJECTION, selection, order, null);
}
public Cursor getMessagesForNotificationState(Collection<MessageNotifierV2.StickyThread> stickyThreads) {
StringBuilder stickyQuery = new StringBuilder();
for (MessageNotifierV2.StickyThread stickyThread : stickyThreads) {
if (stickyQuery.length() > 0) {
stickyQuery.append(" OR ");
}
stickyQuery.append("(")
.append(MmsSmsColumns.THREAD_ID + " = ")
.append(stickyThread.getThreadId())
.append(" AND ")
.append(MmsSmsColumns.NORMALIZED_DATE_RECEIVED)
.append(" >= ")
.append(stickyThread.getEarliestTimestamp())
.append(")");
}
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.NOTIFIED + " = 0 AND (" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1" + (stickyQuery.length() > 0 ? " OR (" + stickyQuery.toString() + ")" : "") + ")";
return queryTables(PROJECTION, selection, order, null);
}
public int getUnreadCount(long threadId) {
String selection = MmsSmsColumns.READ + " = 0 AND " + MmsSmsColumns.NOTIFIED + " = 0 AND " + MmsSmsColumns.THREAD_ID + " = " + threadId;
Cursor cursor = queryTables(PROJECTION, selection, null, null);

View file

@ -749,6 +749,12 @@ public class DefaultMessageNotifier implements MessageNotifier {
alarmManager.cancel(pendingIntent);
}
@Override
public void addStickyThread(long threadId, long earliestTimestamp) {}
@Override
public void removeStickyThread(long threadId) {}
private static class DelayedNotification implements Runnable {
private static final long DELAY = TimeUnit.SECONDS.toMillis(5);

View file

@ -4,8 +4,8 @@ package org.thoughtcrime.securesms.notifications;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -13,30 +13,37 @@ public class DeleteNotificationReceiver extends BroadcastReceiver {
public static String DELETE_NOTIFICATION_ACTION = "org.thoughtcrime.securesms.DELETE_NOTIFICATION";
public static String EXTRA_IDS = "message_ids";
public static String EXTRA_MMS = "is_mms";
public static final String EXTRA_IDS = "message_ids";
public static final String EXTRA_MMS = "is_mms";
public static final String EXTRA_THREAD_IDS = "thread_ids";
@Override
public void onReceive(final Context context, Intent intent) {
if (DELETE_NOTIFICATION_ACTION.equals(intent.getAction())) {
ApplicationDependencies.getMessageNotifier().clearReminder(context);
MessageNotifier notifier = ApplicationDependencies.getMessageNotifier();
notifier.clearReminder(context);
final long[] ids = intent.getLongArrayExtra(EXTRA_IDS);
final boolean[] mms = intent.getBooleanArrayExtra(EXTRA_MMS);
final long[] ids = intent.getLongArrayExtra(EXTRA_IDS);
final boolean[] mms = intent.getBooleanArrayExtra(EXTRA_MMS);
final long[] threadIds = intent.getLongArrayExtra(EXTRA_THREAD_IDS);
if (ids == null || mms == null || ids.length != mms.length) return;
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
for (int i=0;i<ids.length;i++) {
if (!mms[i]) DatabaseFactory.getSmsDatabase(context).markAsNotified(ids[i]);
else DatabaseFactory.getMmsDatabase(context).markAsNotified(ids[i]);
}
return null;
if (threadIds != null) {
for (long threadId : threadIds) {
notifier.removeStickyThread(threadId);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
if (ids == null || mms == null || ids.length != mms.length) return;
SignalExecutors.BOUNDED.execute(() -> {
for (int i = 0; i < ids.length; i++) {
if (!mms[i]) {
DatabaseFactory.getSmsDatabase(context).markAsNotified(ids[i]);
} else {
DatabaseFactory.getMmsDatabase(context).markAsNotified(ids[i]);
}
}
});
}
}
}

View file

@ -12,7 +12,6 @@ import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase.ExpirationInfo;
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
@ -43,6 +42,11 @@ public class MarkReadReceiver extends BroadcastReceiver {
final long[] threadIds = intent.getLongArrayExtra(THREAD_IDS_EXTRA);
if (threadIds != null) {
MessageNotifier notifier = ApplicationDependencies.getMessageNotifier();
for (long threadId : threadIds) {
notifier.removeStickyThread(threadId);
}
NotificationCancellationHelper.cancelLegacy(context, intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1));
SignalExecutors.BOUNDED.execute(() -> {

View file

@ -24,6 +24,8 @@ public interface MessageNotifier {
void updateNotification(@NonNull Context context, long threadId, boolean signal);
void updateNotification(@NonNull Context context, long threadId, boolean signal, int reminderCount, @NonNull BubbleUtil.BubbleState defaultBubbleState);
void clearReminder(@NonNull Context context);
void addStickyThread(long threadId, long earliestTimestamp);
void removeStickyThread(long threadId);
class ReminderReceiver extends BroadcastReceiver {

View file

@ -19,7 +19,9 @@ import org.thoughtcrime.securesms.util.BubbleUtil;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
/**
* Consolidates Notification Cancellation logic to one class.
@ -36,6 +38,10 @@ public final class NotificationCancellationHelper {
private NotificationCancellationHelper() {}
public static void cancelAllMessageNotifications(@NonNull Context context) {
cancelAllMessageNotifications(context, Collections.emptySet());
}
/**
* Cancels all Message-Based notifications. Specifically, this is any notification that is not the
* summary notification assigned to the {@link DefaultMessageNotifier#NOTIFICATION_GROUP} group.
@ -43,7 +49,7 @@ public final class NotificationCancellationHelper {
* We utilize our wrapped cancellation methods and a counter to make sure that we do not lose
* bubble notifications that do not have unread messages in them.
*/
public static void cancelAllMessageNotifications(@NonNull Context context) {
public static void cancelAllMessageNotifications(@NonNull Context context, @NonNull Set<Integer> stickyNotifications) {
if (Build.VERSION.SDK_INT >= 23) {
try {
NotificationManager notifications = ServiceUtil.getNotificationManager(context);
@ -53,7 +59,7 @@ public final class NotificationCancellationHelper {
for (StatusBarNotification activeNotification : activeNotifications) {
if (isSingleThreadNotification(activeNotification)) {
activeCount++;
if (cancel(context, activeNotification.getId())) {
if (!stickyNotifications.contains(activeNotification.getId()) && cancel(context, activeNotification.getId())) {
activeCount--;
}
}

View file

@ -92,6 +92,16 @@ public class OptimizedMessageNotifier implements MessageNotifier {
getNotifier().clearReminder(context);
}
@Override
public void addStickyThread(long threadId, long earliestTimestamp) {
getNotifier().addStickyThread(threadId, earliestTimestamp);
}
@Override
public void removeStickyThread(long threadId) {
getNotifier().removeStickyThread(threadId);
}
private void runOnLimiter(@NonNull Runnable runnable) {
Throwable prettyException = new Throwable();
limiter.run(() -> {

View file

@ -21,13 +21,11 @@ import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.core.app.RemoteInput;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -47,10 +45,10 @@ import java.util.List;
*/
public class RemoteReplyReceiver extends BroadcastReceiver {
public static final String TAG = Log.tag(RemoteReplyReceiver.class);
public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.WEAR_REPLY";
public static final String RECIPIENT_EXTRA = "recipient_extra";
public static final String REPLY_METHOD = "reply_method";
public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.WEAR_REPLY";
public static final String RECIPIENT_EXTRA = "recipient_extra";
public static final String REPLY_METHOD = "reply_method";
public static final String EARLIEST_TIMESTAMP = "earliest_timestamp";
@SuppressLint("StaticFieldLeak")
@Override
@ -109,6 +107,8 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
throw new AssertionError("Unknown Reply method");
}
ApplicationDependencies.getMessageNotifier().addStickyThread(threadId, intent.getLongExtra(EARLIEST_TIMESTAMP, System.currentTimeMillis()));
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId, true);
ApplicationDependencies.getMessageNotifier().updateNotification(context);

View file

@ -49,11 +49,13 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
@Volatile private var previousPrivacyPreference: NotificationPrivacyPreference = TextSecurePreferences.getNotificationPrivacy(context)
private val threadReminders: MutableMap<Long, Reminder> = ConcurrentHashMap()
private val stickyThreads: MutableMap<Long, StickyThread> = mutableMapOf()
private val executor = CancelableExecutor()
override fun setVisibleThread(threadId: Long) {
visibleThread = threadId
stickyThreads.remove(threadId)
}
override fun getVisibleThread(): Long {
@ -112,24 +114,31 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
return
}
val state: NotificationStateV2 = NotificationStateProvider.constructNotificationState(context)
Log.internal().i(TAG, state.toString())
if (state.isEmpty) {
Log.i(TAG, "State is empty, cancelling all notifications")
NotificationCancellationHelper.cancelAllMessageNotifications(context)
updateBadge(context, 0)
clearReminderInternal(context)
return
}
val currentLockStatus: Boolean = KeyCachingService.isLocked(context)
val currentPrivacyPreference: NotificationPrivacyPreference = TextSecurePreferences.getNotificationPrivacy(context)
val notificationConfigurationChanged: Boolean = currentLockStatus != previousLockedStatus || currentPrivacyPreference != previousPrivacyPreference
previousLockedStatus = currentLockStatus
previousPrivacyPreference = currentPrivacyPreference
if (notificationConfigurationChanged) {
stickyThreads.clear()
}
Log.internal().i(TAG, "sticky thread: $stickyThreads")
val state: NotificationStateV2 = NotificationStateProvider.constructNotificationState(context, stickyThreads)
Log.internal().i(TAG, "state: $state")
val retainStickyThreadIds: Set<Long> = state.getThreadsWithMostRecentNotificationFromSelf()
stickyThreads.keys.retainAll { retainStickyThreadIds.contains(it) }
if (state.isEmpty) {
Log.i(TAG, "State is empty, cancelling all notifications")
NotificationCancellationHelper.cancelAllMessageNotifications(context, stickyThreads.map { it.value.notificationId }.toSet())
updateBadge(context, 0)
clearReminderInternal(context)
return
}
val alertOverrides: Set<Long> = threadReminders.filter { (_, reminder) -> reminder.lastNotified < System.currentTimeMillis() - REMINDER_TIMEOUT }.keys
val threadsThatAlerted: Set<Long> = NotificationFactory.notify(
@ -147,7 +156,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
updateReminderTimestamps(context, alertOverrides, threadsThatAlerted)
ServiceUtil.getNotificationManager(context).cancelOrphanedNotifications(context, state)
ServiceUtil.getNotificationManager(context).cancelOrphanedNotifications(context, state, stickyThreads.map { it.value.notificationId }.toSet())
updateBadge(context, state.messageCount)
val smsIds: MutableList<Long> = mutableListOf()
@ -164,6 +173,18 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
Log.i(TAG, "threads: ${state.threadCount} messages: ${state.messageCount}")
}
override fun clearReminder(context: Context) {
// Intentionally left blank
}
override fun addStickyThread(threadId: Long, earliestTimestamp: Long) {
stickyThreads[threadId] = StickyThread(threadId, NotificationIds.getNotificationIdForThread(threadId), earliestTimestamp)
}
override fun removeStickyThread(threadId: Long) {
stickyThreads.remove(threadId)
}
private fun updateReminderTimestamps(context: Context, alertOverrides: Set<Long>, threadsThatAlerted: Set<Long>) {
if (TextSecurePreferences.getRepeatAlertsCount(context) == 0) {
return
@ -216,10 +237,6 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
alarmManager?.cancel(pendingIntent)
}
override fun clearReminder(context: Context) {
// Intentionally left blank
}
companion object {
private val TAG = Log.tag(MessageNotifierV2::class.java)
private val REMINDER_TIMEOUT = TimeUnit.MINUTES.toMillis(2)
@ -233,7 +250,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
}
}
private fun NotificationManager.cancelOrphanedNotifications(context: Context, state: NotificationStateV2) {
private fun NotificationManager.cancelOrphanedNotifications(context: Context, state: NotificationStateV2, stickyNotifications: Set<Int>) {
if (Build.VERSION.SDK_INT < 23) {
return
}
@ -244,7 +261,8 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
notification.id != KeyCachingService.SERVICE_RUNNING_ID &&
notification.id != IncomingMessageObserver.FOREGROUND_ID &&
notification.id != NotificationIds.PENDING_MESSAGES &&
!CallNotificationBuilder.isWebRtcNotification(notification.id)
!CallNotificationBuilder.isWebRtcNotification(notification.id) &&
!stickyNotifications.contains(notification.id)
) {
if (!state.notificationIds.contains(notification.id)) {
Log.d(TAG, "Cancelling orphaned notification: ${notification.id}")
@ -258,6 +276,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
}
}
data class StickyThread(val threadId: Long, val notificationId: Int, val earliestTimestamp: Long)
private data class Reminder(val lastNotified: Long, val count: Int = 0)
}

View file

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.notifications.v2
import android.annotation.TargetApi
import android.app.Notification
import android.app.PendingIntent
import android.app.Person
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
@ -15,7 +16,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.graphics.drawable.IconCompat
import org.thoughtcrime.securesms.R
@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.util.AvatarUtil
import org.thoughtcrime.securesms.util.BubbleUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import androidx.core.app.Person as PersonCompat
private const val BIG_PICTURE_DIMEN = 500
@ -241,12 +242,18 @@ sealed class NotificationBuilder(protected val context: Context) {
return
}
val messagingStyle: NotificationCompat.MessagingStyle = NotificationCompat.MessagingStyle(ConversationUtil.buildPersonCompat(context, Recipient.self()))
val self: PersonCompat = PersonCompat.Builder()
.setBot(false)
.setName(Recipient.self().getDisplayName(context))
.setIcon(Recipient.self().getContactDrawable(context).toLargeBitmap(context).toIconCompat())
.build()
val messagingStyle: NotificationCompat.MessagingStyle = NotificationCompat.MessagingStyle(self)
messagingStyle.conversationTitle = conversation.getConversationTitle(context)
messagingStyle.isGroupConversation = conversation.isGroup
conversation.notificationItems.forEach { notificationItem ->
val personBuilder: Person.Builder = Person.Builder()
val personBuilder: PersonCompat.Builder = PersonCompat.Builder()
.setBot(false)
.setName(notificationItem.getPersonName(context))
.setUri(notificationItem.getPersonUri(context))
@ -400,6 +407,7 @@ sealed class NotificationBuilder(protected val context: Context) {
override fun setWhen(timestamp: Long) {
builder.setWhen(timestamp)
builder.setShowWhen(true)
}
override fun setGroupSummary(isGroupSummary: Boolean) {
@ -480,12 +488,18 @@ sealed class NotificationBuilder(protected val context: Context) {
return
}
val messagingStyle: Notification.MessagingStyle = Notification.MessagingStyle(ConversationUtil.buildPerson(context, Recipient.self()))
val self: Person = Person.Builder()
.setBot(false)
.setName(Recipient.self().getDisplayName(context))
.setIcon(Recipient.self().getContactDrawable(context).toLargeBitmap(context).toIcon())
.build()
val messagingStyle: Notification.MessagingStyle = Notification.MessagingStyle(self)
messagingStyle.conversationTitle = conversation.getConversationTitle(context)
messagingStyle.isGroupConversation = conversation.isGroup
conversation.notificationItems.forEach { notificationItem ->
val personBuilder: android.app.Person.Builder = android.app.Person.Builder()
val personBuilder: Person.Builder = Person.Builder()
.setBot(false)
.setName(notificationItem.getPersonName(context))
.setUri(notificationItem.getPersonUri(context))
@ -623,6 +637,7 @@ sealed class NotificationBuilder(protected val context: Context) {
override fun setWhen(timestamp: Long) {
builder.setWhen(timestamp)
builder.setShowWhen(true)
}
override fun setGroupSummary(isGroupSummary: Boolean) {

View file

@ -34,10 +34,8 @@ private const val LARGE_ICON_DIMEN = 250
class NotificationConversation(
val recipient: Recipient,
val threadId: Long,
unsortedNotificationItems: List<NotificationItemV2>
val notificationItems: List<NotificationItemV2>
) {
val notificationItems: List<NotificationItemV2> = unsortedNotificationItems.sorted()
val mostRecentNotification: NotificationItemV2 = notificationItems.last()
val notificationId: Int = NotificationIds.getNotificationIdForThread(threadId)
val sortKey: Long = Long.MAX_VALUE - mostRecentNotification.timestamp
@ -146,18 +144,18 @@ class NotificationConversation(
}
fun getDeleteIntent(context: Context): PendingIntent? {
var index = 0
val ids = LongArray(notificationItems.size)
val mms = BooleanArray(ids.size)
notificationItems.forEach { notificationItem ->
notificationItems.forEachIndexed { index, notificationItem ->
ids[index] = notificationItem.id
mms[index++] = notificationItem.isMms
mms[index] = notificationItem.isMms
}
val intent = Intent(context, DeleteNotificationReceiver::class.java)
.setAction(DeleteNotificationReceiver.DELETE_NOTIFICATION_ACTION)
.putExtra(DeleteNotificationReceiver.EXTRA_IDS, ids)
.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms)
.putExtra(DeleteNotificationReceiver.EXTRA_THREAD_IDS, longArrayOf(threadId))
.makeUniqueToPreventMerging()
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
@ -185,6 +183,7 @@ class NotificationConversation(
.setAction(RemoteReplyReceiver.REPLY_ACTION)
.putExtra(RemoteReplyReceiver.RECIPIENT_EXTRA, recipient.id)
.putExtra(RemoteReplyReceiver.REPLY_METHOD, replyMethod)
.putExtra(RemoteReplyReceiver.EARLIEST_TIMESTAMP, notificationItems.first().timestamp)
.setPackage(context.packageName)
.makeUniqueToPreventMerging()

View file

@ -9,6 +9,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
@ -26,8 +28,8 @@ fun Drawable?.toLargeBitmap(context: Context): Bitmap? {
}
fun Recipient.getContactDrawable(context: Context): Drawable? {
val contactPhoto: ContactPhoto? = contactPhoto
val fallbackContactPhoto: FallbackContactPhoto = fallbackContactPhoto
val contactPhoto: ContactPhoto? = if (isSelf) ProfileContactPhoto(this, profileAvatar) else contactPhoto
val fallbackContactPhoto: FallbackContactPhoto = if (isSelf) getFallback(context) else fallbackContactPhoto
return if (contactPhoto != null) {
try {
GlideApp.with(context.applicationContext)
@ -67,3 +69,7 @@ fun Uri.toBitmap(context: Context, dimension: Int): Bitmap {
fun Intent.makeUniqueToPreventMerging(): Intent {
return setData((Uri.parse("custom://" + System.currentTimeMillis())))
}
fun Recipient.getFallback(context: Context): FallbackContactPhoto {
return GeneratedContactPhoto(getDisplayName(context), R.drawable.ic_profile_outline_40)
}

View file

@ -69,7 +69,7 @@ object NotificationFactory {
conversation = conversation,
targetThreadId = targetThreadId,
defaultBubbleState = defaultBubbleState,
shouldAlert = conversation.hasNewNotifications() || alertOverrides.contains(conversation.threadId)
shouldAlert = (conversation.hasNewNotifications() || alertOverrides.contains(conversation.threadId)) && !conversation.mostRecentNotification.individualRecipient.isSelf
)
}
}

View file

@ -150,7 +150,7 @@ sealed class NotificationItemV2(val threadRecipient: Recipient, protected val re
*/
class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : NotificationItemV2(threadRecipient, record) {
override val timestamp: Long = record.timestamp
override val individualRecipient: Recipient = record.individualRecipient.resolve()
override val individualRecipient: Recipient = if (record.isOutgoing) Recipient.self() else record.individualRecipient.resolve()
override val isNewNotification: Boolean = notifiedTimestamp == 0L
override fun getPrimaryTextActual(context: Context): CharSequence {

View file

@ -17,10 +17,10 @@ import org.thoughtcrime.securesms.util.CursorUtil
object NotificationStateProvider {
@WorkerThread
fun constructNotificationState(context: Context): NotificationStateV2 {
fun constructNotificationState(context: Context, stickyThreads: Map<Long, MessageNotifierV2.StickyThread>): NotificationStateV2 {
val messages: MutableList<NotificationMessage> = mutableListOf()
DatabaseFactory.getMmsSmsDatabase(context).unread.use { unreadMessages ->
DatabaseFactory.getMmsSmsDatabase(context).getMessagesForNotificationState(stickyThreads.values).use { unreadMessages ->
if (unreadMessages.count == 0) {
return NotificationStateV2.EMPTY
}
@ -32,6 +32,7 @@ object NotificationStateProvider {
messageRecord = record,
threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(record.threadId)?.resolve() ?: Recipient.UNKNOWN,
threadId = record.threadId,
stickyThread = stickyThreads.containsKey(record.threadId),
isUnreadMessage = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.READ) == 0,
hasUnreadReactions = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.REACTIONS_UNREAD) == 1,
lastReactionRead = CursorUtil.requireLong(unreadMessages, MmsSmsColumns.REACTIONS_LAST_SEEN)
@ -44,19 +45,25 @@ object NotificationStateProvider {
val conversations: MutableList<NotificationConversation> = mutableListOf()
messages.groupBy { it.threadId }
.forEach { (threadId, threadMessages) ->
val notificationItems: MutableList<NotificationItemV2> = mutableListOf()
for (notification: NotificationMessage in threadMessages) {
var notificationItems: MutableList<NotificationItemV2> = mutableListOf()
for (notification: NotificationMessage in threadMessages) {
if (notification.includeMessage()) {
notificationItems += MessageNotification(notification.threadRecipient, notification.messageRecord)
notificationItems.add(MessageNotification(notification.threadRecipient, notification.messageRecord))
}
if (notification.hasUnreadReactions) {
notification.messageRecord.reactions.filter { notification.includeReaction(it) }
.forEach { notificationItems += ReactionNotification(notification.threadRecipient, notification.messageRecord, it) }
.forEach { notificationItems.add(ReactionNotification(notification.threadRecipient, notification.messageRecord, it)) }
}
}
notificationItems.sort()
if (notificationItems.isNotEmpty() && stickyThreads.containsKey(threadId) && !notificationItems.last().individualRecipient.isSelf) {
val indexOfOldestNonSelfMessage: Int = notificationItems.indexOfLast { it.individualRecipient.isSelf } + 1
notificationItems = notificationItems.slice(indexOfOldestNonSelfMessage..notificationItems.lastIndex).toMutableList()
}
if (notificationItems.isNotEmpty()) {
conversations += NotificationConversation(notificationItems[0].threadRecipient, threadId, notificationItems)
}
@ -69,14 +76,16 @@ object NotificationStateProvider {
val messageRecord: MessageRecord,
val threadRecipient: Recipient,
val threadId: Long,
val stickyThread: Boolean,
val isUnreadMessage: Boolean,
val hasUnreadReactions: Boolean,
val lastReactionRead: Long
) {
private val isUnreadIncoming: Boolean = isUnreadMessage && !messageRecord.isOutgoing
private val unknownOrNotMutedThread: Boolean = threadRecipient == Recipient.UNKNOWN || threadRecipient.isNotMuted
fun includeMessage(): Boolean {
return isUnreadMessage && (unknownOrNotMutedThread || (threadRecipient.isAlwaysNotifyMentions && messageRecord.hasSelfMention()))
return (isUnreadIncoming || stickyThread) && (unknownOrNotMutedThread || (threadRecipient.isAlwaysNotifyMentions && messageRecord.hasSelfMention()))
}
fun includeReaction(reaction: ReactionRecord): Boolean {

View file

@ -42,8 +42,10 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
fun getDeleteIntent(context: Context): PendingIntent? {
val ids = LongArray(messageCount)
val mms = BooleanArray(ids.size)
val threadIds: MutableList<Long> = mutableListOf()
conversations.forEach { conversation ->
threadIds += conversation.threadId
conversation.notificationItems.forEachIndexed { index, notificationItem ->
ids[index] = notificationItem.id
mms[index] = notificationItem.isMms
@ -54,6 +56,7 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
.setAction(DeleteNotificationReceiver.DELETE_NOTIFICATION_ACTION)
.putExtra(DeleteNotificationReceiver.EXTRA_IDS, ids)
.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms)
.putExtra(DeleteNotificationReceiver.EXTRA_THREAD_IDS, threadIds.toLongArray())
.makeUniqueToPreventMerging()
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
@ -74,6 +77,12 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun getThreadsWithMostRecentNotificationFromSelf(): Set<Long> {
return conversations.filter { it.mostRecentNotification.individualRecipient.isSelf }
.map { it.threadId }
.toSet()
}
companion object {
val EMPTY = NotificationStateV2(emptyList())
}

View file

@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.preferences.widgets;
import androidx.annotation.NonNull;
public class NotificationPrivacyPreference {
import java.util.Objects;
public final class NotificationPrivacyPreference {
private final String preference;
@ -26,4 +28,17 @@ public class NotificationPrivacyPreference {
public @NonNull String toString() {
return preference;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final NotificationPrivacyPreference that = (NotificationPrivacyPreference) o;
return Objects.equals(preference, that.preference);
}
@Override
public int hashCode() {
return Objects.hash(preference);
}
}