Use new MMS APIs in Lollipop onwards

Fixes #1937
Closes #2727
This commit is contained in:
Jake McGinty
2014-12-29 14:01:02 -08:00
committed by Moxie Marlinspike
parent dfda2f733c
commit 427c9a6b21
22 changed files with 841 additions and 460 deletions

View File

@@ -2,22 +2,21 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.net.Uri;
import android.telephony.TelephonyManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.IncomingLollipopMmsConnection;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.IncomingLegacyMmsConnection;
import org.thoughtcrime.securesms.mms.IncomingMmsConnection;
import org.thoughtcrime.securesms.mms.MmsConnection;
import org.thoughtcrime.securesms.mms.MmsRadio;
import org.thoughtcrime.securesms.mms.MmsRadioException;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.service.KeyCachingService;
@@ -32,16 +31,10 @@ import org.whispersystems.libaxolotl.util.guava.Optional;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import ws.com.google.android.mms.InvalidHeaderValueException;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.NotifyRespInd;
import ws.com.google.android.mms.pdu.PduComposer;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.RetrieveConf;
import static org.thoughtcrime.securesms.mms.MmsConnection.Apn;
public class MmsDownloadJob extends MasterSecretJob {
private static final String TAG = MmsDownloadJob.class.getSimpleName();
@@ -73,8 +66,8 @@ public class MmsDownloadJob extends MasterSecretJob {
}
@Override
public void onRun(MasterSecret masterSecret) {
Log.w(TAG, "MmsDownloadJob:onRun()");
public void onRun(MasterSecret masterSecret) {
Log.w(TAG, "onRun()");
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Optional<NotificationInd> notification = database.getNotification(messageId);
@@ -86,71 +79,27 @@ public class MmsDownloadJob extends MasterSecretJob {
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_CONNECTING);
String contentLocation = new String(notification.get().getContentLocation());
byte[] transactionId = notification.get().getTransactionId();
MmsRadio radio = MmsRadio.getInstance(context);
String contentLocation = new String(notification.get().getContentLocation());
byte[] transactionId = notification.get().getTransactionId();
Log.w(TAG, "About to parse URL...");
Log.w(TAG, "Downloading mms at " + Uri.parse(contentLocation).getHost());
Log.w(TAG, "Downloading mms at " + Uri.parse(contentLocation).getHost());
try {
if (isCdmaNetwork()) {
Log.w(TAG, "Connecting directly...");
try {
retrieveAndStore(masterSecret, radio, messageId, threadId, contentLocation,
transactionId, false, false);
return;
} catch (IOException e) {
Log.w(TAG, e);
}
}
Log.w(TAG, "Changing radio to MMS mode..");
radio.connect();
try {
Log.w(TAG, "Downloading in MMS mode with proxy...");
try {
retrieveAndStore(masterSecret, radio, messageId, threadId, contentLocation,
transactionId, true, true);
return;
} catch (IOException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Downloading in MMS mode without proxy...");
try {
retrieveAndStore(masterSecret, radio, messageId, threadId,
contentLocation, transactionId, true, false);
} catch (IOException e) {
Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider),
automatic);
}
} finally {
radio.disconnect();
}
RetrieveConf retrieveConf = getMmsConnection(context).retrieve(contentLocation, transactionId);
storeRetrievedMms(masterSecret, contentLocation, messageId, threadId, retrieveConf);
} catch (ApnUnavailableException e) {
Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId, MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE,
context.getString(R.string.MmsDownloader_error_reading_mms_settings), automatic);
automatic);
} catch (MmsException e) {
Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_HARD_FAILURE,
context.getString(R.string.MmsDownloader_error_storing_mms),
automatic);
} catch (MmsRadioException e) {
} catch (MmsRadioException | IOException e) {
Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider),
automatic);
} catch (DuplicateMessageException e) {
Log.w(TAG, e);
@@ -167,6 +116,16 @@ public class MmsDownloadJob extends MasterSecretJob {
}
}
private IncomingMmsConnection getMmsConnection(Context context)
throws ApnUnavailableException
{
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
return new IncomingLollipopMmsConnection(context);
} else {
return new IncomingLegacyMmsConnection(context);
}
}
@Override
public void onCanceled() {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
@@ -183,23 +142,6 @@ public class MmsDownloadJob extends MasterSecretJob {
return false;
}
private void retrieveAndStore(MasterSecret masterSecret, MmsRadio radio,
long messageId, long threadId,
String contentLocation, byte[] transactionId,
boolean radioEnabled, boolean useProxy)
throws IOException, MmsException, ApnUnavailableException,
DuplicateMessageException, NoSessionException,
InvalidMessageException, LegacyMessageException
{
Apn dbApn = MmsConnection.getApn(context, radio.getApnInformation());
Apn contentApn = new Apn(contentLocation, dbApn.getProxy(), Integer.toString(dbApn.getPort()), dbApn.getUsername(), dbApn.getPassword());
IncomingMmsConnection connection = new IncomingMmsConnection(context, contentApn);
RetrieveConf retrieved = connection.retrieve(radioEnabled, useProxy);
storeRetrievedMms(masterSecret, contentLocation, messageId, threadId, retrieved);
sendRetrievedAcknowledgement(radio, transactionId, radioEnabled, useProxy);
}
private void storeRetrievedMms(MasterSecret masterSecret, String contentLocation,
long messageId, long threadId, RetrieveConf retrieved)
throws MmsException, NoSessionException, DuplicateMessageException, InvalidMessageException,
@@ -222,26 +164,8 @@ public class MmsDownloadJob extends MasterSecretJob {
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void sendRetrievedAcknowledgement(MmsRadio radio,
byte[] transactionId,
boolean usingRadio,
boolean useProxy)
throws ApnUnavailableException
{
try {
NotifyRespInd notifyResponse = new NotifyRespInd(PduHeaders.CURRENT_MMS_VERSION,
transactionId,
PduHeaders.STATUS_RETRIEVED);
OutgoingMmsConnection connection = new OutgoingMmsConnection(context, radio.getApnInformation(), new PduComposer(context, notifyResponse).make());
connection.sendNotificationReceived(usingRadio, useProxy);
} catch (InvalidHeaderValueException | IOException e) {
Log.w(TAG, e);
}
}
private void handleDownloadError(MasterSecret masterSecret, long messageId, long threadId,
int downloadStatus, String error, boolean automatic)
int downloadStatus, boolean automatic)
{
MmsDatabase db = DatabaseFactory.getMmsDatabase(context);
@@ -252,11 +176,4 @@ public class MmsDownloadJob extends MasterSecretJob {
MessageNotifier.updateNotification(context, masterSecret, threadId);
}
}
private boolean isCdmaNetwork() {
return ((TelephonyManager)context
.getSystemService(Context.TELEPHONY_SERVICE))
.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
}
}

