Signal-Android/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java
Moxie Marlinspike 51d6144591 Significant MMS changes
1) Remove all our PDU code and switch to the PDU code from the
   klinker library

2) Switch to using the system Lollipop MMS library by default,
   and falling back to our own custom library if that fails.

3) Format SMIL differently, using code from klinker instead of
   what we've pieced together.

4) Pull per-carrier MMS media constraints from the XML config
   files in the klinker library, instead of hardcoding it at 280kb.

Hopefully this is an improvement, but given that MMS is involved,
it will probably make things worse instead.
2017-05-08 18:14:55 -07:00

193 lines
7.5 KiB
Java

package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.crypto.MediaKey;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirement;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.util.Hex;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import javax.inject.Inject;
import org.thoughtcrime.securesms.mms.MmsException;
public class AttachmentDownloadJob extends MasterSecretJob implements InjectableType {
private static final long serialVersionUID = 1L;
private static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
private static final String TAG = AttachmentDownloadJob.class.getSimpleName();
@Inject transient SignalServiceMessageReceiver messageReceiver;
private final long messageId;
private final long partRowId;
private final long partUniqueId;
public AttachmentDownloadJob(Context context, long messageId, AttachmentId attachmentId) {
super(context, JobParameters.newBuilder()
.withGroupId(AttachmentDownloadJob.class.getCanonicalName())
.withRequirement(new MasterSecretRequirement(context))
.withRequirement(new NetworkRequirement(context))
.withRequirement(new MediaNetworkRequirement(context, messageId, attachmentId))
.withPersistence()
.create());
this.messageId = messageId;
this.partRowId = attachmentId.getRowId();
this.partUniqueId = attachmentId.getUniqueId();
}
@Override
public void onAdded() {
}
@Override
public void onRun(MasterSecret masterSecret) throws IOException {
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, attachmentId);
if (attachment == null) {
Log.w(TAG, "attachment no longer exists.");
return;
}
if (!attachment.isInProgress()) {
Log.w(TAG, "Attachment was already downloaded.");
return;
}
Log.w(TAG, "Downloading push part " + attachmentId);
retrieveAttachment(masterSecret, messageId, attachmentId, attachment);
MessageNotifier.updateNotification(context, masterSecret);
}
@Override
public void onCanceled() {
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
markFailed(messageId, attachmentId);
}
@Override
public boolean onShouldRetryThrowable(Exception exception) {
return (exception instanceof PushNetworkException);
}
private void retrieveAttachment(MasterSecret masterSecret,
long messageId,
final AttachmentId attachmentId,
final Attachment attachment)
throws IOException
{
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
File attachmentFile = null;
try {
attachmentFile = createTempFile();
SignalServiceAttachmentPointer pointer = createAttachmentPointer(masterSecret, attachment);
InputStream stream = messageReceiver.retrieveAttachment(pointer, attachmentFile, MAX_ATTACHMENT_SIZE, new ProgressListener() {
@Override
public void onAttachmentProgress(long total, long progress) {
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
}
});
database.insertAttachmentsForPlaceholder(masterSecret, messageId, attachmentId, stream);
} catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) {
Log.w(TAG, e);
markFailed(messageId, attachmentId);
} finally {
if (attachmentFile != null)
attachmentFile.delete();
}
}
@VisibleForTesting
SignalServiceAttachmentPointer createAttachmentPointer(MasterSecret masterSecret, Attachment attachment)
throws InvalidPartException
{
if (TextUtils.isEmpty(attachment.getLocation())) {
throw new InvalidPartException("empty content id");
}
if (TextUtils.isEmpty(attachment.getKey())) {
throw new InvalidPartException("empty encrypted key");
}
try {
AsymmetricMasterSecret asymmetricMasterSecret = MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret);
long id = Long.parseLong(attachment.getLocation());
byte[] key = MediaKey.getDecrypted(masterSecret, asymmetricMasterSecret, attachment.getKey());
String relay = null;
if (TextUtils.isEmpty(attachment.getRelay())) {
relay = attachment.getRelay();
}
if (attachment.getDigest() != null) {
Log.w(TAG, "Downloading attachment with digest: " + Hex.toString(attachment.getDigest()));
} else {
Log.w(TAG, "Downloading attachment with no digest...");
}
return new SignalServiceAttachmentPointer(id, null, key, relay, Optional.fromNullable(attachment.getDigest()), Optional.fromNullable(attachment.getFileName()));
} catch (InvalidMessageException | IOException e) {
Log.w(TAG, e);
throw new InvalidPartException(e);
}
}
private File createTempFile() throws InvalidPartException {
try {
File file = File.createTempFile("push-attachment", "tmp", context.getCacheDir());
file.deleteOnExit();
return file;
} catch (IOException e) {
throw new InvalidPartException(e);
}
}
private void markFailed(long messageId, AttachmentId attachmentId) {
try {
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
database.setTransferProgressFailed(attachmentId, messageId);
} catch (MmsException e) {
Log.w(TAG, e);
}
}
@VisibleForTesting static class InvalidPartException extends Exception {
public InvalidPartException(String s) {super(s);}
public InvalidPartException(Exception e) {super(e);}
}
}