Upload attachments in a separate job.

This commit is contained in:
Greyson Parrelli 2018-12-07 18:31:39 -08:00
parent 96c641c2a0
commit 19d5ba5c0e
11 changed files with 269 additions and 67 deletions

View File

@ -82,7 +82,7 @@ dependencies {
compile 'com.google.android.exoplayer:exoplayer-core:2.9.1'
compile 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
compile 'org.whispersystems:signal-service-android:2.12.2'
compile 'org.whispersystems:signal-service-android:2.12.3'
compile 'org.whispersystems:webrtc-android:M69'
compile "me.leolin:ShortcutBadger:1.1.16"
@ -177,7 +177,7 @@ dependencyVerification {
'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e',
'com.google.android.exoplayer:exoplayer-ui:7a942afcc402ff01e9bf48e8d3942850986710f06562d50a1408aaf04a683151',
'com.google.android.exoplayer:exoplayer-core:b6ab34abac36bc2bc6934b7a50008162feca2c0fde91aaf1e8c1c22f2c16e2c0',
'org.whispersystems:signal-service-android:26639df2a9c31b6f31f82034091a4ea3002ca6b1088e7fe6d30428a8290dcf2a',
'org.whispersystems:signal-service-android:b02896703fb826792056ad872b812107ec2e80ea18dbeb43b97327d82faa3947',
'org.whispersystems:webrtc-android:5493c92141ce884fc5ce8240d783232f4fe14bd17a8d0d7d1bd4944d0bd1682f',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
@ -244,7 +244,7 @@ dependencyVerification {
'com.android.support:support-annotations:5d5b9414f02d3fa0ee7526b8d5ddae0da67c8ecc8c4d63ffa6cf91488a93b927',
'com.google.guava:listenablefuture:e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069',
'org.signal:signal-metadata-android:d9d798aab7ee7200373ecff8718baf8aaeb632c123604e8a41b7b4c0c97eeee1',
'org.whispersystems:signal-service-java:a156f4025ce59abb1b48c089719323cd0e82d6ab4bdd345c408a5e44121499b1',
'org.whispersystems:signal-service-java:81fcda4be6e0dfc1f3e990b8bf5d2731a2bb6798b67748981e2a0ceba33d5e7a',
'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',

View File

@ -678,7 +678,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
@Override
protected Void doInBackground(Void... voids) {
try {
accountManager.requestVoiceVerificationCode();
accountManager.requestVoiceVerificationCode(Locale.getDefault());
} catch (IOException e) {
Log.w(TAG, e);
}

View File

@ -358,6 +358,21 @@ public class AttachmentDatabase extends Database {
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
}
public void updateAttachmentAfterUpload(@NonNull AttachmentId id, @NonNull Attachment attachment) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
values.put(CONTENT_LOCATION, attachment.getLocation());
values.put(DIGEST, attachment.getDigest());
values.put(CONTENT_DISPOSITION, attachment.getKey());
values.put(NAME, attachment.getRelay());
values.put(SIZE, attachment.getSize());
values.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId());
database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
}
@NonNull Map<Attachment, AttachmentId> insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments, @NonNull List<Attachment> quoteAttachment)
throws MmsException
{

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.dependencies;
import android.content.Context;
import org.thoughtcrime.securesms.gcm.GcmBroadcastReceiver;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
@ -95,7 +96,8 @@ import dagger.Provides;
RotateProfileKeyJob.class,
MultiDeviceConfigurationUpdateJob.class,
RefreshUnidentifiedDeliveryAbilityJob.class,
TypingSendJob.class})
TypingSendJob.class,
AttachmentUploadJob.class})
public class SignalCommunicationModule {
private static final String TAG = SignalCommunicationModule.class.getSimpleName();

View File

@ -56,19 +56,14 @@ public class GcmBroadcastReceiver extends WakefulBroadcastReceiver implements In
return;
}
String receiptData = intent.getStringExtra("receipt");
if (!TextUtils.isEmpty(receiptData)) handleReceivedMessage(context, receiptData);
else if (intent.hasExtra("notification")) handleReceivedNotification(context);
if (intent.hasExtra("notification")) {
handleReceivedNotification(context);
} else {
Log.w(TAG, "Received an unexpected intent.");
}
}
}
private void handleReceivedMessage(Context context, String data) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new PushContentReceiveJob(context, data));
}
private void handleReceivedNotification(Context context) {
if (!incrementActiveGcmCount()) {
Log.i(TAG, "Skipping GCM processing -- there's already one enqueued.");

View File

@ -70,7 +70,10 @@ public class JobManager {
}
List<List<Job>> jobListChain = chain.getJobListChain();
List<List<OneTimeWorkRequest>> requestListChain = Stream.of(jobListChain).map(jl -> Stream.of(jl).map(this::toWorkRequest).toList()).toList();
List<List<OneTimeWorkRequest>> requestListChain = Stream.of(jobListChain)
.filter(jobList -> !jobList.isEmpty())
.map(jobList -> Stream.of(jobList).map(this::toWorkRequest).toList())
.toList();
if (jobListChain.isEmpty()) {
throw new IllegalStateException("Enqueued an empty chain.");

View 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);
}
}
}

View File

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

View File

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

View File

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

View File

@ -199,7 +199,7 @@ public class MessageSender {
private static void sendMediaPush(Context context, Recipient recipient, long messageId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new PushMediaSendJob(context, messageId, recipient.getAddress()));
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress());
}
private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) {