package org.thoughtcrime.securesms.jobs; import android.content.Context; import android.util.Log; 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.PartDatabase; import org.thoughtcrime.securesms.database.PartDatabase.PartId; import org.thoughtcrime.securesms.dependencies.InjectableType; 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.Util; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.textsecure.api.TextSecureMessageReceiver; import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener; import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer; import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException; import java.io.File; import java.io.IOException; import java.io.InputStream; import javax.inject.Inject; import de.greenrobot.event.EventBus; import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.pdu.PduPart; public class AttachmentDownloadJob extends MasterSecretJob implements InjectableType { private static final long serialVersionUID = 1L; private static final String TAG = AttachmentDownloadJob.class.getSimpleName(); @Inject transient TextSecureMessageReceiver messageReceiver; private final long messageId; private final long partRowId; private final long partUniqueId; public AttachmentDownloadJob(Context context, long messageId, PartId partId) { super(context, JobParameters.newBuilder() .withGroupId(AttachmentDownloadJob.class.getCanonicalName()) .withRequirement(new MasterSecretRequirement(context)) .withRequirement(new NetworkRequirement(context)) .withRequirement(new MediaNetworkRequirement(context, messageId, partId)) .withPersistence() .create()); this.messageId = messageId; this.partRowId = partId.getRowId(); this.partUniqueId = partId.getUniqueId(); } @Override public void onAdded() { } @Override public void onRun(MasterSecret masterSecret) throws IOException { final PartId partId = new PartId(partRowId, partUniqueId); final PduPart part = DatabaseFactory.getPartDatabase(context).getPart(partId); if (part == null) { Log.w(TAG, "part no longer exists."); return; } Log.w(TAG, "Downloading push part " + partId); retrievePart(masterSecret, part, messageId); MessageNotifier.updateNotification(context, masterSecret); } @Override public void onCanceled() { final PartId partId = new PartId(partRowId, partUniqueId); final PduPart part = DatabaseFactory.getPartDatabase(context).getPart(partId); markFailed(messageId, part, part.getPartId()); } @Override public boolean onShouldRetryThrowable(Exception exception) { return (exception instanceof PushNetworkException); } private void retrievePart(MasterSecret masterSecret, PduPart part, long messageId) throws IOException { PartDatabase database = DatabaseFactory.getPartDatabase(context); File attachmentFile = null; final PartId partId = part.getPartId(); try { attachmentFile = createTempFile(); TextSecureAttachmentPointer pointer = createAttachmentPointer(masterSecret, part); InputStream attachment = messageReceiver.retrieveAttachment(pointer, attachmentFile, new ProgressListener() { @Override public void onAttachmentProgress(long total, long progress) { EventBus.getDefault().postSticky(new PartProgressEvent(partId, total, progress)); } }); database.updateDownloadedPart(masterSecret, messageId, partId, part, attachment); } catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) { Log.w(TAG, e); markFailed(messageId, part, partId); } finally { if (attachmentFile != null) attachmentFile.delete(); } } private TextSecureAttachmentPointer createAttachmentPointer(MasterSecret masterSecret, PduPart part) throws InvalidPartException { if (part.getContentLocation() == null) throw new InvalidPartException("null content location"); try { AsymmetricMasterSecret asymmetricMasterSecret = MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret); long id = Long.parseLong(Util.toIsoString(part.getContentLocation())); byte[] key = MediaKey.getDecrypted(masterSecret, asymmetricMasterSecret, Util.toIsoString(part.getContentDisposition())); String relay = null; if (part.getName() != null) { relay = Util.toIsoString(part.getName()); } return new TextSecureAttachmentPointer(id, null, key, relay); } 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, PduPart part, PartDatabase.PartId partId) { try { PartDatabase database = DatabaseFactory.getPartDatabase(context); database.updateFailedDownloadedPart(messageId, partId, part); } catch (MmsException e) { Log.w(TAG, e); } } private static class InvalidPartException extends Exception { public InvalidPartException(String s) {super(s);} public InvalidPartException(Exception e) {super(e);} } }