package org.thoughtcrime.securesms.jobs; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.mms.pdu_alt.CharacterSets; import com.google.android.mms.pdu_alt.EncodedStringValue; import com.google.android.mms.pdu_alt.PduBody; import com.google.android.mms.pdu_alt.PduPart; import com.google.android.mms.pdu_alt.RetrieveConf; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.CompatMmsConnection; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsRadioException; import org.thoughtcrime.securesms.mms.PartParser; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; public class MmsDownloadJob extends BaseJob { public static final String KEY = "MmsDownloadJob"; private static final String TAG = MmsDownloadJob.class.getSimpleName(); private static final String KEY_MESSAGE_ID = "message_id"; private static final String KEY_THREAD_ID = "thread_id"; private static final String KEY_AUTOMATIC = "automatic"; private long messageId; private long threadId; private boolean automatic; public MmsDownloadJob(long messageId, long threadId, boolean automatic) { this(new Job.Parameters.Builder() .setQueue("mms-operation") .setMaxAttempts(25) .build(), messageId, threadId, automatic); } private MmsDownloadJob(@NonNull Job.Parameters parameters, long messageId, long threadId, boolean automatic) { super(parameters); this.messageId = messageId; this.threadId = threadId; this.automatic = automatic; } @Override public @NonNull Data serialize() { return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) .putLong(KEY_THREAD_ID, threadId) .putBoolean(KEY_AUTOMATIC, automatic) .build(); } @Override public @NonNull String getFactoryKey() { return KEY; } @Override public void onAdded() { if (automatic && KeyCachingService.isLocked(context)) { DatabaseFactory.getMmsDatabase(context).markIncomingNotificationReceived(threadId); MessageNotifier.updateNotification(context); } } @Override public void onRun() { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); Optional notification = database.getNotification(messageId); if (!notification.isPresent()) { Log.w(TAG, "No notification for ID: " + messageId); return; } try { if (notification.get().getContentLocation() == null) { throw new MmsException("Notification content location was null."); } if (!TextSecurePreferences.isPushRegistered(context)) { throw new MmsException("Not registered"); } database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_CONNECTING); String contentLocation = notification.get().getContentLocation(); byte[] transactionId = new byte[0]; try { if (notification.get().getTransactionId() != null) { transactionId = notification.get().getTransactionId().getBytes(CharacterSets.MIMENAME_ISO_8859_1); } else { Log.w(TAG, "No transaction ID!"); } } catch (UnsupportedEncodingException e) { Log.w(TAG, e); } Log.i(TAG, "Downloading mms at " + Uri.parse(contentLocation).getHost() + ", subscription ID: " + notification.get().getSubscriptionId()); RetrieveConf retrieveConf = new CompatMmsConnection(context).retrieve(contentLocation, transactionId, notification.get().getSubscriptionId()); if (retrieveConf == null) { throw new MmsException("RetrieveConf was null"); } storeRetrievedMms(contentLocation, messageId, threadId, retrieveConf, notification.get().getSubscriptionId(), notification.get().getFrom()); } catch (ApnUnavailableException e) { Log.w(TAG, e); handleDownloadError(messageId, threadId, MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE, automatic); } catch (MmsException e) { Log.w(TAG, e); handleDownloadError(messageId, threadId, MmsDatabase.Status.DOWNLOAD_HARD_FAILURE, automatic); } catch (MmsRadioException | IOException e) { Log.w(TAG, e); handleDownloadError(messageId, threadId, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE, automatic); } } @Override public void onFailure() { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE); if (automatic) { database.markIncomingNotificationReceived(threadId); MessageNotifier.updateNotification(context, threadId); } } @Override public boolean onShouldRetry(@NonNull Exception exception) { return false; } private void storeRetrievedMms(String contentLocation, long messageId, long threadId, RetrieveConf retrieved, int subscriptionId, @Nullable RecipientId notificationFrom) throws MmsException { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); Optional group = Optional.absent(); Set members = new HashSet<>(); String body = null; List attachments = new LinkedList<>(); RecipientId from = null; if (retrieved.getFrom() != null) { from = Recipient.external(context, Util.toIsoString(retrieved.getFrom().getTextString())).getId(); } else if (notificationFrom != null) { from = notificationFrom; } if (retrieved.getTo() != null) { for (EncodedStringValue toValue : retrieved.getTo()) { members.add(Recipient.external(context, Util.toIsoString(toValue.getTextString())).getId()); } } if (retrieved.getCc() != null) { for (EncodedStringValue ccValue : retrieved.getCc()) { members.add(Recipient.external(context, Util.toIsoString(ccValue.getTextString())).getId()); } } if (from != null) { members.add(from); } members.add(Recipient.self().getId()); if (retrieved.getBody() != null) { body = PartParser.getMessageText(retrieved.getBody()); PduBody media = PartParser.getSupportedMediaParts(retrieved.getBody()); for (int i=0;i 2) { List recipients = new ArrayList<>(members); group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipients)); } IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false, false); Optional insertResult = database.insertMessageInbox(message, contentLocation, threadId); if (insertResult.isPresent()) { database.delete(messageId); MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); } } private void handleDownloadError(long messageId, long threadId, int downloadStatus, boolean automatic) { MmsDatabase db = DatabaseFactory.getMmsDatabase(context); db.markDownloadState(messageId, downloadStatus); if (automatic) { db.markIncomingNotificationReceived(threadId); MessageNotifier.updateNotification(context, threadId); } } public static final class Factory implements Job.Factory { @Override public @NonNull MmsDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) { return new MmsDownloadJob(parameters, data.getLong(KEY_MESSAGE_ID), data.getLong(KEY_THREAD_ID), data.getBoolean(KEY_AUTOMATIC)); } } }