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.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(
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 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) {
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue