From 0bbbee645d711658dbea20039870c0eedceb25f0 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Tue, 12 Aug 2025 14:33:47 -0400 Subject: [PATCH] Improve link device good citizenship with backups. --- .../securesms/backup/v2/BackupRepository.kt | 2 +- .../securesms/jobs/AttachmentDownloadJob.kt | 8 +++++++- .../securesms/jobs/AttachmentUploadJob.kt | 5 ++++- .../securesms/jobs/CopyAttachmentToArchiveJob.kt | 6 ++++++ .../securesms/jobs/InAppPaymentKeepAliveJob.kt | 12 ++++++++++++ .../securesms/jobs/RestoreAttachmentJob.kt | 6 ++++-- .../securesms/jobs/RestoreAttachmentThumbnailJob.kt | 4 +++- .../securesms/jobs/UploadAttachmentToArchiveJob.kt | 6 ++++++ .../thoughtcrime/securesms/keyvalue/BackupValues.kt | 9 +++++++++ .../registrationv3/data/RegistrationRepository.kt | 3 --- .../registrationv3/ui/RegistrationViewModel.kt | 1 - .../signalservice/api/backup/MediaRootBackupKey.kt | 13 +++++++++++++ 12 files changed, 65 insertions(+), 10 deletions(-) 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 1878f8e214..dcf0dd3a56 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 @@ -1919,7 +1919,7 @@ object BackupRepository { private fun initBackupAndFetchAuth(): NetworkResult { return if (!RemoteConfig.messageBackups) { NetworkResult.StatusCodeError(555, null, null, emptyMap(), NonSuccessfulResponseCodeException(555, "Backups disabled!")) - } else if (SignalStore.backup.backupsInitialized) { + } else if (SignalStore.backup.backupsInitialized || SignalStore.account.isLinkedDevice) { getArchiveServiceAccessPair() .runOnStatusCodeError(resetInitializedStateErrorAction) .runOnApplicationError(clearAuthCredentials) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt index c17f791f4a..1fc8e2e1af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt @@ -239,6 +239,10 @@ class AttachmentDownloadJob private constructor( Log.i(TAG, "[$attachmentId] Message will expire within 24hrs. Skipping.") } + SignalStore.account.isLinkedDevice -> { + Log.i(TAG, "[$attachmentId] Linked device. Skipping.") + } + else -> { Log.i(TAG, "[$attachmentId] Enqueuing job to copy to archive.") AppDependencies.jobManager.add(CopyAttachmentToArchiveJob(attachmentId)) @@ -302,7 +306,9 @@ class AttachmentDownloadJob private constructor( progressListener ) - SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, decryptingStream) + decryptingStream.use { input -> + SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, input) + } } catch (e: RangeException) { Log.w(TAG, "Range exception, file size " + attachmentFile.length(), e) if (attachmentFile.delete()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt index 2e7fcfbde9..920d310aaf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt @@ -148,7 +148,7 @@ class AttachmentUploadJob private constructor( if (timeSinceUpload < UPLOAD_REUSE_THRESHOLD && !TextUtils.isEmpty(databaseAttachment.remoteLocation)) { Log.i(TAG, "We can re-use an already-uploaded file. It was uploaded $timeSinceUpload ms (${timeSinceUpload.milliseconds.inRoundedDays()} days) ago. Skipping.") SignalDatabase.attachments.setTransferState(databaseAttachment.mmsId, attachmentId, AttachmentTable.TRANSFER_PROGRESS_DONE) - if (BackupRepository.shouldCopyAttachmentToArchive(databaseAttachment.attachmentId, databaseAttachment.mmsId)) { + if (SignalStore.account.isPrimaryDevice && BackupRepository.shouldCopyAttachmentToArchive(databaseAttachment.attachmentId, databaseAttachment.mmsId)) { Log.i(TAG, "[$attachmentId] The re-used file was not copied to the archive. Copying now.") AppDependencies.jobManager.add(CopyAttachmentToArchiveJob(attachmentId)) } @@ -204,6 +204,9 @@ class AttachmentUploadJob private constructor( databaseAttachment.contentType == MediaUtil.LONG_TEXT -> { Log.i(TAG, "[$attachmentId] Long text attachment. Skipping.") } + SignalStore.account.isLinkedDevice -> { + Log.i(TAG, "[$attachmentId] Linked device. Skipping archive.") + } else -> { Log.i(TAG, "[$attachmentId] Enqueuing job to copy to archive.") AppDependencies.jobManager.add(CopyAttachmentToArchiveJob(attachmentId)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt index 5de21ab90b..82b3c48a78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt @@ -69,6 +69,12 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A } override fun run(): Result { + if (SignalStore.account.isLinkedDevice) { + Log.w(TAG, "[$attachmentId] Linked devices don't backup media. Skipping.") + SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.NONE) + return Result.success() + } + if (!SignalStore.backup.backsUpMedia) { Log.w(TAG, "[$attachmentId] This user does not back up media. Skipping.") return Result.success() diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt index 45add7ba53..efe4e5674e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.jobs +import androidx.annotation.VisibleForTesting import okio.ByteString.Companion.toByteString import org.signal.core.util.logging.Log import org.signal.core.util.money.FiatMoney @@ -51,6 +52,7 @@ class InAppPaymentKeepAliveJob private constructor( const val KEEP_ALIVE = "keep-alive" private const val DATA_TYPE = "type" + @VisibleForTesting fun create(type: InAppPaymentSubscriberRecord.Type): Job { return InAppPaymentKeepAliveJob( parameters = Parameters.Builder() @@ -66,6 +68,11 @@ class InAppPaymentKeepAliveJob private constructor( @JvmStatic fun enqueueAndTrackTimeIfNecessary() { + if (SignalStore.account.isLinkedDevice) { + Log.i(TAG, "Linked device. Skipping.") + return + } + // TODO -- This should only be enqueued if we are completely drained of old subscription jobs. (No pending, no runnning) val lastKeepAliveTime = SignalStore.inAppPayments.getLastKeepAliveLaunchTime().milliseconds val now = System.currentTimeMillis().milliseconds @@ -83,6 +90,11 @@ class InAppPaymentKeepAliveJob private constructor( @JvmStatic fun enqueueAndTrackTime(now: Duration) { + if (SignalStore.account.isLinkedDevice) { + Log.i(TAG, "Linked device. Skipping.") + return + } + AppDependencies.jobManager.add(create(InAppPaymentSubscriberRecord.Type.DONATION)) AppDependencies.jobManager.add(create(InAppPaymentSubscriberRecord.Type.BACKUP)) SignalStore.inAppPayments.setLastKeepAliveLaunchTime(now.inWholeMilliseconds) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt index df9b2879f1..9e0d399490 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt @@ -123,7 +123,7 @@ class RestoreAttachmentJob private constructor( attachmentId = attachmentId, messageId = messageId, manual = false, - queue = Queues.INITIAL_RESTORE.random(), + queue = Queues.OFFLOAD_RESTORE.random(), priority = Parameters.PRIORITY_LOW ) } @@ -323,7 +323,9 @@ class RestoreAttachmentJob private constructor( ) } - SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, decryptingStream, if (manual) System.currentTimeMillis().milliseconds else null) + decryptingStream.use { input -> + SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, input, if (manual) System.currentTimeMillis().milliseconds else null) + } } catch (e: RangeException) { Log.w(TAG, "[$attachmentId] Range exception, file size " + attachmentFile.length(), e) if (attachmentFile.delete()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt index 4ee10f98f9..3e97e154f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt @@ -140,7 +140,9 @@ class RestoreAttachmentThumbnailJob private constructor( progressListener ) - SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.dataHash, attachment.remoteKey, decryptingStream, thumbnailTransferFile) + decryptingStream.use { input -> + SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.dataHash, attachment.remoteKey, input, thumbnailTransferFile) + } if (!SignalDatabase.messages.isStory(messageId)) { AppDependencies.messageNotifier.updateNotification(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/UploadAttachmentToArchiveJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/UploadAttachmentToArchiveJob.kt index 1fcf5f5787..6a66a0c2d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/UploadAttachmentToArchiveJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/UploadAttachmentToArchiveJob.kt @@ -93,6 +93,12 @@ class UploadAttachmentToArchiveJob private constructor( } override fun run(): Result { + if (SignalStore.account.isLinkedDevice) { + Log.w(TAG, "[$attachmentId] Linked devices don't backup media. Skipping.") + SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.NONE) + return Result.success() + } + if (!SignalStore.backup.backsUpMedia) { Log.w(TAG, "[$attachmentId] This user does not back up media. Skipping.") SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.NONE) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index c29e08e3e6..9183417684 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -199,6 +199,15 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { } set(value) { lock.withLock { + val currentValue: ByteArray? = getBlob(KEY_MEDIA_ROOT_BACKUP_KEY, null) + if (currentValue != null) { + val current = MediaRootBackupKey(currentValue) + if (current == value) { + Log.i(TAG, "MediaRootBackupKey the same, skipping.") + return + } + } + Log.i(TAG, "Setting MediaRootBackupKey...", Throwable(), true) store.beginWrite().putBlob(KEY_MEDIA_ROOT_BACKUP_KEY, value.value).commit() mediaCredentials.clearAll() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt index 5db8634951..dcfdc04f64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt @@ -256,9 +256,6 @@ object RegistrationRepository { DirectoryRefreshListener.schedule(context) RotateSignedPreKeyListener.schedule(context) } else { - // TODO [linked-device] May want to have a different opt out mechanism for linked devices - SvrRepository.optOutOfPin() - SignalStore.account.isMultiDevice = true SignalStore.registration.hasUploadedProfile = true jobManager.runJobBlocking(RefreshOwnProfileJob(), 30.seconds) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt index 1ab1080700..f48727a159 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt @@ -1145,7 +1145,6 @@ class RegistrationViewModel : ViewModel() { } // TODO [linked-device] Reapply opt-out, backup restore sets pin, may want to have a different opt out mechanism for linked devices - SvrRepository.optOutOfPin() } for (type in SyncMessage.Request.Type.entries) { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaRootBackupKey.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaRootBackupKey.kt index 13eee4b753..54f8d1717e 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaRootBackupKey.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaRootBackupKey.kt @@ -66,6 +66,19 @@ class MediaRootBackupKey(override val value: ByteArray) : BackupKey { ) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MediaRootBackupKey + + return value.contentEquals(other.value) + } + + override fun hashCode(): Int { + return value.contentHashCode() + } + class MediaKeyMaterial( val id: MediaId, val macKey: ByteArray,