View File

@@ -1,7 +1,8 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.telephony.TelephonyManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
@@ -11,9 +12,9 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MmsRadio;
import org.thoughtcrime.securesms.mms.MmsRadioException;
import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
import org.thoughtcrime.securesms.mms.OutgoingLollipopMmsConnection;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
@@ -21,8 +22,8 @@ import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.util.SmilUtil;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
@@ -37,7 +38,6 @@ import ws.com.google.android.mms.pdu.SendConf;
import ws.com.google.android.mms.pdu.SendReq;
public class MmsSendJob extends SendJob {
private static final String TAG = MmsSendJob.class.getSimpleName();
private final long messageId;
@@ -65,10 +65,14 @@ public class MmsSendJob extends SendJob {
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
try {
MmsSendResult result = deliver(masterSecret, message);
validateDestinations(message);
final byte[] pduBytes = getPduBytes(masterSecret, message);
final SendConf sendConf = getMmsConnection(context).send(pduBytes);
final MmsSendResult result = getSendResult(sendConf, message);
database.markAsSent(messageId, result.getMessageId(), result.getResponseStatus());
} catch (UndeliverableMessageException e) {
} catch (UndeliverableMessageException | IOException | ApnUnavailableException e) {
Log.w(TAG, e);
database.markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
@@ -90,59 +94,23 @@ public class MmsSendJob extends SendJob {
notifyMediaMessageDeliveryFailed(context, messageId);
}
public MmsSendResult deliver(MasterSecret masterSecret, SendReq message)
throws UndeliverableMessageException, InsecureFallbackApprovalException
private OutgoingMmsConnection getMmsConnection(Context context)
throws ApnUnavailableException
{
validateDestinations(message);
MmsRadio radio = MmsRadio.getInstance(context);
try {
prepareMessageMedia(masterSecret, message, MediaConstraints.MMS_CONSTRAINTS, true);
if (isCdmaDevice()) {
Log.w(TAG, "Sending MMS directly without radio change...");
try {
return sendMms(masterSecret, radio, message, false, false);
} catch (IOException e) {
Log.w(TAG, e);
}
}
Log.w(TAG, "Sending MMS with radio change and proxy...");
radio.connect();
try {
try {
return sendMms(masterSecret, radio, message, true, true);
} catch (IOException e) {
Log.w(TAG, e);
}
Log.w(TAG, "Sending MMS with radio change and without proxy...");
try {
return sendMms(masterSecret, radio, message, true, false);
} catch (IOException ioe) {
Log.w(TAG, ioe);
throw new UndeliverableMessageException(ioe);
}
} finally {
radio.disconnect();
}
} catch (MmsRadioException | IOException e) {
Log.w(TAG, e);
throw new UndeliverableMessageException(e);
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
return new OutgoingLollipopMmsConnection(context);
} else {
return new OutgoingLegacyMmsConnection(context);
}
}
private MmsSendResult sendMms(MasterSecret masterSecret, MmsRadio radio, SendReq message,
boolean usingMmsRadio, boolean useProxy)
private byte[] getPduBytes(MasterSecret masterSecret, SendReq message)
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
{
String number = TelephonyUtil.getManager(context).getLine1Number();
message = getResolvedMessage(masterSecret, message, MediaConstraints.MMS_CONSTRAINTS, true);
message.setBody(SmilUtil.getSmilBody(message.getBody()));
if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) {
throw new UndeliverableMessageException("Attempt to send encrypted MMS?");
}
@@ -150,28 +118,25 @@ public class MmsSendJob extends SendJob {
if (number != null && number.trim().length() != 0) {
message.setFrom(new EncodedStringValue(number));
}
byte[] pduBytes = new PduComposer(context, message).make();
if (pduBytes == null) {
throw new UndeliverableMessageException("PDU composition failed, null payload");
}
try {
byte[] pdu = new PduComposer(context, message).make();
return pduBytes;
}
if (pdu == null) {
throw new UndeliverableMessageException("PDU composition failed, null payload");
}
OutgoingMmsConnection connection = new OutgoingMmsConnection(context, radio.getApnInformation(), pdu);
SendConf conf = connection.send(usingMmsRadio, useProxy);
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());
}
} catch (ApnUnavailableException aue) {
throw new IOException("no APN was retrievable");
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());
}
}
@@ -181,37 +146,21 @@ public class MmsSendJob extends SendJob {
return !Arrays.equals(message.getTransactionId(), response.getTransactionId());
}
private boolean isCdmaDevice() {
return ((TelephonyManager)context
.getSystemService(Context.TELEPHONY_SERVICE))
.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
}
private void validateDestinations(EncodedStringValue[] destinations) throws UndeliverableMessageException {
if (destinations == null) return;
private void validateDestination(EncodedStringValue destination) throws UndeliverableMessageException {
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
throw new UndeliverableMessageException("Invalid destination: " +
(destination == null ? null : destination.getString()));
for (EncodedStringValue destination : destinations) {
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
throw new UndeliverableMessageException("Invalid destination: " +
(destination == null ? null : destination.getString()));
}
}
}
private void validateDestinations(SendReq message) throws UndeliverableMessageException {
if (message.getTo() != null) {
for (EncodedStringValue to : message.getTo()) {
validateDestination(to);
}
}
if (message.getCc() != null) {
for (EncodedStringValue cc : message.getCc()) {
validateDestination(cc);
}
}
if (message.getBcc() != null) {
for (EncodedStringValue bcc : message.getBcc()) {
validateDestination(bcc);
}
}
validateDestinations(message.getTo());
validateDestinations(message.getCc());
validateDestinations(message.getBcc());
if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) {
throw new UndeliverableMessageException("No to, cc, or bcc specified!");
@@ -226,13 +175,4 @@ public class MmsSendJob extends SendJob {
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
}
}
@Override
protected void prepareMessageMedia(MasterSecret masterSecret, SendReq message,
MediaConstraints constraints, boolean toMemory)
throws IOException, UndeliverableMessageException {
super.prepareMessageMedia(masterSecret, message, constraints, toMemory);
message.setBody(SmilUtil.getSmilBody(message.getBody()));
}
}

