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.
This commit is contained in:
Moxie Marlinspike 2013-02-09 15:17:55 -08:00
parent 71f43075a9
commit 471ef16a5b
8 changed files with 171 additions and 14 deletions

View File

@ -163,6 +163,15 @@
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</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"
android:authorities="org.thoughtcrime.provider.securesms" />

View File

@ -221,6 +221,9 @@
<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>
<!-- 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 -->
<string name="ApplicationMigrationService_migrating">Migrating</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_corrupted_ciphertext">Corrupted ciphertext</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 -->
<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>

View File

@ -24,6 +24,7 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
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) {
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
long count = mmsSmsDatabase.getConversationCount(threadId);

View File

@ -39,6 +39,7 @@ import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MessageDisplayHelper;
@ -74,6 +75,33 @@ public class MessageNotifier {
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) {
updateNotification(context, masterSecret, false);
}
@ -286,7 +314,9 @@ public class MessageNotifier {
return getMmsRecipient(context, cursor);
}
} 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)));
}
}

View File

@ -18,17 +18,21 @@ package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.telephony.TelephonyManager;
import android.util.Log;
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.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Hex;
import ws.com.google.android.mms.ContentType;
@ -49,9 +53,11 @@ import java.util.LinkedList;
public class MmsSender extends MmscProcessor {
private final LinkedList<SendReq[]> pendingMessages = new LinkedList<SendReq[]>();
private final Handler toastHandler;
public MmsSender(Context context) {
public MmsSender(Context context, Handler toastHandler) {
super(context);
this.toastHandler = toastHandler;
}
public void process(MasterSecret masterSecret, Intent intent) {
@ -69,8 +75,8 @@ public class MmsSender extends MmscProcessor {
sendRequests[0] = database.getSendRequest(messageId);
}
if (sendRequests.length > 0)
handleSendMms(sendRequests);
if (sendRequests != null && sendRequests.length > 0)
handleSendMms(sendRequests, messageId != -1);
} catch (MmsException me) {
Log.w("MmsSender", me);
@ -90,10 +96,15 @@ public class MmsSender extends MmscProcessor {
else finishConnectivity();
}
private void handleSendMms(SendReq[] sendRequests) {
private void handleSendMms(SendReq[] sendRequests, boolean targeted) {
if (!isConnectivityPossible()) {
for (int i=0;i<sendRequests.length;i++)
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(sendRequests[i].getDatabaseMessageId());
if (targeted) {
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 {
pendingMessages.add(sendRequests);
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()));
}
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(pdu, conf)) {
db.markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
Log.w("MmsSender", "Got a response for the wrong transaction?");
return;
} else {

View File

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

View File

@ -21,10 +21,12 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
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.WirePrefix;
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;
public class SmsReceiver {
@ -44,9 +48,11 @@ public class SmsReceiver {
private MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler();
private final Context context;
private final ToastHandler toastHandler;
public SmsReceiver(Context context) {
this.context = context;
public SmsReceiver(Context context, ToastHandler toastHandler) {
this.context = context;
this.toastHandler = toastHandler;
}
private String assembleSecureMessageFragments(String sender, String messageBody) {
@ -169,14 +175,24 @@ public class SmsReceiver {
private void handleSentMessage(Intent intent) {
long messageId = intent.getLongExtra("message_id", -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);
if (intent.getIntExtra("ResultCode", -31337) == Activity.RESULT_OK)
if (result == Activity.RESULT_OK) {
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);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
}
}
private void handleDeliveredMessage(Intent intent) {

View File

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