Support for robust delivery.

1) If a message fails to be delivered, post a notification in the
   status bar if that thread is not active and visible.

2) If a message fails to be delivered because there is no service,
   keep retrying every time service becomes available again.
pull/1/head
Moxie Marlinspike 12 years ago
parent 71f43075a9
commit 471ef16a5b

@ -163,6 +163,15 @@
<data android:mimeType="application/vnd.wap.mms-message" /> <data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".service.SystemStateListener"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SERVICE_STATE"></action>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"></action>
</intent-filter>
</receiver>
<provider android:name=".providers.PartProvider" <provider android:name=".providers.PartProvider"
android:authorities="org.thoughtcrime.provider.securesms" /> android:authorities="org.thoughtcrime.provider.securesms" />

@ -221,6 +221,9 @@
<string name="MmsMessageRecord_bad_encrypted_mms_message">Bad encrypted MMS message...</string> <string name="MmsMessageRecord_bad_encrypted_mms_message">Bad encrypted MMS message...</string>
<string name="MmsMessageRecord_mms_message_encrypted_for_non_existing_session">MMS message encrypted for non-existing session...</string> <string name="MmsMessageRecord_mms_message_encrypted_for_non_existing_session">MMS message encrypted for non-existing session...</string>
<!-- MmsSender -->
<string name="MmsSender_currently_unable_to_send_your_mms_message">Currently unable to send your MMS message. It will be sent once service becomes available.</string>
<!-- ApplicationMigrationService --> <!-- ApplicationMigrationService -->
<string name="ApplicationMigrationService_migrating">Migrating</string> <string name="ApplicationMigrationService_migrating">Migrating</string>
<string name="ApplicationMigrationService_migrating_system_text_messages">Migrating System Text Messages</string> <string name="ApplicationMigrationService_migrating_system_text_messages">Migrating System Text Messages</string>
@ -236,7 +239,12 @@
<string name="MessageNotifier_encrypted_message">Encrypted message...</string> <string name="MessageNotifier_encrypted_message">Encrypted message...</string>
<string name="MessageNotifier_corrupted_ciphertext">Corrupted ciphertext</string> <string name="MessageNotifier_corrupted_ciphertext">Corrupted ciphertext</string>
<string name="MessageNotifier_no_subject">(No Subject)</string> <string name="MessageNotifier_no_subject">(No Subject)</string>
<string name="MessageNotifier_message_delivery_failed">Message delivery failed.</string>
<string name="MessageNotifier_failed_to_deliver_message">Failed to deliver message.</string>
<string name="MessageNotifier_error_delivering_message">Error delivering message.</string>
<!-- SmsReceiver -->
<string name="SmsReceiver_currently_unable_to_send_your_sms_message">Currently unable to send your SMS message. It will be sent once service becomes available.</string>
<!-- auto_initiate_activity --> <!-- auto_initiate_activity -->
<string name="auto_initiate_activity__you_have_received_a_message_from_someone_who_supports_textsecure_encrypted_sessions_would_you_like_to_initiate_a_secure_session">You have received a message from someone who supports TextSecure encrypted sessions. Would you like to initiate a secure session?</string> <string name="auto_initiate_activity__you_have_received_a_message_from_someone_who_supports_textsecure_encrypted_sessions_would_you_like_to_initiate_a_secure_session">You have received a message from someone who supports TextSecure encrypted sessions. Would you like to initiate a secure session?</string>

@ -24,6 +24,7 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.Arrays; import java.util.Arrays;
@ -313,6 +314,25 @@ public class ThreadDatabase extends Database {
} }
} }
public Recipients getRecipientsForThreadId(Context context, long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, null, ID + " = ?", new String[] {threadId+""}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String recipientIds = cursor.getString(cursor.getColumnIndexOrThrow(RECIPIENT_IDS));
return RecipientFactory.getRecipientsForIds(context, recipientIds, false);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
public void update(long threadId) { public void update(long threadId) {
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
long count = mmsSmsDatabase.getConversationCount(threadId); long count = mmsSmsDatabase.getConversationCount(threadId);

@ -39,6 +39,7 @@ import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MessageDisplayHelper; import org.thoughtcrime.securesms.crypto.MessageDisplayHelper;
@ -74,6 +75,33 @@ public class MessageNotifier {
visibleThread = threadId; visibleThread = threadId;
} }
public static void notifyMessageDeliveryFailed(Context context, Recipients recipients, long threadId) {
if (visibleThread == threadId) {
sendInThreadNotification(context);
} else {
Intent intent = new Intent(context, ConversationListActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra("recipients", recipients);
intent.putExtra("thread_id", threadId);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder.setSmallIcon(R.drawable.icon_notification);
builder.setLargeIcon(BitmapFactory.decodeResource(context.getResources(),
R.drawable.ic_list_alert_sms_failed));
builder.setContentTitle(context.getString(R.string.MessageNotifier_message_delivery_failed));
builder.setContentText(context.getString(R.string.MessageNotifier_failed_to_deliver_message));
builder.setTicker(context.getString(R.string.MessageNotifier_error_delivering_message));
builder.setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
builder.setAutoCancel(true);
setNotificationAlarms(context, builder, true);
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
.notify((int)threadId, builder.build());
}
}
public static void updateNotification(Context context, MasterSecret masterSecret) { public static void updateNotification(Context context, MasterSecret masterSecret) {
updateNotification(context, masterSecret, false); updateNotification(context, masterSecret, false);
} }
@ -286,7 +314,9 @@ public class MessageNotifier {
return getMmsRecipient(context, cursor); return getMmsRecipient(context, cursor);
} }
} catch (RecipientFormattingException e) { } catch (RecipientFormattingException e) {
return new Recipients(new Recipient("Unknown", null, null)); Log.w("MessageNotifier", e);
return new Recipients(new Recipient("Unknown", "Unknown", null,
ContactPhotoFactory.getDefaultContactPhoto(context)));
} }
} }

