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); + } }