Add job to upload thumbnails to archive.
This commit is contained in:
parent
52fb873b1b
commit
a9a19d3ae0
8 changed files with 207 additions and 4 deletions
|
@ -14,6 +14,7 @@ import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
|
|||
import org.signal.libsignal.messagebackup.MessageBackupKey
|
||||
import org.signal.libsignal.protocol.ServiceId.Aci
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
|
@ -40,6 +41,7 @@ import org.whispersystems.signalservice.api.NetworkResult
|
|||
import org.whispersystems.signalservice.api.StatusCodeErrorAction
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveMediaRequest
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveMediaResponse
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
|
||||
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest
|
||||
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse
|
||||
|
@ -351,15 +353,25 @@ object BackupRepository {
|
|||
}
|
||||
}
|
||||
|
||||
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<Unit> {
|
||||
fun archiveThumbnail(thumbnailAttachment: Attachment, parentAttachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
api.setPublicKey(backupKey, credential)
|
||||
.map { credential }
|
||||
api.archiveAttachmentMedia(
|
||||
backupKey = backupKey,
|
||||
serviceCredential = credential,
|
||||
item = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<Unit> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
val mediaName = attachment.getMediaName()
|
||||
val request = attachment.toArchiveMediaRequest(mediaName, backupKey)
|
||||
|
@ -625,7 +637,11 @@ object BackupRepository {
|
|||
return MediaName.fromDigest(remoteDigest!!)
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.toArchiveMediaRequest(mediaName: MediaName, backupKey: BackupKey): ArchiveMediaRequest {
|
||||
fun DatabaseAttachment.getThumbnailMediaName(): MediaName {
|
||||
return MediaName.fromDigestForThumbnail(remoteDigest!!)
|
||||
}
|
||||
|
||||
private fun Attachment.toArchiveMediaRequest(mediaName: MediaName, backupKey: BackupKey): ArchiveMediaRequest {
|
||||
val mediaSecrets = backupKey.deriveMediaSecrets(mediaName)
|
||||
|
||||
return ArchiveMediaRequest(
|
||||
|
|
|
@ -186,6 +186,7 @@ class ArchiveAttachmentBackfillJob private constructor(
|
|||
Log.d(TAG, "Move complete!")
|
||||
|
||||
SignalDatabase.attachments.setArchiveTransferState(attachmentRecord.attachmentId, AttachmentTable.ArchiveTransferState.FINISHED)
|
||||
ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentRecord.attachmentId)
|
||||
reenqueueWithIncrementedProgress()
|
||||
Result.success()
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ class ArchiveAttachmentJob private constructor(private val attachmentId: Attachm
|
|||
}
|
||||
|
||||
BackupRepository.archiveMedia(attachment).successOrThrow()
|
||||
ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentId)
|
||||
|
||||
SignalStore.backup().usedBackupMediaSpace += attachment.size
|
||||
}
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import android.os.Build
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.protos.resumableuploads.ResumableUpload
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.protos.ArchiveThumbnailUploadJobData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||
import org.thoughtcrime.securesms.util.ImageCompressionUtil
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream
|
||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.lang.RuntimeException
|
||||
import java.util.Optional
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Uploads a thumbnail for the specified attachment to the archive service, if possible.
|
||||
*/
|
||||
class ArchiveThumbnailUploadJob private constructor(
|
||||
params: Parameters,
|
||||
val attachmentId: AttachmentId
|
||||
) : Job(params) {
|
||||
|
||||
companion object {
|
||||
const val KEY = "ArchiveThumbnailUploadJob"
|
||||
private val TAG = Log.tag(ArchiveThumbnailUploadJob::class.java)
|
||||
|
||||
fun enqueueIfNecessary(attachmentId: AttachmentId) {
|
||||
if (SignalStore.backup().canReadWriteToArchiveCdn) {
|
||||
ApplicationDependencies.getJobManager().add(ArchiveThumbnailUploadJob(attachmentId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(attachmentId: AttachmentId) : this(
|
||||
Parameters.Builder()
|
||||
.setQueue("ArchiveThumbnailUploadJob")
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(1.days.inWholeMilliseconds)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
attachmentId
|
||||
)
|
||||
|
||||
override fun serialize(): ByteArray {
|
||||
return ArchiveThumbnailUploadJobData(
|
||||
attachmentId = attachmentId.id
|
||||
).encode()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun run(): Result {
|
||||
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
if (attachment == null) {
|
||||
Log.w(TAG, "$attachmentId not found, assuming this job is no longer necessary.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (attachment.remoteDigest == null) {
|
||||
Log.w(TAG, "$attachmentId was never uploaded! Cannot proceed.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val thumbnailResult = generateThumbnailIfPossible(attachment)
|
||||
if (thumbnailResult == null) {
|
||||
Log.w(TAG, "Unable to generate a thumbnail result for $attachmentId")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val resumableUpload = when (val result = BackupRepository.getMediaUploadSpec()) {
|
||||
is NetworkResult.Success -> {
|
||||
Log.d(TAG, "Got an upload spec!")
|
||||
result.result.toProto()
|
||||
}
|
||||
is NetworkResult.ApplicationError -> {
|
||||
Log.w(TAG, "Failed to get an upload spec due to an application error. Retrying.", result.throwable)
|
||||
return Result.retry(defaultBackoff())
|
||||
}
|
||||
is NetworkResult.NetworkError -> {
|
||||
Log.w(TAG, "Encountered a transient network error when getting upload spec. Retrying.")
|
||||
return Result.retry(defaultBackoff())
|
||||
}
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
Log.w(TAG, "Failed to get an upload spec with status code ${result.code}")
|
||||
return Result.retry(defaultBackoff())
|
||||
}
|
||||
}
|
||||
|
||||
val stream = buildSignalServiceAttachmentStream(thumbnailResult, resumableUpload)
|
||||
|
||||
val attachmentPointer: Attachment = try {
|
||||
val pointer = ApplicationDependencies.getSignalServiceMessageSender().uploadAttachment(stream)
|
||||
PointerAttachment.forPointer(Optional.of(pointer)).get()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to upload attachment", e)
|
||||
return Result.retry(defaultBackoff())
|
||||
}
|
||||
|
||||
return when (val result = BackupRepository.archiveThumbnail(attachmentPointer, attachment)) {
|
||||
is NetworkResult.Success -> {
|
||||
Log.d(TAG, "Successfully archived thumbnail for $attachmentId")
|
||||
Result.success()
|
||||
}
|
||||
is NetworkResult.NetworkError -> {
|
||||
Log.w(TAG, "Hit a network error when trying to archive thumbnail for $attachmentId", result.exception)
|
||||
Result.retry(defaultBackoff())
|
||||
}
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
Log.w(TAG, "Hit a status code error of ${result.code} when trying to archive thumbnail for $attachmentId")
|
||||
Result.retry(defaultBackoff())
|
||||
}
|
||||
is NetworkResult.ApplicationError -> Result.fatalFailure(RuntimeException(result.throwable))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
}
|
||||
|
||||
private fun generateThumbnailIfPossible(attachment: DatabaseAttachment): ImageCompressionUtil.Result? {
|
||||
val uri: DecryptableUri = attachment.uri?.let { DecryptableUri(it) } ?: return null
|
||||
|
||||
return if (MediaUtil.isImageType(attachment.contentType)) {
|
||||
ImageCompressionUtil.compress(context, attachment.contentType, uri, 256, 50)
|
||||
} else if (Build.VERSION.SDK_INT >= 23 && MediaUtil.isVideoType(attachment.contentType)) {
|
||||
MediaUtil.getVideoThumbnail(context, attachment.uri)?.let {
|
||||
ImageCompressionUtil.compress(context, attachment.contentType, uri, 256, 50)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSignalServiceAttachmentStream(result: ImageCompressionUtil.Result, uploadSpec: ResumableUpload): SignalServiceAttachmentStream {
|
||||
return SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(ByteArrayInputStream(result.data))
|
||||
.withContentType(result.mimeType)
|
||||
.withLength(result.data.size.toLong())
|
||||
.withWidth(result.width)
|
||||
.withHeight(result.height)
|
||||
.withUploadTimestamp(System.currentTimeMillis())
|
||||
.withResumableUploadSpec(ResumableUploadSpec.from(uploadSpec))
|
||||
.build()
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<ArchiveThumbnailUploadJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): ArchiveThumbnailUploadJob {
|
||||
val data = ArchiveThumbnailUploadJobData.ADAPTER.decode(serializedData!!)
|
||||
return ArchiveThumbnailUploadJob(
|
||||
params = parameters,
|
||||
attachmentId = AttachmentId(data.attachmentId)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -158,6 +158,7 @@ class AttachmentUploadJob private constructor(
|
|||
val remoteAttachment = messageSender.uploadAttachment(localAttachment)
|
||||
val attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.fastPreflightId).get()
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.uploadTimestamp)
|
||||
ArchiveThumbnailUploadJob.enqueueIfNecessary(databaseAttachment.attachmentId)
|
||||
}
|
||||
}
|
||||
} catch (e: NonSuccessfulResumableUploadResponseCodeException) {
|
||||
|
|
|
@ -102,6 +102,7 @@ public final class JobManagerFactories {
|
|||
put(AnalyzeDatabaseJob.KEY, new AnalyzeDatabaseJob.Factory());
|
||||
put(ArchiveAttachmentJob.KEY, new ArchiveAttachmentJob.Factory());
|
||||
put(ArchiveAttachmentBackfillJob.KEY, new ArchiveAttachmentBackfillJob.Factory());
|
||||
put(ArchiveThumbnailUploadJob.KEY, new ArchiveThumbnailUploadJob.Factory());
|
||||
put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory());
|
||||
put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory());
|
||||
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
|
||||
|
|
|
@ -407,6 +407,11 @@ public class MediaUtil {
|
|||
} else return PartAuthority.isAttachmentUri(uri) && MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @Nullable Bitmap getVideoThumbnail(@NonNull Context context, @Nullable Uri uri) {
|
||||
return getVideoThumbnail(context, uri, 1000);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @Nullable Bitmap getVideoThumbnail(@NonNull Context context, @Nullable Uri uri, long timeUs) {
|
||||
if (uri == null) {
|
||||
|
|
|
@ -57,4 +57,8 @@ message ArchiveAttachmentBackfillJobData {
|
|||
ResumableUpload uploadSpec = 2;
|
||||
optional uint32 count = 3;
|
||||
optional uint32 totalCount = 4;
|
||||
}
|
||||
|
||||
message ArchiveThumbnailUploadJobData {
|
||||
uint64 attachmentId = 1;
|
||||
}
|
Loading…
Add table
Reference in a new issue