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 29090adbaa..a098960375 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; +import org.thoughtcrime.securesms.notifications.v2.DefaultMessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.signal.core.util.CursorUtil; @@ -251,9 +251,9 @@ public class MmsSmsDatabase extends Database { } } - public Cursor getMessagesForNotificationState(Collection stickyThreads) { + public Cursor getMessagesForNotificationState(Collection stickyThreads) { StringBuilder stickyQuery = new StringBuilder(); - for (MessageNotifierV2.StickyThread stickyThread : stickyThreads) { + for (DefaultMessageNotifier.StickyThread stickyThread : stickyThreads) { if (stickyQuery.length() > 0) { stickyQuery.append(" OR "); } 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 f63aa021c5..8c32d75112 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java @@ -14,7 +14,7 @@ import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; +import org.thoughtcrime.securesms.notifications.v2.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.BubbleUtil; @@ -47,7 +47,7 @@ public final class NotificationCancellationHelper { /** * Cancels all Message-Based notifications. Specifically, this is any notification that is not the - * summary notification assigned to the {@link MessageNotifierV2#NOTIFICATION_GROUP} group. + * summary notification assigned to the {@link DefaultMessageNotifier#NOTIFICATION_GROUP} group. * * 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. @@ -113,7 +113,7 @@ public final class NotificationCancellationHelper { @RequiresApi(23) private static boolean isSingleThreadNotification(@NonNull StatusBarNotification statusBarNotification) { return statusBarNotification.getId() != NotificationIds.MESSAGE_SUMMARY && - Objects.equals(statusBarNotification.getNotification().getGroup(), MessageNotifierV2.NOTIFICATION_GROUP); + Objects.equals(statusBarNotification.getNotification().getGroup(), DefaultMessageNotifier.NOTIFICATION_GROUP); } /** 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 1d4b185517..45bca92165 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -10,7 +10,7 @@ import androidx.annotation.Nullable; import org.signal.core.util.ExceptionUtil; import org.signal.core.util.concurrent.SignalExecutors; -import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; +import org.thoughtcrime.securesms.notifications.v2.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.BubbleUtil; @@ -23,13 +23,13 @@ import java.util.Optional; */ public class OptimizedMessageNotifier implements MessageNotifier { - private final LeakyBucketLimiter limiter; - private final MessageNotifierV2 messageNotifierV2; + private final LeakyBucketLimiter limiter; + private final DefaultMessageNotifier defaultMessageNotifier; @MainThread public OptimizedMessageNotifier(@NonNull Application context) { - this.limiter = new LeakyBucketLimiter(5, 1000, new Handler(SignalExecutors.getAndStartHandlerThread("signal-notifier").getLooper())); - this.messageNotifierV2 = new MessageNotifierV2(context); + this.limiter = new LeakyBucketLimiter(5, 1000, new Handler(SignalExecutors.getAndStartHandlerThread("signal-notifier").getLooper())); + this.defaultMessageNotifier = new DefaultMessageNotifier(context); } @Override @@ -119,6 +119,6 @@ public class OptimizedMessageNotifier implements MessageNotifier { } private MessageNotifier getNotifier() { - return messageNotifierV2; + return defaultMessageNotifier; } } 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 4908192508..63f2b91c5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.database.model.ParentStoryId; import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; +import org.thoughtcrime.securesms.notifications.v2.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -67,7 +67,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver { final RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA); final ReplyMethod replyMethod = (ReplyMethod) intent.getSerializableExtra(REPLY_METHOD); - final CharSequence responseText = remoteInput.getCharSequence(MessageNotifierV2.EXTRA_REMOTE_REPLY); + final CharSequence responseText = remoteInput.getCharSequence(DefaultMessageNotifier.EXTRA_REMOTE_REPLY); final long groupStoryId = intent.getLongExtra(GROUP_STORY_ID_EXTRA, Long.MIN_VALUE); if (recipientId == null) throw new AssertionError("No recipientId specified"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt index 5cf090e59a..3c4e66bce8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt @@ -44,14 +44,14 @@ import kotlin.math.max /** * MessageNotifier implementation using the new system for creating and showing notifications. */ -class MessageNotifierV2(context: Application) : MessageNotifier { +class DefaultMessageNotifier(context: Application) : MessageNotifier { @Volatile private var visibleThread: ConversationId? = null @Volatile private var lastDesktopActivityTimestamp: Long = -1 @Volatile private var lastAudibleNotification: Long = -1 @Volatile private var lastScheduledReminder: Long = 0 @Volatile private var previousLockedStatus: Boolean = KeyCachingService.isLocked(context) @Volatile private var previousPrivacyPreference: NotificationPrivacyPreference = SignalStore.settings().messageNotificationsPrivacy - @Volatile private var previousState: NotificationStateV2 = NotificationStateV2.EMPTY + @Volatile private var previousState: NotificationState = NotificationState.EMPTY private val threadReminders: MutableMap = ConcurrentHashMap() private val stickyThreads: MutableMap = mutableMapOf() @@ -132,7 +132,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier { val notificationProfile: NotificationProfile? = NotificationProfiles.getActiveProfile(SignalDatabase.notificationProfiles.getProfiles()) Log.internal().i(TAG, "sticky thread: $stickyThreads active profile: ${notificationProfile?.id ?: "none" }") - var state: NotificationStateV2 = NotificationStateProvider.constructNotificationState(stickyThreads, notificationProfile) + var state: NotificationState = NotificationStateProvider.constructNotificationState(stickyThreads, notificationProfile) Log.internal().i(TAG, "state: $state") if (state.muteFilteredMessages.isNotEmpty()) { @@ -208,13 +208,14 @@ class MessageNotifierV2(context: Application) : MessageNotifier { lastAudibleNotification = System.currentTimeMillis() updateReminderTimestamps(context, alertOverrides, threadsThatAlerted) + NotificationThumbnails.removeAllExcept(state.notificationItems) ServiceUtil.getNotificationManager(context).cancelOrphanedNotifications(context, state, stickyThreads.map { it.value.notificationId }.toSet()) updateBadge(context, state.messageCount) val smsIds: MutableList = mutableListOf() val mmsIds: MutableList = mutableListOf() - for (item: NotificationItemV2 in state.notificationItems) { + for (item: NotificationItem in state.notificationItems) { if (item.isMms) { mmsIds.add(item.id) } else { @@ -301,7 +302,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier { } companion object { - val TAG: String = Log.tag(MessageNotifierV2::class.java) + val TAG: String = Log.tag(DefaultMessageNotifier::class.java) private val REMINDER_TIMEOUT: Long = TimeUnit.MINUTES.toMillis(2) val MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2) @@ -339,12 +340,12 @@ private fun NotificationManager.getDisplayedNotificationIds(): Result> return try { Result.success(activeNotifications.filter { it.isMessageNotification() }.map { it.id }.toSet()) } catch (e: Throwable) { - Log.w(MessageNotifierV2.TAG, e) + Log.w(DefaultMessageNotifier.TAG, e) Result.failure(e) } } -private fun NotificationManager.cancelOrphanedNotifications(context: Context, state: NotificationStateV2, stickyNotifications: Set) { +private fun NotificationManager.cancelOrphanedNotifications(context: Context, state: NotificationState, stickyNotifications: Set) { if (Build.VERSION.SDK_INT < 24) { return } @@ -354,13 +355,13 @@ private fun NotificationManager.cancelOrphanedNotifications(context: Context, st .map { it.id } .filterNot { state.notificationIds.contains(it) } .forEach { id -> - Log.d(MessageNotifierV2.TAG, "Cancelling orphaned notification: $id") + Log.d(DefaultMessageNotifier.TAG, "Cancelling orphaned notification: $id") NotificationCancellationHelper.cancel(context, id) } NotificationCancellationHelper.cancelMessageSummaryIfSoleNotification(context) } catch (e: Throwable) { - Log.w(MessageNotifierV2.TAG, e) + Log.w(DefaultMessageNotifier.TAG, e) } } 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 1499e5c45c..13bc228a99 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 @@ -65,7 +65,7 @@ sealed class NotificationBuilder(protected val context: Context) { abstract fun setOnlyAlertOnce(onlyAlertOnce: Boolean) abstract fun setGroupSummary(isGroupSummary: Boolean) abstract fun setSubText(subText: String) - abstract fun addMarkAsReadActionActual(state: NotificationStateV2) + abstract fun addMarkAsReadActionActual(state: NotificationState) abstract fun setPriority(priority: Int) abstract fun setAlarms(recipient: Recipient?) abstract fun setTicker(ticker: CharSequence?) @@ -79,7 +79,7 @@ sealed class NotificationBuilder(protected val context: Context) { protected abstract fun setWhen(timestamp: Long) protected abstract fun addActions(replyMethod: ReplyMethod, conversation: NotificationConversation) protected abstract fun addMessagesActual(conversation: NotificationConversation, includeShortcut: Boolean) - protected abstract fun addMessagesActual(state: NotificationStateV2) + protected abstract fun addMessagesActual(state: NotificationState) protected abstract fun setBubbleMetadataActual(conversation: NotificationConversation, bubbleState: BubbleUtil.BubbleState) protected abstract fun setLights(@ColorInt color: Int, onTime: Int, offTime: Int) @@ -107,7 +107,7 @@ sealed class NotificationBuilder(protected val context: Context) { } } - fun setWhen(notificationItem: NotificationItemV2?) { + fun setWhen(notificationItem: NotificationItem?) { if (notificationItem != null && notificationItem.timestamp != 0L) { setWhen(notificationItem.timestamp) } @@ -126,7 +126,7 @@ sealed class NotificationBuilder(protected val context: Context) { } } - fun addMarkAsReadAction(state: NotificationStateV2) { + fun addMarkAsReadAction(state: NotificationState) { if (privacy.isDisplayMessage && isNotLocked) { addMarkAsReadActionActual(state) } @@ -136,7 +136,7 @@ sealed class NotificationBuilder(protected val context: Context) { addMessagesActual(conversation, privacy.isDisplayContact) } - fun addMessages(state: NotificationStateV2) { + fun addMessages(state: NotificationState) { if (privacy.isDisplayNothing) { return } @@ -207,7 +207,7 @@ sealed class NotificationBuilder(protected val context: Context) { val label: String = context.getString(replyMethod.toLongDescription()) val replyAction: NotificationCompat.Action = if (Build.VERSION.SDK_INT >= 24) { NotificationCompat.Action.Builder(R.drawable.ic_reply_white_36dp, actionName, remoteReply) - .addRemoteInput(RemoteInput.Builder(MessageNotifierV2.EXTRA_REMOTE_REPLY).setLabel(label).build()) + .addRemoteInput(RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build()) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) .setShowsUserInterface(false) .build() @@ -216,7 +216,7 @@ sealed class NotificationBuilder(protected val context: Context) { } val wearableReplyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, actionName, remoteReply) - .addRemoteInput(RemoteInput.Builder(MessageNotifierV2.EXTRA_REMOTE_REPLY).setLabel(label).build()) + .addRemoteInput(RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build()) .build() builder.addAction(replyAction) @@ -226,7 +226,7 @@ sealed class NotificationBuilder(protected val context: Context) { builder.extend(extender) } - override fun addMarkAsReadActionActual(state: NotificationStateV2) { + override fun addMarkAsReadActionActual(state: NotificationState) { val markAllAsReadAction = NotificationCompat.Action(R.drawable.check, context.getString(R.string.MessageNotifier_mark_all_as_read), state.getMarkAsReadIntent(context)) builder.addAction(markAllAsReadAction) builder.extend(NotificationCompat.WearableExtender().addAction(markAllAsReadAction)) @@ -286,14 +286,14 @@ sealed class NotificationBuilder(protected val context: Context) { builder.setStyle(messagingStyle) } - override fun addMessagesActual(state: NotificationStateV2) { + override fun addMessagesActual(state: NotificationState) { if (Build.VERSION.SDK_INT >= 24) { return } val style: NotificationCompat.InboxStyle = NotificationCompat.InboxStyle() - for (notificationItem: NotificationItemV2 in state.notificationItems) { + for (notificationItem: NotificationItem in state.notificationItems) { val line: CharSequence? = notificationItem.getInboxLine(context) if (line != null) { style.addLine(line) 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 f7db79bd13..727d06b06b 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 @@ -36,9 +36,9 @@ import java.lang.NullPointerException data class NotificationConversation( val recipient: Recipient, val thread: ConversationId, - val notificationItems: List + val notificationItems: List ) { - val mostRecentNotification: NotificationItemV2 = notificationItems.last() + val mostRecentNotification: NotificationItem = notificationItems.last() val notificationId: Int = NotificationIds.getNotificationIdForThread(thread) val sortKey: Long = Long.MAX_VALUE - mostRecentNotification.timestamp val messageCount: Int = notificationItems.size 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 4fcd0d3365..91fa26c7e0 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 @@ -37,18 +37,18 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences */ object NotificationFactory { - val TAG = Log.tag(NotificationFactory::class.java) + val TAG: String = Log.tag(NotificationFactory::class.java) fun notify( context: Context, - state: NotificationStateV2, + state: NotificationState, visibleThread: ConversationId?, targetThread: ConversationId?, defaultBubbleState: BubbleUtil.BubbleState, lastAudibleNotification: Long, notificationConfigurationChanged: Boolean, alertOverrides: Set, - previousState: NotificationStateV2 + previousState: NotificationState ): Set { if (state.isEmpty) { Log.d(TAG, "State is empty, bailing") @@ -85,7 +85,7 @@ object NotificationFactory { private fun notify19( context: Context, - state: NotificationStateV2, + state: NotificationState, visibleThread: ConversationId?, targetThread: ConversationId?, defaultBubbleState: BubbleUtil.BubbleState, @@ -127,7 +127,7 @@ object NotificationFactory { @TargetApi(24) private fun notify24( context: Context, - state: NotificationStateV2, + state: NotificationState, visibleThread: ConversationId?, targetThread: ConversationId?, defaultBubbleState: BubbleUtil.BubbleState, @@ -135,7 +135,7 @@ object NotificationFactory { notificationConfigurationChanged: Boolean, alertOverrides: Set, nonVisibleThreadCount: Int, - previousState: NotificationStateV2 + previousState: NotificationState ): Set { val threadsThatNewlyAlerted: MutableSet = mutableSetOf() @@ -186,7 +186,7 @@ object NotificationFactory { setSmallIcon(R.drawable.ic_notification) setColor(ContextCompat.getColor(context, R.color.core_ultramarine)) setCategory(NotificationCompat.CATEGORY_MESSAGE) - setGroup(MessageNotifierV2.NOTIFICATION_GROUP) + setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) setChannelId(conversation.getChannelId(context)) setContentTitle(conversation.getContentTitle(context)) @@ -224,7 +224,7 @@ object NotificationFactory { NotificationManagerCompat.from(context).safelyNotify(context, conversation.recipient, notificationId, builder.build()) } - private fun notifySummary(context: Context, state: NotificationStateV2) { + private fun notifySummary(context: Context, state: NotificationState) { if (state.messageCount == 0) { return } @@ -235,7 +235,7 @@ object NotificationFactory { setSmallIcon(R.drawable.ic_notification) setColor(ContextCompat.getColor(context, R.color.core_ultramarine)) setCategory(NotificationCompat.CATEGORY_MESSAGE) - setGroup(MessageNotifierV2.NOTIFICATION_GROUP) + setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) setChannelId(NotificationChannels.getMessagesChannel(context)) setContentTitle(context.getString(R.string.app_name)) @@ -263,7 +263,7 @@ object NotificationFactory { private fun notifyInThread(context: Context, recipient: Recipient, lastAudibleNotification: Long) { if (!SignalStore.settings().isMessageNotificationsInChatSoundsEnabled || ServiceUtil.getAudioManager(context).ringerMode != AudioManager.RINGER_MODE_NORMAL || - (System.currentTimeMillis() - lastAudibleNotification) < MessageNotifierV2.MIN_AUDIBLE_PERIOD_MILLIS + (System.currentTimeMillis() - lastAudibleNotification) < DefaultMessageNotifier.MIN_AUDIBLE_PERIOD_MILLIS ) { return } @@ -378,7 +378,7 @@ object NotificationFactory { setSmallIcon(R.drawable.ic_notification) setColor(ContextCompat.getColor(context, R.color.core_ultramarine)) setCategory(NotificationCompat.CATEGORY_MESSAGE) - setGroup(MessageNotifierV2.NOTIFICATION_GROUP) + setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP) setChannelId(conversation.getChannelId(context)) setContentTitle(conversation.getContentTitle(context)) setLargeIcon(conversation.getContactLargeIcon(context).toLargeBitmap(context)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt index 92ce7cc5e3..6beebcda22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt @@ -30,14 +30,14 @@ import org.thoughtcrime.securesms.util.hasSharedContact import org.thoughtcrime.securesms.util.hasSticker import org.thoughtcrime.securesms.util.isMediaMessage -private val TAG: String = Log.tag(NotificationItemV2::class.java) +private val TAG: String = Log.tag(NotificationItem::class.java) private const val EMOJI_REPLACEMENT_STRING = "__EMOJI__" private const val MAX_DISPLAY_LENGTH = 500 /** * Base for messaged-based notifications. Represents a single notification. */ -sealed class NotificationItemV2(val threadRecipient: Recipient, protected val record: MessageRecord) : Comparable { +sealed class NotificationItem(val threadRecipient: Recipient, protected val record: MessageRecord) : Comparable { val id: Long = record.id val thread = ConversationId.fromMessageRecord(record) @@ -104,7 +104,7 @@ sealed class NotificationItemV2(val threadRecipient: Recipient, protected val re } } - override fun compareTo(other: NotificationItemV2): Int { + override fun compareTo(other: NotificationItem): Int { return timestamp.compareTo(other.timestamp) } @@ -143,7 +143,7 @@ sealed class NotificationItemV2(val threadRecipient: Recipient, protected val re } } - fun hasSameContent(other: NotificationItemV2): Boolean { + open fun hasSameContent(other: NotificationItem): Boolean { return timestamp == other.timestamp && id == other.id && isMms == other.isMms && @@ -162,17 +162,23 @@ sealed class NotificationItemV2(val threadRecipient: Recipient, protected val re } } - data class ThumbnailInfo(val uri: Uri? = null, val contentType: String? = null) + data class ThumbnailInfo(val uri: Uri? = null, val contentType: String? = null) { + companion object { + val NONE = ThumbnailInfo() + } + } } /** * Represents a notification associated with a new message. */ -class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : NotificationItemV2(threadRecipient, record) { +class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : NotificationItem(threadRecipient, record) { override val timestamp: Long = record.timestamp override val individualRecipient: Recipient = if (record.isOutgoing) Recipient.self() else record.individualRecipient.resolve() override val isNewNotification: Boolean = notifiedTimestamp == 0L + private var thumbnailInfo: ThumbnailInfo? = null + override fun getPrimaryTextActual(context: Context): CharSequence { return if (KeyCachingService.isLocked(context)) { SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)) @@ -221,12 +227,15 @@ class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : N } override fun getThumbnailInfo(context: Context): ThumbnailInfo { - return if (SignalStore.settings().messageNotificationsPrivacy.isDisplayMessage && !KeyCachingService.isLocked(context)) { - val thumbnailSlide: Slide? = slideDeck?.thumbnailSlide - ThumbnailInfo(thumbnailSlide?.publicUri, thumbnailSlide?.contentType) - } else { - ThumbnailInfo() + if (thumbnailInfo == null) { + thumbnailInfo = if (SignalStore.settings().messageNotificationsPrivacy.isDisplayMessage && !KeyCachingService.isLocked(context)) { + NotificationThumbnails.get(context, this) + } else { + ThumbnailInfo() + } } + + return thumbnailInfo!! } override fun canReply(context: Context): Boolean { @@ -246,6 +255,10 @@ class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : N return true } + override fun hasSameContent(other: NotificationItem): Boolean { + return super.hasSameContent(other) && thumbnailInfo == (other as? MessageNotification)?.thumbnailInfo + } + override fun toString(): String { return "MessageNotification(timestamp=$timestamp, isNewNotification=$isNewNotification)" } @@ -254,7 +267,7 @@ class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : N /** * Represents a notification associated with a new reaction. */ -class ReactionNotification(threadRecipient: Recipient, record: MessageRecord, val reaction: ReactionRecord) : NotificationItemV2(threadRecipient, record) { +class ReactionNotification(threadRecipient: Recipient, record: MessageRecord, val reaction: ReactionRecord) : NotificationItem(threadRecipient, record) { override val timestamp: Long = reaction.dateReceived override val individualRecipient: Recipient = Recipient.resolved(reaction.author) override val isNewNotification: Boolean = timestamp > notifiedTimestamp diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationState.kt similarity index 89% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationState.kt index 6749f4edc8..6b615cd4a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationState.kt @@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.recipients.Recipient /** * Hold all state for notifications for all conversations. */ -data class NotificationStateV2(val conversations: List, val muteFilteredMessages: List, val profileFilteredMessages: List) { +data class NotificationState(val conversations: List, val muteFilteredMessages: List, val profileFilteredMessages: List) { val threadCount: Int = conversations.size val isEmpty: Boolean = conversations.isEmpty() @@ -22,7 +22,7 @@ data class NotificationStateV2(val conversations: List } } - val notificationItems: List by lazy { + val notificationItems: List by lazy { conversations.map { it.notificationItems } .flatten() .sorted() @@ -33,7 +33,7 @@ data class NotificationStateV2(val conversations: List .toSet() } - val mostRecentNotification: NotificationItemV2? + val mostRecentNotification: NotificationItem? get() = notificationItems.lastOrNull() val mostRecentSender: Recipient? @@ -88,6 +88,6 @@ data class NotificationStateV2(val conversations: List data class FilteredMessage(val id: Long, val isMms: Boolean) companion object { - val EMPTY = NotificationStateV2(emptyList(), emptyList(), emptyList()) + val EMPTY = NotificationState(emptyList(), emptyList(), emptyList()) } } 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 c4a11f2880..eb55203793 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 @@ -22,12 +22,12 @@ object NotificationStateProvider { private val TAG = Log.tag(NotificationStateProvider::class.java) @WorkerThread - fun constructNotificationState(stickyThreads: Map, notificationProfile: NotificationProfile?): NotificationStateV2 { + fun constructNotificationState(stickyThreads: Map, notificationProfile: NotificationProfile?): NotificationState { val messages: MutableList = mutableListOf() SignalDatabase.mmsSms.getMessagesForNotificationState(stickyThreads.values).use { unreadMessages -> if (unreadMessages.count == 0) { - return NotificationStateV2.EMPTY + return NotificationState.EMPTY } MmsSmsDatabase.readerFor(unreadMessages).use { reader -> @@ -71,19 +71,19 @@ object NotificationStateProvider { } val conversations: MutableList = mutableListOf() - val muteFilteredMessages: MutableList = mutableListOf() - val profileFilteredMessages: MutableList = mutableListOf() + val muteFilteredMessages: MutableList = mutableListOf() + val profileFilteredMessages: MutableList = mutableListOf() messages.groupBy { it.thread } .forEach { (thread, threadMessages) -> - var notificationItems: MutableList = mutableListOf() + var notificationItems: MutableList = mutableListOf() for (notification: NotificationMessage in threadMessages) { when (notification.includeMessage(notificationProfile)) { MessageInclusion.INCLUDE -> notificationItems.add(MessageNotification(notification.threadRecipient, notification.messageRecord)) MessageInclusion.EXCLUDE -> Unit - MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) - MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) + MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) + MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) } if (notification.hasUnreadReactions) { @@ -91,8 +91,8 @@ object NotificationStateProvider { when (notification.includeReaction(it, notificationProfile)) { MessageInclusion.INCLUDE -> notificationItems.add(ReactionNotification(notification.threadRecipient, notification.messageRecord, it)) MessageInclusion.EXCLUDE -> Unit - MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) - MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) + MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) + MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) } } } @@ -109,7 +109,7 @@ object NotificationStateProvider { } } - return NotificationStateV2(conversations, muteFilteredMessages, profileFilteredMessages) + return NotificationState(conversations, muteFilteredMessages, profileFilteredMessages) } private data class NotificationMessage( diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThumbnails.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThumbnails.kt new file mode 100644 index 0000000000..c7223cb287 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThumbnails.kt @@ -0,0 +1,118 @@ +package org.thoughtcrime.securesms.notifications.v2 + +import android.content.Context +import android.net.Uri +import org.signal.core.util.concurrent.SignalExecutors +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader +import org.thoughtcrime.securesms.mms.Slide +import org.thoughtcrime.securesms.providers.BlobProvider +import org.thoughtcrime.securesms.util.ImageCompressionUtil +import org.thoughtcrime.securesms.util.kb +import org.thoughtcrime.securesms.util.mb + +/** + * Creates and caches attachment thumbnails solely for use by Notifications. + * + * Handles LRU cache on it's own due to needing to cleanup BlobProvider when oldest element is evicted. + * + * Previously the PartProvider was used and it would provide the entire, full-resolution image assets causing + * some OEMs to ANR during file reading. + */ +object NotificationThumbnails { + private val TAG = Log.tag(NotificationThumbnails::class.java) + + private const val MAX_CACHE_SIZE = 16 + private val TARGET_SIZE = 128.kb + private val SUPPORTED_SIZE_THRESHOLD = 1.mb + + private val executor = SignalExecutors.BOUNDED_IO + + private val thumbnailCache = LinkedHashMap(MAX_CACHE_SIZE) + + fun get(context: Context, notificationItem: NotificationItem): NotificationItem.ThumbnailInfo { + val thumbnailSlide: Slide? = notificationItem.slideDeck?.thumbnailSlide + + if (thumbnailSlide == null || thumbnailSlide.uri == null) { + return NotificationItem.ThumbnailInfo.NONE + } + + if (thumbnailSlide.fileSize > SUPPORTED_SIZE_THRESHOLD) { + Log.i(TAG, "Source attachment too large for notification") + return NotificationItem.ThumbnailInfo.NONE + } + + if (thumbnailSlide.fileSize < TARGET_SIZE) { + return NotificationItem.ThumbnailInfo(thumbnailSlide.publicUri, thumbnailSlide.contentType) + } + + val messageId = MessageId(notificationItem.id, notificationItem.isMms) + val thumbnail: CachedThumbnail? = synchronized(thumbnailCache) { thumbnailCache[messageId] } + + if (thumbnail != null) { + return if (thumbnail != CachedThumbnail.PENDING) { + NotificationItem.ThumbnailInfo(thumbnail.uri, thumbnail.contentType) + } else { + NotificationItem.ThumbnailInfo.NONE + } + } + + synchronized(thumbnailCache) { + thumbnailCache[messageId] = CachedThumbnail.PENDING + } + + executor.execute { + val uri = thumbnailSlide.uri + + if (uri != null) { + val result = ImageCompressionUtil.compressWithinConstraints( + context, + thumbnailSlide.contentType, + DecryptableStreamUriLoader.DecryptableUri(uri), + 1024, + TARGET_SIZE, + 60 + ) + + if (result != null) { + val thumbnailUri = BlobProvider + .getInstance() + .forData(result.data) + .withMimeType(result.mimeType) + .withFileName(result.hashCode().toString()) + .createForSingleSessionInMemory() + + synchronized(thumbnailCache) { + if (thumbnailCache.size >= MAX_CACHE_SIZE) { + thumbnailCache.remove(thumbnailCache.keys.first())?.uri?.let { + BlobProvider.getInstance().delete(context, it) + } + } + thumbnailCache[messageId] = CachedThumbnail(thumbnailUri, result.mimeType) + } + + ApplicationDependencies.getMessageNotifier().updateNotification(context, notificationItem.thread) + } else { + Log.i(TAG, "Unable to compress attachment thumbnail for $messageId") + } + } + } + + return NotificationItem.ThumbnailInfo.NONE + } + + fun removeAllExcept(notificationItems: List) { + val currentMessages = notificationItems.map { MessageId(it.id, it.isMms) } + synchronized(thumbnailCache) { + thumbnailCache.keys.removeIf { !currentMessages.contains(it) } + } + } + + private data class CachedThumbnail(val uri: Uri?, val contentType: String?) { + companion object { + val PENDING = CachedThumbnail(null, null) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SizeUnit.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SizeUnit.kt new file mode 100644 index 0000000000..23eacf9e7d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SizeUnit.kt @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.util + +/** Kilobytes in bytes */ +val Int.kb + get() = this * 1024 + +/** Megabytes in bytes. */ +val Int.mb + get() = this * 1024 * 1024