Refactor MMS send/download to be synchronous.

1) Make the radio change a synchronous action with a timeout.

2) Move the send logic into an MmsTransport, in preparation for
   UniversalTransport composition.

3) Move the download logic into a synchronous receiver.
master
Moxie Marlinspike 2013-07-16 19:52:02 -07:00
parent 53803630d4
commit fd045f2354
14 changed files with 476 additions and 532 deletions

View File

@ -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();

View File

@ -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;

View File

@ -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.");
}
}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.mms;
public class MmsRadioException extends Throwable {
public MmsRadioException(String s) {
super(s);
}
}

View File

@ -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;

View File

@ -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<DownloadItem> pendingMessages = new LinkedList<DownloadItem>();
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<retrieved.getBody().getPartsNum();i++) {
Log.w("MmsDownloader", "Got MMS part of content-type: " +
new String(retrieved.getBody().getPart(i).getContentType()));
if (isCdmaNetwork()) {
Log.w("MmsDownloader", "Connecting directly...");
try {
retrieveAndStore(masterSecret, messageId, threadId, contentLocation,
transactionId, false, false);
return;
} catch (IOException e) {
Log.w("MmsDownloader", e);
}
}
storeRetrievedMms(mmsDatabase, item, retrieved);
sendRetrievedAcknowledgement(item);
Log.w("MmsDownloader", "Changing radio to MMS mode..");
radio.connect();
Log.w("MmsDownloader", "Downloading in MMS mode without proxy...");
try {
retrieveAndStore(masterSecret, messageId, threadId, contentLocation,
transactionId, true, false);
radio.disconnect();
return;
} catch (IOException e) {
Log.w("MmsDownloader", e);
}
Log.w("MmsDownloader", "Downloading in MMS mode with proxy...");
try {
retrieveAndStore(masterSecret, messageId, threadId,
contentLocation, transactionId, true, true);
radio.disconnect();
return;
} catch (IOException e) {
Log.w("MmsDownloader", e);
radio.disconnect();
handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider),
automatic);
}
} catch (ApnUnavailableException e) {
Log.w("MmsDownloader", e);
handleDownloadError(item, MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE,
context.getString(R.string.MmsDownloader_error_reading_mms_settings));
} catch (IOException e) {
Log.w("MmsDownloader", e);
if (!item.useMmsRadioMode() && !item.proxyRequestIfPossible()) {
Log.w("MmsDownloader", "Falling back to just radio mode...");
scheduleDownloadWithRadioMode(item);
} else if (!item.proxyRequestIfPossible()) {
Log.w("MmsDownloadeR", "Falling back to radio mode and proxy...");
scheduleDownloadWithRadioModeAndProxy(item);
} else {
handleDownloadError(item, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider));
}
handleDownloadError(masterSecret, messageId, threadId, MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE,
context.getString(R.string.MmsDownloader_error_reading_mms_settings), automatic);
} catch (MmsException e) {
Log.w("MmsDownloader", e);
handleDownloadError(item, MmsDatabase.Status.DOWNLOAD_HARD_FAILURE,
context.getString(R.string.MmsDownloader_error_storing_mms));
handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_HARD_FAILURE,
context.getString(R.string.MmsDownloader_error_storing_mms),
automatic);
} catch (MmsRadioException e) {
Log.w("MmsDownloader", e);
handleDownloadError(masterSecret, messageId, threadId,
MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider),
automatic);
}
}
private void storeRetrievedMms(MmsDatabase mmsDatabase, DownloadItem item, RetrieveConf retrieved)
private void retrieveAndStore(MasterSecret masterSecret, long messageId, long threadId,
String contentLocation, byte[] transactionId,
boolean radioEnabled, boolean useProxy)
throws IOException, MmsException
{
RetrieveConf retrieved = MmsDownloadHelper.retrieveMms(context, contentLocation,
radio.getApnInformation(),
radioEnabled, useProxy);
storeRetrievedMms(masterSecret, contentLocation, messageId, threadId, retrieved);
sendRetrievedAcknowledgement(transactionId, radioEnabled, useProxy);
}
private void storeRetrievedMms(MasterSecret masterSecret, String contentLocation,
long messageId, long threadId, RetrieveConf retrieved)
throws MmsException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Pair<Long, Long> 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<DownloadItem> downloadItems = (LinkedList<DownloadItem>)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<DownloadItem> 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;
}
}

View File

@ -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<SendItem> pendingMessages = new LinkedList<SendItem>();
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<SendReq> 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<request.getBody().getPartsNum();i++) {
Log.w("MmsSender", "Sent MMS part of content-type: " + new String(request.getBody().getPart(i).getContentType()));
for (SendReq message : messages) {
try {
Log.w("MmsSender", "Passing to MMS transport: " + message.getDatabaseMessageId());
database.markAsSending(message.getDatabaseMessageId());
Pair<byte[], Integer> 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<SendReq> 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<SendItem> outgoing = (List<SendItem>)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;
}
}
}

View File

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

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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<byte[], Integer> 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<byte[], Integer> 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<byte[], Integer> 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<byte[], Integer> 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<message.getBody().getPartsNum();i++) {
Log.w("MmsSender", "Sent MMS part of content-type: " + new String(message.getBody().getPart(i).getContentType()));
}
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 Pair<byte[], Integer>(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;
}
}

View File

@ -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) ||