diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 8b7cfdcdc7..08b5f93924 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -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 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 9bfbf08798..bee9792729 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java index 7d961ea434..e90ac4fca5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java @@ -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() { - @Override - protected Void doInBackground(Void... params) { - for (int i=0;i { + for (int i = 0; i < ids.length; i++) { + if (!mms[i]) { + DatabaseFactory.getSmsDatabase(context).markAsNotified(ids[i]); + } else { + DatabaseFactory.getMmsDatabase(context).markAsNotified(ids[i]); + } + } + }); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index 69afd6c07b..707bdf3efc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -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(() -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java index 1c17129f11..138c995804 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java index ab04f95db5..802efdd41d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java @@ -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 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--; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java index 9f85e290c8..a77381345d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -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(() -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 708ad73e39..599f057a8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -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 messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId, true); ApplicationDependencies.getMessageNotifier().updateNotification(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt index 99750c1fb8..c15ff264d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt @@ -49,11 +49,13 @@ class MessageNotifierV2(context: Application) : MessageNotifier { @Volatile private var previousPrivacyPreference: NotificationPrivacyPreference = TextSecurePreferences.getNotificationPrivacy(context) private val threadReminders: MutableMap = ConcurrentHashMap() + private val stickyThreads: MutableMap = 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 = 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 = threadReminders.filter { (_, reminder) -> reminder.lastNotified < System.currentTimeMillis() - REMINDER_TIMEOUT }.keys val threadsThatAlerted: Set = 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 = 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, threadsThatAlerted: Set) { 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) { 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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt index 2fae2a8450..a1d82c5c8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt index 6cf4120d89..3e07b5d645 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt @@ -34,10 +34,8 @@ private const val LARGE_ICON_DIMEN = 250 class NotificationConversation( val recipient: Recipient, val threadId: Long, - unsortedNotificationItems: List + val notificationItems: List ) { - - val notificationItems: List = 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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationExtensions.kt index 52d391833c..7847900cba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationExtensions.kt @@ -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) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt index ec76d51078..9b3298f280 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt @@ -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 ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt index 99decb8dc0..0da8c7a929 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt index 3f9d4e7d28..6e9428e4a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt @@ -17,10 +17,10 @@ import org.thoughtcrime.securesms.util.CursorUtil object NotificationStateProvider { @WorkerThread - fun constructNotificationState(context: Context): NotificationStateV2 { + fun constructNotificationState(context: Context, stickyThreads: Map): NotificationStateV2 { val messages: MutableList = 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 = mutableListOf() messages.groupBy { it.threadId } .forEach { (threadId, threadMessages) -> - val notificationItems: MutableList = mutableListOf() - for (notification: NotificationMessage in threadMessages) { + var notificationItems: MutableList = 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt index a382101284..1469a23d9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt @@ -42,8 +42,10 @@ data class NotificationStateV2(val conversations: List fun getDeleteIntent(context: Context): PendingIntent? { val ids = LongArray(messageCount) val mms = BooleanArray(ids.size) + val threadIds: MutableList = 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 .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 return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } + fun getThreadsWithMostRecentNotificationFromSelf(): Set { + return conversations.filter { it.mostRecentNotification.individualRecipient.isSelf } + .map { it.threadId } + .toSet() + } + companion object { val EMPTY = NotificationStateV2(emptyList()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationPrivacyPreference.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationPrivacyPreference.java index 0212b7a073..5e1a722315 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationPrivacyPreference.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationPrivacyPreference.java @@ -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); + } }