2014-11-08 19:35:58 +00:00
|
|
|
package org.thoughtcrime.securesms.jobs;
|
2013-07-17 02:52:02 +00:00
|
|
|
|
|
|
|
import android.content.Context;
|
2018-08-09 14:15:43 +00:00
|
|
|
import android.support.annotation.NonNull;
|
2015-10-13 01:25:05 +00:00
|
|
|
import android.text.TextUtils;
|
2018-08-09 14:15:43 +00:00
|
|
|
|
|
|
|
import org.thoughtcrime.securesms.jobmanager.SafeData;
|
2018-08-01 15:09:24 +00:00
|
|
|
import org.thoughtcrime.securesms.logging.Log;
|
2017-05-08 22:32:59 +00:00
|
|
|
import android.webkit.MimeTypeMap;
|
|
|
|
|
|
|
|
import com.android.mms.dom.smil.parser.SmilXmlSerializer;
|
|
|
|
import com.google.android.mms.ContentType;
|
|
|
|
import com.google.android.mms.InvalidHeaderValueException;
|
|
|
|
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.PduComposer;
|
|
|
|
import com.google.android.mms.pdu_alt.PduHeaders;
|
|
|
|
import com.google.android.mms.pdu_alt.PduPart;
|
|
|
|
import com.google.android.mms.pdu_alt.SendConf;
|
|
|
|
import com.google.android.mms.pdu_alt.SendReq;
|
|
|
|
import com.google.android.mms.smil.SmilHelper;
|
|
|
|
import com.klinker.android.send_message.Utils;
|
2013-07-17 02:52:02 +00:00
|
|
|
|
2015-10-13 01:25:05 +00:00
|
|
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
2014-11-03 23:16:04 +00:00
|
|
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
2017-07-26 16:59:15 +00:00
|
|
|
import org.thoughtcrime.securesms.database.Address;
|
2014-11-08 19:35:58 +00:00
|
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
2013-07-17 02:52:02 +00:00
|
|
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
2014-11-08 19:35:58 +00:00
|
|
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
2017-08-01 15:56:00 +00:00
|
|
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
2018-06-18 19:27:04 +00:00
|
|
|
import org.thoughtcrime.securesms.jobmanager.JobParameters;
|
2015-05-01 19:03:05 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
|
2015-01-02 23:43:28 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
2017-07-26 16:59:15 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.MmsException;
|
2013-11-19 18:13:24 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.MmsSendResult;
|
2015-10-13 01:25:05 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
|
|
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
2014-11-08 19:35:58 +00:00
|
|
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
2017-08-01 15:56:00 +00:00
|
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
2014-11-08 19:35:58 +00:00
|
|
|
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
|
|
|
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
2014-11-12 19:15:05 +00:00
|
|
|
import org.thoughtcrime.securesms.util.Hex;
|
2014-06-13 23:15:33 +00:00
|
|
|
import org.thoughtcrime.securesms.util.NumberUtil;
|
2017-08-01 15:56:00 +00:00
|
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
2015-10-13 01:25:05 +00:00
|
|
|
import org.thoughtcrime.securesms.util.Util;
|
2013-07-17 02:52:02 +00:00
|
|
|
|
2017-05-08 22:32:59 +00:00
|
|
|
import java.io.ByteArrayOutputStream;
|
2013-07-17 02:52:02 +00:00
|
|
|
import java.io.IOException;
|
|
|
|
import java.util.Arrays;
|
2015-10-13 01:25:05 +00:00
|
|
|
import java.util.List;
|
2013-07-17 02:52:02 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
import androidx.work.Data;
|
|
|
|
|
2015-01-12 04:27:34 +00:00
|
|
|
public class MmsSendJob extends SendJob {
|
2015-10-13 01:25:05 +00:00
|
|
|
|
|
|
|
private static final long serialVersionUID = 0L;
|
|
|
|
|
2014-11-08 19:35:58 +00:00
|
|
|
private static final String TAG = MmsSendJob.class.getSimpleName();
|
2014-11-03 23:16:04 +00:00
|
|
|
|
2018-08-09 14:15:43 +00:00
|
|
|
private static final String KEY_MESSAGE_ID = "message_id";
|
|
|
|
|
|
|
|
private long messageId;
|
|
|
|
|
|
|
|
public MmsSendJob() {
|
|
|
|
super(null, null);
|
|
|
|
}
|
2013-07-17 02:52:02 +00:00
|
|
|
|
2014-11-08 19:35:58 +00:00
|
|
|
public MmsSendJob(Context context, long messageId) {
|
|
|
|
super(context, JobParameters.newBuilder()
|
|
|
|
.withGroupId("mms-operation")
|
2018-08-09 14:15:43 +00:00
|
|
|
.withNetworkRequirement()
|
|
|
|
.withMasterSecretRequirement()
|
|
|
|
.withRetryCount(15)
|
2014-11-08 19:35:58 +00:00
|
|
|
.create());
|
|
|
|
|
|
|
|
this.messageId = messageId;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2018-08-09 14:15:43 +00:00
|
|
|
protected void initialize(@NonNull SafeData data) {
|
|
|
|
messageId = data.getLong(KEY_MESSAGE_ID);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected @NonNull Data serialize(@NonNull Data.Builder dataBuilder) {
|
|
|
|
return dataBuilder.putLong(KEY_MESSAGE_ID, messageId).build();
|
2014-11-08 19:35:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2015-01-12 04:27:34 +00:00
|
|
|
public void onSend(MasterSecret masterSecret) throws MmsException, NoSuchMessageException, IOException {
|
2015-10-13 01:25:05 +00:00
|
|
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
2018-01-25 03:17:44 +00:00
|
|
|
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
2014-11-08 19:35:58 +00:00
|
|
|
|
|
|
|
try {
|
2018-08-02 13:50:36 +00:00
|
|
|
Log.i(TAG, "Sending message: " + messageId);
|
|
|
|
|
2018-01-25 03:17:44 +00:00
|
|
|
SendReq pdu = constructSendPdu(message);
|
2015-10-13 01:25:05 +00:00
|
|
|
|
|
|
|
validateDestinations(message, pdu);
|
2014-12-29 22:01:02 +00:00
|
|
|
|
2015-10-13 01:25:05 +00:00
|
|
|
final byte[] pduBytes = getPduBytes(pdu);
|
2016-02-06 00:10:33 +00:00
|
|
|
final SendConf sendConf = new CompatMmsConnection(context).send(pduBytes, message.getSubscriptionId());
|
2015-10-13 01:25:05 +00:00
|
|
|
final MmsSendResult result = getSendResult(sendConf, pdu);
|
2014-11-08 19:35:58 +00:00
|
|
|
|
2016-12-21 17:49:01 +00:00
|
|
|
database.markAsSent(messageId, false);
|
2015-10-13 01:25:05 +00:00
|
|
|
markAttachmentsUploaded(messageId, message.getAttachments());
|
2018-08-02 13:50:36 +00:00
|
|
|
|
|
|
|
Log.i(TAG, "Sent message: " + messageId);
|
2015-05-01 19:03:05 +00:00
|
|
|
} catch (UndeliverableMessageException | IOException e) {
|
2014-11-08 19:35:58 +00:00
|
|
|
Log.w(TAG, e);
|
|
|
|
database.markAsSentFailed(messageId);
|
|
|
|
notifyMediaMessageDeliveryFailed(context, messageId);
|
|
|
|
} catch (InsecureFallbackApprovalException e) {
|
|
|
|
Log.w(TAG, e);
|
|
|
|
database.markAsPendingInsecureSmsFallback(messageId);
|
|
|
|
notifyMediaMessageDeliveryFailed(context, messageId);
|
|
|
|
}
|
2013-07-17 02:52:02 +00:00
|
|
|
}
|
|
|
|
|
2014-11-08 19:35:58 +00:00
|
|
|
@Override
|
2014-11-12 05:11:57 +00:00
|
|
|
public boolean onShouldRetryThrowable(Exception exception) {
|
2014-11-08 19:35:58 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onCanceled() {
|
2018-08-02 13:50:36 +00:00
|
|
|
Log.i(TAG, "onCanceled() messageId: " + messageId);
|
2014-11-08 19:35:58 +00:00
|
|
|
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
|
|
|
notifyMediaMessageDeliveryFailed(context, messageId);
|
|
|
|
}
|
|
|
|
|
2015-10-13 01:25:05 +00:00
|
|
|
private byte[] getPduBytes(SendReq message)
|
2014-04-01 01:47:24 +00:00
|
|
|
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
|
2013-07-17 02:52:02 +00:00
|
|
|
{
|
2014-12-29 22:01:02 +00:00
|
|
|
byte[] pduBytes = new PduComposer(context, message).make();
|
2015-10-13 01:25:05 +00:00
|
|
|
|
2014-12-29 22:01:02 +00:00
|
|
|
if (pduBytes == null) {
|
|
|
|
throw new UndeliverableMessageException("PDU composition failed, null payload");
|
|
|
|
}
|
2013-07-17 02:52:02 +00:00
|
|
|
|
2014-12-29 22:01:02 +00:00
|
|
|
return pduBytes;
|
|
|
|
}
|
2015-01-13 19:40:29 +00:00
|
|
|
|
2014-12-29 22:01:02 +00:00
|
|
|
private MmsSendResult getSendResult(SendConf conf, SendReq message)
|
|
|
|
throws UndeliverableMessageException
|
|
|
|
{
|
|
|
|
if (conf == null) {
|
|
|
|
throw new UndeliverableMessageException("No M-Send.conf received in response to send.");
|
|
|
|
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
|
|
|
|
throw new UndeliverableMessageException("Got bad response: " + conf.getResponseStatus());
|
|
|
|
} else if (isInconsistentResponse(message, conf)) {
|
|
|
|
throw new UndeliverableMessageException("Mismatched response!");
|
|
|
|
} else {
|
|
|
|
return new MmsSendResult(conf.getMessageId(), conf.getResponseStatus());
|
2013-07-17 02:52:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private boolean isInconsistentResponse(SendReq message, SendConf response) {
|
2018-08-02 13:25:33 +00:00
|
|
|
Log.i(TAG, "Comparing: " + Hex.toString(message.getTransactionId()));
|
|
|
|
Log.i(TAG, "With: " + Hex.toString(response.getTransactionId()));
|
2013-07-17 02:52:02 +00:00
|
|
|
return !Arrays.equals(message.getTransactionId(), response.getTransactionId());
|
|
|
|
}
|
|
|
|
|
2014-12-29 22:01:02 +00:00
|
|
|
private void validateDestinations(EncodedStringValue[] destinations) throws UndeliverableMessageException {
|
|
|
|
if (destinations == null) return;
|
2013-07-17 02:52:02 +00:00
|
|
|
|
2014-12-29 22:01:02 +00:00
|
|
|
for (EncodedStringValue destination : destinations) {
|
|
|
|
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
|
|
|
|
throw new UndeliverableMessageException("Invalid destination: " +
|
|
|
|
(destination == null ? null : destination.getString()));
|
|
|
|
}
|
2014-06-13 23:15:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-13 01:25:05 +00:00
|
|
|
private void validateDestinations(OutgoingMediaMessage media, SendReq message) throws UndeliverableMessageException {
|
2014-12-29 22:01:02 +00:00
|
|
|
validateDestinations(message.getTo());
|
|
|
|
validateDestinations(message.getCc());
|
|
|
|
validateDestinations(message.getBcc());
|
2014-06-13 23:15:33 +00:00
|
|
|
|
|
|
|
if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) {
|
|
|
|
throw new UndeliverableMessageException("No to, cc, or bcc specified!");
|
|
|
|
}
|
2015-10-13 01:25:05 +00:00
|
|
|
|
|
|
|
if (media.isSecure()) {
|
|
|
|
throw new UndeliverableMessageException("Attempt to send encrypted MMS?");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-25 03:17:44 +00:00
|
|
|
private SendReq constructSendPdu(OutgoingMediaMessage message)
|
2015-10-13 01:25:05 +00:00
|
|
|
throws UndeliverableMessageException
|
|
|
|
{
|
2017-05-08 22:32:59 +00:00
|
|
|
SendReq req = new SendReq();
|
2017-12-20 19:21:00 +00:00
|
|
|
String lineNumber = getMyNumber(context);
|
2017-08-01 15:56:00 +00:00
|
|
|
Address destination = message.getRecipient().getAddress();
|
2017-07-26 16:59:15 +00:00
|
|
|
MediaConstraints mediaConstraints = MediaConstraints.getMmsMediaConstraints(message.getSubscriptionId());
|
2018-03-19 18:22:39 +00:00
|
|
|
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
|
2017-05-08 22:32:59 +00:00
|
|
|
|
|
|
|
if (!TextUtils.isEmpty(lineNumber)) {
|
|
|
|
req.setFrom(new EncodedStringValue(lineNumber));
|
2017-08-01 15:56:00 +00:00
|
|
|
} else {
|
|
|
|
req.setFrom(new EncodedStringValue(TextSecurePreferences.getLocalNumber(context)));
|
2015-10-13 01:25:05 +00:00
|
|
|
}
|
|
|
|
|
2017-08-01 15:56:00 +00:00
|
|
|
if (destination.isMmsGroup()) {
|
|
|
|
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(destination.toGroupString(), false);
|
|
|
|
|
|
|
|
for (Recipient member : members) {
|
|
|
|
if (message.getDistributionType() == ThreadDatabase.DistributionTypes.BROADCAST) {
|
|
|
|
req.addBcc(new EncodedStringValue(member.getAddress().serialize()));
|
|
|
|
} else {
|
|
|
|
req.addTo(new EncodedStringValue(member.getAddress().serialize()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
req.addTo(new EncodedStringValue(destination.serialize()));
|
2017-05-08 22:32:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
req.setDate(System.currentTimeMillis() / 1000);
|
|
|
|
|
|
|
|
PduBody body = new PduBody();
|
|
|
|
int size = 0;
|
2015-10-13 01:25:05 +00:00
|
|
|
|
|
|
|
if (!TextUtils.isEmpty(message.getBody())) {
|
|
|
|
PduPart part = new PduPart();
|
2017-05-08 22:32:59 +00:00
|
|
|
String name = String.valueOf(System.currentTimeMillis());
|
2015-10-13 01:25:05 +00:00
|
|
|
part.setData(Util.toUtf8Bytes(message.getBody()));
|
|
|
|
part.setCharset(CharacterSets.UTF_8);
|
|
|
|
part.setContentType(ContentType.TEXT_PLAIN.getBytes());
|
2017-05-08 22:32:59 +00:00
|
|
|
part.setContentId(name.getBytes());
|
|
|
|
part.setContentLocation((name + ".txt").getBytes());
|
|
|
|
part.setName((name + ".txt").getBytes());
|
2015-10-13 01:25:05 +00:00
|
|
|
|
|
|
|
body.addPart(part);
|
2017-05-08 22:32:59 +00:00
|
|
|
size += getPartSize(part);
|
2015-10-13 01:25:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (Attachment attachment : scaledAttachments) {
|
|
|
|
try {
|
|
|
|
if (attachment.getDataUri() == null) throw new IOException("Assertion failed, attachment for outgoing MMS has no data!");
|
|
|
|
|
2017-05-08 22:32:59 +00:00
|
|
|
String fileName = attachment.getFileName();
|
|
|
|
PduPart part = new PduPart();
|
|
|
|
|
|
|
|
if (fileName == null) {
|
|
|
|
fileName = String.valueOf(Math.abs(Util.getSecureRandom().nextLong()));
|
|
|
|
String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.getContentType());
|
|
|
|
|
|
|
|
if (fileExtension != null) fileName = fileName + "." + fileExtension;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (attachment.getContentType().startsWith("text")) {
|
|
|
|
part.setCharset(CharacterSets.UTF_8);
|
|
|
|
}
|
|
|
|
|
|
|
|
part.setContentType(attachment.getContentType().getBytes());
|
|
|
|
part.setContentLocation(fileName.getBytes());
|
|
|
|
part.setName(fileName.getBytes());
|
|
|
|
|
|
|
|
int index = fileName.lastIndexOf(".");
|
|
|
|
String contentId = (index == -1) ? fileName : fileName.substring(0, index);
|
|
|
|
part.setContentId(contentId.getBytes());
|
2018-01-25 03:17:44 +00:00
|
|
|
part.setData(Util.readFully(PartAuthority.getAttachmentStream(context, attachment.getDataUri())));
|
2015-10-13 01:25:05 +00:00
|
|
|
|
|
|
|
body.addPart(part);
|
2017-05-08 22:32:59 +00:00
|
|
|
size += getPartSize(part);
|
2015-10-13 01:25:05 +00:00
|
|
|
} catch (IOException e) {
|
|
|
|
Log.w(TAG, e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-08 22:32:59 +00:00
|
|
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
|
|
SmilXmlSerializer.serialize(SmilHelper.createSmilDocument(body), out);
|
|
|
|
PduPart smilPart = new PduPart();
|
|
|
|
smilPart.setContentId("smil".getBytes());
|
|
|
|
smilPart.setContentLocation("smil.xml".getBytes());
|
|
|
|
smilPart.setContentType(ContentType.APP_SMIL.getBytes());
|
|
|
|
smilPart.setData(out.toByteArray());
|
|
|
|
body.addPart(0, smilPart);
|
|
|
|
|
|
|
|
req.setBody(body);
|
|
|
|
req.setMessageSize(size);
|
|
|
|
req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes());
|
|
|
|
req.setExpiry(7 * 24 * 60 * 60);
|
|
|
|
|
|
|
|
try {
|
|
|
|
req.setPriority(PduHeaders.PRIORITY_NORMAL);
|
|
|
|
req.setDeliveryReport(PduHeaders.VALUE_NO);
|
|
|
|
req.setReadReport(PduHeaders.VALUE_NO);
|
|
|
|
} catch (InvalidHeaderValueException e) {}
|
|
|
|
|
|
|
|
return req;
|
|
|
|
}
|
|
|
|
|
|
|
|
private long getPartSize(PduPart part) {
|
|
|
|
return part.getName().length + part.getContentLocation().length +
|
|
|
|
part.getContentType().length + part.getData().length +
|
|
|
|
part.getContentId().length;
|
2014-06-13 23:15:33 +00:00
|
|
|
}
|
|
|
|
|
2014-11-08 19:35:58 +00:00
|
|
|
private void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
|
2017-08-01 15:56:00 +00:00
|
|
|
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
|
|
|
|
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
2014-11-08 19:35:58 +00:00
|
|
|
|
2017-08-01 15:56:00 +00:00
|
|
|
if (recipient != null) {
|
|
|
|
MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
|
2015-02-11 18:29:05 +00:00
|
|
|
}
|
2014-11-08 19:35:58 +00:00
|
|
|
}
|
2017-12-20 19:21:00 +00:00
|
|
|
|
|
|
|
private String getMyNumber(Context context) throws UndeliverableMessageException {
|
|
|
|
try {
|
|
|
|
return Utils.getMyPhoneNumber(context);
|
|
|
|
} catch (SecurityException e) {
|
|
|
|
throw new UndeliverableMessageException(e);
|
|
|
|
}
|
|
|
|
}
|
2013-07-17 02:52:02 +00:00
|
|
|
}
|