mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-24 05:21:34 +00:00
Upload attachments in a separate job.
This commit is contained in:
144
src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
Normal file
144
src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
Normal file
@@ -0,0 +1,144 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
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.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.work.Data;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
public class AttachmentUploadJob extends ContextJob implements InjectableType {
|
||||
|
||||
private static final String TAG = AttachmentUploadJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_ROW_ID = "row_id";
|
||||
private static final String KEY_UNIQUE_ID = "unique_id";
|
||||
|
||||
private AttachmentId attachmentId;
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
protected AttachmentUploadJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
||||
super(context, workerParameters);
|
||||
}
|
||||
|
||||
protected AttachmentUploadJob(@NonNull Context context, AttachmentId attachmentId) {
|
||||
super(context, new JobParameters.Builder()
|
||||
.withNetworkRequirement()
|
||||
.withRetryDuration(TimeUnit.DAYS.toMillis(1))
|
||||
.create());
|
||||
|
||||
this.attachmentId = attachmentId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(@NonNull SafeData data) {
|
||||
this.attachmentId = new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
||||
return dataBuilder.putLong(KEY_ROW_ID, attachmentId.getRowId())
|
||||
.putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws Exception {
|
||||
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment databaseAttachment = database.getAttachment(attachmentId);
|
||||
|
||||
if (databaseAttachment == null) {
|
||||
throw new IllegalStateException("Cannot find the specified attachment.");
|
||||
}
|
||||
|
||||
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
|
||||
Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
|
||||
SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment);
|
||||
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream());
|
||||
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment)).get();
|
||||
|
||||
database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCanceled() { }
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(Exception exception) {
|
||||
return exception instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
protected SignalServiceAttachment getAttachmentFor(Attachment attachment) {
|
||||
try {
|
||||
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
|
||||
return SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(is)
|
||||
.withContentType(attachment.getContentType())
|
||||
.withLength(attachment.getSize())
|
||||
.withFileName(attachment.getFileName())
|
||||
.withVoiceNote(attachment.isVoiceNote())
|
||||
.withWidth(attachment.getWidth())
|
||||
.withHeight(attachment.getHeight())
|
||||
.withCaption(attachment.getCaption())
|
||||
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
|
||||
.build();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Couldn't open attachment", ioe);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Attachment scaleAndStripExif(@NonNull AttachmentDatabase attachmentDatabase,
|
||||
@NonNull MediaConstraints constraints,
|
||||
@NonNull Attachment attachment)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
try {
|
||||
if (constraints.isSatisfied(context, attachment)) {
|
||||
if (MediaUtil.isJpeg(attachment)) {
|
||||
MediaStream stripped = constraints.getResizedMedia(context, attachment);
|
||||
return attachmentDatabase.updateAttachmentData(attachment, stripped);
|
||||
} else {
|
||||
return attachment;
|
||||
}
|
||||
} else if (constraints.canResize(attachment)) {
|
||||
MediaStream resized = constraints.getResizedMedia(context, attachment);
|
||||
return attachmentDatabase.updateAttachmentData(attachment, resized);
|
||||
} else {
|
||||
throw new UndeliverableMessageException("Size constraints could not be met!");
|
||||
}
|
||||
} catch (IOException | MmsException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,6 @@ import androidx.work.WorkerParameters;
|
||||
public class PushContentReceiveJob extends PushReceivedJob {
|
||||
|
||||
private static final long serialVersionUID = 5685475456901715638L;
|
||||
private static final String TAG = PushContentReceiveJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_DATA = "data";
|
||||
|
||||
private String data;
|
||||
|
||||
public PushContentReceiveJob(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
|
||||
super(context, workerParameters);
|
||||
@@ -31,40 +26,21 @@ public class PushContentReceiveJob extends PushReceivedJob {
|
||||
|
||||
public PushContentReceiveJob(Context context) {
|
||||
super(context, JobParameters.newBuilder().create());
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
public PushContentReceiveJob(Context context, String data) {
|
||||
super(context, JobParameters.newBuilder().create());
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(@NonNull SafeData data) {
|
||||
this.data = data.getString(KEY_DATA);
|
||||
}
|
||||
protected void initialize(@NonNull SafeData data) { }
|
||||
|
||||
@Override
|
||||
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
||||
return dataBuilder.putString(KEY_DATA, data).build();
|
||||
return dataBuilder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() {
|
||||
try {
|
||||
String sessionKey = TextSecurePreferences.getSignalingKey(context);
|
||||
SignalServiceEnvelope envelope = new SignalServiceEnvelope(data, sessionKey);
|
||||
|
||||
processEnvelope(envelope);
|
||||
} catch (IOException | InvalidVersionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
public void onRun() { }
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
|
||||
}
|
||||
public void onCanceled() { }
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(Exception exception) {
|
||||
|
||||
@@ -2,9 +2,12 @@ package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -12,8 +15,10 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.ChainParameters;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -61,6 +66,29 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) {
|
||||
try {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
List<AttachmentUploadJob> attachmentJobs = Stream.of(message.getAttachments()).map(a -> new AttachmentUploadJob(context, ((DatabaseAttachment) a).getAttachmentId())).toList();
|
||||
ChainParameters chainParams = new ChainParameters.Builder().setGroupId(destination.serialize()).build();
|
||||
|
||||
if (attachmentJobs.isEmpty()) {
|
||||
jobManager.add(new PushMediaSendJob(context, messageId, destination));
|
||||
} else {
|
||||
jobManager.startChain(attachmentJobs)
|
||||
.then(new PushMediaSendJob(context, messageId, destination))
|
||||
.enqueue(chainParams);
|
||||
}
|
||||
|
||||
} catch (NoSuchMessageException | MmsException e) {
|
||||
Log.w(TAG, "Failed to enqueue message.", e);
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(@NonNull SafeData data) {
|
||||
messageId = data.getLong(KEY_MESSAGE_ID);
|
||||
@@ -158,23 +186,21 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
try {
|
||||
rotateSenderCertificateIfNecessary();
|
||||
|
||||
SignalServiceAddress address = getPushAddress(message.getRecipient().getAddress());
|
||||
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
|
||||
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
|
||||
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments);
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<SignalServiceDataMessage.Quote> quote = getQuoteFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(attachmentStreams)
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.build();
|
||||
SignalServiceAddress address = getPushAddress(message.getRecipient().getAddress());
|
||||
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(message.getAttachments());
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<SignalServiceDataMessage.Quote> quote = getQuoteFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(serviceAttachments)
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.build();
|
||||
|
||||
return messageSender.sendMessage(address, UnidentifiedAccessUtil.getAccessFor(context, message.getRecipient()), mediaMessage).getSuccess().isUnidentified();
|
||||
} catch (UnregisteredUserException e) {
|
||||
@@ -188,5 +214,4 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
throw new RetryLaterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||
@@ -22,12 +26,15 @@ import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
@@ -37,7 +44,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.work.WorkerParameters;
|
||||
@@ -136,7 +142,43 @@ public abstract class PushSendJob extends SendJob {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
|
||||
protected @Nullable List<SignalServiceAttachment> getAttachmentPointersFor(List<Attachment> attachments) {
|
||||
return Stream.of(attachments).map(this::getAttachmentPointerFor).filter(a -> a != null).toList();
|
||||
}
|
||||
|
||||
protected @Nullable SignalServiceAttachment getAttachmentPointerFor(Attachment attachment) {
|
||||
if (TextUtils.isEmpty(attachment.getLocation())) {
|
||||
Log.w(TAG, "empty content id");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(attachment.getKey())) {
|
||||
Log.w(TAG, "empty encrypted key");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
long id = Long.parseLong(attachment.getLocation());
|
||||
byte[] key = Base64.decode(attachment.getKey());
|
||||
|
||||
return new SignalServiceAttachmentPointer(id,
|
||||
attachment.getContentType(),
|
||||
key,
|
||||
Optional.of(Util.toIntExact(attachment.getSize())),
|
||||
Optional.absent(),
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
Optional.fromNullable(attachment.getDigest()),
|
||||
Optional.fromNullable(attachment.getFileName()),
|
||||
attachment.isVoiceNote(),
|
||||
Optional.fromNullable(attachment.getCaption()));
|
||||
} catch (IOException | ArithmeticException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected static void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
|
||||
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
|
||||
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user