From c4f511014893bb935a9808498a84e0219f9b34e8 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 27 Oct 2023 12:02:35 -0700 Subject: [PATCH] Stop falling back to CDN0 for attachments. --- .../securesms/jobs/AttachmentUploadJob.kt | 268 ++++++++++++++++++ .../securesms/jobs/JobManagerFactories.java | 5 +- ...ob.java => LegacyAttachmentUploadJob.java} | 33 +-- .../securesms/jobs/PushSendJob.java | 13 +- .../jobs/ResumableUploadSpecJob.java | 12 +- .../securesms/sms/MessageSender.java | 9 +- .../securesms/sms/UploadDependencyGraph.kt | 3 - app/src/main/protowire/JobData.proto | 9 + .../sms/UploadDependencyGraphTest.kt | 15 +- .../org/signal/core/util/ByteExtensions.kt | 12 + .../signal/core/util/DurationExtensions.kt | 14 + .../api/SignalServiceMessageSender.java | 1 + .../push/http/ResumableUploadSpec.java | 60 ++-- 13 files changed, 364 insertions(+), 90 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt rename app/src/main/java/org/thoughtcrime/securesms/jobs/{AttachmentUploadJob.java => LegacyAttachmentUploadJob.java} (88%) create mode 100644 core-util-jvm/src/main/java/org/signal/core/util/DurationExtensions.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt new file mode 100644 index 0000000000..6b83845dfd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt @@ -0,0 +1,268 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.thoughtcrime.securesms.jobs + +import android.graphics.Bitmap +import android.os.Build +import android.text.TextUtils +import org.greenrobot.eventbus.EventBus +import org.signal.core.util.inRoundedDays +import org.signal.core.util.logging.Log +import org.signal.core.util.mebiBytes +import org.signal.protos.resumableuploads.ResumableUpload +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.attachments.Attachment +import org.thoughtcrime.securesms.attachments.AttachmentId +import org.thoughtcrime.securesms.attachments.PointerAttachment +import org.thoughtcrime.securesms.blurhash.BlurHashEncoder +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.events.PartProgressEvent +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil.startGenericTaskWhenCapable +import org.thoughtcrime.securesms.jobs.protos.AttachmentUploadJobData +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.net.NotPushRegisteredException +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.service.NotificationController +import org.thoughtcrime.securesms.util.FeatureFlags +import org.thoughtcrime.securesms.util.MediaUtil +import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResumableUploadResponseCodeException +import org.whispersystems.signalservice.internal.crypto.PaddingInputStream +import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec +import java.io.IOException +import java.util.Objects +import java.util.Optional +import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.milliseconds + +/** + * Uploads an attachment without alteration. + * + * Queue [AttachmentCompressionJob] before to compress. + */ +class AttachmentUploadJob private constructor( + parameters: Parameters, + private val attachmentId: AttachmentId, + private var uploadSpec: ResumableUpload? +) : BaseJob(parameters) { + + companion object { + const val KEY = "AttachmentUploadJobV3" + + private val TAG = Log.tag(AttachmentUploadJob::class.java) + + private val UPLOAD_REUSE_THRESHOLD = TimeUnit.DAYS.toMillis(3) + + /** + * Foreground notification shows while uploading attachments above this. + */ + private val FOREGROUND_LIMIT = 10.mebiBytes.inWholeBytes + + @JvmStatic + val maxPlaintextSize: Long + get() { + val maxCipherTextSize = FeatureFlags.maxAttachmentSizeBytes() + val maxPaddedSize = AttachmentCipherStreamUtil.getPlaintextLength(maxCipherTextSize) + return PaddingInputStream.getMaxUnpaddedSize(maxPaddedSize) + } + } + + constructor(attachmentId: AttachmentId) : this( + Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + attachmentId, + null + ) + + override fun serialize(): ByteArray { + return AttachmentUploadJobData( + attachmentRowId = attachmentId.rowId, + attachmentUniqueId = attachmentId.uniqueId, + uploadSpec = uploadSpec + ).encode() + } + + override fun getFactoryKey(): String = KEY + + override fun shouldTrace(): Boolean = true + + @Throws(Exception::class) + public override fun onRun() { + if (!Recipient.self().isRegistered) { + throw NotPushRegisteredException() + } + + val messageSender = ApplicationDependencies.getSignalServiceMessageSender() + val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId) ?: throw InvalidAttachmentException("Cannot find the specified attachment.") + + val timeSinceUpload = System.currentTimeMillis() - databaseAttachment.uploadTimestamp + if (timeSinceUpload < UPLOAD_REUSE_THRESHOLD && !TextUtils.isEmpty(databaseAttachment.location)) { + Log.i(TAG, "We can re-use an already-uploaded file. It was uploaded $timeSinceUpload ms (${timeSinceUpload.milliseconds.inRoundedDays()} days) ago. Skipping.") + return + } else if (databaseAttachment.uploadTimestamp > 0) { + Log.i(TAG, "This file was previously-uploaded, but too long ago to be re-used. Age: $timeSinceUpload ms (${timeSinceUpload.milliseconds.inRoundedDays()} days)") + } + + if (uploadSpec != null && System.currentTimeMillis() > uploadSpec!!.timeout) { + Log.w(TAG, "Upload spec expired! Clearing.") + uploadSpec = null + } + + if (uploadSpec == null) { + Log.d(TAG, "Need an upload spec. Fetching...") + uploadSpec = ApplicationDependencies.getSignalServiceMessageSender().getResumableUploadSpec().toProto() + } else { + Log.d(TAG, "Re-using existing upload spec.") + } + + Log.i(TAG, "Uploading attachment for message " + databaseAttachment.mmsId + " with ID " + databaseAttachment.attachmentId) + try { + getAttachmentNotificationIfNeeded(databaseAttachment).use { notification -> + buildAttachmentStream(databaseAttachment, notification, uploadSpec!!).use { localAttachment -> + val remoteAttachment = messageSender.uploadAttachment(localAttachment) + val attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.fastPreflightId).get() + SignalDatabase.attachments.updateAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.uploadTimestamp) + } + } + } catch (e: NonSuccessfulResumableUploadResponseCodeException) { + if (e.code == 400) { + Log.w(TAG, "Failed to upload due to a 400 when getting resumable upload information. Clearing upload spec.", e) + uploadSpec = null + } + + throw e + } + } + + private fun getAttachmentNotificationIfNeeded(attachment: Attachment): NotificationController? { + return if (attachment.size >= FOREGROUND_LIMIT) { + try { + startGenericTaskWhenCapable(context, context.getString(R.string.AttachmentUploadJob_uploading_media)) + } catch (e: UnableToStartException) { + Log.w(TAG, "Unable to start foreground service", e) + null + } + } else { + null + } + } + + override fun onFailure() { + if (isCanceled) { + SignalDatabase.attachments.deleteAttachment(attachmentId) + } + } + + override fun onShouldRetry(exception: Exception): Boolean { + return exception is IOException && exception !is NotPushRegisteredException + } + + @Throws(InvalidAttachmentException::class) + private fun buildAttachmentStream(attachment: Attachment, notification: NotificationController?, resumableUploadSpec: ResumableUpload): SignalServiceAttachmentStream { + if (attachment.uri == null || attachment.size == 0L) { + throw InvalidAttachmentException(IOException("Outgoing attachment has no data!")) + } + + return try { + val inputStream = PartAuthority.getAttachmentStream(context, attachment.uri!!) + val builder = SignalServiceAttachment.newStreamBuilder() + .withStream(inputStream) + .withContentType(attachment.contentType) + .withLength(attachment.size) + .withFileName(attachment.fileName) + .withVoiceNote(attachment.isVoiceNote) + .withBorderless(attachment.isBorderless) + .withGif(attachment.isVideoGif) + .withWidth(attachment.width) + .withHeight(attachment.height) + .withUploadTimestamp(System.currentTimeMillis()) + .withCaption(attachment.caption) + .withResumableUploadSpec(ResumableUploadSpec.from(resumableUploadSpec)) + .withCancelationSignal { this.isCanceled } + .withListener(object : SignalServiceAttachment.ProgressListener { + override fun onAttachmentProgress(total: Long, progress: Long) { + EventBus.getDefault().postSticky(PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)) + notification?.setProgress(total, progress) + } + + override fun shouldCancel(): Boolean { + return isCanceled + } + }) + + if (MediaUtil.isImageType(attachment.contentType)) { + builder.withBlurHash(getImageBlurHash(attachment)).build() + } else if (MediaUtil.isVideoType(attachment.contentType)) { + builder.withBlurHash(getVideoBlurHash(attachment)).build() + } else { + builder.build() + } + } catch (e: IOException) { + throw InvalidAttachmentException(e) + } + } + + @Throws(IOException::class) + private fun getImageBlurHash(attachment: Attachment): String? { + if (attachment.blurHash != null) { + return attachment.blurHash!!.hash + } + + if (attachment.uri == null) { + return null + } + + return PartAuthority.getAttachmentStream(context, attachment.uri!!).use { inputStream -> + BlurHashEncoder.encode(inputStream) + } + } + + @Throws(IOException::class) + private fun getVideoBlurHash(attachment: Attachment): String? { + if (attachment.blurHash != null) { + return attachment.blurHash!!.hash + } + + if (Build.VERSION.SDK_INT < 23) { + Log.w(TAG, "Video thumbnails not supported...") + return null + } + + return MediaUtil.getVideoThumbnail(context, Objects.requireNonNull(attachment.uri), 1000)?.let { bitmap -> + val thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false) + bitmap.recycle() + + Log.i(TAG, "Generated video thumbnail...") + val hash = BlurHashEncoder.encode(thumb) + thumb.recycle() + + hash + } + } + + private inner class InvalidAttachmentException : Exception { + constructor(message: String?) : super(message) + constructor(e: Exception?) : super(e) + } + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): AttachmentUploadJob { + val data = AttachmentUploadJobData.ADAPTER.decode(serializedData!!) + return AttachmentUploadJob( + parameters = parameters, + attachmentId = AttachmentId(data.attachmentRowId, data.attachmentUniqueId), + data.uploadSpec + ) + } + } +} 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 c51e795896..dcf7c2e383 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -95,11 +95,11 @@ public final class JobManagerFactories { public static Map getJobFactories(@NonNull Application application) { return new HashMap() {{ put(AccountConsistencyWorkerJob.KEY, new AccountConsistencyWorkerJob.Factory()); + put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory()); put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory()); put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory()); - put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory()); put(AttachmentMarkUploadedJob.KEY, new AttachmentMarkUploadedJob.Factory()); - put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory()); + put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory()); put(AutomaticSessionResetJob.KEY, new AutomaticSessionResetJob.Factory()); put(AvatarGroupsV1DownloadJob.KEY, new AvatarGroupsV1DownloadJob.Factory()); put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory()); @@ -132,6 +132,7 @@ public final class JobManagerFactories { put(IndividualSendJob.KEY, new IndividualSendJob.Factory()); put(LeaveGroupV2Job.KEY, new LeaveGroupV2Job.Factory()); put(LeaveGroupV2WorkerJob.KEY, new LeaveGroupV2WorkerJob.Factory()); + put(LegacyAttachmentUploadJob.KEY, new LegacyAttachmentUploadJob.Factory()); put(LocalBackupJob.KEY, new LocalBackupJob.Factory()); put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory()); put(MarkerJob.KEY, new MarkerJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LegacyAttachmentUploadJob.java similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java rename to app/src/main/java/org/thoughtcrime/securesms/jobs/LegacyAttachmentUploadJob.java index f6a302c866..2c1271c06f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LegacyAttachmentUploadJob.java @@ -26,21 +26,17 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JsonJobData; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.NotificationController; -import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.MediaUtil; import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResumableUploadResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException; -import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import java.io.IOException; @@ -54,12 +50,13 @@ import java.util.concurrent.TimeUnit; *

* Queue {@link AttachmentCompressionJob} before to compress. */ -public final class AttachmentUploadJob extends BaseJob { +@Deprecated +public final class LegacyAttachmentUploadJob extends BaseJob { public static final String KEY = "AttachmentUploadJobV2"; @SuppressWarnings("unused") - private static final String TAG = Log.tag(AttachmentUploadJob.class); + private static final String TAG = Log.tag(LegacyAttachmentUploadJob.class); private static final long UPLOAD_REUSE_THRESHOLD = TimeUnit.DAYS.toMillis(3); @@ -72,27 +69,11 @@ public final class AttachmentUploadJob extends BaseJob { */ private static final int FOREGROUND_LIMIT = 10 * 1024 * 1024; - public static long getMaxPlaintextSize() { - long maxCipherTextSize = FeatureFlags.maxAttachmentSizeBytes(); - long maxPaddedSize = AttachmentCipherStreamUtil.getPlaintextLength(maxCipherTextSize); - return PaddingInputStream.getMaxUnpaddedSize(maxPaddedSize); - } - private final AttachmentId attachmentId; private boolean forceV2; - public AttachmentUploadJob(AttachmentId attachmentId) { - this(new Job.Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), - attachmentId, - false); - } - - private AttachmentUploadJob(@NonNull Job.Parameters parameters, @NonNull AttachmentId attachmentId, boolean forceV2) { + private LegacyAttachmentUploadJob(@NonNull Job.Parameters parameters, @NonNull AttachmentId attachmentId, boolean forceV2) { super(parameters); this.attachmentId = attachmentId; this.forceV2 = forceV2; @@ -289,12 +270,12 @@ public final class AttachmentUploadJob extends BaseJob { } } - public static final class Factory implements Job.Factory { + public static final class Factory implements Job.Factory { @Override - public @NonNull AttachmentUploadJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { + public @NonNull LegacyAttachmentUploadJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { JsonJobData data = JsonJobData.deserialize(serializedData); - return new AttachmentUploadJob(parameters, new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID)), data.getBooleanOrDefault(KEY_FORCE_V2, false)); + return new LegacyAttachmentUploadJob(parameters, new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID)), data.getBooleanOrDefault(KEY_FORCE_V2, false)); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 43c23b6025..90d16c0721 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -242,16 +242,9 @@ public abstract class PushSendJob extends SendJob { return new HashSet<>(Stream.of(attachments).map(a -> { AttachmentUploadJob attachmentUploadJob = new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId()); - if (message.isGroup()) { - jobManager.startChain(AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1)) - .then(attachmentUploadJob) - .enqueue(); - } else { - jobManager.startChain(AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1)) - .then(new ResumableUploadSpecJob()) - .then(attachmentUploadJob) - .enqueue(); - } + jobManager.startChain(AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1)) + .then(attachmentUploadJob) + .enqueue(); return attachmentUploadJob.getId(); }) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java index b73b2b8a0c..47147ff4a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ResumableUploadSpecJob.java @@ -13,6 +13,10 @@ import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import java.io.IOException; import java.util.concurrent.TimeUnit; +/** + * No longer used. Functionality has been merged into {@link AttachmentUploadJob}. + */ +@Deprecated public class ResumableUploadSpecJob extends BaseJob { private static final String TAG = Log.tag(ResumableUploadSpecJob.class); @@ -21,14 +25,6 @@ public class ResumableUploadSpecJob extends BaseJob { public static final String KEY = "ResumableUploadSpecJob"; - public ResumableUploadSpecJob() { - this(new Job.Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build()); - } - private ResumableUploadSpecJob(@NonNull Parameters parameters) { super(parameters); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index 4e51e50247..55b52d5d6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -59,7 +59,6 @@ import org.thoughtcrime.securesms.jobs.PushGroupSendJob; import org.thoughtcrime.securesms.jobs.IndividualSendJob; import org.thoughtcrime.securesms.jobs.ReactionSendJob; import org.thoughtcrime.securesms.jobs.RemoteDeleteSendJob; -import org.thoughtcrime.securesms.jobs.ResumableUploadSpecJob; import org.thoughtcrime.securesms.jobs.SmsSendJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.linkpreview.LinkPreview; @@ -418,17 +417,15 @@ public class MessageSender { AttachmentTable attachmentDatabase = SignalDatabase.attachments(); DatabaseAttachment databaseAttachment = attachmentDatabase.insertAttachmentForPreUpload(attachment); - Job compressionJob = AttachmentCompressionJob.fromAttachment(databaseAttachment, false, -1); - Job resumableUploadSpecJob = new ResumableUploadSpecJob(); - Job uploadJob = new AttachmentUploadJob(databaseAttachment.getAttachmentId()); + Job compressionJob = AttachmentCompressionJob.fromAttachment(databaseAttachment, false, -1); + Job uploadJob = new AttachmentUploadJob(databaseAttachment.getAttachmentId()); ApplicationDependencies.getJobManager() .startChain(compressionJob) - .then(resumableUploadSpecJob) .then(uploadJob) .enqueue(); - return new PreUploadResult(media, databaseAttachment.getAttachmentId(), Arrays.asList(compressionJob.getId(), resumableUploadSpecJob.getId(), uploadJob.getId())); + return new PreUploadResult(media, databaseAttachment.getAttachmentId(), Arrays.asList(compressionJob.getId(), uploadJob.getId())); } catch (MmsException e) { Log.w(TAG, "preUploadPushAttachment() - Failed to upload!", e); return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/UploadDependencyGraph.kt b/app/src/main/java/org/thoughtcrime/securesms/sms/UploadDependencyGraph.kt index 04f6ab202b..b2d475e0e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/UploadDependencyGraph.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/UploadDependencyGraph.kt @@ -11,7 +11,6 @@ import org.thoughtcrime.securesms.jobmanager.JobManager import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob import org.thoughtcrime.securesms.jobs.AttachmentCopyJob import org.thoughtcrime.securesms.jobs.AttachmentUploadJob -import org.thoughtcrime.securesms.jobs.ResumableUploadSpecJob import org.thoughtcrime.securesms.mms.OutgoingMessage /** @@ -196,12 +195,10 @@ class UploadDependencyGraph private constructor( */ private fun createAttachmentUploadChain(jobManager: JobManager, databaseAttachment: DatabaseAttachment): Pair { val compressionJob: Job = AttachmentCompressionJob.fromAttachment(databaseAttachment, false, -1) - val resumableUploadSpecJob: Job = ResumableUploadSpecJob() val uploadJob: Job = AttachmentUploadJob(databaseAttachment.attachmentId) return uploadJob.id to jobManager .startChain(compressionJob) - .then(resumableUploadSpecJob) .then(uploadJob) } } diff --git a/app/src/main/protowire/JobData.proto b/app/src/main/protowire/JobData.proto index aa7020bed5..6503ce3c12 100644 --- a/app/src/main/protowire/JobData.proto +++ b/app/src/main/protowire/JobData.proto @@ -2,9 +2,12 @@ syntax = "proto3"; package signal; +import "ResumableUploads.proto"; + option java_package = "org.thoughtcrime.securesms.jobs.protos"; option java_multiple_files = true; + message CallSyncEventJobRecord { uint64 recipientId = 1; reserved 2; @@ -28,3 +31,9 @@ message CallLogEventSendJobData { message CallLinkUpdateSendJobData { string callLinkRoomId = 1; } + +message AttachmentUploadJobData { + uint64 attachmentRowId = 1; + uint64 attachmentUniqueId = 2; + optional ResumableUpload uploadSpec = 3; +} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt b/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt index bcf5946565..9b499e7187 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.jobmanager.JsonJobData import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob import org.thoughtcrime.securesms.jobs.AttachmentCopyJob import org.thoughtcrime.securesms.jobs.AttachmentUploadJob -import org.thoughtcrime.securesms.jobs.ResumableUploadSpecJob +import org.thoughtcrime.securesms.jobs.protos.AttachmentUploadJobData import org.thoughtcrime.securesms.mms.OutgoingMessage import org.thoughtcrime.securesms.mms.SentMediaQuality import org.thoughtcrime.securesms.recipients.Recipient @@ -206,16 +206,15 @@ class UploadDependencyGraphTest { assertTrue(steps.all { it.size == 1 }) assertTrue(steps[0][0] is AttachmentCompressionJob) - assertTrue(steps[1][0] is ResumableUploadSpecJob) - assertTrue(steps[2][0] is AttachmentUploadJob) + assertTrue(steps[1][0] is AttachmentUploadJob) if (expectedCopyDestinationCount > 0) { - assertTrue(steps[3][0] is AttachmentCopyJob) + assertTrue(steps[2][0] is AttachmentCopyJob) - val uploadData = JsonJobData.deserialize(steps[2][0].serialize()) - val copyData = JsonJobData.deserialize(steps[3][0].serialize()) + val uploadData = AttachmentUploadJobData.ADAPTER.decode(steps[1][0].serialize()!!) + val copyData = JsonJobData.deserialize(steps[2][0].serialize()) - val uploadAttachmentId = AttachmentId(uploadData.getLong("row_id"), uploadData.getLong("unique_id")) + val uploadAttachmentId = AttachmentId(uploadData.attachmentRowId, uploadData.attachmentUniqueId) val copySourceAttachmentId = JsonUtils.fromJson(copyData.getString("source_id"), AttachmentId::class.java) assertEquals(uploadAttachmentId, copySourceAttachmentId) @@ -223,7 +222,7 @@ class UploadDependencyGraphTest { val copyDestinations = copyData.getStringArray("destination_ids") assertEquals(expectedCopyDestinationCount, copyDestinations.size) } else { - assertEquals(3, steps.size) + assertEquals(2, steps.size) } } diff --git a/core-util-jvm/src/main/java/org/signal/core/util/ByteExtensions.kt b/core-util-jvm/src/main/java/org/signal/core/util/ByteExtensions.kt index 52067d0d1b..5b8f5b9a8b 100644 --- a/core-util-jvm/src/main/java/org/signal/core/util/ByteExtensions.kt +++ b/core-util-jvm/src/main/java/org/signal/core/util/ByteExtensions.kt @@ -8,15 +8,27 @@ package org.signal.core.util inline val Long.bytes: ByteSize get() = ByteSize(this) +inline val Int.bytes: ByteSize + get() = ByteSize(this.toLong()) + inline val Long.kibiBytes: ByteSize get() = (this * 1024).bytes +inline val Int.kibiBytes: ByteSize + get() = (this * 1024).bytes + inline val Long.mebiBytes: ByteSize get() = (this * 1024).kibiBytes +inline val Int.mebiBytes: ByteSize + get() = (this * 1024).kibiBytes + inline val Long.gibiBytes: ByteSize get() = (this * 1024).mebiBytes +inline val Int.gibiBytes: ByteSize + get() = (this * 1024).mebiBytes + class ByteSize(val bytes: Long) { val inWholeBytes: Long get() = bytes diff --git a/core-util-jvm/src/main/java/org/signal/core/util/DurationExtensions.kt b/core-util-jvm/src/main/java/org/signal/core/util/DurationExtensions.kt new file mode 100644 index 0000000000..d3fb7e867e --- /dev/null +++ b/core-util-jvm/src/main/java/org/signal/core/util/DurationExtensions.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.util + +import kotlin.time.Duration +import kotlin.time.DurationUnit + +fun Duration.inRoundedMilliseconds(places: Int = 2) = this.toDouble(DurationUnit.MILLISECONDS).roundedString(places) +fun Duration.inRoundedMinutes(places: Int = 2) = this.toDouble(DurationUnit.MINUTES).roundedString(places) +fun Duration.inRoundedHours(places: Int = 2) = this.toDouble(DurationUnit.HOURS).roundedString(places) +fun Duration.inRoundedDays(places: Int = 2) = this.toDouble(DurationUnit.DAYS).roundedString(places) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 7cd3a3a7c9..39b854cf9a 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -798,6 +798,7 @@ public class SignalServiceMessageSender { if (attachment.getResumableUploadSpec().isPresent()) { return uploadAttachmentV4(attachment, attachmentKey, attachmentData); } else { + Log.w(TAG, "Using legacy attachment upload endpoint."); return uploadAttachmentV2(attachment, attachmentKey, attachmentData); } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/http/ResumableUploadSpec.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/http/ResumableUploadSpec.java index c1f28c4edf..d3fe2d2419 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/http/ResumableUploadSpec.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/http/ResumableUploadSpec.java @@ -6,7 +6,6 @@ import org.signal.core.util.Base64; import java.io.IOException; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -68,15 +67,15 @@ public final class ResumableUploadSpec { return headers; } - public String serialize() { + public ResumableUpload toProto() { ResumableUpload.Builder builder = new ResumableUpload.Builder() - .secretKey(ByteString.of(getSecretKey())) - .iv(ByteString.of(getIV())) - .timeout(getExpirationTimestamp()) - .cdnNumber(getCdnNumber()) - .cdnKey(getCdnKey()) - .location(getResumeLocation()) - .timeout(getExpirationTimestamp()); + .secretKey(ByteString.of(getSecretKey())) + .iv(ByteString.of(getIV())) + .timeout(getExpirationTimestamp()) + .cdnNumber(getCdnNumber()) + .cdnKey(getCdnKey()) + .location(getResumeLocation()) + .timeout(getExpirationTimestamp()); builder.headers( headers.entrySet() @@ -85,31 +84,38 @@ public final class ResumableUploadSpec { .collect(Collectors.toList()) ); - return Base64.encodeWithPadding(builder.build().encode()); + return builder.build(); + } + + public String serialize() { + return Base64.encodeWithPadding(toProto().encode()); } public static ResumableUploadSpec deserialize(String serializedSpec) throws ResumeLocationInvalidException { - if (serializedSpec == null) return null; - try { ResumableUpload resumableUpload = ResumableUpload.ADAPTER.decode(Base64.decode(serializedSpec)); - - Map headers = new HashMap<>(); - for (ResumableUpload.Header header : resumableUpload.headers) { - headers.put(header.key, header.value_); - } - - return new ResumableUploadSpec( - resumableUpload.secretKey.toByteArray(), - resumableUpload.iv.toByteArray(), - resumableUpload.cdnKey, - resumableUpload.cdnNumber, - resumableUpload.location, - resumableUpload.timeout, - headers - ); + return from(resumableUpload); } catch (IOException e) { throw new ResumeLocationInvalidException(); } } + + public static ResumableUploadSpec from(ResumableUpload resumableUpload) throws ResumeLocationInvalidException { + if (resumableUpload == null) return null; + + Map headers = new HashMap<>(); + for (ResumableUpload.Header header : resumableUpload.headers) { + headers.put(header.key, header.value_); + } + + return new ResumableUploadSpec( + resumableUpload.secretKey.toByteArray(), + resumableUpload.iv.toByteArray(), + resumableUpload.cdnKey, + resumableUpload.cdnNumber, + resumableUpload.location, + resumableUpload.timeout, + headers + ); + } }