Add job to upload thumbnails to archive.

This commit is contained in:
Greyson Parrelli 2024-05-07 16:38:47 -04:00 committed by Alex Hart
parent 52fb873b1b
commit a9a19d3ae0
8 changed files with 207 additions and 4 deletions

View file

@ -14,6 +14,7 @@ import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
import org.signal.libsignal.messagebackup.MessageBackupKey import org.signal.libsignal.messagebackup.MessageBackupKey
import org.signal.libsignal.protocol.ServiceId.Aci import org.signal.libsignal.protocol.ServiceId.Aci
import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment 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.StatusCodeErrorAction
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
import org.whispersystems.signalservice.api.archive.ArchiveMediaRequest 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.ArchiveServiceCredential
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse 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 api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey() val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return initBackupAndFetchAuth(backupKey) return initBackupAndFetchAuth(backupKey)
.then { credential -> .then { credential ->
api.setPublicKey(backupKey, credential) api.archiveAttachmentMedia(
.map { credential } 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 -> .then { credential ->
val mediaName = attachment.getMediaName() val mediaName = attachment.getMediaName()
val request = attachment.toArchiveMediaRequest(mediaName, backupKey) val request = attachment.toArchiveMediaRequest(mediaName, backupKey)
@ -625,7 +637,11 @@ object BackupRepository {
return MediaName.fromDigest(remoteDigest!!) 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) val mediaSecrets = backupKey.deriveMediaSecrets(mediaName)
return ArchiveMediaRequest( return ArchiveMediaRequest(

View file

@ -186,6 +186,7 @@ class ArchiveAttachmentBackfillJob private constructor(
Log.d(TAG, "Move complete!") Log.d(TAG, "Move complete!")
SignalDatabase.attachments.setArchiveTransferState(attachmentRecord.attachmentId, AttachmentTable.ArchiveTransferState.FINISHED) SignalDatabase.attachments.setArchiveTransferState(attachmentRecord.attachmentId, AttachmentTable.ArchiveTransferState.FINISHED)
ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentRecord.attachmentId)
reenqueueWithIncrementedProgress() reenqueueWithIncrementedProgress()
Result.success() Result.success()
} }

View file

@ -61,6 +61,7 @@ class ArchiveAttachmentJob private constructor(private val attachmentId: Attachm
} }
BackupRepository.archiveMedia(attachment).successOrThrow() BackupRepository.archiveMedia(attachment).successOrThrow()
ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentId)
SignalStore.backup().usedBackupMediaSpace += attachment.size SignalStore.backup().usedBackupMediaSpace += attachment.size
} }

View file

@ -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)
)
}
}
}

View file

@ -158,6 +158,7 @@ class AttachmentUploadJob private constructor(
val remoteAttachment = messageSender.uploadAttachment(localAttachment) val remoteAttachment = messageSender.uploadAttachment(localAttachment)
val attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.fastPreflightId).get() val attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.fastPreflightId).get()
SignalDatabase.attachments.finalizeAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.uploadTimestamp) SignalDatabase.attachments.finalizeAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.uploadTimestamp)
ArchiveThumbnailUploadJob.enqueueIfNecessary(databaseAttachment.attachmentId)
} }
} }
} catch (e: NonSuccessfulResumableUploadResponseCodeException) { } catch (e: NonSuccessfulResumableUploadResponseCodeException) {

View file

@ -102,6 +102,7 @@ public final class JobManagerFactories {
put(AnalyzeDatabaseJob.KEY, new AnalyzeDatabaseJob.Factory()); put(AnalyzeDatabaseJob.KEY, new AnalyzeDatabaseJob.Factory());
put(ArchiveAttachmentJob.KEY, new ArchiveAttachmentJob.Factory()); put(ArchiveAttachmentJob.KEY, new ArchiveAttachmentJob.Factory());
put(ArchiveAttachmentBackfillJob.KEY, new ArchiveAttachmentBackfillJob.Factory()); put(ArchiveAttachmentBackfillJob.KEY, new ArchiveAttachmentBackfillJob.Factory());
put(ArchiveThumbnailUploadJob.KEY, new ArchiveThumbnailUploadJob.Factory());
put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory()); put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory());
put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory()); put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory());
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory()); put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());

View file

@ -407,6 +407,11 @@ public class MediaUtil {
} else return PartAuthority.isAttachmentUri(uri) && MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri)); } 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 @WorkerThread
public static @Nullable Bitmap getVideoThumbnail(@NonNull Context context, @Nullable Uri uri, long timeUs) { public static @Nullable Bitmap getVideoThumbnail(@NonNull Context context, @Nullable Uri uri, long timeUs) {
if (uri == null) { if (uri == null) {

View file

@ -58,3 +58,7 @@ message ArchiveAttachmentBackfillJobData {
optional uint32 count = 3; optional uint32 count = 3;
optional uint32 totalCount = 4; optional uint32 totalCount = 4;
} }
message ArchiveThumbnailUploadJobData {
uint64 attachmentId = 1;
}