View File

@@ -104,7 +104,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
String destination = message.getTo()[0].getString();
try {
prepareMessageMedia(masterSecret, message, MediaConstraints.PUSH_CONSTRAINTS, false);
message = getResolvedMessage(masterSecret, message, MediaConstraints.PUSH_CONSTRAINTS, false);
TextSecureAddress address = getPushAddress(destination);
List<TextSecureAttachment> attachments = getAttachments(masterSecret, message);

View File

@@ -17,6 +17,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendReq;
@@ -40,22 +41,23 @@ public abstract class SendJob extends MasterSecretJob {
protected abstract void onSend(MasterSecret masterSecret) throws Exception;
// FIXME: This should return a value rather than modifying one.
protected void prepareMessageMedia(MasterSecret masterSecret, SendReq message,
MediaConstraints constraints, boolean toMemory)
protected SendReq getResolvedMessage(MasterSecret masterSecret, SendReq message,
MediaConstraints constraints, boolean toMemory)
throws IOException, UndeliverableMessageException
{
PduBody body = new PduBody();
try {
for (int i = 0; i < message.getBody().getPartsNum(); i++) {
preparePart(masterSecret, constraints, message.getBody().getPart(i), toMemory);
body.addPart(getResolvedPart(masterSecret, constraints, message.getBody().getPart(i), toMemory));
}
} catch (MmsException me) {
throw new UndeliverableMessageException(me);
}
return new SendReq(message.getPduHeaders(), body);
}
private void preparePart(MasterSecret masterSecret, MediaConstraints constraints,
PduPart part, boolean toMemory)
private PduPart getResolvedPart(MasterSecret masterSecret, MediaConstraints constraints,
PduPart part, boolean toMemory)
throws IOException, MmsException, UndeliverableMessageException
{
byte[] resizedData = null;
@@ -64,7 +66,7 @@ public abstract class SendJob extends MasterSecretJob {
if (!constraints.canResize(part)) {
throw new UndeliverableMessageException("Size constraints could not be satisfied.");
}
resizedData = resizePart(masterSecret, constraints, part);
resizedData = getResizedPartData(masterSecret, constraints, part);
}
if (toMemory && part.getDataUri() != null) {
@@ -74,10 +76,11 @@ public abstract class SendJob extends MasterSecretJob {
if (resizedData != null) {
part.setDataSize(resizedData.length);
}
return part;
}
private byte[] resizePart(MasterSecret masterSecret, MediaConstraints constraints,
PduPart part)
private byte[] getResizedPartData(MasterSecret masterSecret, MediaConstraints constraints,
PduPart part)
throws IOException, MmsException
{
Log.w(TAG, "resizing part " + part.getId());