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 ffbdf9874b..0165825524 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -263,6 +263,7 @@ public final class JobManagerFactories { put(ResumableUploadSpecJob.KEY, new ResumableUploadSpecJob.Factory()); put(RequestGroupV2InfoWorkerJob.KEY, new RequestGroupV2InfoWorkerJob.Factory()); put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory()); + put(LocalBackupRestoreMediaJob.KEY, new LocalBackupRestoreMediaJob.Factory()); put(RestoreAttachmentJob.KEY, new RestoreAttachmentJob.Factory()); put(RestoreAttachmentThumbnailJob.KEY, new RestoreAttachmentThumbnailJob.Factory()); put(RestoreLocalAttachmentJob.KEY, new RestoreLocalAttachmentJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupRestoreMediaJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupRestoreMediaJob.kt new file mode 100644 index 0000000000..848473bea9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalBackupRestoreMediaJob.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.thoughtcrime.securesms.jobs + +import android.net.Uri +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobs.protos.LocalBackupRestoreMediaJobData +import java.io.File + +/** + * Scans the local backup files directory and enqueues individual [RestoreLocalAttachmentJob]s for each restorable attachment. + */ +class LocalBackupRestoreMediaJob private constructor( + parameters: Parameters, + private val backupDirectoryUri: Uri +) : Job(parameters) { + + companion object { + const val KEY = "LocalBackupRestoreMediaJob" + private val TAG = Log.tag(LocalBackupRestoreMediaJob::class) + + fun create(backupDirectoryUri: Uri): LocalBackupRestoreMediaJob { + return LocalBackupRestoreMediaJob( + Parameters.Builder() + .setLifespan(Parameters.IMMORTAL) + .setMaxAttempts(1) + .build(), + backupDirectoryUri = backupDirectoryUri + ) + } + } + + override fun serialize(): ByteArray { + return LocalBackupRestoreMediaJobData( + backupDirectoryUri = backupDirectoryUri.toString() + ).encode() + } + + override fun getFactoryKey(): String = KEY + + override fun run(): Result { + val archiveFileSystem = when (backupDirectoryUri.scheme) { + "content" -> ArchiveFileSystem.openForRestore(context, backupDirectoryUri) ?: run { + Log.w(TAG, "Unable to open backup directory: $backupDirectoryUri") + return Result.failure() + } + else -> ArchiveFileSystem.fromFile(context, File(backupDirectoryUri.path!!)) + } + + val mediaNameToFileInfo = archiveFileSystem.filesFileSystem.allFiles() + RestoreLocalAttachmentJob.enqueueRestoreLocalAttachmentsJobs(mediaNameToFileInfo) + return Result.success() + } + + override fun onFailure() = Unit + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): LocalBackupRestoreMediaJob { + val data = LocalBackupRestoreMediaJobData.ADAPTER.decode(serializedData!!) + return LocalBackupRestoreMediaJob( + parameters = parameters, + backupDirectoryUri = Uri.parse(data.backupDirectoryUri) + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupActivityViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupActivityViewModel.kt index 2e4d91b89f..30c23e247c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupActivityViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupActivityViewModel.kt @@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.jobs.RestoreLocalAttachmentJob +import org.thoughtcrime.securesms.jobs.LocalBackupRestoreMediaJob import org.thoughtcrime.securesms.keyvalue.Completed import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient @@ -124,8 +124,7 @@ class RestoreLocalBackupActivityViewModel : ViewModel() { if (result is Result.Success) { Log.i(TAG, "Local backup import succeeded") - val mediaNameToFileInfo = archiveFileSystem.filesFileSystem.allFiles() - RestoreLocalAttachmentJob.enqueueRestoreLocalAttachmentsJobs(mediaNameToFileInfo) + AppDependencies.jobManager.add(LocalBackupRestoreMediaJob.create(Uri.parse(backupDirectory))) val actualBackupId = LocalArchiver.getBackupId(snapshotFileSystem, messageBackupKey) val expectedBackupId = SignalStore.account.accountEntropyPool diff --git a/app/src/main/protowire/JobData.proto b/app/src/main/protowire/JobData.proto index 1376ebe635..259acba701 100644 --- a/app/src/main/protowire/JobData.proto +++ b/app/src/main/protowire/JobData.proto @@ -122,6 +122,10 @@ message BackfillCollapsedMessageJobData { int64 lastDateReceived = 1; } +message LocalBackupRestoreMediaJobData { + string backupDirectoryUri = 1; +} + message BackfillDigestJobData { uint64 attachmentId = 1; } diff --git a/app/src/quickstart/java/org/thoughtcrime/securesms/QuickstartRestoreActivity.kt b/app/src/quickstart/java/org/thoughtcrime/securesms/QuickstartRestoreActivity.kt index 8f6c71a1c1..fccd370a52 100644 --- a/app/src/quickstart/java/org/thoughtcrime/securesms/QuickstartRestoreActivity.kt +++ b/app/src/quickstart/java/org/thoughtcrime/securesms/QuickstartRestoreActivity.kt @@ -36,9 +36,9 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.ImportResult import org.thoughtcrime.securesms.backup.v2.RestoreV2Event -import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle -import org.thoughtcrime.securesms.jobs.RestoreLocalAttachmentJob +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobs.EnqueueRestoreLocalAttachmentsJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.registration.ui.shared.RegistrationScreen @@ -172,10 +172,7 @@ class QuickstartRestoreActivity : BaseActivity() { withContext(Dispatchers.Main) { restoreStatus = "Restoring attachments..." } - // Enqueue attachment restore jobs via ArchiveFileSystem (which handles the files/ directory) - val archiveFileSystem = ArchiveFileSystem.fromFile(applicationContext, backupDir) - val mediaNameToFileInfo = archiveFileSystem.filesFileSystem.allFiles() - RestoreLocalAttachmentJob.enqueueRestoreLocalAttachmentsJobs(mediaNameToFileInfo) + AppDependencies.jobManager.add(EnqueueRestoreLocalAttachmentsJob.create(Uri.fromFile(backupDir))) QuickstartInitializer.pendingBackupDir = null