diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index 03355f4da9..b045492bc4 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -240,6 +240,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns { notifyConversationListeners(getThreadIdForMessage(messageId)); } + public void markAsSending(long messageId) { + updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE); + notifyConversationListeners(getThreadIdForMessage(messageId)); + } + public void markAsSent(long messageId, byte[] mmsId, long status) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues contentValues = new ContentValues(); diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java index 534644f42b..4634bb953f 100644 --- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -349,7 +349,7 @@ public class ThreadDatabase extends Database { } } - public Recipients getRecipientsForThreadId(Context context, long threadId) { + public Recipients getRecipientsForThreadId(long threadId) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); Cursor cursor = null; diff --git a/src/org/thoughtcrime/securesms/mms/MmsCommunication.java b/src/org/thoughtcrime/securesms/mms/MmsCommunication.java index a1cc941feb..83a386e734 100644 --- a/src/org/thoughtcrime/securesms/mms/MmsCommunication.java +++ b/src/org/thoughtcrime/securesms/mms/MmsCommunication.java @@ -153,7 +153,7 @@ public class MmsCommunication { int ipAddress = Conversions.byteArrayToIntLittleEndian(ipAddressBytes, 0); ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (!manager.requestRouteToHost(MmsDownloader.TYPE_MOBILE_MMS, ipAddress)) + if (!manager.requestRouteToHost(MmsRadio.TYPE_MOBILE_MMS, ipAddress)) throw new IOException("Connection manager could not obtain route to host."); } } diff --git a/src/org/thoughtcrime/securesms/mms/MmsRadio.java b/src/org/thoughtcrime/securesms/mms/MmsRadio.java new file mode 100644 index 0000000000..c8e5de8563 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/MmsRadio.java @@ -0,0 +1,143 @@ +package org.thoughtcrime.securesms.mms; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.PowerManager; +import android.util.Log; + +import org.thoughtcrime.securesms.util.Util; + +public class MmsRadio { + + private static MmsRadio instance; + + public static synchronized MmsRadio getInstance(Context context) { + if (instance == null) + instance = new MmsRadio(context); + + return instance; + } + + /// + + private static final String FEATURE_ENABLE_MMS = "enableMMS"; + private static final int APN_ALREADY_ACTIVE = 0; + public static final int TYPE_MOBILE_MMS = 2; + + private final Context context; + + private ConnectivityManager connectivityManager; + private ConnectivityListener connectivityListener; + private PowerManager.WakeLock wakeLock; + private int connectedCounter = 0; + + private MmsRadio(Context context) { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + this.context = context; + this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + this.wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MMS Connection"); + 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(); + connectedCounter--; + + Log.w("MmsRadio", "Reference count: " + connectedCounter); + + if (connectedCounter == 0) { + Log.w("MmsRadio", "Turning off MMS radio..."); + connectivityManager.stopUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS); + + if (connectivityListener != null) { + Log.w("MmsRadio", "Unregistering receiver..."); + context.unregisterReceiver(connectivityListener); + connectivityListener = null; + } + } + } + + public synchronized void connect() throws MmsRadioException { + int status = connectivityManager.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, + FEATURE_ENABLE_MMS); + + Log.w("MmsRadio", "startUsingNetworkFeature status: " + status); + + if (status == APN_ALREADY_ACTIVE) { + wakeLock.acquire(); + connectedCounter++; + return; + } else { + wakeLock.acquire(); + connectedCounter++; + + if (connectivityListener == null) { + IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + connectivityListener = new ConnectivityListener(); + context.registerReceiver(connectivityListener, filter); + } + + Util.wait(this, 30000); + + if (!isConnected()) { + Log.w("MmsRadio", "Got back from connectivity wait, and not connected..."); + disconnect(); + throw new MmsRadioException("Unable to successfully enable MMS radio."); + } + } + } + + private boolean isConnected() { + NetworkInfo info = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS); + + if ((info == null) || (info.getType() != TYPE_MOBILE_MMS) || !info.isConnected()) + return false; + + return true; + } + + private boolean isConnectivityPossible() { + NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS); + + return networkInfo != null && networkInfo.isAvailable(); + } + + private boolean isConnectivityFailure() { + NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS); + + return networkInfo == null || networkInfo.getDetailedState() == NetworkInfo.DetailedState.FAILED; + } + + private synchronized void issueConnectivityChange() { + if (isConnected()) { + Log.w("MmsRadio", "Notifying connected..."); + notifyAll(); + return; + } + + if (!isConnected() && (isConnectivityFailure() || !isConnectivityPossible())) { + Log.w("MmsRadio", "Notifying not connected..."); + notifyAll(); + return; + } + } + + private class ConnectivityListener extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Log.w("MmsRadio", "Got connectivity change..."); + issueConnectivityChange(); + } + } + + +} diff --git a/src/org/thoughtcrime/securesms/mms/MmsRadioException.java b/src/org/thoughtcrime/securesms/mms/MmsRadioException.java new file mode 100644 index 0000000000..9919541b88 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/MmsRadioException.java @@ -0,0 +1,7 @@ +package org.thoughtcrime.securesms.mms; + +public class MmsRadioException extends Throwable { + public MmsRadioException(String s) { + super(s); + } +} diff --git a/src/org/thoughtcrime/securesms/mms/MmsSendHelper.java b/src/org/thoughtcrime/securesms/mms/MmsSendHelper.java index e1f288e67e..4ec0ca0f68 100644 --- a/src/org/thoughtcrime/securesms/mms/MmsSendHelper.java +++ b/src/org/thoughtcrime/securesms/mms/MmsSendHelper.java @@ -24,10 +24,8 @@ import android.util.Log; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; -import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ByteArrayEntity; -import org.thoughtcrime.securesms.service.MmscProcessor; import org.whispersystems.textsecure.util.Util; import java.io.IOException; @@ -39,7 +37,9 @@ import ws.com.google.android.mms.pdu.SendConf; public class MmsSendHelper extends MmsCommunication { - private static byte[] makePost(Context context, MmsConnectionParameters parameters, byte[] mms) throws ClientProtocolException, IOException { + private static byte[] makePost(Context context, MmsConnectionParameters parameters, byte[] mms) + throws IOException + { AndroidHttpClient client = null; try { @@ -114,7 +114,7 @@ public class MmsSendHelper extends MmsCommunication { public static boolean hasNecessaryApnDetails(Context context) { try { ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - String apn = connectivityManager.getNetworkInfo(MmscProcessor.TYPE_MOBILE_MMS).getExtraInfo(); + String apn = connectivityManager.getNetworkInfo(MmsRadio.TYPE_MOBILE_MMS).getExtraInfo(); MmsCommunication.getMmsConnectionParameters(context, apn, true); return true; diff --git a/src/org/thoughtcrime/securesms/service/MmsDownloader.java b/src/org/thoughtcrime/securesms/service/MmsDownloader.java index 687d004617..fff28f7f7f 100644 --- a/src/org/thoughtcrime/securesms/service/MmsDownloader.java +++ b/src/org/thoughtcrime/securesms/service/MmsDownloader.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2011 Whisper Systems + * Copyright (C) 2013 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 @@ -30,6 +30,8 @@ import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.MmsDownloadHelper; +import org.thoughtcrime.securesms.mms.MmsRadio; +import org.thoughtcrime.securesms.mms.MmsRadioException; import org.thoughtcrime.securesms.mms.MmsSendHelper; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.protocol.WirePrefix; @@ -45,48 +47,26 @@ import ws.com.google.android.mms.pdu.PduComposer; import ws.com.google.android.mms.pdu.PduHeaders; import ws.com.google.android.mms.pdu.RetrieveConf; -public class MmsDownloader extends MmscProcessor { +public class MmsDownloader { - private final LinkedList pendingMessages = new LinkedList(); + private final Context context; private final SendReceiveService.ToastHandler toastHandler; + private final MmsRadio radio; public MmsDownloader(Context context, SendReceiveService.ToastHandler toastHandler) { - super(context); + this.context = context; this.toastHandler = toastHandler; + this.radio = MmsRadio.getInstance(context); } public void process(MasterSecret masterSecret, Intent intent) { if (intent.getAction().equals(SendReceiveService.DOWNLOAD_MMS_ACTION)) { - boolean isCdma = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA; - DownloadItem item = new DownloadItem(masterSecret, !isCdma, false, - intent.getLongExtra("message_id", -1), - intent.getLongExtra("thread_id", -1), - intent.getBooleanExtra("automatic", false), - intent.getStringExtra("content_location"), - intent.getByteArrayExtra("transaction_id")); - - handleDownloadMmsAction(item); - } else if (intent.getAction().equals(SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION)) { - handleConnectivityChange(); + handleDownloadMms(masterSecret, intent); } else if (intent.getAction().equals(SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION)) { handleMmsPendingApnDownloads(masterSecret); } } - private void handleDownloadMmsAction(DownloadItem item) { - if (!isConnectivityPossible()) { - Log.w("MmsDownloader", "No MMS connectivity available!"); - handleDownloadError(item, MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY, - context.getString(R.string.MmsDownloader_no_connectivity_available_for_mms_download_try_again_later)); - return; - } - - DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_CONNECTING); - - if (item.useMmsRadioMode()) downloadMmsWithRadioChange(item); - else downloadMms(item); - } - private void handleMmsPendingApnDownloads(MasterSecret masterSecret) { if (!MmsDownloadHelper.isMmsConnectionParametersAvailable(context, null, false)) return; @@ -109,86 +89,125 @@ public class MmsDownloader extends MmscProcessor { stalledMmsReader.close(); } - private void downloadMmsWithRadioChange(DownloadItem item) { - Log.w("MmsDownloader", "Handling MMS download with radio change..."); - pendingMessages.add(item); - issueConnectivityRequest(); - } + private void handleDownloadMms(MasterSecret masterSecret, Intent intent) { + long messageId = intent.getLongExtra("message_id", -1); + long threadId = intent.getLongExtra("thread_id", -1); + byte[] transactionId = intent.getByteArrayExtra("transaction_id"); + String contentLocation = intent.getStringExtra("content_location"); + boolean automatic = intent.getBooleanExtra("automatic", false); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - private void downloadMms(DownloadItem item) { - Log.w("MmsDownloadService", "Handling actual MMS download..."); - MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); + database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_CONNECTING); try { - RetrieveConf retrieved = MmsDownloadHelper.retrieveMms(context, item.getContentLocation(), - getApnInformation(), - item.useMmsRadioMode(), - item.proxyRequestIfPossible()); - - for (int i=0;i messageAndThreadId; if (retrieved.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieved.getSubject().getString())) { - messageAndThreadId = mmsDatabase.insertSecureMessageInbox(item.getMasterSecret(), retrieved, - item.getContentLocation(), - item.getThreadId()); + messageAndThreadId = database.insertSecureMessageInbox(masterSecret, retrieved, + contentLocation, threadId); - if (item.getMasterSecret() != null) - DecryptingQueue.scheduleDecryption(context, item.getMasterSecret(), messageAndThreadId.first, + if (masterSecret != null) + DecryptingQueue.scheduleDecryption(context, masterSecret, messageAndThreadId.first, messageAndThreadId.second, retrieved); } else { - messageAndThreadId = mmsDatabase.insertMessageInbox(item.getMasterSecret(), retrieved, - item.getContentLocation(), - item.getThreadId()); + messageAndThreadId = database.insertMessageInbox(masterSecret, retrieved, + contentLocation, threadId); } - mmsDatabase.delete(item.getMessageId()); - MessageNotifier.updateNotification(context, item.getMasterSecret(), messageAndThreadId.second); + database.delete(messageId); + MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); } - private void sendRetrievedAcknowledgement(DownloadItem item) { + private void sendRetrievedAcknowledgement(byte[] transactionId, + boolean usingRadio, + boolean useProxy) + { try { NotifyRespInd notifyResponse = new NotifyRespInd(PduHeaders.CURRENT_MMS_VERSION, - item.getTransactionId(), + transactionId, PduHeaders.STATUS_RETRIEVED); MmsSendHelper.sendNotificationReceived(context, new PduComposer(context, notifyResponse).make(), - getApnInformation(), item.useMmsRadioMode(), - item.proxyRequestIfPossible()); + radio.getApnInformation(), usingRadio, useProxy); } catch (InvalidHeaderValueException e) { Log.w("MmsDownloader", e); } catch (IOException e) { @@ -196,116 +215,25 @@ public class MmsDownloader extends MmscProcessor { } } - protected void handleConnectivityChange() { - LinkedList downloadItems = (LinkedList)pendingMessages.clone(); - if (isConnected()) { - pendingMessages.clear(); - - for (DownloadItem item : downloadItems) { - downloadMms(item); - } - - if (pendingMessages.isEmpty()) - finishConnectivity(); - - } else if (!isConnected() && (!isConnectivityPossible() || isConnectivityFailure())) { - pendingMessages.clear(); - handleDownloadError(downloadItems, MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY, - context.getString(R.string.MmsDownloader_no_connectivity_available_for_mms_download_try_again_later)); - finishConnectivity(); - } - } - - private void handleDownloadError(List items, int downloadStatus, String error) { - for (DownloadItem item : items) { - handleDownloadError(item, downloadStatus, error); - } - } - - private void handleDownloadError(DownloadItem item, int downloadStatus, String error) { + private void handleDownloadError(MasterSecret masterSecret, long messageId, long threadId, + int downloadStatus, String error, boolean automatic) + { MmsDatabase db = DatabaseFactory.getMmsDatabase(context); - db.markDownloadState(item.getMessageId(), downloadStatus); - if (item.isAutomatic()) { - db.markIncomingNotificationReceived(item.getThreadId()); - MessageNotifier.updateNotification(context, item.getMasterSecret(), item.getThreadId()); + db.markDownloadState(messageId, downloadStatus); + + if (automatic) { + db.markIncomingNotificationReceived(threadId); + MessageNotifier.updateNotification(context, masterSecret, threadId); } toastHandler.makeToast(error); } - private void scheduleDownloadWithRadioMode(DownloadItem item) { - item.mmsRadioMode = true; - handleDownloadMmsAction(item); - } - - private void scheduleDownloadWithRadioModeAndProxy(DownloadItem item) { - item.mmsRadioMode = true; - item.proxyIfPossible = true; - handleDownloadMmsAction(item); - } - - private static class DownloadItem { - private final MasterSecret masterSecret; - private boolean mmsRadioMode; - private boolean proxyIfPossible; - - private long threadId; - private long messageId; - private byte[] transactionId; - private String contentLocation; - private boolean automatic; - - public DownloadItem(MasterSecret masterSecret, boolean mmsRadioMode, boolean proxyIfPossible, - long messageId, long threadId, boolean automatic, String contentLocation, - byte[] transactionId) - { - this.masterSecret = masterSecret; - this.mmsRadioMode = mmsRadioMode; - this.proxyIfPossible = proxyIfPossible; - this.threadId = threadId; - this.messageId = messageId; - this.contentLocation = contentLocation; - this.transactionId = transactionId; - this.automatic = automatic; - } - - public long getThreadId() { - return threadId; - } - - public long getMessageId() { - return messageId; - } - - public String getContentLocation() { - return contentLocation; - } - - public byte[] getTransactionId() { - return transactionId; - } - - public MasterSecret getMasterSecret() { - return masterSecret; - } - - public boolean proxyRequestIfPossible() { - return proxyIfPossible; - } - - public boolean useMmsRadioMode() { - return mmsRadioMode; - } - - public boolean isAutomatic() { - return automatic; - } - } - - @Override - protected String getConnectivityAction() { - return SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION; + private boolean isCdmaNetwork() { + return ((TelephonyManager)context + .getSystemService(Context.TELEPHONY_SERVICE)) + .getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA; } } diff --git a/src/org/thoughtcrime/securesms/service/MmsSender.java b/src/org/thoughtcrime/securesms/service/MmsSender.java index a5a8b88277..398efa5553 100644 --- a/src/org/thoughtcrime/securesms/service/MmsSender.java +++ b/src/org/thoughtcrime/securesms/service/MmsSender.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2011 Whisper Systems + * Copyright (C) 2013 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 @@ -18,250 +18,66 @@ package org.thoughtcrime.securesms.service; import android.content.Context; import android.content.Intent; -import android.telephony.TelephonyManager; import android.util.Log; +import android.util.Pair; -import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.crypto.SessionCipher; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.mms.MmsSendHelper; -import org.thoughtcrime.securesms.mms.TextTransport; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.protocol.WirePrefix; -import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler; -import org.thoughtcrime.securesms.util.Hex; +import org.thoughtcrime.securesms.transport.MmsTransport; +import org.thoughtcrime.securesms.transport.UndeliverableMessageException; -import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.MmsException; -import ws.com.google.android.mms.pdu.EncodedStringValue; -import ws.com.google.android.mms.pdu.PduBody; -import ws.com.google.android.mms.pdu.PduComposer; -import ws.com.google.android.mms.pdu.PduHeaders; -import ws.com.google.android.mms.pdu.PduPart; -import ws.com.google.android.mms.pdu.SendConf; import ws.com.google.android.mms.pdu.SendReq; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; +public class MmsSender { -public class MmsSender extends MmscProcessor { - - private final LinkedList pendingMessages = new LinkedList(); + private final Context context; private final ToastHandler toastHandler; public MmsSender(Context context, ToastHandler toastHandler) { - super(context); + this.context = context; this.toastHandler = toastHandler; } public void process(MasterSecret masterSecret, Intent intent) { + Log.w("MmsSender", "Got intent action: " + intent.getAction()); if (intent.getAction().equals(SendReceiveService.SEND_MMS_ACTION)) { - long messageId = intent.getLongExtra("message_id", -1); - boolean isCdma = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA; - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - - try { - List sendRequests = getOutgoingMessages(masterSecret, messageId); - - for (SendReq sendRequest : sendRequests) { - handleSendMmsAction(new SendItem(masterSecret, sendRequest, messageId != -1, !isCdma, false)); - } - - } catch (MmsException me) { - Log.w("MmsSender", me); - if (messageId != -1) - database.markAsSentFailed(messageId); - } - } else if (intent.getAction().equals(SendReceiveService.SEND_MMS_CONNECTIVITY_ACTION)) { - handleConnectivityChange(); + handleSendMms(masterSecret, intent); } } - private void handleSendMmsAction(SendItem item) { - if (!isConnectivityPossible()) { - if (item.targeted) { - toastHandler - .obtainMessage(0, context.getString(R.string.MmsSender_currently_unable_to_send_your_mms_message)) - .sendToTarget(); - } - - return; - } - - if (item.useMmsRadio) sendMmsMessageWithRadioChange(item); - else sendMmsMessage(item); - } - - private void sendMmsMessageWithRadioChange(SendItem item) { - Log.w("MmsSender", "Sending MMS with radio change.."); - pendingMessages.add(item); - issueConnectivityRequest(); - } - - private void sendMmsMessage(SendItem item) { - Log.w("MmsSender", "Sending MMS SendItem..."); - MmsDatabase db = DatabaseFactory.getMmsDatabase(context); - String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number(); - long messageId = item.request.getDatabaseMessageId(); - long messageBox = item.request.getDatabaseMessageBox(); - SendReq request = item.request; - - - if (MmsDatabase.Types.isSecureType(messageBox)) { - request = getEncryptedMms(item.masterSecret, request, messageId); - } - - if (number != null && number.trim().length() != 0) { - request.setFrom(new EncodedStringValue(number)); - } + private void handleSendMms(MasterSecret masterSecret, Intent intent) { + long messageId = intent.getLongExtra("message_id", -1); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context); + MmsTransport transport = new MmsTransport(context, masterSecret); try { - SendConf conf = MmsSendHelper.sendMms(context, new PduComposer(context, request).make(), - getApnInformation(), item.useMmsRadio, item.useProxyIfAvailable); + SendReq[] messages = database.getOutgoingMessages(masterSecret, messageId); - for (int i=0;i result = transport.deliver(message); + database.markAsSent(message.getDatabaseMessageId(), result.first, result.second); + } catch (UndeliverableMessageException e) { + Log.w("MmsSender", e); + database.markAsSentFailed(message.getDatabaseMessageId()); + long threadId = database.getThreadIdForMessage(messageId); + Recipients recipients = threads.getRecipientsForThreadId(threadId); + MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId); + } } - - long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId); - Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(context, threadId); - - if (conf == null) { - db.markAsSentFailed(messageId); - MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId); - Log.w("MmsSender", "No M-Send.conf received in response to send."); - return; - } else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) { - Log.w("MmsSender", "Got bad response: " + conf.getResponseStatus()); - db.updateResponseStatus(messageId, conf.getResponseStatus()); - db.markAsSentFailed(messageId); - MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId); - return; - } else if (isInconsistentResponse(request, conf)) { - db.markAsSentFailed(messageId); - MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId); - Log.w("MmsSender", "Got a response for the wrong transaction?"); - return; - } else { - Log.w("MmsSender", "Successful send! " + messageId); - db.markAsSent(messageId, conf.getMessageId(), conf.getResponseStatus()); - } - } catch (IOException ioe) { - Log.w("MmsSender", ioe); - if (!item.useMmsRadio) scheduleSendWithMmsRadio(item); - else if (!item.useProxyIfAvailable) scheduleSendWithMmsRadioAndProxy(item); - else db.markAsSentFailed(messageId); + } catch (MmsException e) { + Log.w("MmsSender", e); + if (messageId != -1) + database.markAsSentFailed(messageId); } } - - private List getOutgoingMessages(MasterSecret masterSecret, long messageId) - throws MmsException - { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - return Arrays.asList(database.getOutgoingMessages(masterSecret, messageId)); - } - - protected void handleConnectivityChange() { - if (!isConnected()) { - if ((!isConnectivityPossible() || isConnectivityFailure()) && !pendingMessages.isEmpty()) { - DatabaseFactory.getMmsDatabase(context).markAsSentFailed(pendingMessages.remove().request.getDatabaseMessageId()); - toastHandler.makeToast(context.getString(R.string.MmsSender_currently_unable_to_send_your_mms_message)); - Log.w("MmsSender", "Unable to send MMS."); - finishConnectivity(); - } - - return; - } - - List outgoing = (List)pendingMessages.clone(); - pendingMessages.clear(); - - for (SendItem item : outgoing) { - sendMmsMessage(item); - } - - if (pendingMessages.isEmpty()) - finishConnectivity(); - } - - private boolean isInconsistentResponse(SendReq send, SendConf response) { - Log.w("MmsSenderService", "Comparing: " + Hex.toString(send.getTransactionId())); - Log.w("MmsSenderService", "With: " + Hex.toString(response.getTransactionId())); - return !Arrays.equals(send.getTransactionId(), response.getTransactionId()); - } - - private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipient, byte[] pduBytes) { - synchronized (SessionCipher.CIPHER_LOCK) { - SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, recipient, null, null), new TextTransport()); - return cipher.encryptMessage(pduBytes); - } - } - - private SendReq getEncryptedMms(MasterSecret masterSecret, SendReq pdu, long messageId) { - Log.w("MmsSender", "Sending Secure MMS."); - EncodedStringValue[] encodedRecipient = pdu.getTo(); - String recipient = encodedRecipient[0].getString(); - byte[] pduBytes = new PduComposer(context, pdu).make(); - byte[] encryptedPduBytes = getEncryptedPdu(masterSecret, recipient, pduBytes); - - PduBody body = new PduBody(); - PduPart part = new PduPart(); - SendReq encryptedPdu = new SendReq(pdu.getPduHeaders(), body); - - part.setContentId((System.currentTimeMillis()+"").getBytes()); - part.setContentType(ContentType.TEXT_PLAIN.getBytes()); - part.setName((System.currentTimeMillis()+"").getBytes()); - part.setData(encryptedPduBytes); - body.addPart(part); - encryptedPdu.setSubject(new EncodedStringValue(WirePrefix.calculateEncryptedMmsSubject())); - encryptedPdu.setBody(body); - - return encryptedPdu; - } - - private void scheduleSendWithMmsRadioAndProxy(SendItem item) { - Log.w("MmsSender", "Falling back to sending MMS with radio and proxy..."); - item.useMmsRadio = true; - item.useProxyIfAvailable = true; - handleSendMmsAction(item); - } - - private void scheduleSendWithMmsRadio(SendItem item) { - Log.w("MmsSender", "Falling back to sending MMS with radio only..."); - item.useMmsRadio = true; - handleSendMmsAction(item); - } - - @Override - protected String getConnectivityAction() { - return SendReceiveService.SEND_MMS_CONNECTIVITY_ACTION; - } - - private static class SendItem { - private final MasterSecret masterSecret; - - private boolean useMmsRadio; - private boolean useProxyIfAvailable; - private SendReq request; - private boolean targeted; - - public SendItem(MasterSecret masterSecret, SendReq request, - boolean targeted, boolean useMmsRadio, - boolean useProxyIfAvailable) - { - this.masterSecret = masterSecret; - this.request = request; - this.targeted = targeted; - this.useMmsRadio = useMmsRadio; - this.useProxyIfAvailable = useProxyIfAvailable; - } - } - } diff --git a/src/org/thoughtcrime/securesms/service/MmscProcessor.java b/src/org/thoughtcrime/securesms/service/MmscProcessor.java deleted file mode 100644 index 42ccbe3e07..0000000000 --- a/src/org/thoughtcrime/securesms/service/MmscProcessor.java +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (C) 2011 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.service; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import android.util.Log; - -public abstract class MmscProcessor { - - private static final String FEATURE_ENABLE_MMS = "enableMMS"; - private static final int APN_ALREADY_ACTIVE = 0; - public static final int TYPE_MOBILE_MMS = 2; - - private ConnectivityManager connectivityManager; - private ConnectivityListener connectivityListener; - private WakeLock wakeLock; - - protected final Context context; - - public MmscProcessor(Context context) { - this.context = context; - PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE); - this.connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - this.wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MMS Connection"); - this.wakeLock.setReferenceCounted(false); - } - - protected String getApnInformation() { - return connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS).getExtraInfo(); - } - - protected boolean isConnected() { - NetworkInfo info = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS); - - Log.w("MmsService", "NetworkInfo: " + info); - - if ((info == null) || (info.getType() != TYPE_MOBILE_MMS) || !info.isConnected()) - return false; - - return true; - } - - protected abstract String getConnectivityAction(); - - protected void issueConnectivityRequest() { - int status = connectivityManager.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS); - - Log.w("MmscProcessor", "startUsingNetworkFeature status: " + status); - - if (status == APN_ALREADY_ACTIVE) { - issueConnectivityChange(); - } else if (connectivityListener == null) { - IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); - connectivityListener = new ConnectivityListener(); - context.registerReceiver(connectivityListener, filter); - - wakeLock.acquire(); - } - } - - protected boolean isConnectivityFailure() { - NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS); - - return networkInfo == null || networkInfo.getDetailedState() == NetworkInfo.DetailedState.FAILED; - } - - - protected boolean isConnectivityPossible() { - NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS); - Log.w("MmsService", "Got network info: " + networkInfo); - - return networkInfo != null && networkInfo.isAvailable(); - } - - protected void finishConnectivity() { - Log.w("MmsService", "Calling stop using network feature!"); - connectivityManager.stopUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS); - - if (connectivityListener != null) { - context.unregisterReceiver(connectivityListener); - connectivityListener = null; - } - - if (this.wakeLock.isHeld()) - this.wakeLock.release(); - } - - private void issueConnectivityChange() { - Intent intent = new Intent(context, SendReceiveService.class); - intent.setAction(getConnectivityAction()); - context.startService(intent); - } - - private class ConnectivityListener extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - Log.w("MmsService", "Dispatching connectivity change..."); - issueConnectivityChange(); - Log.w("MmsService", "Dispatched..."); - } - } -} diff --git a/src/org/thoughtcrime/securesms/service/SendReceiveService.java b/src/org/thoughtcrime/securesms/service/SendReceiveService.java index 0ab4214612..5fb2097d34 100644 --- a/src/org/thoughtcrime/securesms/service/SendReceiveService.java +++ b/src/org/thoughtcrime/securesms/service/SendReceiveService.java @@ -50,7 +50,6 @@ public class SendReceiveService extends Service { public static final String DELIVERED_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DELIVERED_SMS_ACTION"; public static final String RECEIVE_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_SMS_ACTION"; public static final String SEND_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_ACTION"; - public static final String SEND_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_CONNECTIVITY_ACTION"; public static final String RECEIVE_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_MMS_ACTION"; public static final String DOWNLOAD_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_ACTION"; public static final String DOWNLOAD_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION"; @@ -101,11 +100,11 @@ public class SendReceiveService extends Service { scheduleIntent(SEND_SMS, intent); else if (intent.getAction().equals(DELIVERED_SMS_ACTION)) scheduleIntent(SEND_SMS, intent); - else if (intent.getAction().equals(SEND_MMS_ACTION) || intent.getAction().equals(SEND_MMS_CONNECTIVITY_ACTION)) + else if (intent.getAction().equals(SEND_MMS_ACTION)) scheduleSecretRequiredIntent(SEND_MMS, intent); else if (intent.getAction().equals(RECEIVE_MMS_ACTION)) scheduleIntent(RECEIVE_MMS, intent); - else if (intent.getAction().equals(DOWNLOAD_MMS_ACTION) || intent.getAction().equals(DOWNLOAD_MMS_CONNECTIVITY_ACTION)) + else if (intent.getAction().equals(DOWNLOAD_MMS_ACTION)) scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent); else if (intent.getAction().equals(DOWNLOAD_MMS_PENDING_APN_ACTION)) scheduleSecretRequiredIntent(DOWNLOAD_MMS_PENDING, intent); diff --git a/src/org/thoughtcrime/securesms/service/SmsSender.java b/src/org/thoughtcrime/securesms/service/SmsSender.java index c35cce6dee..5132978606 100644 --- a/src/org/thoughtcrime/securesms/service/SmsSender.java +++ b/src/org/thoughtcrime/securesms/service/SmsSender.java @@ -100,7 +100,7 @@ public class SmsSender { registerForRadioChanges(); } else { long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId); - Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(context, threadId); + Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId); diff --git a/src/org/thoughtcrime/securesms/service/SystemStateListener.java b/src/org/thoughtcrime/securesms/service/SystemStateListener.java index ef8cf8bc52..3b165126e9 100644 --- a/src/org/thoughtcrime/securesms/service/SystemStateListener.java +++ b/src/org/thoughtcrime/securesms/service/SystemStateListener.java @@ -7,6 +7,8 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.telephony.ServiceState; +import org.thoughtcrime.securesms.mms.MmsRadio; + public class SystemStateListener extends BroadcastReceiver { public static final String ACTION_SERVICE_STATE = "android.intent.action.SERVICE_STATE"; @@ -42,7 +44,7 @@ public class SystemStateListener extends BroadcastReceiver { ConnectivityManager connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmscProcessor.TYPE_MOBILE_MMS); + NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmsRadio.TYPE_MOBILE_MMS); if (networkInfo != null && networkInfo.isAvailable()) { sendMmsOutbox(context); diff --git a/src/org/thoughtcrime/securesms/transport/MmsTransport.java b/src/org/thoughtcrime/securesms/transport/MmsTransport.java new file mode 100644 index 0000000000..9b150f9fd2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/transport/MmsTransport.java @@ -0,0 +1,157 @@ +package org.thoughtcrime.securesms.transport; + +import android.content.Context; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.util.Pair; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.crypto.SessionCipher; +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.mms.MmsRadio; +import org.thoughtcrime.securesms.mms.MmsRadioException; +import org.thoughtcrime.securesms.mms.MmsSendHelper; +import org.thoughtcrime.securesms.mms.TextTransport; +import org.thoughtcrime.securesms.protocol.WirePrefix; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.Hex; + +import java.io.IOException; +import java.util.Arrays; + +import ws.com.google.android.mms.ContentType; +import ws.com.google.android.mms.pdu.EncodedStringValue; +import ws.com.google.android.mms.pdu.PduBody; +import ws.com.google.android.mms.pdu.PduComposer; +import ws.com.google.android.mms.pdu.PduHeaders; +import ws.com.google.android.mms.pdu.PduPart; +import ws.com.google.android.mms.pdu.SendConf; +import ws.com.google.android.mms.pdu.SendReq; + +public class MmsTransport { + + private final Context context; + private final MasterSecret masterSecret; + private final MmsRadio radio; + + public MmsTransport(Context context, MasterSecret masterSecret) { + this.context = context; + this.masterSecret = masterSecret; + this.radio = MmsRadio.getInstance(context); + } + + public Pair deliver(SendReq message) throws UndeliverableMessageException { + try { + if (isCdmaDevice()) { + Log.w("MmsTransport", "Sending MMS directly without radio change..."); + try { + return sendMms(message, false, false); + } catch (IOException e) { + Log.w("MmsTransport", e); + } + } + + Log.w("MmsTransport", "Sending MMS with radio change..."); + radio.connect(); + + try { + Pair result = sendMms(message, true, false); + radio.disconnect(); + return result; + } catch (IOException e) { + Log.w("MmsTransport", e); + } + + Log.w("MmsTransport", "Sending MMS with radio change and proxy..."); + + try { + Pair result = sendMms(message, true, true); + radio.disconnect(); + return result; + } catch (IOException ioe) { + Log.w("MmsTransport", ioe); + radio.disconnect(); + throw new UndeliverableMessageException(ioe); + } + + } catch (MmsRadioException mre) { + Log.w("MmsTransport", mre); + throw new UndeliverableMessageException(mre); + } + } + + private Pair sendMms(SendReq message, boolean usingMmsRadio, boolean useProxy) + throws IOException, UndeliverableMessageException + { + String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number(); + + if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) { + message = getEncryptedMessage(message); + } + + if (number != null && number.trim().length() != 0) { + message.setFrom(new EncodedStringValue(number)); + } + + SendConf conf = MmsSendHelper.sendMms(context, new PduComposer(context, message).make(), + radio.getApnInformation(), usingMmsRadio, useProxy); + + for (int i=0;i(conf.getMessageId(), conf.getResponseStatus()); + } + } + + private SendReq getEncryptedMessage(SendReq pdu) { + EncodedStringValue[] encodedRecipient = pdu.getTo(); + String recipient = encodedRecipient[0].getString(); + byte[] pduBytes = new PduComposer(context, pdu).make(); + byte[] encryptedPduBytes = getEncryptedPdu(masterSecret, recipient, pduBytes); + + PduBody body = new PduBody(); + PduPart part = new PduPart(); + SendReq encryptedPdu = new SendReq(pdu.getPduHeaders(), body); + + part.setContentId((System.currentTimeMillis()+"").getBytes()); + part.setContentType(ContentType.TEXT_PLAIN.getBytes()); + part.setName((System.currentTimeMillis()+"").getBytes()); + part.setData(encryptedPduBytes); + body.addPart(part); + encryptedPdu.setSubject(new EncodedStringValue(WirePrefix.calculateEncryptedMmsSubject())); + encryptedPdu.setBody(body); + + return encryptedPdu; + } + + private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipient, byte[] pduBytes) { + synchronized (SessionCipher.CIPHER_LOCK) { + SessionCipher cipher = new SessionCipher(context, masterSecret, + new Recipient(null, recipient, null, null), + new TextTransport()); + + return cipher.encryptMessage(pduBytes); + } + } + + private boolean isInconsistentResponse(SendReq message, SendConf response) { + Log.w("MmsTransport", "Comparing: " + Hex.toString(message.getTransactionId())); + Log.w("MmsTransport", "With: " + Hex.toString(response.getTransactionId())); + return !Arrays.equals(message.getTransactionId(), response.getTransactionId()); + } + + private boolean isCdmaDevice() { + return ((TelephonyManager)context + .getSystemService(Context.TELEPHONY_SERVICE)) + .getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA; + } + +} diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java index 1cb9bfab78..b78f56791d 100644 --- a/src/org/thoughtcrime/securesms/util/Util.java +++ b/src/org/thoughtcrime/securesms/util/Util.java @@ -25,6 +25,8 @@ import android.widget.EditText; import android.os.Build; import android.provider.Telephony; +import org.thoughtcrime.securesms.mms.MmsRadio; + import java.io.UnsupportedEncodingException; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; @@ -134,6 +136,14 @@ public class Util { } } + public static void wait(Object lock, int timeout) { + try { + lock.wait(timeout); + } catch (InterruptedException ie) { + throw new AssertionError(ie); + } + } + public static boolean isDefaultSmsProvider(Context context){ return (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) ||