@ -18,17 +18,21 @@ package org.thoughtcrime.securesms.service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Handler;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SessionCipher; import org.thoughtcrime.securesms.crypto.SessionCipher;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.mms.MmsSendHelper; import org.thoughtcrime.securesms.mms.MmsSendHelper;
import org.thoughtcrime.securesms.mms.TextTransport; import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.Hex;
import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.ContentType;
@ -49,9 +53,11 @@ import java.util.LinkedList;
public class MmsSender extends MmscProcessor { public class MmsSender extends MmscProcessor {
private final LinkedList<SendReq[]> pendingMessages = new LinkedList<SendReq[]>(); private final LinkedList<SendReq[]> pendingMessages = new LinkedList<SendReq[]>();
private final Handler toastHandler;
public MmsSender(Context context) { public MmsSender(Context context, Handler toastHandler) {
super(context); super(context);
this.toastHandler = toastHandler;
} }
public void process(MasterSecret masterSecret, Intent intent) { public void process(MasterSecret masterSecret, Intent intent) {
@ -69,8 +75,8 @@ public class MmsSender extends MmscProcessor {
sendRequests[0] = database.getSendRequest(messageId); sendRequests[0] = database.getSendRequest(messageId);
} }
if (sendRequests.length > 0) if (sendRequests != null && sendRequests.length > 0)
handleSendMms(sendRequests); handleSendMms(sendRequests, messageId != -1);
} catch (MmsException me) { } catch (MmsException me) {
Log.w("MmsSender", me); Log.w("MmsSender", me);
@ -90,10 +96,15 @@ public class MmsSender extends MmscProcessor {
else finishConnectivity(); else finishConnectivity();
} }
private void handleSendMms(SendReq[] sendRequests) { private void handleSendMms(SendReq[] sendRequests, boolean targeted) {
if (!isConnectivityPossible()) { if (!isConnectivityPossible()) {
for (int i=0;i<sendRequests.length;i++) if (targeted) {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(sendRequests[i].getDatabaseMessageId()); toastHandler
.obtainMessage(0, context.getString(R.string.MmsSender_currently_unable_to_send_your_mms_message))
.sendToTarget();
}
// for (int i=0;i<sendRequests.length;i++)
// DatabaseFactory.getMmsDatabase(context).markAsSentFailed(sendRequests[i].getDatabaseMessageId());
} else { } else {
pendingMessages.add(sendRequests); pendingMessages.add(sendRequests);
issueConnectivityRequest(); issueConnectivityRequest();
@ -146,17 +157,23 @@ public class MmsSender extends MmscProcessor {
Log.w("MmsSender", "Sent MMS part of content-type: " + new String(pdu.getBody().getPart(i).getContentType())); Log.w("MmsSender", "Sent MMS part of content-type: " + new String(pdu.getBody().getPart(i).getContentType()));
} }
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(context, threadId);
if (conf == null) { if (conf == null) {
db.markAsSentFailed(messageId); db.markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
Log.w("MmsSender", "No M-Send.conf received in response to send."); Log.w("MmsSender", "No M-Send.conf received in response to send.");
return; return;
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) { } else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
Log.w("MmsSender", "Got bad response: " + conf.getResponseStatus()); Log.w("MmsSender", "Got bad response: " + conf.getResponseStatus());
db.updateResponseStatus(messageId, conf.getResponseStatus()); db.updateResponseStatus(messageId, conf.getResponseStatus());
db.markAsSentFailed(messageId); db.markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
return; return;
} else if (isInconsistentResponse(pdu, conf)) { } else if (isInconsistentResponse(pdu, conf)) {
db.markAsSentFailed(messageId); db.markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
Log.w("MmsSender", "Got a response for the wrong transaction?"); Log.w("MmsSender", "Got a response for the wrong transaction?");
return; return;
} else { } else {

@ -131,10 +131,10 @@ public class SendReceiveService extends Service {
} }
private void initializeProcessors() { private void initializeProcessors() {
smsReceiver = new SmsReceiver(this); smsReceiver = new SmsReceiver(this, toastHandler);
smsSender = new SmsSender(this); smsSender = new SmsSender(this);
mmsReceiver = new MmsReceiver(this); mmsReceiver = new MmsReceiver(this);
mmsSender = new MmsSender(this); mmsSender = new MmsSender(this, toastHandler);
mmsDownloader = new MmsDownloader(this, toastHandler); mmsDownloader = new MmsDownloader(this, toastHandler);
} }

@ -21,10 +21,12 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.telephony.SmsManager;
import android.telephony.SmsMessage; import android.telephony.SmsMessage;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.InvalidKeyException; import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.InvalidVersionException; import org.thoughtcrime.securesms.crypto.InvalidVersionException;
@ -37,6 +39,8 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.Prefix; import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
import org.thoughtcrime.securesms.sms.MultipartMessageHandler; import org.thoughtcrime.securesms.sms.MultipartMessageHandler;
public class SmsReceiver { public class SmsReceiver {
@ -44,9 +48,11 @@ public class SmsReceiver {
private MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler(); private MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler();
private final Context context; private final Context context;
private final ToastHandler toastHandler;
public SmsReceiver(Context context) { public SmsReceiver(Context context, ToastHandler toastHandler) {
this.context = context; this.context = context;
this.toastHandler = toastHandler;
} }
private String assembleSecureMessageFragments(String sender, String messageBody) { private String assembleSecureMessageFragments(String sender, String messageBody) {
@ -169,14 +175,24 @@ public class SmsReceiver {
private void handleSentMessage(Intent intent) { private void handleSentMessage(Intent intent) {
long messageId = intent.getLongExtra("message_id", -1); long messageId = intent.getLongExtra("message_id", -1);
long type = intent.getLongExtra("type", -1); long type = intent.getLongExtra("type", -1);
int result = intent.getIntExtra("ResultCode", -31337);
Log.w("SMSReceiverService", "Intent resultcode: " + intent.getIntExtra("ResultCode", 42)); Log.w("SMSReceiverService", "Intent resultcode: " + result);
Log.w("SMSReceiverService", "Running sent callback: " + messageId + "," + type); Log.w("SMSReceiverService", "Running sent callback: " + messageId + "," + type);
if (intent.getIntExtra("ResultCode", -31337) == Activity.RESULT_OK) if (result == Activity.RESULT_OK) {
DatabaseFactory.getSmsDatabase(context).markAsSent(messageId, type); DatabaseFactory.getSmsDatabase(context).markAsSent(messageId, type);
else } else if (result == SmsManager.RESULT_ERROR_NO_SERVICE || result == SmsManager.RESULT_ERROR_RADIO_OFF) {
toastHandler
.obtainMessage(0, context.getString(R.string.SmsReceiver_currently_unable_to_send_your_sms_message))
.sendToTarget();
} else {
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(context, threadId);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
}
} }
private void handleDeliveredMessage(Intent intent) { private void handleDeliveredMessage(Intent intent) {

@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.telephony.ServiceState;
public class SystemStateListener extends BroadcastReceiver {
private static final String ACTION_SERVICE_STATE = "android.intent.action.SERVICE_STATE";
private static final String ACTION_CONNECTIVITY_CHANGE = "android.net.conn.CONNECTIVITY_CHANGE";
private void sendSmsOutbox(Context context) {
Intent smsSenderIntent = new Intent(SendReceiveService.SEND_SMS_ACTION, null, context,
SendReceiveService.class);
context.startService(smsSenderIntent);
}
private void sendMmsOutbox(Context context) {
Intent mmsSenderIntent = new Intent(SendReceiveService.SEND_MMS_ACTION, null, context,
SendReceiveService.class);
context.startService(mmsSenderIntent);
}
private void handleRadioServiceStateChange(Context context, Intent intent) {
int state = intent.getIntExtra("state", -31337);
if (state == ServiceState.STATE_IN_SERVICE) {
sendSmsOutbox(context);
}
}
private void handleDataServiceStateChange(Context context, Intent intent) {
ConnectivityManager connectivityManager
= (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(MmscProcessor.TYPE_MOBILE_MMS);
if (networkInfo != null && networkInfo.isAvailable()) {
sendMmsOutbox(context);
}
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) return;
if (intent.getAction().equals(ACTION_SERVICE_STATE)) {
handleRadioServiceStateChange(context, intent);
} else if (intent.getAction().equals(ACTION_CONNECTIVITY_CHANGE)) {
handleDataServiceStateChange(context, intent);
}
}
}
Loading…
Cancel
Save