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

@@ -305,6 +305,10 @@
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:authorities="org.thoughtcrime.provider.securesms" /> android:authorities="org.thoughtcrime.provider.securesms" />
<provider android:name=".providers.MmsBodyProvider"
android:grantUriPermissions="true"
android:authorities="org.thoughtcrime.provider.securesms.mms" />
<receiver android:name=".service.RegistrationNotifier" <receiver android:name=".service.RegistrationNotifier"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@@ -330,5 +334,6 @@
<action android:name="org.thoughtcrime.securesms.MessageNotifier.DELETE_REMINDER_ACTION"/> <action android:name="org.thoughtcrime.securesms.MessageNotifier.DELETE_REMINDER_ACTION"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
</application> </application>
</manifest> </manifest>

View File

@@ -123,8 +123,8 @@ dependencyVerification {
} }
android { android {
compileSdkVersion 21 compileSdkVersion 22
buildToolsVersion '21.1.2' buildToolsVersion '22.0.1'
dexOptions { dexOptions {
javaMaxHeapSize "4g" javaMaxHeapSize "4g"

View File

@@ -113,6 +113,8 @@
<string name="ConversationActivity_get_with_it">Get with it: %s</string> <string name="ConversationActivity_get_with_it">Get with it: %s</string>
<string name="ConversationActivity_lets_use_this_to_chat">Let\'s use this to chat: %s</string> <string name="ConversationActivity_lets_use_this_to_chat">Let\'s use this to chat: %s</string>
<string name="ConversationActivity_error_leaving_group">Error leaving group...</string> <string name="ConversationActivity_error_leaving_group">Error leaving group...</string>
<string name="ConversationActivity_mms_not_supported_title">MMS not supported</string>
<string name="ConversationActivity_mms_not_supported_message">This message cannot be sent since your carrier doesn\'t support MMS.</string>
<!-- ConversationFragment --> <!-- ConversationFragment -->
<string name="ConversationFragment_message_details">Message details</string> <string name="ConversationFragment_message_details">Message details</string>

View File

@@ -73,7 +73,6 @@ import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.MmsMediaConstraints; import org.thoughtcrime.securesms.mms.MmsMediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -414,7 +413,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
Context self = ConversationActivity.this; Context self = ConversationActivity.this;
try { try {
byte[] groupId = GroupUtil.getDecodedId(getRecipients().getPrimaryRecipient().getNumber()); byte[] groupId = GroupUtil.getDecodedId(getRecipients().getPrimaryRecipient().getNumber());
DatabaseFactory.getGroupDatabase(self).setActive(groupId, false); DatabaseFactory.getGroupDatabase(self).setActive(groupId, false);
GroupContext context = GroupContext.newBuilder() GroupContext context = GroupContext.newBuilder()
@@ -664,7 +663,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new AsyncTask<Void, Void, Boolean>() { new AsyncTask<Void, Void, Boolean>() {
@Override @Override
protected Boolean doInBackground(Void... params) { protected Boolean doInBackground(Void... params) {
return OutgoingMmsConnection.isConnectionPossible(ConversationActivity.this); return Util.isMmsCapable(ConversationActivity.this);
} }
@Override @Override

View File

@@ -23,8 +23,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.LegacyMmsConnection.Apn;
import org.thoughtcrime.securesms.mms.MmsConnection.Apn;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.libaxolotl.util.guava.Optional;

View File

@@ -2,22 +2,21 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context; import android.content.Context;
import android.net.Uri; 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.Log;
import android.util.Pair; import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.IncomingLollipopMmsConnection;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.IncomingLegacyMmsConnection;
import org.thoughtcrime.securesms.mms.IncomingMmsConnection; 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.MmsRadioException;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
@@ -32,16 +31,10 @@ import org.whispersystems.libaxolotl.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.TimeUnit; 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.MmsException;
import ws.com.google.android.mms.pdu.NotificationInd; 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 ws.com.google.android.mms.pdu.RetrieveConf;
import static org.thoughtcrime.securesms.mms.MmsConnection.Apn;
public class MmsDownloadJob extends MasterSecretJob { public class MmsDownloadJob extends MasterSecretJob {
private static final String TAG = MmsDownloadJob.class.getSimpleName(); private static final String TAG = MmsDownloadJob.class.getSimpleName();
@@ -73,8 +66,8 @@ public class MmsDownloadJob extends MasterSecretJob {
} }
@Override @Override
public void onRun(MasterSecret masterSecret) { public void onRun(MasterSecret masterSecret) {
Log.w(TAG, "MmsDownloadJob:onRun()"); Log.w(TAG, "onRun()");
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Optional<NotificationInd> notification = database.getNotification(messageId); Optional<NotificationInd> notification = database.getNotification(messageId);
@@ -86,71 +79,27 @@ public class MmsDownloadJob extends MasterSecretJob {
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_CONNECTING); database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_CONNECTING);
String contentLocation = new String(notification.get().getContentLocation()); String contentLocation = new String(notification.get().getContentLocation());
byte[] transactionId = notification.get().getTransactionId(); byte[] transactionId = notification.get().getTransactionId();
MmsRadio radio = MmsRadio.getInstance(context);
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 { try {
if (isCdmaNetwork()) { RetrieveConf retrieveConf = getMmsConnection(context).retrieve(contentLocation, transactionId);
Log.w(TAG, "Connecting directly..."); storeRetrievedMms(masterSecret, contentLocation, messageId, threadId, retrieveConf);
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();
}
} catch (ApnUnavailableException e) { } catch (ApnUnavailableException e) {
Log.w(TAG, e); Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId, MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE, handleDownloadError(masterSecret, messageId, threadId, MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE,
context.getString(R.string.MmsDownloader_error_reading_mms_settings), automatic); automatic);
} catch (MmsException e) { } catch (MmsException e) {
Log.w(TAG, e); Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId, handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_HARD_FAILURE, MmsDatabase.Status.DOWNLOAD_HARD_FAILURE,
context.getString(R.string.MmsDownloader_error_storing_mms),
automatic); automatic);
} catch (MmsRadioException e) { } catch (MmsRadioException | IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
handleDownloadError(masterSecret, messageId, threadId, handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider),
automatic); automatic);
} catch (DuplicateMessageException e) { } catch (DuplicateMessageException e) {
Log.w(TAG, 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 @Override
public void onCanceled() { public void onCanceled() {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
@@ -183,23 +142,6 @@ public class MmsDownloadJob extends MasterSecretJob {
return false; 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, private void storeRetrievedMms(MasterSecret masterSecret, String contentLocation,
long messageId, long threadId, RetrieveConf retrieved) long messageId, long threadId, RetrieveConf retrieved)
throws MmsException, NoSessionException, DuplicateMessageException, InvalidMessageException, throws MmsException, NoSessionException, DuplicateMessageException, InvalidMessageException,
@@ -222,26 +164,8 @@ public class MmsDownloadJob extends MasterSecretJob {
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); 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, private void handleDownloadError(MasterSecret masterSecret, long messageId, long threadId,
int downloadStatus, String error, boolean automatic) int downloadStatus, boolean automatic)
{ {
MmsDatabase db = DatabaseFactory.getMmsDatabase(context); MmsDatabase db = DatabaseFactory.getMmsDatabase(context);
@@ -252,11 +176,4 @@ public class MmsDownloadJob extends MasterSecretJob {
MessageNotifier.updateNotification(context, masterSecret, threadId); 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; package org.thoughtcrime.securesms.jobs;
import android.content.Context; import android.content.Context;
import android.telephony.TelephonyManager; import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret; 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.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.MediaConstraints; 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.MmsSendResult;
import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
import org.thoughtcrime.securesms.mms.OutgoingLollipopMmsConnection;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection; import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients; 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.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.NumberUtil; import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.util.SmilUtil; import org.thoughtcrime.securesms.util.SmilUtil;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement; 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; import ws.com.google.android.mms.pdu.SendReq;
public class MmsSendJob extends SendJob { public class MmsSendJob extends SendJob {
private static final String TAG = MmsSendJob.class.getSimpleName(); private static final String TAG = MmsSendJob.class.getSimpleName();
private final long messageId; private final long messageId;
@@ -65,10 +65,14 @@ public class MmsSendJob extends SendJob {
SendReq message = database.getOutgoingMessage(masterSecret, messageId); SendReq message = database.getOutgoingMessage(masterSecret, messageId);
try { 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()); database.markAsSent(messageId, result.getMessageId(), result.getResponseStatus());
} catch (UndeliverableMessageException e) { } catch (UndeliverableMessageException | IOException | ApnUnavailableException e) {
Log.w(TAG, e); Log.w(TAG, e);
database.markAsSentFailed(messageId); database.markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId); notifyMediaMessageDeliveryFailed(context, messageId);
@@ -90,59 +94,23 @@ public class MmsSendJob extends SendJob {
notifyMediaMessageDeliveryFailed(context, messageId); notifyMediaMessageDeliveryFailed(context, messageId);
} }
public MmsSendResult deliver(MasterSecret masterSecret, SendReq message) private OutgoingMmsConnection getMmsConnection(Context context)
throws UndeliverableMessageException, InsecureFallbackApprovalException throws ApnUnavailableException
{ {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
validateDestinations(message); return new OutgoingLollipopMmsConnection(context);
} else {
MmsRadio radio = MmsRadio.getInstance(context); return new OutgoingLegacyMmsConnection(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);
} }
} }
private MmsSendResult sendMms(MasterSecret masterSecret, MmsRadio radio, SendReq message, private byte[] getPduBytes(MasterSecret masterSecret, SendReq message)
boolean usingMmsRadio, boolean useProxy)
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
{ {
String number = TelephonyUtil.getManager(context).getLine1Number(); 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())) { if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) {
throw new UndeliverableMessageException("Attempt to send encrypted MMS?"); throw new UndeliverableMessageException("Attempt to send encrypted MMS?");
} }
@@ -150,28 +118,25 @@ public class MmsSendJob extends SendJob {
if (number != null && number.trim().length() != 0) { if (number != null && number.trim().length() != 0) {
message.setFrom(new EncodedStringValue(number)); message.setFrom(new EncodedStringValue(number));
} }
byte[] pduBytes = new PduComposer(context, message).make();
if (pduBytes == null) {
throw new UndeliverableMessageException("PDU composition failed, null payload");
}
try { return pduBytes;
byte[] pdu = new PduComposer(context, message).make(); }
if (pdu == null) { private MmsSendResult getSendResult(SendConf conf, SendReq message)
throw new UndeliverableMessageException("PDU composition failed, null payload"); throws UndeliverableMessageException
} {
if (conf == null) {
OutgoingMmsConnection connection = new OutgoingMmsConnection(context, radio.getApnInformation(), pdu); throw new UndeliverableMessageException("No M-Send.conf received in response to send.");
SendConf conf = connection.send(usingMmsRadio, useProxy); } else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
throw new UndeliverableMessageException("Got bad response: " + conf.getResponseStatus());
if (conf == null) { } else if (isInconsistentResponse(message, conf)) {
throw new UndeliverableMessageException("No M-Send.conf received in response to send."); throw new UndeliverableMessageException("Mismatched response!");
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) { } else {
throw new UndeliverableMessageException("Got bad response: " + conf.getResponseStatus()); return new MmsSendResult(conf.getMessageId(), 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");
} }
} }
@@ -181,37 +146,21 @@ public class MmsSendJob extends SendJob {
return !Arrays.equals(message.getTransactionId(), response.getTransactionId()); return !Arrays.equals(message.getTransactionId(), response.getTransactionId());
} }
private boolean isCdmaDevice() { private void validateDestinations(EncodedStringValue[] destinations) throws UndeliverableMessageException {
return ((TelephonyManager)context if (destinations == null) return;
.getSystemService(Context.TELEPHONY_SERVICE))
.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
}
private void validateDestination(EncodedStringValue destination) throws UndeliverableMessageException { for (EncodedStringValue destination : destinations) {
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) { if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
throw new UndeliverableMessageException("Invalid destination: " + throw new UndeliverableMessageException("Invalid destination: " +
(destination == null ? null : destination.getString())); (destination == null ? null : destination.getString()));
}
} }
} }
private void validateDestinations(SendReq message) throws UndeliverableMessageException { private void validateDestinations(SendReq message) throws UndeliverableMessageException {
if (message.getTo() != null) { validateDestinations(message.getTo());
for (EncodedStringValue to : message.getTo()) { validateDestinations(message.getCc());
validateDestination(to); validateDestinations(message.getBcc());
}
}
if (message.getCc() != null) {
for (EncodedStringValue cc : message.getCc()) {
validateDestination(cc);
}
}
if (message.getBcc() != null) {
for (EncodedStringValue bcc : message.getBcc()) {
validateDestination(bcc);
}
}
if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) { if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) {
throw new UndeliverableMessageException("No to, cc, or bcc specified!"); throw new UndeliverableMessageException("No to, cc, or bcc specified!");
@@ -226,13 +175,4 @@ public class MmsSendJob extends SendJob {
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId); 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(); String destination = message.getTo()[0].getString();
try { try {
prepareMessageMedia(masterSecret, message, MediaConstraints.PUSH_CONSTRAINTS, false); message = getResolvedMessage(masterSecret, message, MediaConstraints.PUSH_CONSTRAINTS, false);
TextSecureAddress address = getPushAddress(destination); TextSecureAddress address = getPushAddress(destination);
List<TextSecureAttachment> attachments = getAttachments(masterSecret, message); List<TextSecureAttachment> attachments = getAttachments(masterSecret, message);

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.mms; 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.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; import ws.com.google.android.mms.pdu.RetrieveConf;
public class IncomingMmsConnection extends MmsConnection { public interface IncomingMmsConnection {
private static final String TAG = IncomingMmsConnection.class.getSimpleName(); RetrieveConf retrieve(String contentLocation, byte[] transactionId) throws MmsException, MmsRadioException, ApnUnavailableException, IOException;
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;
}
} }

View File

@@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context; import android.content.Context;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@@ -50,19 +51,19 @@ import java.net.URL;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
public abstract class MmsConnection { @SuppressWarnings("deprecation")
public abstract class LegacyMmsConnection {
private static final String TAG = "MmsCommunication"; private static final String TAG = "MmsCommunication";
protected final Context context; protected final Context context;
protected final Apn apn; protected final Apn apn;
protected MmsConnection(Context context, Apn apn) { protected LegacyMmsConnection(Context context) throws ApnUnavailableException {
this.context = context; this.context = context;
this.apn = apn; this.apn = getApn(context);
} }
public static Apn getApn(Context context, String apnName) throws ApnUnavailableException { public static Apn getApn(Context context) throws ApnUnavailableException {
Log.w(TAG, "Getting MMSC params for apn " + apnName);
try { try {
Optional<Apn> params = ApnDatabase.getInstance(context) Optional<Apn> 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) protected static boolean checkRouteToHost(Context context, String host, boolean usingMmsRadio)
throws IOException throws IOException
{ {
@@ -146,14 +151,12 @@ public abstract class MmsConnection {
.build(); .build();
} }
protected byte[] makeRequest(boolean useProxy) throws IOException { protected byte[] execute(HttpUriRequest request) throws IOException {
Log.w(TAG, "connecting to " + apn.getMmsc() + (useProxy ? " using proxy" : "")); Log.w(TAG, "connecting to " + apn.getMmsc());
HttpUriRequest request;
CloseableHttpClient client = null; CloseableHttpClient client = null;
CloseableHttpResponse response = null; CloseableHttpResponse response = null;
try { try {
request = constructRequest(useProxy);
client = constructHttpClient(); client = constructHttpClient();
response = client.execute(request); response = client.execute(request);
@@ -170,8 +173,6 @@ public abstract class MmsConnection {
throw new IOException("unhandled response code"); throw new IOException("unhandled response code");
} }
protected abstract HttpUriRequest constructRequest(boolean useProxy) throws IOException;
protected List<Header> getBaseHeaders() { protected List<Header> getBaseHeaders() {
final String number = TelephonyUtil.getManager(context).getLine1Number(); final String number = TelephonyUtil.getManager(context).getLine1Number();
return new LinkedList<Header>() {{ return new LinkedList<Header>() {{

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -43,10 +43,6 @@ public class MmsRadio {
this.wakeLock.setReferenceCounted(true); this.wakeLock.setReferenceCounted(true);
} }
public String getApnInformation() {
return connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS).getExtraInfo();
}
public synchronized void disconnect() { public synchronized void disconnect() {
Log.w("MmsRadio", "MMS Radio Disconnect Called..."); Log.w("MmsRadio", "MMS Radio Disconnect Called...");
wakeLock.release(); wakeLock.release();

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import android.content.Context; import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
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.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; import ws.com.google.android.mms.pdu.SendConf;
public class OutgoingMmsConnection extends MmsConnection { public interface OutgoingMmsConnection {
private final static String TAG = OutgoingMmsConnection.class.getSimpleName(); SendConf send(byte[] pduBytes) throws UndeliverableMessageException;
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;
}
}
} }

View File

@@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.CustomDefaultPreference; import org.thoughtcrime.securesms.components.CustomDefaultPreference;
import org.thoughtcrime.securesms.database.ApnDatabase; 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.TelephonyUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -52,10 +52,10 @@ public class MmsPreferencesFragment extends PreferenceFragment {
new LoadApnDefaultsTask().execute(); new LoadApnDefaultsTask().execute();
} }
private class LoadApnDefaultsTask extends AsyncTask<Void, Void, MmsConnection.Apn> { private class LoadApnDefaultsTask extends AsyncTask<Void, Void, LegacyMmsConnection.Apn> {
@Override @Override
protected MmsConnection.Apn doInBackground(Void... params) { protected LegacyMmsConnection.Apn doInBackground(Void... params) {
try { try {
Context context = getActivity(); Context context = getActivity();
@@ -72,7 +72,7 @@ public class MmsPreferencesFragment extends PreferenceFragment {
} }
@Override @Override
protected void onPostExecute(MmsConnection.Apn apnDefaults) { protected void onPostExecute(LegacyMmsConnection.Apn apnDefaults) {
((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_HOST_PREF)) ((CustomDefaultPreference)findPreference(TextSecurePreferences.MMSC_HOST_PREF))
.setValidator(new CustomDefaultPreference.UriValidator()) .setValidator(new CustomDefaultPreference.UriValidator())
.setDefaultValue(apnDefaults.getMmsc()); .setDefaultValue(apnDefaults.getMmsc());

View File

@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.preferences;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceScreen; import android.preference.PreferenceScreen;
@@ -40,12 +42,13 @@ public class SmsMmsPreferenceFragment extends PreferenceFragment {
} }
private void initializePlatformSpecificOptions() { private void initializePlatformSpecificOptions() {
PreferenceScreen preferenceScreen = getPreferenceScreen(); PreferenceScreen preferenceScreen = getPreferenceScreen();
Preference defaultPreference = findPreference(KITKAT_DEFAULT_PREF); Preference defaultPreference = findPreference(KITKAT_DEFAULT_PREF);
Preference allSmsPreference = findPreference(TextSecurePreferences.ALL_SMS_PREF); Preference allSmsPreference = findPreference(TextSecurePreferences.ALL_SMS_PREF);
Preference allMmsPreference = findPreference(TextSecurePreferences.ALL_MMS_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 (allSmsPreference != null) preferenceScreen.removePreference(allSmsPreference);
if (allMmsPreference != null) preferenceScreen.removePreference(allMmsPreference); if (allMmsPreference != null) preferenceScreen.removePreference(allMmsPreference);
@@ -63,6 +66,10 @@ public class SmsMmsPreferenceFragment extends PreferenceFragment {
} else if (defaultPreference != null) { } else if (defaultPreference != null) {
preferenceScreen.removePreference(defaultPreference); preferenceScreen.removePreference(defaultPreference);
} }
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && manualMmsPreference != null) {
preferenceScreen.removePreference(manualMmsPreference);
}
} }
private class ApnPreferencesClickListener implements Preference.OnPreferenceClickListener { private class ApnPreferencesClickListener implements Preference.OnPreferenceClickListener {

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@@ -17,6 +17,7 @@
package org.thoughtcrime.securesms.util; package org.thoughtcrime.securesms.util;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Shader; import android.graphics.Shader;
@@ -24,7 +25,10 @@ import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.provider.Telephony; import android.provider.Telephony;
import android.telephony.SmsManager;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
@@ -34,6 +38,7 @@ import android.widget.EditText;
import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.TextSecureExpiredException; import org.thoughtcrime.securesms.TextSecureExpiredException;
import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
import org.whispersystems.textsecure.api.util.InvalidNumberException; import org.whispersystems.textsecure.api.util.InvalidNumberException;
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; 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 { try {
lock.wait(timeout); lock.wait(timeout);
} catch (InterruptedException ie) { } catch (InterruptedException ie) {
@@ -279,4 +284,9 @@ public class Util {
public static boolean isBuildFresh() { public static boolean isBuildFresh() {
return BuildConfig.BUILD_TIMESTAMP + TimeUnit.DAYS.toMillis(180) > System.currentTimeMillis(); 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);
}
} }