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.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; 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.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.CursorUtil; 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(); StringBuilder stickyQuery = new StringBuilder();
for (MessageNotifierV2.StickyThread stickyThread : stickyThreads) { for (DefaultMessageNotifier.StickyThread stickyThread : stickyThreads) {
if (stickyQuery.length() > 0) { if (stickyQuery.length() > 0) {
stickyQuery.append(" OR "); stickyQuery.append(" OR ");
} }

View file

@ -14,7 +14,7 @@ import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; 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.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BubbleUtil; 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 * 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 * 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. * bubble notifications that do not have unread messages in them.
@ -113,7 +113,7 @@ public final class NotificationCancellationHelper {
@RequiresApi(23) @RequiresApi(23)
private static boolean isSingleThreadNotification(@NonNull StatusBarNotification statusBarNotification) { private static boolean isSingleThreadNotification(@NonNull StatusBarNotification statusBarNotification) {
return statusBarNotification.getId() != NotificationIds.MESSAGE_SUMMARY && 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.ExceptionUtil;
import org.signal.core.util.concurrent.SignalExecutors; 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.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BubbleUtil; import org.thoughtcrime.securesms.util.BubbleUtil;
@ -24,12 +24,12 @@ import java.util.Optional;
public class OptimizedMessageNotifier implements MessageNotifier { public class OptimizedMessageNotifier implements MessageNotifier {
private final LeakyBucketLimiter limiter; private final LeakyBucketLimiter limiter;
private final MessageNotifierV2 messageNotifierV2; private final DefaultMessageNotifier defaultMessageNotifier;
@MainThread @MainThread
public OptimizedMessageNotifier(@NonNull Application context) { public OptimizedMessageNotifier(@NonNull Application context) {
this.limiter = new LeakyBucketLimiter(5, 1000, new Handler(SignalExecutors.getAndStartHandlerThread("signal-notifier").getLooper())); this.limiter = new LeakyBucketLimiter(5, 1000, new Handler(SignalExecutors.getAndStartHandlerThread("signal-notifier").getLooper()));
this.messageNotifierV2 = new MessageNotifierV2(context); this.defaultMessageNotifier = new DefaultMessageNotifier(context);
} }
@Override @Override
@ -119,6 +119,6 @@ public class OptimizedMessageNotifier implements MessageNotifier {
} }
private MessageNotifier getNotifier() { 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.database.model.StoryType;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; 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.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
@ -67,7 +67,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
final RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA); final RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA);
final ReplyMethod replyMethod = (ReplyMethod) intent.getSerializableExtra(REPLY_METHOD); 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); final long groupStoryId = intent.getLongExtra(GROUP_STORY_ID_EXTRA, Long.MIN_VALUE);
if (recipientId == null) throw new AssertionError("No recipientId specified"); 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. * 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 visibleThread: ConversationId? = null
@Volatile private var lastDesktopActivityTimestamp: Long = -1 @Volatile private var lastDesktopActivityTimestamp: Long = -1
@Volatile private var lastAudibleNotification: Long = -1 @Volatile private var lastAudibleNotification: Long = -1
@Volatile private var lastScheduledReminder: Long = 0 @Volatile private var lastScheduledReminder: Long = 0
@Volatile private var previousLockedStatus: Boolean = KeyCachingService.isLocked(context) @Volatile private var previousLockedStatus: Boolean = KeyCachingService.isLocked(context)
@Volatile private var previousPrivacyPreference: NotificationPrivacyPreference = SignalStore.settings().messageNotificationsPrivacy @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 threadReminders: MutableMap<ConversationId, Reminder> = ConcurrentHashMap()
private val stickyThreads: MutableMap<ConversationId, StickyThread> = mutableMapOf() private val stickyThreads: MutableMap<ConversationId, StickyThread> = mutableMapOf()
@ -132,7 +132,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
val notificationProfile: NotificationProfile? = NotificationProfiles.getActiveProfile(SignalDatabase.notificationProfiles.getProfiles()) val notificationProfile: NotificationProfile? = NotificationProfiles.getActiveProfile(SignalDatabase.notificationProfiles.getProfiles())
Log.internal().i(TAG, "sticky thread: $stickyThreads active profile: ${notificationProfile?.id ?: "none" }") 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") Log.internal().i(TAG, "state: $state")
if (state.muteFilteredMessages.isNotEmpty()) { if (state.muteFilteredMessages.isNotEmpty()) {
@ -208,13 +208,14 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
lastAudibleNotification = System.currentTimeMillis() lastAudibleNotification = System.currentTimeMillis()
updateReminderTimestamps(context, alertOverrides, threadsThatAlerted) updateReminderTimestamps(context, alertOverrides, threadsThatAlerted)
NotificationThumbnails.removeAllExcept(state.notificationItems)
ServiceUtil.getNotificationManager(context).cancelOrphanedNotifications(context, state, stickyThreads.map { it.value.notificationId }.toSet()) ServiceUtil.getNotificationManager(context).cancelOrphanedNotifications(context, state, stickyThreads.map { it.value.notificationId }.toSet())
updateBadge(context, state.messageCount) updateBadge(context, state.messageCount)
val smsIds: MutableList<Long> = mutableListOf() val smsIds: MutableList<Long> = mutableListOf()
val mmsIds: MutableList<Long> = mutableListOf() val mmsIds: MutableList<Long> = mutableListOf()
for (item: NotificationItemV2 in state.notificationItems) { for (item: NotificationItem in state.notificationItems) {
if (item.isMms) { if (item.isMms) {
mmsIds.add(item.id) mmsIds.add(item.id)
} else { } else {
@ -301,7 +302,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
} }
companion object { 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) private val REMINDER_TIMEOUT: Long = TimeUnit.MINUTES.toMillis(2)
val MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2) val MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2)
@ -339,12 +340,12 @@ private fun NotificationManager.getDisplayedNotificationIds(): Result<Set<Int>>
return try { return try {
Result.success(activeNotifications.filter { it.isMessageNotification() }.map { it.id }.toSet()) Result.success(activeNotifications.filter { it.isMessageNotification() }.map { it.id }.toSet())
} catch (e: Throwable) { } catch (e: Throwable) {
Log.w(MessageNotifierV2.TAG, e) Log.w(DefaultMessageNotifier.TAG, e)
Result.failure(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) { if (Build.VERSION.SDK_INT < 24) {
return return
} }
@ -354,13 +355,13 @@ private fun NotificationManager.cancelOrphanedNotifications(context: Context, st
.map { it.id } .map { it.id }
.filterNot { state.notificationIds.contains(it) } .filterNot { state.notificationIds.contains(it) }
.forEach { id -> .forEach { id ->
Log.d(MessageNotifierV2.TAG, "Cancelling orphaned notification: $id") Log.d(DefaultMessageNotifier.TAG, "Cancelling orphaned notification: $id")
NotificationCancellationHelper.cancel(context, id) NotificationCancellationHelper.cancel(context, id)
} }
NotificationCancellationHelper.cancelMessageSummaryIfSoleNotification(context) NotificationCancellationHelper.cancelMessageSummaryIfSoleNotification(context)
} catch (e: Throwable) { } 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 setOnlyAlertOnce(onlyAlertOnce: Boolean)
abstract fun setGroupSummary(isGroupSummary: Boolean) abstract fun setGroupSummary(isGroupSummary: Boolean)
abstract fun setSubText(subText: String) abstract fun setSubText(subText: String)
abstract fun addMarkAsReadActionActual(state: NotificationStateV2) abstract fun addMarkAsReadActionActual(state: NotificationState)
abstract fun setPriority(priority: Int) abstract fun setPriority(priority: Int)
abstract fun setAlarms(recipient: Recipient?) abstract fun setAlarms(recipient: Recipient?)
abstract fun setTicker(ticker: CharSequence?) 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 setWhen(timestamp: Long)
protected abstract fun addActions(replyMethod: ReplyMethod, conversation: NotificationConversation) protected abstract fun addActions(replyMethod: ReplyMethod, conversation: NotificationConversation)
protected abstract fun addMessagesActual(conversation: NotificationConversation, includeShortcut: Boolean) 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 setBubbleMetadataActual(conversation: NotificationConversation, bubbleState: BubbleUtil.BubbleState)
protected abstract fun setLights(@ColorInt color: Int, onTime: Int, offTime: Int) 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) { if (notificationItem != null && notificationItem.timestamp != 0L) {
setWhen(notificationItem.timestamp) 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) { if (privacy.isDisplayMessage && isNotLocked) {
addMarkAsReadActionActual(state) addMarkAsReadActionActual(state)
} }
@ -136,7 +136,7 @@ sealed class NotificationBuilder(protected val context: Context) {
addMessagesActual(conversation, privacy.isDisplayContact) addMessagesActual(conversation, privacy.isDisplayContact)
} }
fun addMessages(state: NotificationStateV2) { fun addMessages(state: NotificationState) {
if (privacy.isDisplayNothing) { if (privacy.isDisplayNothing) {
return return
} }
@ -207,7 +207,7 @@ sealed class NotificationBuilder(protected val context: Context) {
val label: String = context.getString(replyMethod.toLongDescription()) val label: String = context.getString(replyMethod.toLongDescription())
val replyAction: NotificationCompat.Action = if (Build.VERSION.SDK_INT >= 24) { val replyAction: NotificationCompat.Action = if (Build.VERSION.SDK_INT >= 24) {
NotificationCompat.Action.Builder(R.drawable.ic_reply_white_36dp, actionName, remoteReply) 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) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
.setShowsUserInterface(false) .setShowsUserInterface(false)
.build() .build()
@ -216,7 +216,7 @@ sealed class NotificationBuilder(protected val context: Context) {
} }
val wearableReplyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, actionName, remoteReply) 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() .build()
builder.addAction(replyAction) builder.addAction(replyAction)
@ -226,7 +226,7 @@ sealed class NotificationBuilder(protected val context: Context) {
builder.extend(extender) 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)) val markAllAsReadAction = NotificationCompat.Action(R.drawable.check, context.getString(R.string.MessageNotifier_mark_all_as_read), state.getMarkAsReadIntent(context))
builder.addAction(markAllAsReadAction) builder.addAction(markAllAsReadAction)
builder.extend(NotificationCompat.WearableExtender().addAction(markAllAsReadAction)) builder.extend(NotificationCompat.WearableExtender().addAction(markAllAsReadAction))
@ -286,14 +286,14 @@ sealed class NotificationBuilder(protected val context: Context) {
builder.setStyle(messagingStyle) builder.setStyle(messagingStyle)
} }
override fun addMessagesActual(state: NotificationStateV2) { override fun addMessagesActual(state: NotificationState) {
if (Build.VERSION.SDK_INT >= 24) { if (Build.VERSION.SDK_INT >= 24) {
return return
} }
val style: NotificationCompat.InboxStyle = NotificationCompat.InboxStyle() val style: NotificationCompat.InboxStyle = NotificationCompat.InboxStyle()
for (notificationItem: NotificationItemV2 in state.notificationItems) { for (notificationItem: NotificationItem in state.notificationItems) {
val line: CharSequence? = notificationItem.getInboxLine(context) val line: CharSequence? = notificationItem.getInboxLine(context)
if (line != null) { if (line != null) {
style.addLine(line) style.addLine(line)

View file

@ -36,9 +36,9 @@ import java.lang.NullPointerException
data class NotificationConversation( data class NotificationConversation(
val recipient: Recipient, val recipient: Recipient,
val thread: ConversationId, 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 notificationId: Int = NotificationIds.getNotificationIdForThread(thread)
val sortKey: Long = Long.MAX_VALUE - mostRecentNotification.timestamp val sortKey: Long = Long.MAX_VALUE - mostRecentNotification.timestamp
val messageCount: Int = notificationItems.size val messageCount: Int = notificationItems.size

View file

@ -37,18 +37,18 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
*/ */
object NotificationFactory { object NotificationFactory {
val TAG = Log.tag(NotificationFactory::class.java) val TAG: String = Log.tag(NotificationFactory::class.java)
fun notify( fun notify(
context: Context, context: Context,
state: NotificationStateV2, state: NotificationState,
visibleThread: ConversationId?, visibleThread: ConversationId?,
targetThread: ConversationId?, targetThread: ConversationId?,
defaultBubbleState: BubbleUtil.BubbleState, defaultBubbleState: BubbleUtil.BubbleState,
lastAudibleNotification: Long, lastAudibleNotification: Long,
notificationConfigurationChanged: Boolean, notificationConfigurationChanged: Boolean,
alertOverrides: Set<ConversationId>, alertOverrides: Set<ConversationId>,
previousState: NotificationStateV2 previousState: NotificationState
): Set<ConversationId> { ): Set<ConversationId> {
if (state.isEmpty) { if (state.isEmpty) {
Log.d(TAG, "State is empty, bailing") Log.d(TAG, "State is empty, bailing")
@ -85,7 +85,7 @@ object NotificationFactory {
private fun notify19( private fun notify19(
context: Context, context: Context,
state: NotificationStateV2, state: NotificationState,
visibleThread: ConversationId?, visibleThread: ConversationId?,
targetThread: ConversationId?, targetThread: ConversationId?,
defaultBubbleState: BubbleUtil.BubbleState, defaultBubbleState: BubbleUtil.BubbleState,
@ -127,7 +127,7 @@ object NotificationFactory {
@TargetApi(24) @TargetApi(24)
private fun notify24( private fun notify24(
context: Context, context: Context,
state: NotificationStateV2, state: NotificationState,
visibleThread: ConversationId?, visibleThread: ConversationId?,
targetThread: ConversationId?, targetThread: ConversationId?,
defaultBubbleState: BubbleUtil.BubbleState, defaultBubbleState: BubbleUtil.BubbleState,
@ -135,7 +135,7 @@ object NotificationFactory {
notificationConfigurationChanged: Boolean, notificationConfigurationChanged: Boolean,
alertOverrides: Set<ConversationId>, alertOverrides: Set<ConversationId>,
nonVisibleThreadCount: Int, nonVisibleThreadCount: Int,
previousState: NotificationStateV2 previousState: NotificationState
): Set<ConversationId> { ): Set<ConversationId> {
val threadsThatNewlyAlerted: MutableSet<ConversationId> = mutableSetOf() val threadsThatNewlyAlerted: MutableSet<ConversationId> = mutableSetOf()
@ -186,7 +186,7 @@ object NotificationFactory {
setSmallIcon(R.drawable.ic_notification) setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine)) setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE) setCategory(NotificationCompat.CATEGORY_MESSAGE)
setGroup(MessageNotifierV2.NOTIFICATION_GROUP) setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
setChannelId(conversation.getChannelId(context)) setChannelId(conversation.getChannelId(context))
setContentTitle(conversation.getContentTitle(context)) setContentTitle(conversation.getContentTitle(context))
@ -224,7 +224,7 @@ object NotificationFactory {
NotificationManagerCompat.from(context).safelyNotify(context, conversation.recipient, notificationId, builder.build()) 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) { if (state.messageCount == 0) {
return return
} }
@ -235,7 +235,7 @@ object NotificationFactory {
setSmallIcon(R.drawable.ic_notification) setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine)) setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE) setCategory(NotificationCompat.CATEGORY_MESSAGE)
setGroup(MessageNotifierV2.NOTIFICATION_GROUP) setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
setChannelId(NotificationChannels.getMessagesChannel(context)) setChannelId(NotificationChannels.getMessagesChannel(context))
setContentTitle(context.getString(R.string.app_name)) setContentTitle(context.getString(R.string.app_name))
@ -263,7 +263,7 @@ object NotificationFactory {
private fun notifyInThread(context: Context, recipient: Recipient, lastAudibleNotification: Long) { private fun notifyInThread(context: Context, recipient: Recipient, lastAudibleNotification: Long) {
if (!SignalStore.settings().isMessageNotificationsInChatSoundsEnabled || if (!SignalStore.settings().isMessageNotificationsInChatSoundsEnabled ||
ServiceUtil.getAudioManager(context).ringerMode != AudioManager.RINGER_MODE_NORMAL || 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 return
} }
@ -378,7 +378,7 @@ object NotificationFactory {
setSmallIcon(R.drawable.ic_notification) setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine)) setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE) setCategory(NotificationCompat.CATEGORY_MESSAGE)
setGroup(MessageNotifierV2.NOTIFICATION_GROUP) setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP)
setChannelId(conversation.getChannelId(context)) setChannelId(conversation.getChannelId(context))
setContentTitle(conversation.getContentTitle(context)) setContentTitle(conversation.getContentTitle(context))
setLargeIcon(conversation.getContactLargeIcon(context).toLargeBitmap(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.hasSticker
import org.thoughtcrime.securesms.util.isMediaMessage 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 EMOJI_REPLACEMENT_STRING = "__EMOJI__"
private const val MAX_DISPLAY_LENGTH = 500 private const val MAX_DISPLAY_LENGTH = 500
/** /**
* Base for messaged-based notifications. Represents a single notification. * 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 id: Long = record.id
val thread = ConversationId.fromMessageRecord(record) 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) 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 && return timestamp == other.timestamp &&
id == other.id && id == other.id &&
isMms == other.isMms && 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. * 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 timestamp: Long = record.timestamp
override val individualRecipient: Recipient = if (record.isOutgoing) Recipient.self() else record.individualRecipient.resolve() override val individualRecipient: Recipient = if (record.isOutgoing) Recipient.self() else record.individualRecipient.resolve()
override val isNewNotification: Boolean = notifiedTimestamp == 0L override val isNewNotification: Boolean = notifiedTimestamp == 0L
private var thumbnailInfo: ThumbnailInfo? = null
override fun getPrimaryTextActual(context: Context): CharSequence { override fun getPrimaryTextActual(context: Context): CharSequence {
return if (KeyCachingService.isLocked(context)) { return if (KeyCachingService.isLocked(context)) {
SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)) SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message))
@ -221,14 +227,17 @@ class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : N
} }
override fun getThumbnailInfo(context: Context): ThumbnailInfo { override fun getThumbnailInfo(context: Context): ThumbnailInfo {
return if (SignalStore.settings().messageNotificationsPrivacy.isDisplayMessage && !KeyCachingService.isLocked(context)) { if (thumbnailInfo == null) {
val thumbnailSlide: Slide? = slideDeck?.thumbnailSlide thumbnailInfo = if (SignalStore.settings().messageNotificationsPrivacy.isDisplayMessage && !KeyCachingService.isLocked(context)) {
ThumbnailInfo(thumbnailSlide?.publicUri, thumbnailSlide?.contentType) NotificationThumbnails.get(context, this)
} else { } else {
ThumbnailInfo() ThumbnailInfo()
} }
} }
return thumbnailInfo!!
}
override fun canReply(context: Context): Boolean { override fun canReply(context: Context): Boolean {
if (KeyCachingService.isLocked(context) || if (KeyCachingService.isLocked(context) ||
record.isRemoteDelete || record.isRemoteDelete ||
@ -246,6 +255,10 @@ class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : N
return true return true
} }
override fun hasSameContent(other: NotificationItem): Boolean {
return super.hasSameContent(other) && thumbnailInfo == (other as? MessageNotification)?.thumbnailInfo
}
override fun toString(): String { override fun toString(): String {
return "MessageNotification(timestamp=$timestamp, isNewNotification=$isNewNotification)" 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. * 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 timestamp: Long = reaction.dateReceived
override val individualRecipient: Recipient = Recipient.resolved(reaction.author) override val individualRecipient: Recipient = Recipient.resolved(reaction.author)
override val isNewNotification: Boolean = timestamp > notifiedTimestamp 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. * 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 threadCount: Int = conversations.size
val isEmpty: Boolean = conversations.isEmpty() 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 } conversations.map { it.notificationItems }
.flatten() .flatten()
.sorted() .sorted()
@ -33,7 +33,7 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
.toSet() .toSet()
} }
val mostRecentNotification: NotificationItemV2? val mostRecentNotification: NotificationItem?
get() = notificationItems.lastOrNull() get() = notificationItems.lastOrNull()
val mostRecentSender: Recipient? val mostRecentSender: Recipient?
@ -88,6 +88,6 @@ data class NotificationStateV2(val conversations: List<NotificationConversation>
data class FilteredMessage(val id: Long, val isMms: Boolean) data class FilteredMessage(val id: Long, val isMms: Boolean)
companion object { 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) private val TAG = Log.tag(NotificationStateProvider::class.java)
@WorkerThread @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() val messages: MutableList<NotificationMessage> = mutableListOf()
SignalDatabase.mmsSms.getMessagesForNotificationState(stickyThreads.values).use { unreadMessages -> SignalDatabase.mmsSms.getMessagesForNotificationState(stickyThreads.values).use { unreadMessages ->
if (unreadMessages.count == 0) { if (unreadMessages.count == 0) {
return NotificationStateV2.EMPTY return NotificationState.EMPTY
} }
MmsSmsDatabase.readerFor(unreadMessages).use { reader -> MmsSmsDatabase.readerFor(unreadMessages).use { reader ->
@ -71,19 +71,19 @@ object NotificationStateProvider {
} }
val conversations: MutableList<NotificationConversation> = mutableListOf() val conversations: MutableList<NotificationConversation> = mutableListOf()
val muteFilteredMessages: MutableList<NotificationStateV2.FilteredMessage> = mutableListOf() val muteFilteredMessages: MutableList<NotificationState.FilteredMessage> = mutableListOf()
val profileFilteredMessages: MutableList<NotificationStateV2.FilteredMessage> = mutableListOf() val profileFilteredMessages: MutableList<NotificationState.FilteredMessage> = mutableListOf()
messages.groupBy { it.thread } messages.groupBy { it.thread }
.forEach { (thread, threadMessages) -> .forEach { (thread, threadMessages) ->
var notificationItems: MutableList<NotificationItemV2> = mutableListOf() var notificationItems: MutableList<NotificationItem> = mutableListOf()
for (notification: NotificationMessage in threadMessages) { for (notification: NotificationMessage in threadMessages) {
when (notification.includeMessage(notificationProfile)) { when (notification.includeMessage(notificationProfile)) {
MessageInclusion.INCLUDE -> notificationItems.add(MessageNotification(notification.threadRecipient, notification.messageRecord)) MessageInclusion.INCLUDE -> notificationItems.add(MessageNotification(notification.threadRecipient, notification.messageRecord))
MessageInclusion.EXCLUDE -> Unit MessageInclusion.EXCLUDE -> Unit
MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms)
MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms)
} }
if (notification.hasUnreadReactions) { if (notification.hasUnreadReactions) {
@ -91,8 +91,8 @@ object NotificationStateProvider {
when (notification.includeReaction(it, notificationProfile)) { when (notification.includeReaction(it, notificationProfile)) {
MessageInclusion.INCLUDE -> notificationItems.add(ReactionNotification(notification.threadRecipient, notification.messageRecord, it)) MessageInclusion.INCLUDE -> notificationItems.add(ReactionNotification(notification.threadRecipient, notification.messageRecord, it))
MessageInclusion.EXCLUDE -> Unit MessageInclusion.EXCLUDE -> Unit
MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationStateV2.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms)
MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationStateV2.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( 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