diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 56ed030e3..1f8eea306 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -305,6 +305,10 @@
android:grantUriPermissions="true"
android:authorities="org.thoughtcrime.provider.securesms" />
+
+
@@ -330,5 +334,6 @@
+
diff --git a/build.gradle b/build.gradle
index 0d1c8e7c9..36ea316f6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -123,8 +123,8 @@ dependencyVerification {
}
android {
- compileSdkVersion 21
- buildToolsVersion '21.1.2'
+ compileSdkVersion 22
+ buildToolsVersion '22.0.1'
dexOptions {
javaMaxHeapSize "4g"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d79a2cc54..1f19340f2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -113,6 +113,8 @@
Get with it: %s
Let\'s use this to chat: %s
Error leaving group...
+ MMS not supported
+ This message cannot be sent since your carrier doesn\'t support MMS.
Message details
diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java
index a98ac2129..c522ceb06 100644
--- a/src/org/thoughtcrime/securesms/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationActivity.java
@@ -73,7 +73,6 @@ import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.MmsMediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
-import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -414,7 +413,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void onClick(DialogInterface dialog, int which) {
Context self = ConversationActivity.this;
try {
- byte[] groupId = GroupUtil.getDecodedId(getRecipients().getPrimaryRecipient().getNumber());
+ byte[] groupId = GroupUtil.getDecodedId(getRecipients().getPrimaryRecipient().getNumber());
DatabaseFactory.getGroupDatabase(self).setActive(groupId, false);
GroupContext context = GroupContext.newBuilder()
@@ -664,7 +663,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new AsyncTask() {
@Override
protected Boolean doInBackground(Void... params) {
- return OutgoingMmsConnection.isConnectionPossible(ConversationActivity.this);
+ return Util.isMmsCapable(ConversationActivity.this);
}
@Override
diff --git a/src/org/thoughtcrime/securesms/database/ApnDatabase.java b/src/org/thoughtcrime/securesms/database/ApnDatabase.java
index a6694d9b7..77a9c282d 100644
--- a/src/org/thoughtcrime/securesms/database/ApnDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/ApnDatabase.java
@@ -23,8 +23,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import android.util.Log;
-import org.thoughtcrime.securesms.mms.ApnUnavailableException;
-import org.thoughtcrime.securesms.mms.MmsConnection.Apn;
+import org.thoughtcrime.securesms.mms.LegacyMmsConnection.Apn;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.util.guava.Optional;
diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
index 5036552c9..e8a24abd8 100644
--- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
@@ -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 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;
- }
-
}
diff --git a/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java
index e1c2d1c76..332c6e56f 100644
--- a/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java
@@ -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()));
- }
-
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
index 943259be1..38f171877 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
@@ -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 attachments = getAttachments(masterSecret, message);
diff --git a/src/org/thoughtcrime/securesms/jobs/SendJob.java b/src/org/thoughtcrime/securesms/jobs/SendJob.java
index f7aa7a060..3d8ad73eb 100644
--- a/src/org/thoughtcrime/securesms/jobs/SendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/SendJob.java
@@ -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());
diff --git a/src/org/thoughtcrime/securesms/mms/IncomingLegacyMmsConnection.java b/src/org/thoughtcrime/securesms/mms/IncomingLegacyMmsConnection.java
new file mode 100644
index 000000000..c96a5b005
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/IncomingLegacyMmsConnection.java
@@ -0,0 +1,143 @@
+/**
+ * Copyright (C) 2015 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.thoughtcrime.securesms.mms;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpGetHC4;
+import org.apache.http.client.methods.HttpUriRequest;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import ws.com.google.android.mms.InvalidHeaderValueException;
+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.PduParser;
+import ws.com.google.android.mms.pdu.RetrieveConf;
+
+@SuppressWarnings("deprecation")
+public class IncomingLegacyMmsConnection extends LegacyMmsConnection implements IncomingMmsConnection {
+ private static final String TAG = IncomingLegacyMmsConnection.class.getSimpleName();
+
+ public IncomingLegacyMmsConnection(Context context) throws ApnUnavailableException {
+ super(context);
+ }
+
+ private HttpUriRequest constructRequest(Apn contentApn, boolean useProxy) throws IOException {
+ HttpGetHC4 request = new HttpGetHC4(contentApn.getMmsc());
+ for (Header header : getBaseHeaders()) {
+ request.addHeader(header);
+ }
+ if (useProxy) {
+ HttpHost proxy = new HttpHost(contentApn.getProxy(), contentApn.getPort());
+ request.setConfig(RequestConfig.custom().setProxy(proxy).build());
+ }
+ return request;
+ }
+
+ @Override
+ public RetrieveConf retrieve(String contentLocation, byte[] transactionId) throws MmsRadioException, ApnUnavailableException, IOException {
+ MmsRadio radio = MmsRadio.getInstance(context);
+ Apn contentApn = new Apn(contentLocation, apn.getProxy(), Integer.toString(apn.getPort()), apn.getUsername(), apn.getPassword());
+ if (isCdmaDevice()) {
+ Log.w(TAG, "Connecting directly...");
+ try {
+ return retrieve(contentApn, transactionId, false, false);
+ } catch (IOException | ApnUnavailableException 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 {
+ return retrieve(contentApn, transactionId, true, true);
+ } catch (IOException | ApnUnavailableException e) {
+ Log.w(TAG, e);
+ }
+
+ Log.w(TAG, "Downloading in MMS mode without proxy...");
+
+ return retrieve(contentApn, transactionId, true, false);
+
+ } finally {
+ radio.disconnect();
+ }
+ }
+
+ public RetrieveConf retrieve(Apn contentApn, byte[] transactionId, boolean usingMmsRadio, boolean useProxyIfAvailable)
+ throws IOException, ApnUnavailableException
+ {
+ byte[] pdu = null;
+
+ final boolean useProxy = useProxyIfAvailable && contentApn.hasProxy();
+ final String targetHost = useProxy
+ ? contentApn.getProxy()
+ : Uri.parse(contentApn.getMmsc()).getHost();
+ try {
+ if (checkRouteToHost(context, targetHost, usingMmsRadio)) {
+ Log.w(TAG, "got successful route to host " + targetHost);
+ pdu = execute(constructRequest(contentApn, useProxy));
+ }
+ } catch (IOException ioe) {
+ Log.w(TAG, ioe);
+ }
+
+ if (pdu == null) {
+ throw new IOException("Connection manager could not obtain route to host.");
+ }
+
+ RetrieveConf retrieved = (RetrieveConf)new PduParser(pdu).parse();
+
+ if (retrieved == null) {
+ Log.w(TAG, "Couldn't parse PDU, byte response: " + Arrays.toString(pdu));
+ Log.w(TAG, "Couldn't parse PDU, ASCII: " + new String(pdu));
+ throw new IOException("Bad retrieved PDU");
+ }
+
+ sendRetrievedAcknowledgement(transactionId, usingMmsRadio, useProxy);
+ return retrieved;
+ }
+
+ private void sendRetrievedAcknowledgement(byte[] transactionId,
+ boolean usingRadio,
+ boolean useProxy)
+ throws ApnUnavailableException
+ {
+ try {
+ NotifyRespInd notifyResponse = new NotifyRespInd(PduHeaders.CURRENT_MMS_VERSION,
+ transactionId,
+ PduHeaders.STATUS_RETRIEVED);
+
+ OutgoingLegacyMmsConnection connection = new OutgoingLegacyMmsConnection(context);
+ connection.sendNotificationReceived(new PduComposer(context, notifyResponse).make(), usingRadio, useProxy);
+ } catch (InvalidHeaderValueException | IOException e) {
+ Log.w(TAG, e);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mms/IncomingLollipopMmsConnection.java b/src/org/thoughtcrime/securesms/mms/IncomingLollipopMmsConnection.java
new file mode 100644
index 000000000..facad4ff2
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/IncomingLollipopMmsConnection.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright (C) 2015 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.thoughtcrime.securesms.mms;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.telephony.SmsManager;
+import android.util.Log;
+
+import org.thoughtcrime.securesms.providers.MmsBodyProvider;
+import org.thoughtcrime.securesms.util.Hex;
+import org.thoughtcrime.securesms.util.Util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.concurrent.TimeoutException;
+
+import ws.com.google.android.mms.MmsException;
+import ws.com.google.android.mms.pdu.PduParser;
+import ws.com.google.android.mms.pdu.RetrieveConf;
+
+public class IncomingLollipopMmsConnection extends LollipopMmsConnection implements IncomingMmsConnection {
+ public static final String ACTION = IncomingLollipopMmsConnection.class.getCanonicalName() + "MMS_DOWNLOADED_ACTION";
+ private static final String TAG = IncomingLollipopMmsConnection.class.getSimpleName();
+
+ public IncomingLollipopMmsConnection(Context context) {
+ super(context, ACTION);
+ }
+
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ @Override
+ public synchronized void onResult(Context context, Intent intent) {
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
+ Log.w(TAG, "HTTP status: " + intent.getIntExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, -1));
+ }
+ Log.w(TAG, "code: " + getResultCode() + ", result string: " + getResultData());
+ }
+
+ @Override
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ public synchronized RetrieveConf retrieve(String contentLocation, byte[] transactionId) throws MmsException {
+ beginTransaction();
+
+ try {
+ MmsBodyProvider.Pointer pointer = MmsBodyProvider.makeTemporaryPointer(getContext());
+
+ Log.w(TAG, "downloading multimedia from " + contentLocation + " to " + pointer.getUri());
+ SmsManager.getDefault().downloadMultimediaMessage(getContext(),
+ contentLocation,
+ pointer.getUri(),
+ null,
+ getPendingIntent());
+
+ waitForResult();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Util.copy(pointer.getInputStream(), baos);
+ pointer.close();
+
+ Log.w(TAG, baos.size() + "-byte response: " + Hex.dump(baos.toByteArray()));
+
+ return (RetrieveConf) new PduParser(baos.toByteArray()).parse();
+ } catch (IOException | TimeoutException e) {
+ Log.w(TAG, e);
+ throw new MmsException(e);
+ } finally {
+ endTransaction();
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMmsConnection.java b/src/org/thoughtcrime/securesms/mms/IncomingMmsConnection.java
index a3c24a4b0..e07672e94 100644
--- a/src/org/thoughtcrime/securesms/mms/IncomingMmsConnection.java
+++ b/src/org/thoughtcrime/securesms/mms/IncomingMmsConnection.java
@@ -1,96 +1,10 @@
-/**
- * Copyright (C) 2014 Open Whisper Systems
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
package org.thoughtcrime.securesms.mms;
-import android.content.Context;
-import android.net.Uri;
-import android.util.Log;
-
-import org.apache.http.Header;
-import org.apache.http.HttpHost;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.HttpGetHC4;
-import org.apache.http.client.methods.HttpUriRequest;
-
import java.io.IOException;
-import java.util.Arrays;
-import ws.com.google.android.mms.pdu.PduParser;
+import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.RetrieveConf;
-public class IncomingMmsConnection extends MmsConnection {
- private static final String TAG = IncomingMmsConnection.class.getSimpleName();
-
- public IncomingMmsConnection(Context context, Apn apn) {
- super(context, apn);
- }
-
- @Override
- protected HttpUriRequest constructRequest(boolean useProxy) throws IOException {
- HttpGetHC4 request = new HttpGetHC4(apn.getMmsc());
- for (Header header : getBaseHeaders()) {
- request.addHeader(header);
- }
- if (useProxy) {
- HttpHost proxy = new HttpHost(apn.getProxy(), apn.getPort());
- request.setConfig(RequestConfig.custom().setProxy(proxy).build());
- }
- return request;
- }
-
- public static boolean isConnectionPossible(Context context, String apn) {
- try {
- getApn(context, apn);
- return true;
- } catch (ApnUnavailableException e) {
- return false;
- }
- }
-
- public RetrieveConf retrieve(boolean usingMmsRadio, boolean useProxyIfAvailable)
- throws IOException, ApnUnavailableException
- {
- byte[] pdu = null;
-
- final boolean useProxy = useProxyIfAvailable && apn.hasProxy();
- final String targetHost = useProxy
- ? apn.getProxy()
- : Uri.parse(apn.getMmsc()).getHost();
- try {
- if (checkRouteToHost(context, targetHost, usingMmsRadio)) {
- Log.w(TAG, "got successful route to host " + targetHost);
- pdu = makeRequest(useProxy);
- }
- } catch (IOException ioe) {
- Log.w(TAG, ioe);
- }
-
- if (pdu == null) {
- throw new IOException("Connection manager could not obtain route to host.");
- }
-
- RetrieveConf retrieved = (RetrieveConf)new PduParser(pdu).parse();
-
- if (retrieved == null) {
- Log.w(TAG, "Couldn't parse PDU, byte response: " + Arrays.toString(pdu));
- Log.w(TAG, "Couldn't parse PDU, ASCII: " + new String(pdu));
- throw new IOException("Bad retrieved PDU");
- }
-
- return retrieved;
- }
+public interface IncomingMmsConnection {
+ RetrieveConf retrieve(String contentLocation, byte[] transactionId) throws MmsException, MmsRadioException, ApnUnavailableException, IOException;
}
diff --git a/src/org/thoughtcrime/securesms/mms/MmsConnection.java b/src/org/thoughtcrime/securesms/mms/LegacyMmsConnection.java
similarity index 93%
rename from src/org/thoughtcrime/securesms/mms/MmsConnection.java
rename to src/org/thoughtcrime/securesms/mms/LegacyMmsConnection.java
index fc26ad99b..ecead3399 100644
--- a/src/org/thoughtcrime/securesms/mms/MmsConnection.java
+++ b/src/org/thoughtcrime/securesms/mms/LegacyMmsConnection.java
@@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.ConnectivityManager;
+import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
@@ -50,19 +51,19 @@ import java.net.URL;
import java.util.LinkedList;
import java.util.List;
-public abstract class MmsConnection {
+@SuppressWarnings("deprecation")
+public abstract class LegacyMmsConnection {
private static final String TAG = "MmsCommunication";
protected final Context context;
protected final Apn apn;
- protected MmsConnection(Context context, Apn apn) {
+ protected LegacyMmsConnection(Context context) throws ApnUnavailableException {
this.context = context;
- this.apn = apn;
+ this.apn = getApn(context);
}
- public static Apn getApn(Context context, String apnName) throws ApnUnavailableException {
- Log.w(TAG, "Getting MMSC params for apn " + apnName);
+ public static Apn getApn(Context context) throws ApnUnavailableException {
try {
Optional params = ApnDatabase.getInstance(context)
@@ -79,6 +80,10 @@ public abstract class MmsConnection {
}
}
+ protected boolean isCdmaDevice() {
+ return ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
+ }
+
protected static boolean checkRouteToHost(Context context, String host, boolean usingMmsRadio)
throws IOException
{
@@ -146,14 +151,12 @@ public abstract class MmsConnection {
.build();
}
- protected byte[] makeRequest(boolean useProxy) throws IOException {
- Log.w(TAG, "connecting to " + apn.getMmsc() + (useProxy ? " using proxy" : ""));
+ protected byte[] execute(HttpUriRequest request) throws IOException {
+ Log.w(TAG, "connecting to " + apn.getMmsc());
- HttpUriRequest request;
CloseableHttpClient client = null;
CloseableHttpResponse response = null;
try {
- request = constructRequest(useProxy);
client = constructHttpClient();
response = client.execute(request);
@@ -170,8 +173,6 @@ public abstract class MmsConnection {
throw new IOException("unhandled response code");
}
- protected abstract HttpUriRequest constructRequest(boolean useProxy) throws IOException;
-
protected List getBaseHeaders() {
final String number = TelephonyUtil.getManager(context).getLine1Number();
return new LinkedList() {{
diff --git a/src/org/thoughtcrime/securesms/mms/LollipopMmsConnection.java b/src/org/thoughtcrime/securesms/mms/LollipopMmsConnection.java
new file mode 100644
index 000000000..0e5c83edd
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/LollipopMmsConnection.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (C) 2015 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.thoughtcrime.securesms.mms;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+
+import org.thoughtcrime.securesms.util.Util;
+
+import java.util.concurrent.TimeoutException;
+
+public abstract class LollipopMmsConnection extends BroadcastReceiver {
+ private static final String TAG = LollipopMmsConnection.class.getSimpleName();
+
+ private final Context context;
+ private final String action;
+
+ private boolean resultAvailable;
+
+ public abstract void onResult(Context context, Intent intent);
+
+ protected LollipopMmsConnection(Context context, String action) {
+ super();
+ this.context = context;
+ this.action = action;
+ }
+
+ @Override
+ public synchronized void onReceive(Context context, Intent intent) {
+ Log.w(TAG, "onReceive()");
+ if (!action.equals(intent.getAction())) {
+ Log.w(TAG, "received broadcast with unexpected action " + intent.getAction());
+ return;
+ }
+
+ onResult(context, intent);
+
+ resultAvailable = true;
+ notifyAll();
+ }
+
+ protected void beginTransaction() {
+ getContext().getApplicationContext().registerReceiver(this, new IntentFilter(action));
+ }
+
+ protected void endTransaction() {
+ getContext().getApplicationContext().unregisterReceiver(this);
+ resultAvailable = false;
+ }
+
+ protected void waitForResult() throws TimeoutException {
+ long timeoutExpiration = System.currentTimeMillis() + 30000;
+ while (!resultAvailable) {
+ Util.wait(this, Math.max(1, timeoutExpiration - System.currentTimeMillis()));
+ if (System.currentTimeMillis() >= timeoutExpiration) {
+ throw new TimeoutException("timeout when waiting for MMS");
+ }
+ }
+ }
+
+ protected PendingIntent getPendingIntent() {
+ return PendingIntent.getBroadcast(getContext(), 1, new Intent(action), PendingIntent.FLAG_ONE_SHOT);
+ }
+
+ protected Context getContext() {
+ return context;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mms/MmsRadio.java b/src/org/thoughtcrime/securesms/mms/MmsRadio.java
index 24483c3d3..7a3bcc63e 100644
--- a/src/org/thoughtcrime/securesms/mms/MmsRadio.java
+++ b/src/org/thoughtcrime/securesms/mms/MmsRadio.java
@@ -43,10 +43,6 @@ public class MmsRadio {
this.wakeLock.setReferenceCounted(true);
}
- public String getApnInformation() {
- return connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS).getExtraInfo();
- }
-
public synchronized void disconnect() {
Log.w("MmsRadio", "MMS Radio Disconnect Called...");
wakeLock.release();
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingLegacyMmsConnection.java b/src/org/thoughtcrime/securesms/mms/OutgoingLegacyMmsConnection.java
new file mode 100644
index 000000000..ba55e94da
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingLegacyMmsConnection.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright (C) 2015 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.thoughtcrime.securesms.mms;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.util.Log;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpPostHC4;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.ByteArrayEntityHC4;
+import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
+
+import java.io.IOException;
+
+import ws.com.google.android.mms.pdu.PduParser;
+import ws.com.google.android.mms.pdu.SendConf;
+
+@SuppressWarnings("deprecation")
+public class OutgoingLegacyMmsConnection extends LegacyMmsConnection implements OutgoingMmsConnection {
+ private final static String TAG = OutgoingLegacyMmsConnection.class.getSimpleName();
+
+ public OutgoingLegacyMmsConnection(Context context) throws ApnUnavailableException {
+ super(context);
+ }
+
+ private HttpUriRequest constructRequest(byte[] pduBytes, boolean useProxy)
+ throws IOException
+ {
+ try {
+ HttpPostHC4 request = new HttpPostHC4(apn.getMmsc());
+ for (Header header : getBaseHeaders()) {
+ request.addHeader(header);
+ }
+
+ request.setEntity(new ByteArrayEntityHC4(pduBytes));
+ if (useProxy) {
+ HttpHost proxy = new HttpHost(apn.getProxy(), apn.getPort());
+ request.setConfig(RequestConfig.custom().setProxy(proxy).build());
+ }
+ return request;
+ } catch (IllegalArgumentException iae) {
+ throw new IOException(iae);
+ }
+ }
+
+ public void sendNotificationReceived(byte[] pduBytes, boolean usingMmsRadio, boolean useProxyIfAvailable)
+ throws IOException
+ {
+ sendBytes(pduBytes, usingMmsRadio, useProxyIfAvailable);
+ }
+
+ @Override
+ public SendConf send(byte[] pduBytes) throws UndeliverableMessageException {
+ try {
+ MmsRadio radio = MmsRadio.getInstance(context);
+
+ if (isCdmaDevice()) {
+ Log.w(TAG, "Sending MMS directly without radio change...");
+ try {
+ return send(pduBytes, false, false);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ }
+ }
+
+ Log.w(TAG, "Sending MMS with radio change and proxy...");
+ radio.connect();
+
+ try {
+ try {
+ return send(pduBytes, true, true);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ }
+
+ Log.w(TAG, "Sending MMS with radio change and without proxy...");
+
+ try {
+ return send(pduBytes, true, false);
+ } catch (IOException ioe) {
+ Log.w(TAG, ioe);
+ throw new UndeliverableMessageException(ioe);
+ }
+ } finally {
+ radio.disconnect();
+ }
+
+ } catch (MmsRadioException e) {
+ Log.w(TAG, e);
+ throw new UndeliverableMessageException(e);
+ }
+
+ }
+
+ private SendConf send(byte[] pduBytes, boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
+ byte[] response = sendBytes(pduBytes, useMmsRadio, useProxyIfAvailable);
+ return (SendConf) new PduParser(response).parse();
+ }
+
+ private byte[] sendBytes(byte[] pduBytes, boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
+ final boolean useProxy = useProxyIfAvailable && apn.hasProxy();
+ final String targetHost = useProxy
+ ? apn.getProxy()
+ : Uri.parse(apn.getMmsc()).getHost();
+
+ Log.w(TAG, "Sending MMS of length: " + pduBytes.length
+ + (useMmsRadio ? ", using mms radio" : "")
+ + (useProxy ? ", using proxy" : ""));
+
+ try {
+ if (checkRouteToHost(context, targetHost, useMmsRadio)) {
+ Log.w(TAG, "got successful route to host " + targetHost);
+ byte[] response = execute(constructRequest(pduBytes, useProxy));
+ if (response != null) return response;
+ }
+ } catch (IOException ioe) {
+ Log.w(TAG, ioe);
+ }
+ throw new IOException("Connection manager could not obtain route to host.");
+ }
+
+
+ public static boolean isConnectionPossible(Context context) {
+ try {
+ ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmsRadio.TYPE_MOBILE_MMS);
+ if (networkInfo == null) {
+ Log.w(TAG, "MMS network info was null, unsupported by this device");
+ return false;
+ }
+
+ getApn(context);
+ return true;
+ } catch (ApnUnavailableException e) {
+ Log.w(TAG, e);
+ return false;
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingLollipopMmsConnection.java b/src/org/thoughtcrime/securesms/mms/OutgoingLollipopMmsConnection.java
new file mode 100644
index 000000000..d83a05964
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingLollipopMmsConnection.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (C) 2015 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.thoughtcrime.securesms.mms;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.telephony.SmsManager;
+import android.util.Log;
+
+import org.thoughtcrime.securesms.providers.MmsBodyProvider;
+import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
+import org.thoughtcrime.securesms.util.Util;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.concurrent.TimeoutException;
+
+import ws.com.google.android.mms.pdu.PduParser;
+import ws.com.google.android.mms.pdu.SendConf;
+
+public class OutgoingLollipopMmsConnection extends LollipopMmsConnection implements OutgoingMmsConnection {
+ private static final String TAG = OutgoingLollipopMmsConnection.class.getSimpleName();
+ private static final String ACTION = OutgoingLollipopMmsConnection.class.getCanonicalName() + "MMS_SENT_ACTION";
+
+ private byte[] response;
+
+ public OutgoingLollipopMmsConnection(Context context) {
+ super(context, ACTION);
+ }
+
+ @TargetApi(VERSION_CODES.LOLLIPOP_MR1)
+ @Override
+ public synchronized void onResult(Context context, Intent intent) {
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
+ Log.w(TAG, "HTTP status: " + intent.getIntExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, -1));
+ }
+
+ response = intent.getByteArrayExtra(SmsManager.EXTRA_MMS_DATA);
+ }
+
+ @Override
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ public synchronized SendConf send(byte[] pduBytes) throws UndeliverableMessageException {
+ beginTransaction();
+ try {
+ MmsBodyProvider.Pointer pointer = MmsBodyProvider.makeTemporaryPointer(getContext());
+ Util.copy(new ByteArrayInputStream(pduBytes), pointer.getOutputStream());
+
+ SmsManager.getDefault().sendMultimediaMessage(getContext(),
+ pointer.getUri(),
+ null,
+ null,
+ getPendingIntent());
+
+ waitForResult();
+
+ Log.w(TAG, "MMS broadcast received and processed.");
+ pointer.close();
+
+ return (SendConf) new PduParser(response).parse();
+ } catch (IOException | TimeoutException e) {
+ throw new UndeliverableMessageException(e);
+ } finally {
+ endTransaction();
+ }
+ }
+}
+
diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingMmsConnection.java b/src/org/thoughtcrime/securesms/mms/OutgoingMmsConnection.java
index 35b461f96..94cd32407 100644
--- a/src/org/thoughtcrime/securesms/mms/OutgoingMmsConnection.java
+++ b/src/org/thoughtcrime/securesms/mms/OutgoingMmsConnection.java
@@ -1,121 +1,9 @@
-/**
- * Copyright (C) 2014 Open Whisper Systems
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
package org.thoughtcrime.securesms.mms;
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.Uri;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
-import android.util.Log;
+import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
-import org.apache.http.Header;
-import org.apache.http.HttpHost;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.HttpPostHC4;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.entity.ByteArrayEntityHC4;
-import org.thoughtcrime.securesms.util.TelephonyUtil;
-import org.thoughtcrime.securesms.util.Util;
-
-import java.io.IOException;
-
-import ws.com.google.android.mms.pdu.PduParser;
import ws.com.google.android.mms.pdu.SendConf;
-public class OutgoingMmsConnection extends MmsConnection {
- private final static String TAG = OutgoingMmsConnection.class.getSimpleName();
-
- private final byte[] mms;
-
- public OutgoingMmsConnection(Context context, String apnName, byte[] mms) throws ApnUnavailableException {
- super(context, getApn(context, apnName));
- this.mms = mms;
- }
-
- @Override
- protected HttpUriRequest constructRequest(boolean useProxy)
- throws IOException
- {
- try {
- HttpPostHC4 request = new HttpPostHC4(apn.getMmsc());
- for (Header header : getBaseHeaders()) {
- request.addHeader(header);
- }
-
- request.setEntity(new ByteArrayEntityHC4(mms));
- if (useProxy) {
- HttpHost proxy = new HttpHost(apn.getProxy(), apn.getPort());
- request.setConfig(RequestConfig.custom().setProxy(proxy).build());
- }
- return request;
- } catch (IllegalArgumentException iae) {
- throw new IOException(iae);
- }
- }
-
- public void sendNotificationReceived(boolean usingMmsRadio, boolean useProxyIfAvailable)
- throws IOException
- {
- sendBytes(usingMmsRadio, useProxyIfAvailable);
- }
-
- public SendConf send(boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
- byte[] response = sendBytes(useMmsRadio, useProxyIfAvailable);
- return (SendConf) new PduParser(response).parse();
- }
-
- private byte[] sendBytes(boolean useMmsRadio, boolean useProxyIfAvailable) throws IOException {
- final boolean useProxy = useProxyIfAvailable && apn.hasProxy();
- final String targetHost = useProxy
- ? apn.getProxy()
- : Uri.parse(apn.getMmsc()).getHost();
-
- Log.w(TAG, "Sending MMS of length: " + mms.length
- + (useMmsRadio ? ", using mms radio" : "")
- + (useProxy ? ", using proxy" : ""));
-
- try {
- if (checkRouteToHost(context, targetHost, useMmsRadio)) {
- Log.w(TAG, "got successful route to host " + targetHost);
- byte[] response = makeRequest(useProxy);
- if (response != null) return response;
- }
- } catch (IOException ioe) {
- Log.w(TAG, ioe);
- }
- throw new IOException("Connection manager could not obtain route to host.");
- }
-
- public static boolean isConnectionPossible(Context context) {
- try {
- ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmsRadio.TYPE_MOBILE_MMS);
- if (networkInfo == null) {
- Log.w(TAG, "MMS network info was null, unsupported by this device");
- return false;
- }
-
- getApn(context, networkInfo.getExtraInfo());
- return true;
- } catch (ApnUnavailableException e) {
- Log.w(TAG, e);
- return false;
- }
- }
+public interface OutgoingMmsConnection {
+ SendConf send(byte[] pduBytes) throws UndeliverableMessageException;
}
diff --git a/src/org/thoughtcrime/securesms/preferences/MmsPreferencesFragment.java b/src/org/thoughtcrime/securesms/preferences/MmsPreferencesFragment.java
index 48f640913..243e2c5f0 100644
--- a/src/org/thoughtcrime/securesms/preferences/MmsPreferencesFragment.java
+++ b/src/org/thoughtcrime/securesms/preferences/MmsPreferencesFragment.java
@@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.CustomDefaultPreference;
import org.thoughtcrime.securesms.database.ApnDatabase;
-import org.thoughtcrime.securesms.mms.MmsConnection;
+import org.thoughtcrime.securesms.mms.LegacyMmsConnection;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -52,10 +52,10 @@ public class MmsPreferencesFragment extends PreferenceFragment {
new LoadApnDefaultsTask().execute();
}
- private class LoadApnDefaultsTask extends AsyncTask {
+ private class LoadApnDefaultsTask extends AsyncTask {
@Override
- protected MmsConnection.Apn doInBackground(Void... params) {
+ protected LegacyMmsConnection.Apn doInBackground(Void... params) {
try {
Context context = getActivity();
@@ -72,7 +72,7 @@ public class MmsPreferencesFragment extends PreferenceFragment {
}
@Override
- protected void onPostExecute(MmsConnection.Apn apnDefaults) {
+ protected void onPostExecute(LegacyMmsConnection.Apn apnDefaults) {
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_HOST_PREF))
.setValidator(new CustomDefaultPreference.UriValidator())
.setDefaultValue(apnDefaults.getMmsc());
diff --git a/src/org/thoughtcrime/securesms/preferences/SmsMmsPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/SmsMmsPreferenceFragment.java
index 54ac9c5da..bd0059b6a 100644
--- a/src/org/thoughtcrime/securesms/preferences/SmsMmsPreferenceFragment.java
+++ b/src/org/thoughtcrime/securesms/preferences/SmsMmsPreferenceFragment.java
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.preferences;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceScreen;
@@ -40,12 +42,13 @@ public class SmsMmsPreferenceFragment extends PreferenceFragment {
}
private void initializePlatformSpecificOptions() {
- PreferenceScreen preferenceScreen = getPreferenceScreen();
- Preference defaultPreference = findPreference(KITKAT_DEFAULT_PREF);
- Preference allSmsPreference = findPreference(TextSecurePreferences.ALL_SMS_PREF);
- Preference allMmsPreference = findPreference(TextSecurePreferences.ALL_MMS_PREF);
+ PreferenceScreen preferenceScreen = getPreferenceScreen();
+ Preference defaultPreference = findPreference(KITKAT_DEFAULT_PREF);
+ Preference allSmsPreference = findPreference(TextSecurePreferences.ALL_SMS_PREF);
+ Preference allMmsPreference = findPreference(TextSecurePreferences.ALL_MMS_PREF);
+ Preference manualMmsPreference = findPreference(MMS_PREF);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ) {
+ if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
if (allSmsPreference != null) preferenceScreen.removePreference(allSmsPreference);
if (allMmsPreference != null) preferenceScreen.removePreference(allMmsPreference);
@@ -63,6 +66,10 @@ public class SmsMmsPreferenceFragment extends PreferenceFragment {
} else if (defaultPreference != null) {
preferenceScreen.removePreference(defaultPreference);
}
+
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && manualMmsPreference != null) {
+ preferenceScreen.removePreference(manualMmsPreference);
+ }
}
private class ApnPreferencesClickListener implements Preference.OnPreferenceClickListener {
diff --git a/src/org/thoughtcrime/securesms/providers/MmsBodyProvider.java b/src/org/thoughtcrime/securesms/providers/MmsBodyProvider.java
new file mode 100644
index 000000000..8011a6705
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/providers/MmsBodyProvider.java
@@ -0,0 +1,140 @@
+/**
+ * Copyright (C) 2015 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.thoughtcrime.securesms.providers;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class MmsBodyProvider extends ContentProvider {
+ private static final String TAG = MmsBodyProvider.class.getSimpleName();
+ private static final String CONTENT_URI_STRING = "content://org.thoughtcrime.provider.securesms.mms/mms";
+ public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
+ private static final int SINGLE_ROW = 1;
+
+ private static final UriMatcher uriMatcher;
+
+ static {
+ uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ uriMatcher.addURI("org.thoughtcrime.provider.securesms.mms", "mms/#", SINGLE_ROW);
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+
+ private File getFile(Uri uri) {
+ long id = Long.parseLong(uri.getPathSegments().get(1));
+ return new File(getContext().getCacheDir(), id + ".mmsbody");
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ Log.w(TAG, "openFile(" + uri + ", " + mode + ")");
+
+ switch (uriMatcher.match(uri)) {
+ case SINGLE_ROW:
+ Log.w(TAG, "Fetching message body for a single row...");
+ File tmpFile = getFile(uri);
+
+ final int fileMode;
+ switch (mode) {
+ case "w": fileMode = ParcelFileDescriptor.MODE_TRUNCATE |
+ ParcelFileDescriptor.MODE_CREATE |
+ ParcelFileDescriptor.MODE_WRITE_ONLY; break;
+ case "r": fileMode = ParcelFileDescriptor.MODE_READ_ONLY; break;
+ default: throw new IllegalArgumentException("requested file mode unsupported");
+ }
+
+ Log.w(TAG, "returning file " + tmpFile.getAbsolutePath());
+ return ParcelFileDescriptor.open(tmpFile, fileMode);
+ }
+
+ throw new FileNotFoundException("Request for bad message.");
+ }
+
+ @Override
+ public int delete(Uri uri, String arg1, String[] arg2) {
+ switch (uriMatcher.match(uri)) {
+ case SINGLE_ROW:
+ return getFile(uri).delete() ? 1 : 0;
+ }
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri arg0) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri arg0, ContentValues arg1) {
+ return null;
+ }
+
+ @Override
+ public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) {
+ return null;
+ }
+
+ @Override
+ public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
+ return 0;
+ }
+ public static Pointer makeTemporaryPointer(Context context) {
+ return new Pointer(context, ContentUris.withAppendedId(MmsBodyProvider.CONTENT_URI, System.currentTimeMillis()));
+ }
+
+ public static class Pointer {
+ private final Context context;
+ private final Uri uri;
+
+ public Pointer(Context context, Uri uri) {
+ this.context = context;
+ this.uri = uri;
+ }
+
+ public Uri getUri() {
+ return uri;
+ }
+
+ public OutputStream getOutputStream() throws FileNotFoundException {
+ return context.getContentResolver().openOutputStream(uri, "w");
+ }
+
+ public InputStream getInputStream() throws FileNotFoundException {
+ return context.getContentResolver().openInputStream(uri);
+ }
+
+ public void close() {
+ context.getContentResolver().delete(uri, null, null);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java
index 47d77e064..515c7f377 100644
--- a/src/org/thoughtcrime/securesms/util/Util.java
+++ b/src/org/thoughtcrime/securesms/util/Util.java
@@ -17,6 +17,7 @@
package org.thoughtcrime.securesms.util;
import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Shader;
@@ -24,7 +25,10 @@ import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
import android.provider.Telephony;
+import android.telephony.SmsManager;
import android.telephony.TelephonyManager;
import android.text.Spannable;
import android.text.SpannableString;
@@ -34,6 +38,7 @@ import android.widget.EditText;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.TextSecureExpiredException;
+import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
@@ -126,7 +131,7 @@ public class Util {
}
}
- public static void wait(Object lock, int timeout) {
+ public static void wait(Object lock, long timeout) {
try {
lock.wait(timeout);
} catch (InterruptedException ie) {
@@ -279,4 +284,9 @@ public class Util {
public static boolean isBuildFresh() {
return BuildConfig.BUILD_TIMESTAMP + TimeUnit.DAYS.toMillis(180) > System.currentTimeMillis();
}
+
+ @TargetApi(VERSION_CODES.LOLLIPOP)
+ public static boolean isMmsCapable(Context context) {
+ return (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) || OutgoingLegacyMmsConnection.isConnectionPossible(context);
+ }
}