Refactor notification thumbnails to reduce chances for ANR.

This commit is contained in:
Cody Henthorne 2022-08-24 16:04:05 -04:00 committed by Greyson Parrelli
parent a9fc5622cd
commit 1e499fd12f
13 changed files with 213 additions and 72 deletions

View file

@ -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<MessageNotifierV2.StickyThread> stickyThreads) {
public Cursor getMessagesForNotificationState(Collection<DefaultMessageNotifier.StickyThread> stickyThreads) {
StringBuilder stickyQuery = new StringBuilder();
for (MessageNotifierV2.StickyThread stickyThread : stickyThreads) {
for (DefaultMessageNotifier.StickyThread stickyThread : stickyThreads) {
if (stickyQuery.length() > 0) {
stickyQuery.append(" OR ");
}

View file

@ -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);
}
/**

View file

@ -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;
}
}

View file

@ -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");

View file

@ -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<ConversationId, Reminder> = ConcurrentHashMap()
private val stickyThreads: MutableMap<ConversationId, StickyThread> = 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<Long> = mutableListOf()
val mmsIds: MutableList<Long> = 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<Set<Int>>
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<Int>) {
private fun NotificationManager.cancelOrphanedNotifications(context: Context, state: NotificationState, stickyNotifications: Set<Int>) {
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)
}
}

View file

@ -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)

View file

@ -36,9 +36,9 @@ import java.lang.NullPointerException
data class NotificationConversation(
val recipient: Recipient,
val thread: ConversationId,
val notificationItems: List<NotificationItemV2>
val notificationItems: List<NotificationItem>
) {
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

View file

@ -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<ConversationId>,
previousState: NotificationStateV2
previousState: NotificationState
): Set<ConversationId> {
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<ConversationId>,
nonVisibleThreadCount: Int,
previousState: NotificationStateV2
previousState: NotificationState
): Set<ConversationId> {
val threadsThatNewlyAlerted: MutableSet<ConversationId> = 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))

View file

@ -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<NotificationItemV2> {
sealed class NotificationItem(val threadRecipient: Recipient, protected val record: MessageRecord) : Comparable<NotificationItem> {
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

View file

@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
/**
* Hold all state for notifications for all conversations.
*/
data class NotificationStateV2(val conversations: List<NotificationConversation>, val muteFilteredMessages: List<FilteredMessage>, val profileFilteredMessages: List<FilteredMessage>) {
data class NotificationState(val conversations: List<NotificationConversation>, val muteFilteredMessages: List<FilteredMessage>, val profileFilteredMessages: List<FilteredMessage>) {
val threadCount: Int = conversations.size
val isEmpty: Boolean = conversations.isEmpty()
@ -22,7 +22,7 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
}
}
val notificationItems: List<NotificationItemV2> by lazy {
val notificationItems: List<NotificationItem> by lazy {
conversations.map { it.notificationItems }
.flatten()
.sorted()
@ -33,7 +33,7 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
.toSet()
}
val mostRecentNotification: NotificationItemV2?
val mostRecentNotification: NotificationItem?
get() = notificationItems.lastOrNull()
val mostRecentSender: Recipient?
@ -88,6 +88,6 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
data class FilteredMessage(val id: Long, val isMms: Boolean)
companion object {
val EMPTY = NotificationStateV2(emptyList(), emptyList(), emptyList())
val EMPTY = NotificationState(emptyList(), emptyList(), emptyList())
}
}

View file

@ -22,12 +22,12 @@ object NotificationStateProvider {
private val TAG = Log.tag(NotificationStateProvider::class.java)
@WorkerThread
fun constructNotificationState(stickyThreads: Map<ConversationId, MessageNotifierV2.StickyThread>, notificationProfile: NotificationProfile?): NotificationStateV2 {
fun constructNotificationState(stickyThreads: Map<ConversationId, DefaultMessageNotifier.StickyThread>, notificationProfile: NotificationProfile?): NotificationState {
val messages: MutableList<NotificationMessage> = 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<NotificationConversation> = mutableListOf()
val muteFilteredMessages: MutableList<NotificationStateV2.FilteredMessage> = mutableListOf()
val profileFilteredMessages: MutableList<NotificationStateV2.FilteredMessage> = mutableListOf()
val muteFilteredMessages: MutableList<NotificationState.FilteredMessage> = mutableListOf()
val profileFilteredMessages: MutableList<NotificationState.FilteredMessage> = mutableListOf()
messages.groupBy { it.thread }
.forEach { (thread, threadMessages) ->
var notificationItems: MutableList<NotificationItemV2> = mutableListOf()
var notificationItems: MutableList<NotificationItem> = 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(

View file

@ -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<MessageId, CachedThumbnail>(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<NotificationItem>) {
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)
}
}
}

View file

@ -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