diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 38c3b45189..88b223c5c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -79,6 +79,7 @@ import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob; import org.thoughtcrime.securesms.jobs.InAppPaymentAuthCheckJob; import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob; import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob; +import org.thoughtcrime.securesms.jobs.MessageSendLogCleanupJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.PreKeysSyncJob; import org.thoughtcrime.securesms.jobs.ProfileUploadJob; @@ -229,7 +230,7 @@ public class ApplicationContext extends Application implements AppForegroundObse .addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary) .addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this)) .addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary) - .addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), RemoteConfig.retryRespondMaxAge())) + .addPostRender(MessageSendLogCleanupJob::enqueue) .addPostRender(() -> JumboEmoji.updateCurrentVersion(this)) .addPostRender(RetrieveRemoteAnnouncementsJob::enqueue) .addPostRender(AndroidTelecomUtil::registerPhoneAccount) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index fabad0906e..22f724bf73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -1465,6 +1465,7 @@ object BackupRepository { } SignalDatabase.remappedRecords.clearCache() + SignalDatabase.remappedRecords.trimStaleMappings() AppDependencies.recipientCache.clear() AppDependencies.recipientCache.warmUp() SignalDatabase.threads.clearCache() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RemappedRecordTables.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RemappedRecordTables.kt index 4b765802fe..4989b2ea11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RemappedRecordTables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RemappedRecordTables.kt @@ -56,16 +56,11 @@ class RemappedRecordTables internal constructor(context: Context?, databaseHelpe fun getAllRecipientMappings(): Map { val recipientMap: MutableMap = HashMap() - readableDatabase.withinTransaction { db -> - trimInvalidRecipientEntries(db) - trimInvalidThreadEntries(db) - - val mappings = getAllMappings(db, Recipients.TABLE_NAME) - for (mapping in mappings) { - val oldId = RecipientId.from(mapping.oldId) - val newId = RecipientId.from(mapping.newId) - recipientMap[oldId] = newId - } + val mappings = getAllMappings(readableDatabase, Recipients.TABLE_NAME) + for (mapping in mappings) { + val oldId = RecipientId.from(mapping.oldId) + val newId = RecipientId.from(mapping.newId) + recipientMap[oldId] = newId } return recipientMap @@ -74,16 +69,21 @@ class RemappedRecordTables internal constructor(context: Context?, databaseHelpe fun getAllThreadMappings(): Map { val threadMap: MutableMap = HashMap() - readableDatabase.withinTransaction { db -> - val mappings = getAllMappings(db, Threads.TABLE_NAME) - for (mapping in mappings) { - threadMap[mapping.oldId] = mapping.newId - } + val mappings = getAllMappings(readableDatabase, Threads.TABLE_NAME) + for (mapping in mappings) { + threadMap[mapping.oldId] = mapping.newId } return threadMap } + fun trimStaleMappings() { + writableDatabase.withinTransaction { db -> + trimInvalidRecipientEntries(db) + trimInvalidThreadEntries(db) + } + } + fun addRecipientMapping(oldId: RecipientId, newId: RecipientId) { addMapping(Recipients.TABLE_NAME, Mapping(oldId.toLong(), newId.toLong())) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RemappedRecords.java b/app/src/main/java/org/thoughtcrime/securesms/database/RemappedRecords.java index 77c2ce6a16..8f7a712c50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RemappedRecords.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RemappedRecords.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database; import androidx.annotation.NonNull; +import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.recipients.RecipientId; import org.signal.network.util.Preconditions; @@ -11,6 +12,7 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; /** * Merging together recipients and threads is messy business. We can easily replace *almost* all of @@ -30,8 +32,10 @@ class RemappedRecords { private static final RemappedRecords INSTANCE = new RemappedRecords(); - private Map recipientMap; - private Map threadMap; + private volatile Map recipientMap; + private volatile Map threadMap; + + private final AtomicBoolean staleTrimScheduled = new AtomicBoolean(false); private RemappedRecords() {} @@ -106,13 +110,31 @@ class RemappedRecords { private void ensureRecipientMapIsPopulated() { if (recipientMap == null) { - recipientMap = SignalDatabase.remappedRecords().getAllRecipientMappings(); + Map loaded = SignalDatabase.remappedRecords().getAllRecipientMappings(); + synchronized (this) { + if (recipientMap == null) { + recipientMap = loaded; + } + } + scheduleStaleTrimIfNeeded(loaded.isEmpty()); } } private void ensureThreadMapIsPopulated() { if (threadMap == null) { - threadMap = SignalDatabase.remappedRecords().getAllThreadMappings(); + Map loaded = SignalDatabase.remappedRecords().getAllThreadMappings(); + synchronized (this) { + if (threadMap == null) { + threadMap = loaded; + } + } + scheduleStaleTrimIfNeeded(loaded.isEmpty()); + } + } + + private void scheduleStaleTrimIfNeeded(boolean loadedMapWasEmpty) { + if (!loadedMapWasEmpty && staleTrimScheduled.compareAndSet(false, true)) { + SignalExecutors.BOUNDED.execute(() -> SignalDatabase.remappedRecords().trimStaleMappings()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 466e6710bc..ca62bd0067 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -205,6 +205,7 @@ public final class JobManagerFactories { put(LocalPlaintextArchiveJob.KEY, new LocalPlaintextArchiveJob.Factory()); put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory()); put(MarkerJob.KEY, new MarkerJob.Factory()); + put(MessageSendLogCleanupJob.KEY, new MessageSendLogCleanupJob.Factory()); put(MultiDeviceAttachmentBackfillMissingJob.KEY, new MultiDeviceAttachmentBackfillMissingJob.Factory()); put(MultiDeviceAttachmentBackfillUpdateJob.KEY, new MultiDeviceAttachmentBackfillUpdateJob.Factory()); put(MultiDeviceBlockedUpdateJob.KEY, new MultiDeviceBlockedUpdateJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MessageSendLogCleanupJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MessageSendLogCleanupJob.kt new file mode 100644 index 0000000000..ba3a3d034e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MessageSendLogCleanupJob.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobs + +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.util.RemoteConfig +import java.util.concurrent.TimeUnit + +/** + * Trims expired entries out of the message send log after a delay. + */ +class MessageSendLogCleanupJob private constructor(parameters: Parameters) : Job(parameters) { + companion object { + const val KEY = "MessageSendLogCleanupJob" + + @JvmStatic + fun enqueue() { + AppDependencies.jobManager.add( + MessageSendLogCleanupJob( + Parameters.Builder() + .setInitialDelay(TimeUnit.MINUTES.toMillis(1)) + .setLifespan(TimeUnit.HOURS.toMillis(1)) + .setMaxInstancesForFactory(1) + .setQueue(KEY) + .build() + ) + ) + } + } + + override fun serialize(): ByteArray? = null + + override fun getFactoryKey(): String = KEY + + override fun onFailure() = Unit + + override fun run(): Result { + SignalDatabase.messageLog.trimOldMessages(System.currentTimeMillis(), RemoteConfig.retryRespondMaxAge) + return Result.success() + } + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): MessageSendLogCleanupJob { + return MessageSendLogCleanupJob(parameters) + } + } +}