Refactor notification thumbnails to reduce chances for ANR.
This commit is contained in:
parent
a9fc5622cd
commit
1e499fd12f
13 changed files with 213 additions and 72 deletions
|
@ -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 ");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
Loading…
Add table
Reference in a new issue