package org.thoughtcrime.securesms.jobs; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; import java.io.IOException; import java.util.LinkedList; import java.util.List; import javax.inject.Inject; public class PushGroupSendJob extends PushSendJob implements InjectableType { private static final long serialVersionUID = 1L; private static final String TAG = PushGroupSendJob.class.getSimpleName(); @Inject transient SignalServiceMessageSender messageSender; private final long messageId; private final long filterRecipientId; // Deprecated private final String filterAddress; public PushGroupSendJob(Context context, long messageId, @NonNull Address destination, @Nullable Address filterAddress) { super(context, JobParameters.newBuilder() .withPersistence() .withGroupId(destination.toGroupString()) .withRequirement(new MasterSecretRequirement(context)) .withRequirement(new NetworkRequirement(context)) .withRetryCount(5) .create()); this.messageId = messageId; this.filterAddress = filterAddress == null ? null :filterAddress.toPhoneString(); this.filterRecipientId = -1; } @Override public void onAdded() { } @Override public void onPushSend(MasterSecret masterSecret) throws MmsException, IOException, NoSuchMessageException { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId); try { deliver(masterSecret, message, filterAddress == null ? null : Address.fromSerialized(filterAddress)); database.markAsSent(messageId, true); markAttachmentsUploaded(messageId, message.getAttachments()); if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) { database.markExpireStarted(messageId); ApplicationContext.getInstance(context) .getExpiringMessageManager() .scheduleDeletion(messageId, true, message.getExpiresIn()); } } catch (InvalidNumberException | RecipientFormattingException | UndeliverableMessageException e) { Log.w(TAG, e); database.markAsSentFailed(messageId); notifyMediaMessageDeliveryFailed(context, messageId); } catch (EncapsulatedExceptions e) { Log.w(TAG, e); List failures = new LinkedList<>(); for (NetworkFailureException nfe : e.getNetworkExceptions()) { failures.add(new NetworkFailure(Address.fromSerialized(nfe.getE164number()))); } for (UntrustedIdentityException uie : e.getUntrustedIdentityExceptions()) { database.addMismatchedIdentity(messageId, Address.fromSerialized(uie.getE164Number()), uie.getIdentityKey()); } database.addFailures(messageId, failures); if (e.getNetworkExceptions().isEmpty() && e.getUntrustedIdentityExceptions().isEmpty()) { database.markAsSent(messageId, true); markAttachmentsUploaded(messageId, message.getAttachments()); } else { database.markAsSentFailed(messageId); notifyMediaMessageDeliveryFailed(context, messageId); } } } @Override public boolean onShouldRetryThrowable(Exception exception) { if (exception instanceof IOException) return true; return false; } @Override public void onCanceled() { DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); } private void deliver(MasterSecret masterSecret, OutgoingMediaMessage message, @Nullable Address filterAddress) throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions, UndeliverableMessageException { String groupId = message.getRecipient().getAddress().toGroupString(); Optional profileKey = getProfileKey(message.getRecipient()); List recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); List scaledAttachments = scaleAttachments(masterSecret, mediaConstraints, message.getAttachments()); List attachmentStreams = getAttachmentsFor(masterSecret, scaledAttachments); List addresses; if (filterAddress != null) addresses = getPushAddresses(filterAddress); else addresses = getPushAddresses(recipients); if (message.isGroup()) { OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message; GroupContext groupContext = groupMessage.getGroupContext(); SignalServiceAttachment avatar = attachmentStreams.isEmpty() ? null : attachmentStreams.get(0); SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE; SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupContext.getName(), groupContext.getMembersList(), avatar); SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(message.getSentTimeMillis()) .asGroupMessage(group) .build(); messageSender.sendMessage(addresses, groupDataMessage); } else { SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId)); SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(message.getSentTimeMillis()) .asGroupMessage(group) .withAttachments(attachmentStreams) .withBody(message.getBody()) .withExpiration((int)(message.getExpiresIn() / 1000)) .asExpirationUpdate(message.isExpirationUpdate()) .withProfileKey(profileKey.orNull()) .build(); messageSender.sendMessage(addresses, groupMessage); } } private List getPushAddresses(Address address) { List addresses = new LinkedList<>(); addresses.add(getPushAddress(address)); return addresses; } private List getPushAddresses(List recipients) { List addresses = new LinkedList<>(); for (Recipient recipient : recipients) { addresses.add(getPushAddress(recipient.getAddress())); } return addresses; } }