Transition the outbound pipeline to JobManager jobs.

This commit is contained in:
Moxie Marlinspike 2014-11-08 11:35:58 -08:00
parent 99f42e2ee1
commit cafe03a70a
38 changed files with 1590 additions and 1817 deletions

View File

@ -36,13 +36,16 @@ public class JobManager implements RequirementListener {
private final Executor eventExecutor = Executors.newSingleThreadExecutor(); private final Executor eventExecutor = Executors.newSingleThreadExecutor();
private final AtomicBoolean hasLoadedEncrypted = new AtomicBoolean(false); private final AtomicBoolean hasLoadedEncrypted = new AtomicBoolean(false);
private final PersistentStorage persistentStorage; private final PersistentStorage persistentStorage;
private final List<RequirementProvider> requirementProviders;
public JobManager(Context context, String name, public JobManager(Context context, String name,
List<RequirementProvider> requirementProviders, List<RequirementProvider> requirementProviders,
JobSerializer jobSerializer, int consumers) JobSerializer jobSerializer, int consumers)
{ {
this.persistentStorage = new PersistentStorage(context, name, jobSerializer); this.persistentStorage = new PersistentStorage(context, name, jobSerializer);
this.requirementProviders = requirementProviders;
eventExecutor.execute(new LoadTask(null)); eventExecutor.execute(new LoadTask(null));
if (requirementProviders != null && !requirementProviders.isEmpty()) { if (requirementProviders != null && !requirementProviders.isEmpty()) {
@ -56,6 +59,16 @@ public class JobManager implements RequirementListener {
} }
} }
public RequirementProvider getRequirementProvider(String name) {
for (RequirementProvider provider : requirementProviders) {
if (provider.getName().equals(name)) {
return provider;
}
}
return null;
}
public void setEncryptionKeys(EncryptionKeys keys) { public void setEncryptionKeys(EncryptionKeys keys) {
if (hasLoadedEncrypted.compareAndSet(false, true)) { if (hasLoadedEncrypted.compareAndSet(false, true)) {
eventExecutor.execute(new LoadTask(keys)); eventExecutor.execute(new LoadTask(keys));

View File

@ -46,6 +46,11 @@ public class NetworkRequirementProvider implements RequirementProvider {
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
} }
@Override
public String getName() {
return "network";
}
@Override @Override
public void setListener(RequirementListener listener) { public void setListener(RequirementListener listener) {
this.listener = listener; this.listener = listener;

View File

@ -17,5 +17,6 @@
package org.whispersystems.jobqueue.requirements; package org.whispersystems.jobqueue.requirements;
public interface RequirementProvider { public interface RequirementProvider {
public String getName();
public void setListener(RequirementListener listener); public void setListener(RequirementListener listener);
} }

View File

@ -64,7 +64,10 @@ import org.thoughtcrime.securesms.components.EmojiToggle;
import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator; import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent; import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DraftDatabase; import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft; import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
@ -104,18 +107,13 @@ import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.libaxolotl.state.SessionStore;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.RecipientDevice;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.whispersystems.textsecure.util.Util; import org.whispersystems.textsecure.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import ws.com.google.android.mms.MmsException;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import static org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener; import static org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
@ -420,14 +418,22 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
if (isSingleConversation()) { if (isSingleConversation()) {
ConversationActivity self = ConversationActivity.this; final Context context = getApplicationContext();
OutgoingEndSessionMessage endSessionMessage = OutgoingEndSessionMessage endSessionMessage =
new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE")); new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE"));
long allocatedThreadId = MessageSender.send(self, masterSecret, endSessionMessage, threadId, false); new AsyncTask<OutgoingEndSessionMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingEndSessionMessage... messages) {
return MessageSender.send(context, masterSecret, messages[0], threadId, false);
}
sendComplete(recipients, allocatedThreadId, allocatedThreadId != self.threadId); @Override
protected void onPostExecute(Long result) {
sendComplete(result);
}
}.execute(endSessionMessage);
} }
} }
}); });
@ -468,9 +474,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
Toast.makeText(ConversationActivity.this, "Error leaving group....", Toast.LENGTH_LONG).show(); Toast.makeText(ConversationActivity.this, "Error leaving group....", Toast.LENGTH_LONG).show();
} catch (MmsException e) {
Log.w(TAG, e);
Toast.makeText(ConversationActivity.this, "Error leaving group...", Toast.LENGTH_LONG).show();
} }
} }
}); });
@ -842,12 +845,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void addAttachmentImage(Uri imageUri) { private void addAttachmentImage(Uri imageUri) {
try { try {
attachmentManager.setImage(imageUri); attachmentManager.setImage(imageUri);
} catch (IOException e) { } catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
attachmentManager.clear();
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
Toast.LENGTH_LONG).show();
} catch (BitmapDecodingException e) {
Log.w(TAG, e); Log.w(TAG, e);
attachmentManager.clear(); attachmentManager.clear();
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment, Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
@ -917,7 +915,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
private List<Draft> getDraftsForCurrentState() { private List<Draft> getDraftsForCurrentState() {
List<Draft> drafts = new LinkedList<Draft>(); List<Draft> drafts = new LinkedList<>();
if (!Util.isEmpty(composeText)) { if (!Util.isEmpty(composeText)) {
drafts.add(new Draft(Draft.TEXT, composeText.getText().toString())); drafts.add(new Draft(Draft.TEXT, composeText.getText().toString()));
@ -1032,43 +1030,38 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(threadId); }.execute(threadId);
} }
private void sendComplete(Recipients recipients, long threadId, boolean refreshFragment) { private void sendComplete(long threadId) {
attachmentManager.clear(); boolean refreshFragment = (threadId != this.threadId);
composeText.setText(""); this.threadId = threadId;
this.recipients = recipients;
this.threadId = threadId;
ConversationFragment fragment = (ConversationFragment) getSupportFragmentManager() ConversationFragment fragment = (ConversationFragment) getSupportFragmentManager()
.findFragmentById(R.id.fragment_content); .findFragmentById(R.id.fragment_content);
if (refreshFragment) { if (refreshFragment) {
fragment.reload(recipients, threadId); fragment.reload(recipients, threadId);
initializeTitleBar(); initializeTitleBar();
initializeSecurity(); initializeSecurity();
} }
fragment.scrollToBottom(); fragment.scrollToBottom();
} }
private void sendMessage(boolean forcePlaintext, boolean forceSms) { private void sendMessage(boolean forcePlaintext, boolean forceSms) {
try { try {
Recipients recipients = getRecipients(); final Recipients recipients = getRecipients();
if (recipients == null) if (recipients == null)
throw new RecipientFormattingException("Badly formatted"); throw new RecipientFormattingException("Badly formatted");
long allocatedThreadId;
if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) { if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) {
handleManualMmsRequired(); handleManualMmsRequired();
return;
} else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) { } else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) {
allocatedThreadId = sendMediaMessage(forcePlaintext, forceSms); sendMediaMessage(forcePlaintext, forceSms);
} else { } else {
allocatedThreadId = sendTextMessage(forcePlaintext, forceSms); sendTextMessage(forcePlaintext, forceSms);
} }
sendComplete(recipients, allocatedThreadId, allocatedThreadId != this.threadId);
} catch (RecipientFormattingException ex) { } catch (RecipientFormattingException ex) {
Toast.makeText(ConversationActivity.this, Toast.makeText(ConversationActivity.this,
R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation, R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation,
@ -1078,17 +1071,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_message_is_empty_exclamation, Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_message_is_empty_exclamation,
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
Log.w(TAG, ex); Log.w(TAG, ex);
} catch (MmsException e) {
Log.w(TAG, e);
} }
} }
private long sendMediaMessage(boolean forcePlaintext, boolean forceSms) private void sendMediaMessage(boolean forcePlaintext, final boolean forceSms)
throws InvalidMessageException, MmsException throws InvalidMessageException
{ {
final Context context = getApplicationContext();
SlideDeck slideDeck; SlideDeck slideDeck;
if (attachmentManager.isAttachmentPresent()) slideDeck = attachmentManager.getSlideDeck(); if (attachmentManager.isAttachmentPresent()) slideDeck = new SlideDeck(attachmentManager.getSlideDeck());
else slideDeck = new SlideDeck(); else slideDeck = new SlideDeck();
OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(this, recipients, slideDeck, OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(this, recipients, slideDeck,
@ -1098,12 +1090,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessage); outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessage);
} }
return MessageSender.send(this, masterSecret, outgoingMessage, threadId, forceSms); attachmentManager.clear();
composeText.setText("");
new AsyncTask<OutgoingMediaMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingMediaMessage... messages) {
return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms);
}
@Override
protected void onPostExecute(Long result) {
sendComplete(result);
}
}.execute(outgoingMessage);
} }
private long sendTextMessage(boolean forcePlaintext, boolean forceSms) private void sendTextMessage(boolean forcePlaintext, final boolean forceSms)
throws InvalidMessageException throws InvalidMessageException
{ {
final Context context = getApplicationContext();
OutgoingTextMessage message; OutgoingTextMessage message;
if (isEncryptedConversation && !forcePlaintext) { if (isEncryptedConversation && !forcePlaintext) {
@ -1112,9 +1118,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
message = new OutgoingTextMessage(recipients, getMessage()); message = new OutgoingTextMessage(recipients, getMessage());
} }
Log.w(TAG, "Sending message..."); this.composeText.setText("");
return MessageSender.send(ConversationActivity.this, masterSecret, message, threadId, forceSms); new AsyncTask<OutgoingTextMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingTextMessage... messages) {
return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms);
}
@Override
protected void onPostExecute(Long result) {
sendComplete(result);
}
}.execute(message);
} }

View File

@ -6,6 +6,7 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.support.v4.app.ListFragment; import android.support.v4.app.ListFragment;
@ -241,10 +242,15 @@ public class ConversationFragment extends ListFragment
startActivity(composeIntent); startActivity(composeIntent);
} }
private void handleResendMessage(MessageRecord message) { private void handleResendMessage(final MessageRecord message) {
long messageId = message.getId(); final Context context = getActivity().getApplicationContext();
final Activity activity = getActivity(); new AsyncTask<MessageRecord, Void, Void>() {
MessageSender.resend(activity, messageId, message.isMms()); @Override
protected Void doInBackground(MessageRecord... messageRecords) {
MessageSender.resend(context, masterSecret, messageRecords[0]);
return null;
}
}.execute(message);
} }
private void handleSaveAttachment(final MediaMmsMessageRecord message) { private void handleSaveAttachment(final MediaMmsMessageRecord message) {

View File

@ -46,10 +46,11 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.jobs.MmsDownloadJob; import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.Emoji; import org.thoughtcrime.securesms.util.Emoji;
@ -561,6 +562,10 @@ public class ConversationItem extends LinearLayout {
} }
database.markAsOutbox(messageRecord.getId()); database.markAsOutbox(messageRecord.getId());
database.markAsForcedSms(messageRecord.getId()); database.markAsForcedSms(messageRecord.getId());
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MmsSendJob(context, messageRecord.getId()));
} else { } else {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context); SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
if (messageRecord.isPendingInsecureSmsFallback()) { if (messageRecord.isPendingInsecureSmsFallback()) {
@ -568,15 +573,12 @@ public class ConversationItem extends LinearLayout {
} }
database.markAsOutbox(messageRecord.getId()); database.markAsOutbox(messageRecord.getId());
database.markAsForcedSms(messageRecord.getId()); database.markAsForcedSms(messageRecord.getId());
ApplicationContext.getInstance(context)
.getJobManager()
.add(new SmsSendJob(context, messageRecord.getId(),
messageRecord.getIndividualRecipient().getNumber()));
} }
Intent intent = new Intent(context, SendReceiveService.class);
intent.setAction(messageRecord.isMms() ?
SendReceiveService.SEND_MMS_ACTION :
SendReceiveService.SEND_SMS_ACTION);
intent.putExtra(SendReceiveService.MASTER_SECRET_EXTRA, masterSecret);
context.startService(intent);
} }
}); });

View File

@ -38,18 +38,17 @@ import android.widget.AdapterView;
import android.widget.ListView; import android.widget.ListView;
import android.widget.SimpleAdapter; import android.widget.SimpleAdapter;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class ConversationListActivity extends PassphraseRequiredActionBarActivity public class ConversationListActivity extends PassphraseRequiredActionBarActivity
@ -72,7 +71,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
getSupportActionBar().setTitle(R.string.app_name); getSupportActionBar().setTitle(R.string.app_name);
initializeSenderReceiverService();
initializeResources(); initializeResources();
initializeContactUpdatesReceiver(); initializeContactUpdatesReceiver();
@ -247,15 +245,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
true, observer); true, observer);
} }
private void initializeSenderReceiverService() {
Intent smsSenderIntent = new Intent(SendReceiveService.SEND_SMS_ACTION, null, this,
SendReceiveService.class);
Intent mmsSenderIntent = new Intent(SendReceiveService.SEND_MMS_ACTION, null, this,
SendReceiveService.class);
startService(smsSenderIntent);
startService(mmsSenderIntent);
}
private void initializeResources() { private void initializeResources() {
this.masterSecret = getIntent().getParcelableExtra("master_secret"); this.masterSecret = getIntent().getParcelableExtra("master_secret");

View File

@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.whispersystems.textsecure.directory.Directory; import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException; import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.InvalidNumberException;
import org.whispersystems.textsecure.util.ListenableFutureTask;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -76,6 +77,7 @@ import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.MmsException;
@ -443,7 +445,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity {
private Pair<Long, Recipients> handlePushOperation(byte[] groupId, String groupName, byte[] avatar, private Pair<Long, Recipients> handlePushOperation(byte[] groupId, String groupName, byte[] avatar,
Set<String> e164numbers) Set<String> e164numbers)
throws MmsException, InvalidNumberException throws InvalidNumberException
{ {
try { try {
@ -460,12 +462,9 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity {
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatar); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatar);
long threadId = MessageSender.send(this, masterSecret, outgoingMessage, -1, false); long threadId = MessageSender.send(this, masterSecret, outgoingMessage, -1, false);
return new Pair<Long, Recipients>(threadId, groupRecipient); return new Pair<>(threadId, groupRecipient);
} catch (RecipientFormattingException e) { } catch (RecipientFormattingException e) {
throw new AssertionError(e); throw new AssertionError(e);
} catch (MmsException e) {
Log.w(TAG, e);
throw new MmsException(e);
} }
} }

View File

@ -25,6 +25,7 @@ import android.util.Pair;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher; import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret; import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
import org.thoughtcrime.securesms.database.model.DisplayRecord; import org.thoughtcrime.securesms.database.model.DisplayRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.LRUCache; import org.thoughtcrime.securesms.util.LRUCache;
@ -58,8 +59,8 @@ public class EncryptingSmsDatabase extends SmsDatabase {
return ciphertext; return ciphertext;
} }
public List<Long> insertMessageOutbox(MasterSecret masterSecret, long threadId, public long insertMessageOutbox(MasterSecret masterSecret, long threadId,
OutgoingTextMessage message, boolean forceSms) OutgoingTextMessage message, boolean forceSms)
{ {
long type = Types.BASE_OUTBOX_TYPE; long type = Types.BASE_OUTBOX_TYPE;
message = message.withBody(getEncryptedBody(masterSecret, message.getMessageBody())); message = message.withBody(getEncryptedBody(masterSecret, message.getMessageBody()));
@ -120,9 +121,15 @@ public class EncryptingSmsDatabase extends SmsDatabase {
return new DecryptingReader(masterSecret, cursor); return new DecryptingReader(masterSecret, cursor);
} }
public Reader getMessage(MasterSecret masterSecret, long messageId) { public SmsMessageRecord getMessage(MasterSecret masterSecret, long messageId) throws NoSuchMessageException {
Cursor cursor = super.getMessage(messageId); Cursor cursor = super.getMessage(messageId);
return new DecryptingReader(masterSecret, cursor); DecryptingReader reader = new DecryptingReader(masterSecret, cursor);
SmsMessageRecord record = reader.getNext();
reader.close();
if (record == null) throw new NoSuchMessageException("No message for ID: " + messageId);
else return record;
} }
public Reader getDecryptInProgressMessages(MasterSecret masterSecret) { public Reader getDecryptInProgressMessages(MasterSecret masterSecret) {

View File

@ -456,39 +456,22 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
} }
} }
public SendReq[] getOutgoingMessages(MasterSecret masterSecret, long messageId) public SendReq getOutgoingMessage(MasterSecret masterSecret, long messageId)
throws MmsException throws MmsException, NoSuchMessageException
{ {
MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context); MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context);
PartDatabase partDatabase = getPartDatabase(masterSecret); PartDatabase partDatabase = getPartDatabase(masterSecret);
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
MasterCipher masterCipher = masterSecret == null ? null : new MasterCipher(masterSecret); MasterCipher masterCipher = new MasterCipher(masterSecret);
Cursor cursor = null; Cursor cursor = null;
String selection = ID_WHERE;
String selection; String[] selectionArgs = new String[]{String.valueOf(messageId)};
String[] selectionArgs;
if (messageId > 0) {
selection = ID_WHERE;
selectionArgs = new String[]{messageId + ""};
} else {
selection = MESSAGE_BOX + " & " + Types.BASE_TYPE_MASK + " = " + Types.BASE_OUTBOX_TYPE;
selectionArgs = null;
}
try { try {
cursor = database.query(TABLE_NAME, MMS_PROJECTION, selection, selectionArgs, null, null, null); cursor = database.query(TABLE_NAME, MMS_PROJECTION, selection, selectionArgs, null, null, null);
if (cursor == null || cursor.getCount() == 0) if (cursor != null && cursor.moveToNext()) {
return new SendReq[0];
SendReq[] requests = new SendReq[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
messageId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)); long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)); long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
@ -507,10 +490,10 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
Log.w("MmsDatabase", e); Log.w("MmsDatabase", e);
} }
requests[i++] = new SendReq(headers, body, messageId, outboxType, timestamp); return new SendReq(headers, body, messageId, outboxType, timestamp);
} }
return requests; throw new NoSuchMessageException("No record found for id: " + messageId);
} finally { } finally {
if (cursor != null) if (cursor != null)
cursor.close(); cursor.close();
@ -527,17 +510,20 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
} }
public long copyMessageInbox(MasterSecret masterSecret, long messageId) throws MmsException { public long copyMessageInbox(MasterSecret masterSecret, long messageId) throws MmsException {
SendReq[] request = getOutgoingMessages(masterSecret, messageId); try {
SendReq request = getOutgoingMessage(masterSecret, messageId);
ContentValues contentValues = getContentValuesFromHeader(request.getPduHeaders());
ContentValues contentValues = getContentValuesFromHeader(request[0].getPduHeaders()); contentValues.put(MESSAGE_BOX, Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_SYMMETRIC_BIT);
contentValues.put(THREAD_ID, getThreadIdForMessage(messageId));
contentValues.put(READ, 1);
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
contentValues.put(MESSAGE_BOX, Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_SYMMETRIC_BIT); return insertMediaMessage(masterSecret, request.getPduHeaders(),
contentValues.put(THREAD_ID, getThreadIdForMessage(messageId)); request.getBody(), contentValues);
contentValues.put(READ, 1); } catch (NoSuchMessageException e) {
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT)); throw new MmsException(e);
}
return insertMediaMessage(masterSecret, request[0].getPduHeaders(),
request[0].getBody(), contentValues);
} }
private Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, IncomingMediaMessage retrieved, private Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, IncomingMediaMessage retrieved,

View File

@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.database;
public class NoSuchMessageException extends Exception {
public NoSuchMessageException(String s) {super(s);}
public NoSuchMessageException(Exception e) {super(e);}
}

View File

@ -108,10 +108,4 @@ public class PushDatabase extends Database {
this.cursor.close(); this.cursor.close();
} }
} }
public static class NoSuchMessageException extends Exception {
public NoSuchMessageException(String s) {super(s);}
public NoSuchMessageException(Exception e) {super(e);}
}
} }

View File

@ -428,36 +428,33 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE); return insertMessageInbox(message, Types.BASE_INBOX_TYPE);
} }
protected List<Long> insertMessageOutbox(long threadId, OutgoingTextMessage message, protected long insertMessageOutbox(long threadId, OutgoingTextMessage message,
long type, boolean forceSms) long type, boolean forceSms)
{ {
if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT; if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT;
else if (message.isSecureMessage()) type |= Types.SECURE_MESSAGE_BIT; else if (message.isSecureMessage()) type |= Types.SECURE_MESSAGE_BIT;
else if (message.isEndSession()) type |= Types.END_SESSION_BIT; else if (message.isEndSession()) type |= Types.END_SESSION_BIT;
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
long date = System.currentTimeMillis(); long date = System.currentTimeMillis();
List<Long> messageIds = new LinkedList<Long>();
for (Recipient recipient : message.getRecipients().getRecipientsList()) { ContentValues contentValues = new ContentValues(6);
ContentValues contentValues = new ContentValues(6); contentValues.put(ADDRESS, PhoneNumberUtils.formatNumber(message.getRecipients().getPrimaryRecipient().getNumber()));
contentValues.put(ADDRESS, PhoneNumberUtils.formatNumber(recipient.getNumber())); contentValues.put(THREAD_ID, threadId);
contentValues.put(THREAD_ID, threadId); contentValues.put(BODY, message.getMessageBody());
contentValues.put(BODY, message.getMessageBody()); contentValues.put(DATE_RECEIVED, date);
contentValues.put(DATE_RECEIVED, date); contentValues.put(DATE_SENT, date);
contentValues.put(DATE_SENT, date); contentValues.put(READ, 1);
contentValues.put(READ, 1); contentValues.put(TYPE, type);
contentValues.put(TYPE, type);
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
messageIds.add(db.insert(TABLE_NAME, ADDRESS, contentValues)); long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
DatabaseFactory.getThreadDatabase(context).update(threadId); DatabaseFactory.getThreadDatabase(context).update(threadId);
notifyConversationListeners(threadId); notifyConversationListeners(threadId);
Trimmer.trimThread(context, threadId); Trimmer.trimThread(context, threadId);
}
return messageIds; return messageId;
} }
Cursor getMessages(int skip, int limit) { Cursor getMessages(int skip, int limit) {

View File

@ -57,7 +57,7 @@ public class MmsDownloadJob extends MasterSecretJob {
.withPersistence() .withPersistence()
.withRequirement(new MasterSecretRequirement(context)) .withRequirement(new MasterSecretRequirement(context))
.withRequirement(new NetworkRequirement(context)) .withRequirement(new NetworkRequirement(context))
.withGroupId("mms-download") .withGroupId("mms-operation")
.create()); .create());
this.messageId = messageId; this.messageId = messageId;
@ -170,7 +170,13 @@ public class MmsDownloadJob extends MasterSecretJob {
@Override @Override
public void onCanceled() { public void onCanceled() {
// TODO MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE);
if (automatic) {
database.markIncomingNotificationReceived(threadId);
MessageNotifier.updateNotification(context, null, threadId);
}
} }
@Override @Override

View File

@ -1,21 +1,4 @@
/** package org.thoughtcrime.securesms.jobs;
* 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
* 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.transport;
import android.content.Context; import android.content.Context;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
@ -24,51 +7,108 @@ import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MmsCipher; import org.thoughtcrime.securesms.crypto.MmsCipher;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore; import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.MmsRadio; import org.thoughtcrime.securesms.mms.MmsRadio;
import org.thoughtcrime.securesms.mms.MmsRadioException; import org.thoughtcrime.securesms.mms.MmsRadioException;
import org.thoughtcrime.securesms.mms.MmsSendResult; import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection; import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.NumberUtil; import org.thoughtcrime.securesms.util.NumberUtil;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libaxolotl.NoSessionException; import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Hex;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.EncodedStringValue; import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduComposer; import ws.com.google.android.mms.pdu.PduComposer;
import ws.com.google.android.mms.pdu.PduHeaders; import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.SendConf; import ws.com.google.android.mms.pdu.SendConf;
import ws.com.google.android.mms.pdu.SendReq; import ws.com.google.android.mms.pdu.SendReq;
public class MmsTransport { public class MmsSendJob extends MasterSecretJob {
private static final String TAG = MmsTransport.class.getSimpleName(); private static final String TAG = MmsSendJob.class.getSimpleName();
private final Context context; private final long messageId;
private final MasterSecret masterSecret;
private final MmsRadio radio;
public MmsTransport(Context context, MasterSecret masterSecret) { public MmsSendJob(Context context, long messageId) {
this.context = context; super(context, JobParameters.newBuilder()
this.masterSecret = masterSecret; .withGroupId("mms-operation")
this.radio = MmsRadio.getInstance(context); .withRequirement(new NetworkRequirement(context))
.withRequirement(new MasterSecretRequirement(context))
.withPersistence()
.create());
this.messageId = messageId;
} }
public MmsSendResult deliver(SendReq message) throws UndeliverableMessageException, @Override
InsecureFallbackApprovalException public void onAdded() {
}
@Override
public void onRun() throws RequirementNotMetException, MmsException, NoSuchMessageException {
MasterSecret masterSecret = getMasterSecret();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
try {
MmsSendResult result = deliver(masterSecret, message);
if (result.isUpgradedSecure()) {
database.markAsSecure(messageId);
}
database.markAsSent(messageId, result.getMessageId(), result.getResponseStatus());
} catch (UndeliverableMessageException e) {
Log.w(TAG, e);
database.markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
} catch (InsecureFallbackApprovalException e) {
Log.w(TAG, e);
database.markAsPendingInsecureSmsFallback(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof RequirementNotMetException) return true;
return false;
}
@Override
public void onCanceled() {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
public MmsSendResult deliver(MasterSecret masterSecret, SendReq message)
throws UndeliverableMessageException, InsecureFallbackApprovalException
{ {
validateDestinations(message); validateDestinations(message);
MmsRadio radio = MmsRadio.getInstance(context);
try { try {
if (isCdmaDevice()) { if (isCdmaDevice()) {
Log.w(TAG, "Sending MMS directly without radio change..."); Log.w(TAG, "Sending MMS directly without radio change...");
try { try {
return sendMms(message, false, false); return sendMms(masterSecret, radio, message, false, false);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
} }
@ -78,7 +118,7 @@ public class MmsTransport {
radio.connect(); radio.connect();
try { try {
MmsSendResult result = sendMms(message, true, true); MmsSendResult result = sendMms(masterSecret, radio, message, true, true);
radio.disconnect(); radio.disconnect();
return result; return result;
} catch (IOException e) { } catch (IOException e) {
@ -88,7 +128,7 @@ public class MmsTransport {
Log.w(TAG, "Sending MMS with radio change and without proxy..."); Log.w(TAG, "Sending MMS with radio change and without proxy...");
try { try {
MmsSendResult result = sendMms(message, true, false); MmsSendResult result = sendMms(masterSecret, radio, message, true, false);
radio.disconnect(); radio.disconnect();
return result; return result;
} catch (IOException ioe) { } catch (IOException ioe) {
@ -103,14 +143,15 @@ public class MmsTransport {
} }
} }
private MmsSendResult sendMms(SendReq message, boolean usingMmsRadio, boolean useProxy) private MmsSendResult sendMms(MasterSecret masterSecret, MmsRadio radio, SendReq message,
boolean usingMmsRadio, boolean useProxy)
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
{ {
String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number(); String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
boolean upgradedSecure = false; boolean upgradedSecure = false;
if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) { if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) {
message = getEncryptedMessage(message); message = getEncryptedMessage(masterSecret, message);
upgradedSecure = true; upgradedSecure = true;
} }
@ -140,7 +181,9 @@ public class MmsTransport {
} }
} }
private SendReq getEncryptedMessage(SendReq pdu) throws InsecureFallbackApprovalException { private SendReq getEncryptedMessage(MasterSecret masterSecret, SendReq pdu)
throws InsecureFallbackApprovalException
{
try { try {
MmsCipher cipher = new MmsCipher(new TextSecureAxolotlStore(context, masterSecret)); MmsCipher cipher = new MmsCipher(new TextSecureAxolotlStore(context, masterSecret));
return cipher.encrypt(context, pdu); return cipher.encrypt(context, pdu);
@ -166,7 +209,7 @@ public class MmsTransport {
private void validateDestination(EncodedStringValue destination) throws UndeliverableMessageException { private void validateDestination(EncodedStringValue destination) throws UndeliverableMessageException {
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) { if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
throw new UndeliverableMessageException("Invalid destination: " + throw new UndeliverableMessageException("Invalid destination: " +
(destination == null ? null : destination.getString())); (destination == null ? null : destination.getString()));
} }
} }
@ -194,4 +237,13 @@ public class MmsTransport {
} }
} }
private void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
}
} }

View File

@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor; import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
@ -68,18 +69,13 @@ public class PushDecryptJob extends MasterSecretJob {
} }
@Override @Override
public void onRun() throws RequirementNotMetException { public void onRun() throws RequirementNotMetException, NoSuchMessageException {
try { MasterSecret masterSecret = getMasterSecret();
MasterSecret masterSecret = getMasterSecret(); PushDatabase database = DatabaseFactory.getPushDatabase(context);
PushDatabase database = DatabaseFactory.getPushDatabase(context); TextSecureEnvelope envelope = database.get(messageId);
TextSecureEnvelope envelope = database.get(messageId);
handleMessage(masterSecret, envelope); handleMessage(masterSecret, envelope);
database.delete(messageId); database.delete(messageId);
} catch (PushDatabase.NoSuchMessageException e) {
Log.w(TAG, e);
}
} }
@Override @Override

View File

@ -0,0 +1,148 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.push.TextSecureMessageSenderFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.whispersystems.textsecure.push.PushAddress;
import org.whispersystems.textsecure.push.PushMessageProtos;
import org.whispersystems.textsecure.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.SendReq;
public class PushGroupSendJob extends PushSendJob {
private static final String TAG = PushGroupSendJob.class.getSimpleName();
private final long messageId;
public PushGroupSendJob(Context context, long messageId, String destination) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withGroupId(destination)
.withRequirement(new MasterSecretRequirement(context))
.withRequirement(new NetworkRequirement(context))
.withRetryCount(5)
.create());
this.messageId = messageId;
}
@Override
public void onAdded() {
}
@Override
public void onRun() throws RequirementNotMetException, MmsException, IOException, NoSuchMessageException {
MasterSecret masterSecret = getMasterSecret();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
try {
deliver(masterSecret, message);
database.markAsPush(messageId);
database.markAsSecure(messageId);
database.markAsSent(messageId, "push".getBytes(), 0);
} catch (InvalidNumberException | RecipientFormattingException e) {
Log.w(TAG, e);
database.markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
} catch (EncapsulatedExceptions e) {
Log.w(TAG, e);
if (!e.getUnregisteredUserExceptions().isEmpty()) {
database.markAsSentFailed(messageId);
}
for (UntrustedIdentityException uie : e.getUntrustedIdentityExceptions()) {
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
database.markAsSentFailed(messageId);
}
notifyMediaMessageDeliveryFailed(context, messageId);
}
}
@Override
public void onCanceled() {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof RequirementNotMetException) return true;
if (throwable instanceof IOException) return true;
return false;
}
private void deliver(MasterSecret masterSecret, SendReq message)
throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions
{
TextSecureMessageSender messageSender = TextSecureMessageSenderFactory.create(context, masterSecret);
byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString());
Recipients recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
List<PushAddress> addresses = getPushAddresses(recipients);
List<TextSecureAttachment> attachments = getAttachments(message);
if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) ||
MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()))
{
String content = PartParser.getMessageText(message.getBody());
if (content != null && !content.trim().isEmpty()) {
PushMessageProtos.PushMessageContent.GroupContext groupContext = PushMessageProtos.PushMessageContent.GroupContext.parseFrom(Base64.decode(content));
TextSecureAttachment avatar = attachments.isEmpty() ? null : attachments.get(0);
TextSecureGroup.Type type = MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()) ? TextSecureGroup.Type.QUIT : TextSecureGroup.Type.UPDATE;
TextSecureGroup group = new TextSecureGroup(type, groupId, groupContext.getName(), groupContext.getMembersList(), avatar);
TextSecureMessage groupMessage = new TextSecureMessage(message.getSentTimestamp(), group, null, null);
messageSender.sendMessage(addresses, groupMessage);
}
} else {
String body = PartParser.getMessageText(message.getBody());
TextSecureGroup group = new TextSecureGroup(groupId);
TextSecureMessage groupMessage = new TextSecureMessage(message.getSentTimestamp(), group, attachments, body);
messageSender.sendMessage(addresses, groupMessage);
}
}
private List<PushAddress> getPushAddresses(Recipients recipients) throws InvalidNumberException {
List<PushAddress> addresses = new LinkedList<>();
for (Recipient recipient : recipients.getRecipientsList()) {
addresses.add(getPushAddress(recipient));
}
return addresses;
}
}

View File

@ -0,0 +1,150 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.push.TextSecureMessageSenderFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.whispersystems.textsecure.push.PushAddress;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException;
import java.util.List;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.SendReq;
public class PushMediaSendJob extends PushSendJob {
private static final String TAG = PushMediaSendJob.class.getSimpleName();
private final long messageId;
public PushMediaSendJob(Context context, long messageId, String destination) {
super(context, constructParameters(context, destination));
this.messageId = messageId;
}
@Override
public void onAdded() {
}
@Override
public void onRun()
throws RequirementNotMetException, RetryLaterException, MmsException, NoSuchMessageException
{
MasterSecret masterSecret = getMasterSecret();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
try {
deliver(masterSecret, message);
database.markAsPush(messageId);
database.markAsSecure(messageId);
database.markAsSent(messageId, "push".getBytes(), 0);
} catch (InsecureFallbackApprovalException ifae) {
Log.w(TAG, ifae);
database.markAsPendingInsecureSmsFallback(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
} catch (SecureFallbackApprovalException sfae) {
Log.w(TAG, sfae);
database.markAsPendingSecureSmsFallback(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
} catch (UntrustedIdentityException uie) {
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
database.markAsSentFailed(messageId);
}
}
@Override
public void onCanceled() {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof RetryLaterException) return true;
if (throwable instanceof RequirementNotMetException) return true;
return false;
}
private void deliver(MasterSecret masterSecret, SendReq message)
throws RetryLaterException, SecureFallbackApprovalException,
InsecureFallbackApprovalException, UntrustedIdentityException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
TextSecureMessageSender messageSender = TextSecureMessageSenderFactory.create(context, masterSecret);
String destination = message.getTo()[0].getString();
boolean isSmsFallbackSupported = isSmsFallbackSupported(context, destination);
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
PushAddress address = getPushAddress(recipients.getPrimaryRecipient());
List<TextSecureAttachment> attachments = getAttachments(message);
String body = PartParser.getMessageText(message.getBody());
TextSecureMessage mediaMessage = new TextSecureMessage(message.getSentTimestamp(), attachments, body);
messageSender.sendMessage(address, mediaMessage);
} catch (InvalidNumberException | UnregisteredUserException e) {
Log.w(TAG, e);
if (isSmsFallbackSupported) fallbackOrAskApproval(masterSecret, message, destination);
else database.markAsSentFailed(messageId);
} catch (IOException | RecipientFormattingException e) {
Log.w(TAG, e);
if (isSmsFallbackSupported) fallbackOrAskApproval(masterSecret, message, destination);
else throw new RetryLaterException(e);
}
}
private void fallbackOrAskApproval(MasterSecret masterSecret, SendReq mediaMessage, String destination)
throws SecureFallbackApprovalException, InsecureFallbackApprovalException
{
try {
Recipient recipient = RecipientFactory.getRecipientsFromString(context, destination, false).getPrimaryRecipient();
boolean isSmsFallbackApprovalRequired = isSmsFallbackApprovalRequired(destination);
AxolotlStore axolotlStore = new TextSecureAxolotlStore(context, masterSecret);
if (!isSmsFallbackApprovalRequired) {
Log.w(TAG, "Falling back to MMS");
DatabaseFactory.getMmsDatabase(context).markAsForcedSms(mediaMessage.getDatabaseMessageId());
ApplicationContext.getInstance(context).getJobManager().add(new MmsSendJob(context, messageId));
} else if (!axolotlStore.containsSession(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID)) {
Log.w(TAG, "Marking message as pending insecure SMS fallback");
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
} else {
Log.w(TAG, "Marking message as pending secure SMS fallback");
throw new SecureFallbackApprovalException("Pending user approval for fallback secure to SMS");
}
} catch (RecipientFormattingException rfe) {
Log.w(TAG, rfe);
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
}
}
}

View File

@ -0,0 +1,98 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.push.PushAddress;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.ByteArrayInputStream;
import java.util.LinkedList;
import java.util.List;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.SendReq;
public abstract class PushSendJob extends MasterSecretJob {
private static final String TAG = PushSendJob.class.getSimpleName();
protected PushSendJob(Context context, JobParameters parameters) {
super(context, parameters);
}
protected static JobParameters constructParameters(Context context, String destination) {
JobParameters.Builder builder = JobParameters.newBuilder();
builder.withPersistence();
builder.withGroupId(destination);
builder.withRequirement(new MasterSecretRequirement(context));
if (!isSmsFallbackSupported(context, destination)) {
builder.withRequirement(new NetworkRequirement(context));
builder.withRetryCount(5);
}
return builder.create();
}
protected static boolean isSmsFallbackSupported(Context context, String destination) {
if (GroupUtil.isEncodedGroup(destination)) {
return false;
}
if (!TextSecurePreferences.isFallbackSmsAllowed(context)) {
return false;
}
Directory directory = Directory.getInstance(context);
return directory.isSmsFallbackSupported(destination);
}
protected PushAddress getPushAddress(Recipient recipient) throws InvalidNumberException {
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
String relay = Directory.getInstance(context).getRelay(e164number);
return new PushAddress(recipient.getRecipientId(), e164number, 1, relay);
}
protected boolean isSmsFallbackApprovalRequired(String destination) {
return (isSmsFallbackSupported(context, destination) && TextSecurePreferences.isFallbackSmsAskRequired(context));
}
protected List<TextSecureAttachment> getAttachments(SendReq message) {
List<TextSecureAttachment> attachments = new LinkedList<>();
for (int i=0;i<message.getBody().getPartsNum();i++) {
String contentType = Util.toIsoString(message.getBody().getPart(i).getContentType());
if (ContentType.isImageType(contentType) ||
ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType))
{
byte[] data = message.getBody().getPart(i).getData();
Log.w(TAG, "Adding attachment...");
attachments.add(new TextSecureAttachmentStream(new ByteArrayInputStream(data), contentType, data.length));
}
}
return attachments;
}
protected void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
}
}

View File

@ -0,0 +1,148 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.push.TextSecureMessageSenderFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.whispersystems.textsecure.push.PushAddress;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException;
public class PushTextSendJob extends PushSendJob {
private static final String TAG = PushTextSendJob.class.getSimpleName();
private final long messageId;
public PushTextSendJob(Context context, long messageId, String destination) {
super(context, constructParameters(context, destination));
this.messageId = messageId;
}
@Override
public void onAdded() {
}
@Override
public void onRun() throws RequirementNotMetException, NoSuchMessageException, RetryLaterException
{
MasterSecret masterSecret = getMasterSecret();
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
String destination = record.getIndividualRecipient().getNumber();
try {
Log.w(TAG, "Sending message: " + messageId);
deliver(masterSecret, record, destination);
database.markAsPush(messageId);
database.markAsSecure(messageId);
database.markAsSent(messageId);
} catch (InsecureFallbackApprovalException e) {
Log.w(TAG, e);
database.markAsPendingInsecureSmsFallback(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (SecureFallbackApprovalException e) {
Log.w(TAG, e);
database.markAsPendingSecureSmsFallback(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(e.getE164Number(), e.getIdentityKey());
database.insertMessageInbox(masterSecret, identityUpdateMessage);
database.markAsSentFailed(record.getId());
}
}
public void deliver(MasterSecret masterSecret, SmsMessageRecord message, String destination)
throws UntrustedIdentityException, SecureFallbackApprovalException,
InsecureFallbackApprovalException, RetryLaterException
{
boolean isSmsFallbackSupported = isSmsFallbackSupported(context, destination);
try {
PushAddress address = getPushAddress(message.getIndividualRecipient());
TextSecureMessageSender messageSender = TextSecureMessageSenderFactory.create(context, masterSecret);
if (message.isEndSession()) {
messageSender.sendMessage(address, new TextSecureMessage(message.getDateSent(), null,
null, null, true, true));
} else {
messageSender.sendMessage(address, new TextSecureMessage(message.getDateSent(), null,
message.getBody().getBody()));
}
} catch (InvalidNumberException | UnregisteredUserException e) {
Log.w(TAG, e);
if (isSmsFallbackSupported) fallbackOrAskApproval(masterSecret, message, destination);
else DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
} catch (IOException e) {
Log.w(TAG, e);
if (isSmsFallbackSupported) fallbackOrAskApproval(masterSecret, message, destination);
else throw new RetryLaterException(e);
}
}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof RequirementNotMetException) return true;
if (throwable instanceof RetryLaterException) return true;
return false;
}
@Override
public void onCanceled() {
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
}
private void fallbackOrAskApproval(MasterSecret masterSecret, SmsMessageRecord smsMessage, String destination)
throws SecureFallbackApprovalException, InsecureFallbackApprovalException
{
Recipient recipient = smsMessage.getIndividualRecipient();
boolean isSmsFallbackApprovalRequired = isSmsFallbackApprovalRequired(destination);
AxolotlStore axolotlStore = new TextSecureAxolotlStore(context, masterSecret);
if (!isSmsFallbackApprovalRequired) {
Log.w(TAG, "Falling back to SMS");
DatabaseFactory.getSmsDatabase(context).markAsForcedSms(smsMessage.getId());
ApplicationContext.getInstance(context).getJobManager().add(new SmsSendJob(context, messageId, destination));
} else if (!axolotlStore.containsSession(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID)) {
Log.w(TAG, "Marking message as pending insecure fallback.");
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
} else {
Log.w(TAG, "Marking message as pending secure fallback.");
throw new SecureFallbackApprovalException("Pending user approval for fallback to secure SMS");
}
}
}

View File

@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.crypto.SmsCipher;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore; import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
@ -61,18 +62,15 @@ public class SmsDecryptJob extends MasterSecretJob {
} }
@Override @Override
public void onRun() throws RequirementNotMetException { public void onRun() throws RequirementNotMetException, NoSuchMessageException {
MasterSecret masterSecret = getMasterSecret(); MasterSecret masterSecret = getMasterSecret();
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsDatabase.Reader reader = null;
try { try {
reader = database.getMessage(masterSecret, messageId); SmsMessageRecord record = database.getMessage(masterSecret, messageId);
IncomingTextMessage message = createIncomingTextMessage(masterSecret, record);
SmsMessageRecord record = reader.getNext(); long messageId = record.getId();
IncomingTextMessage message = createIncomingTextMessage(masterSecret, record); long threadId = record.getThreadId();
long messageId = record.getId();
long threadId = record.getThreadId();
if (message.isSecureMessage()) handleSecureMessage(masterSecret, messageId, message); if (message.isSecureMessage()) handleSecureMessage(masterSecret, messageId, message);
else if (message.isPreKeyBundle()) handlePreKeyWhisperMessage(masterSecret, messageId, threadId, (IncomingPreKeyBundleMessage) message); else if (message.isPreKeyBundle()) handlePreKeyWhisperMessage(masterSecret, messageId, threadId, (IncomingPreKeyBundleMessage) message);
@ -93,9 +91,6 @@ public class SmsDecryptJob extends MasterSecretJob {
} catch (NoSessionException e) { } catch (NoSessionException e) {
Log.w(TAG, e); Log.w(TAG, e);
database.markAsNoSession(messageId); database.markAsNoSession(messageId);
} finally {
if (reader != null)
reader.close();
} }
} }

View File

@ -1,23 +1,46 @@
package org.thoughtcrime.securesms.jobs; package org.thoughtcrime.securesms.jobs;
import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.telephony.SmsManager;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ParcelUtil; import org.thoughtcrime.securesms.crypto.SmsCipher;
import org.whispersystems.jobqueue.EncryptionKeys; import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirement;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.libaxolotl.NoSessionException;
public class SmsSendJob extends ContextJob { import java.util.ArrayList;
private transient MasterSecret masterSecret; public class SmsSendJob extends MasterSecretJob {
private static final String TAG = SmsSendJob.class.getSimpleName();
private final long messageId; private final long messageId;
public SmsSendJob(Context context, MasterSecret masterSecret, long messageId, String name) { public SmsSendJob(Context context, long messageId, String name) {
super(context, JobParameters.newBuilder() super(context, JobParameters.newBuilder()
.withPersistence() .withPersistence()
.withEncryption(new EncryptionKeys(ParcelUtil.serialize(masterSecret))) .withRequirement(new MasterSecretRequirement(context))
.withRequirement(new ServiceRequirement(context))
.withGroupId(name) .withGroupId(name)
.create()); .create());
@ -30,19 +53,191 @@ public class SmsSendJob extends ContextJob {
} }
@Override @Override
public void onRun() { public void onRun() throws RequirementNotMetException, NoSuchMessageException {
MasterSecret masterSecret = ParcelUtil.deserialize(getEncryptionKeys().getEncoded(), MasterSecret.CREATOR); MasterSecret masterSecret = getMasterSecret();
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
try {
Log.w(TAG, "Sending message: " + messageId);
deliver(masterSecret, record);
} catch (UndeliverableMessageException ude) {
Log.w(TAG, ude);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (InsecureFallbackApprovalException ifae) {
Log.w(TAG, ifae);
DatabaseFactory.getSmsDatabase(context).markAsPendingInsecureSmsFallback(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
}
} }
@Override @Override
public void onCanceled() { public void onCanceled() {
Log.w(TAG, "onCanceled()");
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
} }
@Override @Override
public boolean onShouldRetry(Throwable throwable) { public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof RequirementNotMetException) return true;
return false; return false;
} }
private void deliver(MasterSecret masterSecret, SmsMessageRecord record)
throws UndeliverableMessageException, InsecureFallbackApprovalException
{
if (!NumberUtil.isValidSmsOrEmail(record.getIndividualRecipient().getNumber())) {
throw new UndeliverableMessageException("Not a valid SMS destination! " + record.getIndividualRecipient().getNumber());
}
if (record.isSecure() || record.isKeyExchange() || record.isEndSession()) {
deliverSecureMessage(masterSecret, record);
} else {
deliverPlaintextMessage(record);
}
}
private void deliverSecureMessage(MasterSecret masterSecret, SmsMessageRecord message)
throws UndeliverableMessageException, InsecureFallbackApprovalException
{
MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
if (message.isSecure() || message.isEndSession()) {
transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage);
}
ArrayList<String> messages = multipartMessageHandler.divideMessage(transportMessage);
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure());
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
Log.w("SmsTransport", "Secure divide into message parts: " + messages.size());
for (int i=0;i<messages.size();i++) {
// NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients
// and messages, this will throw an NPE. We have no idea why, so we're just
// catching it and marking the message as a failure. That way at least it
// doesn't repeatedly crash every time you start the app.
try {
SmsManager.getDefault().sendTextMessage(message.getIndividualRecipient().getNumber(), null, messages.get(i),
sentIntents.get(i),
deliveredIntents == null ? null : deliveredIntents.get(i));
} catch (NullPointerException npe) {
Log.w(TAG, npe);
Log.w(TAG, "Recipient: " + message.getIndividualRecipient().getNumber());
Log.w(TAG, "Message Total Parts/Current: " + messages.size() + "/" + i);
Log.w(TAG, "Message Part Length: " + messages.get(i).getBytes().length);
throw new UndeliverableMessageException(npe);
} catch (IllegalArgumentException iae) {
Log.w(TAG, iae);
throw new UndeliverableMessageException(iae);
}
}
}
private void deliverPlaintextMessage(SmsMessageRecord message)
throws UndeliverableMessageException
{
ArrayList<String> messages = SmsManager.getDefault().divideMessage(message.getBody().getBody());
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false);
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
String recipient = message.getIndividualRecipient().getNumber();
// NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients
// and messages, this will throw an NPE. We have no idea why, so we're just
// catching it and marking the message as a failure. That way at least it doesn't
// repeatedly crash every time you start the app.
try {
SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
} catch (NullPointerException npe) {
Log.w(TAG, npe);
Log.w(TAG, "Recipient: " + recipient);
Log.w(TAG, "Message Parts: " + messages.size());
try {
for (int i=0;i<messages.size();i++) {
SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i),
sentIntents.get(i),
deliveredIntents == null ? null : deliveredIntents.get(i));
}
} catch (NullPointerException npe2) {
Log.w(TAG, npe);
throw new UndeliverableMessageException(npe2);
}
}
}
private OutgoingTextMessage getAsymmetricEncrypt(MasterSecret masterSecret,
OutgoingTextMessage message)
throws InsecureFallbackApprovalException
{
try {
return new SmsCipher(new TextSecureAxolotlStore(context, masterSecret)).encrypt(message);
} catch (NoSessionException e) {
throw new InsecureFallbackApprovalException(e);
}
}
private ArrayList<PendingIntent> constructSentIntents(long messageId, long type,
ArrayList<String> messages, boolean secure)
{
ArrayList<PendingIntent> sentIntents = new ArrayList<>(messages.size());
for (String ignored : messages) {
sentIntents.add(PendingIntent.getBroadcast(context, 0,
constructSentIntent(context, messageId, type, secure, false),
0));
}
return sentIntents;
}
private ArrayList<PendingIntent> constructDeliveredIntents(long messageId, long type, ArrayList<String> messages) {
if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) {
return null;
}
ArrayList<PendingIntent> deliveredIntents = new ArrayList<>(messages.size());
for (String ignored : messages) {
deliveredIntents.add(PendingIntent.getBroadcast(context, 0,
constructDeliveredIntent(context, messageId, type),
0));
}
return deliveredIntents;
}
private Intent constructSentIntent(Context context, long messageId, long type,
boolean upgraded, boolean push)
{
Intent pending = new Intent(SmsDeliveryListener.SENT_SMS_ACTION,
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
context, SmsDeliveryListener.class);
pending.putExtra("type", type);
pending.putExtra("message_id", messageId);
pending.putExtra("upgraded", upgraded);
pending.putExtra("push", push);
return pending;
}
protected Intent constructDeliveredIntent(Context context, long messageId, long type) {
Intent pending = new Intent(SmsDeliveryListener.DELIVERED_SMS_ACTION,
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
context, SmsDeliveryListener.class);
pending.putExtra("type", type);
pending.putExtra("message_id", messageId);
return pending;
}
} }

View File

@ -0,0 +1,109 @@
package org.thoughtcrime.securesms.jobs;
import android.app.Activity;
import android.content.Context;
import android.telephony.SmsManager;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libaxolotl.state.SessionStore;
public class SmsSentJob extends MasterSecretJob {
private static final String TAG = SmsSentJob.class.getSimpleName();
private final long messageId;
private final String action;
private final int result;
public SmsSentJob(Context context, long messageId, String action, int result) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.create());
this.messageId = messageId;
this.action = action;
this.result = result;
}
@Override
public void onAdded() {
}
@Override
public void onRun() throws RequirementNotMetException {
Log.w(TAG, "Got SMS callback: " + action + " , " + result);
MasterSecret masterSecret = getMasterSecret();
switch (action) {
case SmsDeliveryListener.SENT_SMS_ACTION:
handleSentResult(masterSecret, messageId, result);
break;
case SmsDeliveryListener.DELIVERED_SMS_ACTION:
handleDeliveredResult(messageId, result);
break;
}
}
@Override
public boolean onShouldRetry(Throwable throwable) {
if (throwable instanceof RequirementNotMetException) return true;
return false;
}
@Override
public void onCanceled() {
}
private void handleDeliveredResult(long messageId, int result) {
DatabaseFactory.getEncryptingSmsDatabase(context).markStatus(messageId, result);
}
private void handleSentResult(MasterSecret masterSecret, long messageId, int result) {
try {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
switch (result) {
case Activity.RESULT_OK:
database.markAsSent(messageId);
if (record != null && record.isEndSession()) {
Log.w(TAG, "Ending session...");
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
sessionStore.deleteAllSessions(record.getIndividualRecipient().getRecipientId());
SecurityEvent.broadcastSecurityUpdateEvent(context, record.getThreadId());
}
break;
case SmsManager.RESULT_ERROR_NO_SERVICE:
case SmsManager.RESULT_ERROR_RADIO_OFF:
Log.w(TAG, "Service connectivity problem, requeuing...");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new SmsSendJob(context, messageId, record.getIndividualRecipient().getNumber()));
break;
default:
database.markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
}
} catch (NoSuchMessageException e) {
Log.w(TAG, e);
}
}
}

View File

@ -29,6 +29,11 @@ public class MasterSecretRequirementProvider implements RequirementProvider {
context.registerReceiver(newKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null); context.registerReceiver(newKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
} }
@Override
public String getName() {
return "master_secret";
}
@Override @Override
public void setListener(RequirementListener listener) { public void setListener(RequirementListener listener) {
this.listener = listener; this.listener = listener;

View File

@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.jobs.requirements;
import android.content.Context;
import android.os.Looper;
import android.os.MessageQueue;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.sms.TelephonyServiceState;
import org.whispersystems.jobqueue.dependencies.ContextDependent;
import org.whispersystems.jobqueue.requirements.Requirement;
public class ServiceRequirement implements Requirement, ContextDependent {
private static final String TAG = ServiceRequirement.class.getSimpleName();
private final transient ServiceRequirementProvider provider;
private transient Context context;
public ServiceRequirement(Context context) {
this.context = context;
this.provider = (ServiceRequirementProvider)ApplicationContext.getInstance(context)
.getJobManager()
.getRequirementProvider("telephony-service");
}
@Override
public void setContext(Context context) {
this.context = context;
}
@Override
public boolean isPresent() {
TelephonyServiceState telephonyServiceState = new TelephonyServiceState();
return telephonyServiceState.isConnected(context);
}
}

View File

@ -0,0 +1,61 @@
package org.thoughtcrime.securesms.jobs.requirements;
import android.content.Context;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import org.whispersystems.jobqueue.requirements.RequirementListener;
import org.whispersystems.jobqueue.requirements.RequirementProvider;
import java.util.concurrent.atomic.AtomicBoolean;
public class ServiceRequirementProvider implements RequirementProvider {
private final TelephonyManager telephonyManager;
private final ServiceStateListener serviceStateListener;
private final AtomicBoolean listeningForServiceState;
private RequirementListener requirementListener;
public ServiceRequirementProvider(Context context) {
this.telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
this.serviceStateListener = new ServiceStateListener();
this.listeningForServiceState = new AtomicBoolean(false);
}
@Override
public String getName() {
return "telephony-service";
}
@Override
public void setListener(RequirementListener requirementListener) {
this.requirementListener = requirementListener;
}
public void start() {
if (listeningForServiceState.compareAndSet(false, true)) {
this.telephonyManager.listen(serviceStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
}
}
private void handleInService() {
if (listeningForServiceState.compareAndSet(true, false)) {
this.telephonyManager.listen(serviceStateListener, PhoneStateListener.LISTEN_NONE);
}
if (requirementListener != null) {
requirementListener.onRequirementStatusChanged();
}
}
private class ServiceStateListener extends PhoneStateListener {
@Override
public void onServiceStateChanged(ServiceState serviceState) {
if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
handleInService();
}
}
}
}

View File

@ -33,7 +33,12 @@ import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduPart; import ws.com.google.android.mms.pdu.PduPart;
public class SlideDeck { public class SlideDeck {
private final List<Slide> slides = new LinkedList<Slide>();
private final List<Slide> slides = new LinkedList<>();
public SlideDeck(SlideDeck copy) {
this.slides.addAll(copy.getSlides());
}
public SlideDeck(Context context, MasterSecret masterSecret, PduBody body) { public SlideDeck(Context context, MasterSecret masterSecret, PduBody body) {
try { try {

View File

@ -1,136 +0,0 @@
/**
* 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
* 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.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.transport.UniversalTransport;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.SendReq;
public class MmsSender {
private final Context context;
private final SystemStateListener systemStateListener;
private final ToastHandler toastHandler;
public MmsSender(Context context, SystemStateListener systemStateListener, ToastHandler toastHandler) {
this.context = context;
this.systemStateListener = systemStateListener;
this.toastHandler = toastHandler;
}
public void process(MasterSecret masterSecret, Intent intent) {
Log.w("MmsSender", "Got intent action: " + intent.getAction());
if (SendReceiveService.SEND_MMS_ACTION.equals(intent.getAction())) {
handleSendMms(masterSecret, intent);
}
}
private void handleSendMms(MasterSecret masterSecret, Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
UniversalTransport transport = new UniversalTransport(context, masterSecret);
try {
SendReq[] messages = database.getOutgoingMessages(masterSecret, messageId);
for (SendReq message : messages) {
long threadId = database.getThreadIdForMessage(message.getDatabaseMessageId());
try {
Log.w("MmsSender", "Passing to MMS transport: " + message.getDatabaseMessageId());
database.markAsSending(message.getDatabaseMessageId());
MmsSendResult result = transport.deliver(message);
if (result.isUpgradedSecure()) database.markAsSecure(message.getDatabaseMessageId());
if (result.isPush()) database.markAsPush(message.getDatabaseMessageId());
database.markAsSent(message.getDatabaseMessageId(), result.getMessageId(),
result.getResponseStatus());
systemStateListener.unregisterForConnectivityChange();
} catch (InsecureFallbackApprovalException ifae) {
Log.w("MmsSender", ifae);
database.markAsPendingInsecureSmsFallback(message.getDatabaseMessageId());
notifyMessageDeliveryFailed(context, threads, threadId);
} catch (SecureFallbackApprovalException sfae) {
Log.w("MmsSender", sfae);
database.markAsPendingSecureSmsFallback(message.getDatabaseMessageId());
notifyMessageDeliveryFailed(context, threads, threadId);
} catch (UndeliverableMessageException e) {
Log.w("MmsSender", e);
database.markAsSentFailed(message.getDatabaseMessageId());
notifyMessageDeliveryFailed(context, threads, threadId);
} catch (UntrustedIdentityException uie) {
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
database.markAsSentFailed(messageId);
} catch (RetryLaterException e) {
Log.w("MmsSender", e);
database.markAsOutbox(message.getDatabaseMessageId());
if (systemStateListener.isConnected()) scheduleQuickRetryAlarm();
else systemStateListener.registerForConnectivityChange();
toastHandler
.obtainMessage(0, context.getString(R.string.SmsReceiver_currently_unable_to_send_your_sms_message))
.sendToTarget();
}
}
} catch (MmsException e) {
Log.w("MmsSender", e);
if (messageId != -1)
database.markAsSentFailed(messageId);
}
}
private static void notifyMessageDeliveryFailed(Context context, ThreadDatabase threads, long threadId) {
Recipients recipients = threads.getRecipientsForThreadId(threadId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
}
private void scheduleQuickRetryAlarm() {
((AlarmManager)context.getSystemService(Context.ALARM_SERVICE))
.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (30 * 1000),
PendingIntent.getService(context, 0,
new Intent(SendReceiveService.SEND_MMS_ACTION,
null, context, SendReceiveService.class),
PendingIntent.FLAG_UPDATE_CURRENT));
}
}

View File

@ -1,363 +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.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.database.CanonicalSessionMigrator;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.WorkerThread;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* Services that handles sending/receiving of SMS/MMS.
*
* @author Moxie Marlinspike
*/
public class SendReceiveService extends Service {
public static final String SEND_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_SMS_ACTION";
public static final String SENT_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SENT_SMS_ACTION";
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 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";
// public static final String DOWNLOAD_MMS_PENDING_APN_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_PENDING_APN_ACTION";
// public static final String RECEIVE_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_PUSH_ACTION";
// public static final String DECRYPTED_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DECRYPTED_PUSH_ACTION";
// public static final String DOWNLOAD_PUSH_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_PUSH_ACTION";
// public static final String DOWNLOAD_AVATAR_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_AVATAR_ACTION";
public static final String MASTER_SECRET_EXTRA = "master_secret";
private static final int SEND_SMS = 0;
// private static final int RECEIVE_SMS = 1;
private static final int SEND_MMS = 2;
// private static final int RECEIVE_MMS = 3;
// private static final int DOWNLOAD_MMS = 4;
// private static final int DOWNLOAD_MMS_PENDING = 5;
// private static final int RECEIVE_PUSH = 6;
// private static final int DOWNLOAD_PUSH = 7;
// private static final int DOWNLOAD_AVATAR = 8;
private ToastHandler toastHandler;
private SystemStateListener systemStateListener;
// private MmsReceiver mmsReceiver;
// private SmsReceiver smsReceiver;
private SmsSender smsSender;
private MmsSender mmsSender;
// private MmsDownloader mmsDownloader;
// private PushReceiver pushReceiver;
// private PushDownloader pushDownloader;
// private AvatarDownloader avatarDownloader;
private MasterSecret masterSecret;
private boolean hasSecret;
private NewKeyReceiver newKeyReceiver;
private ClearKeyReceiver clearKeyReceiver;
private List<Runnable> workQueue;
private List<Runnable> pendingSecretList;
@Override
public void onCreate() {
initializeHandlers();
initializeProcessors();
initializeAddressCanonicalization();
initializeWorkQueue();
initializeMasterSecret();
}
@Override
public void onStart(Intent intent, int startId) {
if (intent == null) return;
String action = intent.getAction();
if (action.equals(SEND_SMS_ACTION))
scheduleSecretRequiredIntent(SEND_SMS, intent);
// else if (action.equals(RECEIVE_SMS_ACTION))
// scheduleIntent(RECEIVE_SMS, intent);
else if (action.equals(SENT_SMS_ACTION))
scheduleIntent(SEND_SMS, intent);
else if (action.equals(DELIVERED_SMS_ACTION))
scheduleIntent(SEND_SMS, intent);
else if (action.equals(SEND_MMS_ACTION))
scheduleSecretRequiredIntent(SEND_MMS, intent);
// else if (action.equals(RECEIVE_MMS_ACTION))
// scheduleIntent(RECEIVE_MMS, intent);
// else if (action.equals(DOWNLOAD_MMS_ACTION))
// scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent);
// else if (intent.getAction().equals(DOWNLOAD_MMS_PENDING_APN_ACTION))
// scheduleSecretRequiredIntent(DOWNLOAD_MMS_PENDING, intent);
// else if (action.equals(RECEIVE_PUSH_ACTION))
// scheduleIntent(RECEIVE_PUSH, intent);
// else if (action.equals(DECRYPTED_PUSH_ACTION))
// scheduleSecretRequiredIntent(RECEIVE_PUSH, intent);
// else if (action.equals(DOWNLOAD_PUSH_ACTION))
// scheduleSecretRequiredIntent(DOWNLOAD_PUSH, intent);
// else if (action.equals(DOWNLOAD_AVATAR_ACTION))
// scheduleIntent(DOWNLOAD_AVATAR, intent);
else
Log.w("SendReceiveService", "Received intent with unknown action: " + intent.getAction());
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
Log.w("SendReceiveService", "onDestroy()...");
super.onDestroy();
if (newKeyReceiver != null)
unregisterReceiver(newKeyReceiver);
if (clearKeyReceiver != null)
unregisterReceiver(clearKeyReceiver);
}
private void initializeHandlers() {
systemStateListener = new SystemStateListener(this);
toastHandler = new ToastHandler();
}
private void initializeProcessors() {
// smsReceiver = new SmsReceiver(this);
smsSender = new SmsSender(this, systemStateListener, toastHandler);
// mmsReceiver = new MmsReceiver(this);
mmsSender = new MmsSender(this, systemStateListener, toastHandler);
// mmsDownloader = new MmsDownloader(this, toastHandler);
// pushReceiver = new PushReceiver(this);
// pushDownloader = new PushDownloader(this);
// avatarDownloader = new AvatarDownloader(this);
}
private void initializeWorkQueue() {
pendingSecretList = new LinkedList<Runnable>();
workQueue = new LinkedList<Runnable>();
Thread workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
workerThread.start();
}
private void initializeMasterSecret() {
hasSecret = false;
newKeyReceiver = new NewKeyReceiver();
clearKeyReceiver = new ClearKeyReceiver();
IntentFilter newKeyFilter = new IntentFilter(KeyCachingService.NEW_KEY_EVENT);
registerReceiver(newKeyReceiver, newKeyFilter, KeyCachingService.KEY_PERMISSION, null);
IntentFilter clearKeyFilter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT);
registerReceiver(clearKeyReceiver, clearKeyFilter, KeyCachingService.KEY_PERMISSION, null);
initializeWithMasterSecret(KeyCachingService.getMasterSecret(this));
// Intent bindIntent = new Intent(this, KeyCachingService.class);
// startService(bindIntent);
// bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE);
}
private void initializeWithMasterSecret(MasterSecret masterSecret) {
Log.w("SendReceiveService", "SendReceive service got master secret.");
if (masterSecret != null) {
synchronized (workQueue) {
this.masterSecret = masterSecret;
this.hasSecret = true;
Iterator<Runnable> iterator = pendingSecretList.iterator();
while (iterator.hasNext())
workQueue.add(iterator.next());
workQueue.notifyAll();
}
}
}
private void initializeAddressCanonicalization() {
CanonicalSessionMigrator.migrateSessions(this);
}
private MasterSecret getPlaceholderSecret() {
try {
return MasterSecretUtil.getMasterSecret(SendReceiveService.this,
MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
} catch (InvalidPassphraseException e) {
Log.w("SendReceiveService", e);
return null;
}
}
private void scheduleIntent(int what, Intent intent) {
Runnable work = new SendReceiveWorkItem(intent, what);
synchronized (workQueue) {
workQueue.add(work);
workQueue.notifyAll();
}
}
private void scheduleSecretRequiredIntent(int what, Intent intent) {
Runnable work = new SendReceiveWorkItem(intent, what);
synchronized (workQueue) {
if (!hasSecret && TextSecurePreferences.isPasswordDisabled(SendReceiveService.this)) {
initializeWithMasterSecret(getPlaceholderSecret());
}
if (hasSecret) {
workQueue.add(work);
workQueue.notifyAll();
} else {
pendingSecretList.add(work);
}
}
}
private class SendReceiveWorkItem implements Runnable {
private final Intent intent;
private final int what;
public SendReceiveWorkItem(Intent intent, int what) {
this.intent = intent;
this.what = what;
}
@Override
public void run() {
MasterSecret masterSecret = SendReceiveService.this.masterSecret;
if (masterSecret == null && TextSecurePreferences.isPasswordDisabled(SendReceiveService.this)) {
masterSecret = getPlaceholderSecret();
}
switch (what) {
// case RECEIVE_SMS: smsReceiver.process(masterSecret, intent); return;
case SEND_SMS: smsSender.process(masterSecret, intent); return;
// case RECEIVE_MMS: mmsReceiver.process(masterSecret, intent); return;
case SEND_MMS: mmsSender.process(masterSecret, intent); return;
// case DOWNLOAD_MMS: mmsDownloader.process(masterSecret, intent); return;
// case DOWNLOAD_MMS_PENDING: mmsDownloader.process(masterSecret, intent); return;
// case RECEIVE_PUSH: pushReceiver.process(masterSecret, intent); return;
// case DOWNLOAD_PUSH: pushDownloader.process(masterSecret, intent); return;
// case DOWNLOAD_AVATAR: avatarDownloader.process(masterSecret, intent); return;
}
}
}
public class ToastHandler extends Handler {
public void makeToast(String toast) {
Message message = this.obtainMessage();
message.obj = toast;
this.sendMessage(message);
}
@Override
public void handleMessage(Message message) {
Toast.makeText(SendReceiveService.this, (String)message.obj, Toast.LENGTH_LONG).show();
}
}
// private ServiceConnection serviceConnection = new ServiceConnection() {
// @Override
// public void onServiceConnected(ComponentName className, IBinder service) {
// KeyCachingService keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService();
// MasterSecret masterSecret = keyCachingService.getMasterSecret();
//
// initializeWithMasterSecret(masterSecret);
//
// SendReceiveService.this.unbindService(this);
// }
//
// @Override
// public void onServiceDisconnected(ComponentName name) {}
// };
private class NewKeyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.w("SendReceiveService", "Got a MasterSecret broadcast...");
initializeWithMasterSecret((MasterSecret)intent.getParcelableExtra(MASTER_SECRET_EXTRA));
}
}
/**
* This class receives broadcast notifications to clear the MasterSecret.
*
* We don't want to clear it immediately, since there are potentially jobs
* in the work queue which require the master secret. Instead, we reset a
* flag so that new incoming jobs will be evaluated as if no mastersecret is
* present.
*
* Then, we add a job to the end of the queue which actually clears the masterSecret
* value. That way all jobs before this moment will be processed correctly, and all
* jobs after this moment will be evaluated as if no mastersecret is present (and potentially
* held).
*
* When we go to actually clear the mastersecret, we ensure that the flag is still false.
* This allows a new mastersecret broadcast to come in correctly without us clobbering it.
*
*/
private class ClearKeyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.w("SendReceiveService", "Got a clear mastersecret broadcast...");
synchronized (workQueue) {
SendReceiveService.this.hasSecret = false;
workQueue.add(new Runnable() {
@Override
public void run() {
Log.w("SendReceiveService", "Running clear key work item...");
synchronized (workQueue) {
if (!SendReceiveService.this.hasSecret) {
Log.w("SendReceiveService", "Actually clearing key...");
SendReceiveService.this.masterSecret = null;
}
}
}
});
workQueue.notifyAll();
}
}
};
}

View File

@ -3,19 +3,50 @@ package org.thoughtcrime.securesms.service;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.telephony.SmsMessage;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.jobs.SmsSentJob;
import org.whispersystems.jobqueue.JobManager;
public class SmsDeliveryListener extends BroadcastReceiver { public class SmsDeliveryListener extends BroadcastReceiver {
private static final String TAG = SmsDeliveryListener.class.getSimpleName();
public static final String SENT_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SENT_SMS_ACTION";
public static final String DELIVERED_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DELIVERED_SMS_ACTION";
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) { JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
intent.putExtra("ResultCode", this.getResultCode()); long messageId = intent.getLongExtra("message_id", -1);
intent.setClass(context, SendReceiveService.class);
context.startService(intent); switch (intent.getAction()) {
} else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) { case SENT_SMS_ACTION:
intent.putExtra("ResultCode", this.getResultCode()); int result = getResultCode();
intent.setClass(context, SendReceiveService.class);
context.startService(intent); jobManager.add(new SmsSentJob(context, messageId, SENT_SMS_ACTION, result));
break;
case DELIVERED_SMS_ACTION:
byte[] pdu = intent.getByteArrayExtra("pdu");
if (pdu == null) {
Log.w(TAG, "No PDU in delivery receipt!");
break;
}
SmsMessage message = SmsMessage.createFromPdu(pdu);
if (message == null) {
Log.w(TAG, "Delivery receipt failed to parse!");
break;
}
jobManager.add(new SmsSentJob(context, messageId, DELIVERED_SMS_ACTION, message.getStatus()));
break;
default:
Log.w(TAG, "Unknown action: " + intent.getAction());
} }
} }
} }

View File

@ -1,194 +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.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.transport.UniversalTransport;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
public class SmsSender {
private final Context context;
private final SystemStateListener systemStateListener;
private final ToastHandler toastHandler;
public SmsSender(Context context, SystemStateListener systemStateListener, ToastHandler toastHandler) {
this.context = context;
this.systemStateListener = systemStateListener;
this.toastHandler = toastHandler;
}
public void process(MasterSecret masterSecret, Intent intent) {
if (SendReceiveService.SEND_SMS_ACTION.equals(intent.getAction())) {
handleSendMessage(masterSecret, intent);
} else if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) {
handleSentMessage(masterSecret, intent);
} else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) {
handleDeliveredMessage(intent);
}
}
private void handleSendMessage(MasterSecret masterSecret, Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
UniversalTransport transport = new UniversalTransport(context, masterSecret);
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
EncryptingSmsDatabase.Reader reader = null;
SmsMessageRecord record;
Log.w("SmsSender", "Sending message: " + messageId);
try {
if (messageId != -1) reader = database.getMessage(masterSecret, messageId);
else reader = database.getOutgoingMessages(masterSecret);
while (reader != null && (record = reader.getNext()) != null) {
try {
database.markAsSending(record.getId());
transport.deliver(record);
} catch (InsecureFallbackApprovalException ifae) {
Log.w("SmsSender", ifae);
DatabaseFactory.getSmsDatabase(context).markAsPendingInsecureSmsFallback(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (SecureFallbackApprovalException sfae) {
Log.w("SmsSender", sfae);
DatabaseFactory.getSmsDatabase(context).markAsPendingSecureSmsFallback(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (UntrustedIdentityException e) {
Log.w("SmsSender", e);
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(e.getE164Number(), e.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId());
} catch (UndeliverableMessageException ude) {
Log.w("SmsSender", ude);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (RetryLaterException rle) {
Log.w("SmsSender", rle);
DatabaseFactory.getSmsDatabase(context).markAsOutbox(record.getId());
if (systemStateListener.isConnected()) scheduleQuickRetryAlarm();
else systemStateListener.registerForConnectivityChange();
}
}
} finally {
if (reader != null)
reader.close();
}
}
private void handleSentMessage(MasterSecret masterSecret, Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
int result = intent.getIntExtra("ResultCode", -31337);
boolean upgraded = intent.getBooleanExtra("upgraded", false);
boolean push = intent.getBooleanExtra("push", false);
Log.w("SMSReceiverService", "Intent resultcode: " + result);
Log.w("SMSReceiverService", "Running sent callback: " + messageId);
if (result == Activity.RESULT_OK) {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
Cursor cursor = database.getMessage(messageId);
SmsDatabase.Reader reader = database.readerFor(cursor);
if (push) database.markAsPush(messageId);
if (upgraded) database.markAsSecure(messageId);
database.markAsSent(messageId);
SmsMessageRecord record = reader.getNext();
if (record != null && record.isEndSession()) {
Log.w("SmsSender", "Ending session...");
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
sessionStore.deleteAllSessions(record.getIndividualRecipient().getRecipientId());
SecurityEvent.broadcastSecurityUpdateEvent(context, record.getThreadId());
}
unregisterForRadioChanges();
} else if (result == SmsManager.RESULT_ERROR_NO_SERVICE || result == SmsManager.RESULT_ERROR_RADIO_OFF) {
DatabaseFactory.getSmsDatabase(context).markAsOutbox(messageId);
toastHandler
.obtainMessage(0, context.getString(R.string.SmsReceiver_currently_unable_to_send_your_sms_message))
.sendToTarget();
registerForRadioChanges();
} else {
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
unregisterForRadioChanges();
}
}
private void handleDeliveredMessage(Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
byte[] pdu = intent.getByteArrayExtra("pdu");
SmsMessage message = SmsMessage.createFromPdu(pdu);
if (message == null) {
return;
}
DatabaseFactory.getSmsDatabase(context).markStatus(messageId, message.getStatus());
}
private void registerForRadioChanges() {
if (systemStateListener.isConnected()) systemStateListener.registerForRadioChange();
else systemStateListener.registerForConnectivityChange();
}
private void unregisterForRadioChanges() {
systemStateListener.unregisterForConnectivityChange();
}
private void scheduleQuickRetryAlarm() {
((AlarmManager)context.getSystemService(Context.ALARM_SERVICE))
.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (30 * 1000),
PendingIntent.getService(context, 0,
new Intent(SendReceiveService.SEND_SMS_ACTION,
null, context, SendReceiveService.class),
PendingIntent.FLAG_UPDATE_CURRENT));
}
}

View File

@ -1,95 +0,0 @@
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.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.util.Log;
public class SystemStateListener {
private final TelephonyListener telephonyListener = new TelephonyListener();
private final ConnectivityListener connectivityListener = new ConnectivityListener();
private final Context context;
private final TelephonyManager telephonyManager;
private final ConnectivityManager connectivityManager;
public SystemStateListener(Context context) {
this.context = context.getApplicationContext();
this.telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
public void registerForRadioChange() {
Log.w("SystemStateListener", "Registering for radio changes...");
unregisterForConnectivityChange();
telephonyManager.listen(telephonyListener, PhoneStateListener.LISTEN_SERVICE_STATE);
}
public void registerForConnectivityChange() {
Log.w("SystemStateListener", "Registering for any connectivity changes...");
unregisterForConnectivityChange();
telephonyManager.listen(telephonyListener, PhoneStateListener.LISTEN_SERVICE_STATE);
context.registerReceiver(connectivityListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
public void unregisterForConnectivityChange() {
telephonyManager.listen(telephonyListener, 0);
try {
context.unregisterReceiver(connectivityListener);
} catch (IllegalArgumentException iae) {
Log.w("SystemStateListener", iae);
}
}
public boolean isConnected() {
return
connectivityManager.getActiveNetworkInfo() != null &&
connectivityManager.getActiveNetworkInfo().isConnected();
}
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 class TelephonyListener extends PhoneStateListener {
@Override
public void onServiceStateChanged(ServiceState state) {
if (state.getState() == ServiceState.STATE_IN_SERVICE) {
Log.w("SystemStateListener", "In service, sending sms/mms outboxes...");
sendSmsOutbox(context);
sendMmsOutbox(context);
}
}
}
private class ConnectivityListener extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
if (connectivityManager.getActiveNetworkInfo() != null &&
connectivityManager.getActiveNetworkInfo().isConnected())
{
Log.w("SystemStateListener", "Got connectivity action: " + intent.toString());
sendSmsOutbox(context);
sendMmsOutbox(context);
}
}
}
}
}

View File

@ -17,106 +17,231 @@
package org.thoughtcrime.securesms.sms; package org.thoughtcrime.securesms.sms;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.util.DirectoryUtil;
import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.InvalidNumberException;
import java.util.List; import java.io.IOException;
import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.MmsException;
public class MessageSender { public class MessageSender {
public static long send(Context context, MasterSecret masterSecret, private static final String TAG = MessageSender.class.getSimpleName();
OutgoingTextMessage message, long threadId,
boolean forceSms) public static long send(final Context context,
final MasterSecret masterSecret,
final OutgoingTextMessage message,
final long threadId,
final boolean forceSms)
{ {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Recipients recipients = message.getRecipients();
boolean keyExchange = message.isKeyExchange();
long allocatedThreadId;
if (threadId == -1) { if (threadId == -1) {
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(message.getRecipients()); allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
}
List<Long> messageIds = database.insertMessageOutbox(masterSecret, threadId, message, forceSms);
if (!forceSms && isSelfSend(context, message.getRecipients())) {
for (long messageId : messageIds) {
database.markAsSent(messageId);
database.markAsPush(messageId);
Pair<Long, Long> messageAndThreadId = database.copyMessageInbox(messageId);
database.markAsPush(messageAndThreadId.first);
}
} else { } else {
for (long messageId : messageIds) { allocatedThreadId = threadId;
Log.w("SMSSender", "Got message id for new message: " + messageId);
Intent intent = new Intent(SendReceiveService.SEND_SMS_ACTION, null,
context, SendReceiveService.class);
intent.putExtra("message_id", messageId);
context.startService(intent);
}
} }
return threadId; long messageId = database.insertMessageOutbox(masterSecret, allocatedThreadId, message, forceSms);
sendTextMessage(context, recipients, forceSms, keyExchange, messageId);
return allocatedThreadId;
} }
public static long send(Context context, MasterSecret masterSecret, public static long send(final Context context,
OutgoingMediaMessage message, final MasterSecret masterSecret,
long threadId, boolean forceSms) final OutgoingMediaMessage message,
final long threadId,
final boolean forceSms)
{
try {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
long allocatedThreadId;
if (threadId == -1) {
allocatedThreadId = threadDatabase.getThreadIdFor(message.getRecipients(), message.getDistributionType());
} else {
allocatedThreadId = threadId;
}
Recipients recipients = message.getRecipients();
long messageId = database.insertMessageOutbox(masterSecret, message, allocatedThreadId, forceSms);
sendMediaMessage(context, masterSecret, recipients, forceSms, messageId);
return allocatedThreadId;
} catch (MmsException e) {
Log.w(TAG, e);
return threadId;
}
}
public static void resend(Context context, MasterSecret masterSecret, MessageRecord messageRecord) {
try {
Recipients recipients = messageRecord.getRecipients();
long messageId = messageRecord.getId();
boolean forceSms = messageRecord.isForcedSms();
boolean keyExchange = messageRecord.isKeyExchange();
if (messageRecord.isMms()) {
sendMediaMessage(context, masterSecret, recipients, forceSms, messageId);
} else {
sendTextMessage(context, recipients, forceSms, keyExchange, messageId);
}
} catch (MmsException e) {
Log.w(TAG, e);
}
}
private static void sendMediaMessage(Context context, MasterSecret masterSecret,
Recipients recipients, boolean forceSms, long messageId)
throws MmsException
{
if (!forceSms && isSelfSend(context, recipients)) {
sendMediaSelf(context, masterSecret, messageId);
} else if (isGroupPushSend(recipients)) {
sendGroupPush(context, recipients, messageId);
} else if (!forceSms && isPushMediaSend(context, recipients)) {
sendMediaPush(context, recipients, messageId);
} else {
sendMms(context, messageId);
}
}
private static void sendTextMessage(Context context, Recipients recipients,
boolean forceSms, boolean keyExchange, long messageId)
{
if (!forceSms && isSelfSend(context, recipients)) {
sendTextSelf(context, messageId);
} else if (!forceSms && isPushTextSend(context, recipients, keyExchange)) {
sendTextPush(context, recipients, messageId);
} else {
sendSms(context, recipients, messageId);
}
}
private static void sendTextSelf(Context context, long messageId) {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
database.markAsSent(messageId);
database.markAsPush(messageId);
Pair<Long, Long> messageAndThreadId = database.copyMessageInbox(messageId);
database.markAsPush(messageAndThreadId.first);
}
private static void sendMediaSelf(Context context, MasterSecret masterSecret, long messageId)
throws MmsException throws MmsException
{ {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.markAsSent(messageId, "self-send".getBytes(), 0);
database.markAsPush(messageId);
if (threadId == -1) { long newMessageId = database.copyMessageInbox(masterSecret, messageId);
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(message.getRecipients(), message.getDistributionType()); database.markAsPush(newMessageId);
}
long messageId = database.insertMessageOutbox(masterSecret, message, threadId, forceSms);
if (!forceSms && isSelfSend(context, message.getRecipients())) {
database.markAsSent(messageId, "self-send".getBytes(), 0);
database.markAsPush(messageId);
long newMessageId = database.copyMessageInbox(masterSecret, messageId);
database.markAsPush(newMessageId);
} else {
Intent intent = new Intent(SendReceiveService.SEND_MMS_ACTION, null,
context, SendReceiveService.class);
intent.putExtra("message_id", messageId);
intent.putExtra("thread_id", threadId);
context.startService(intent);
}
return threadId;
} }
public static void resend(Context context, long messageId, boolean isMms) private static void sendTextPush(Context context, Recipients recipients, long messageId) {
{ JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new PushTextSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber()));
}
Intent intent; private static void sendMediaPush(Context context, Recipients recipients, long messageId) {
if (isMms) { JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
DatabaseFactory.getMmsDatabase(context).markAsSending(messageId); jobManager.add(new PushMediaSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber()));
intent = new Intent(SendReceiveService.SEND_MMS_ACTION, null, }
context, SendReceiveService.class);
} else { private static void sendGroupPush(Context context, Recipients recipients, long messageId) {
DatabaseFactory.getSmsDatabase(context).markAsSending(messageId); JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
intent = new Intent(SendReceiveService.SEND_SMS_ACTION, null, jobManager.add(new PushGroupSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber()));
context, SendReceiveService.class); }
private static void sendSms(Context context, Recipients recipients, long messageId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new SmsSendJob(context, messageId, recipients.getPrimaryRecipient().getName()));
}
private static void sendMms(Context context, long messageId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new MmsSendJob(context, messageId));
}
private static boolean isPushTextSend(Context context, Recipients recipients, boolean keyExchange) {
try {
if (!TextSecurePreferences.isPushRegistered(context)) {
return false;
}
if (keyExchange) {
return false;
}
Recipient recipient = recipients.getPrimaryRecipient();
String destination = Util.canonicalizeNumber(context, recipient.getNumber());
return isPushDestination(context, destination);
} catch (InvalidNumberException e) {
Log.w(TAG, e);
return false;
} }
intent.putExtra("message_id", messageId); }
context.startService(intent);
private static boolean isPushMediaSend(Context context, Recipients recipients) {
try {
if (!TextSecurePreferences.isPushRegistered(context)) {
return false;
}
if (recipients.getRecipientsList().size() > 1) {
return false;
}
Recipient recipient = recipients.getPrimaryRecipient();
String destination = Util.canonicalizeNumber(context, recipient.getNumber());
return isPushDestination(context, destination);
} catch (InvalidNumberException e) {
Log.w(TAG, e);
return false;
}
}
private static boolean isGroupPushSend(Recipients recipients) {
return GroupUtil.isEncodedGroup(recipients.getPrimaryRecipient().getNumber());
} }
private static boolean isSelfSend(Context context, Recipients recipients) { private static boolean isSelfSend(Context context, Recipients recipients) {
@ -137,4 +262,32 @@ public class MessageSender {
} }
} }
private static boolean isPushDestination(Context context, String destination) {
Directory directory = Directory.getInstance(context);
try {
return directory.isActiveNumber(destination);
} catch (NotInDirectoryException e) {
try {
PushServiceSocket socket = PushServiceSocketFactory.create(context);
String contactToken = DirectoryUtil.getDirectoryServerToken(destination);
ContactTokenDetails registeredUser = socket.getContactTokenDetails(contactToken);
if (registeredUser == null) {
registeredUser = new ContactTokenDetails();
registeredUser.setNumber(destination);
directory.setNumber(registeredUser, false);
return false;
} else {
registeredUser.setNumber(destination);
directory.setNumber(registeredUser, true);
return true;
}
} catch (IOException e1) {
Log.w(TAG, e1);
return false;
}
}
}
} }

View File

@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.sms;
import android.content.Context;
import android.os.Looper;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
public class TelephonyServiceState {
public boolean isConnected(Context context) {
ListenThread listenThread = new ListenThread(context);
listenThread.start();
return listenThread.get();
}
private static class ListenThread extends Thread {
private final Context context;
private boolean complete;
private boolean result;
public ListenThread(Context context) {
this.context = context.getApplicationContext();
}
@Override
public void run() {
Looper looper = initializeLooper();
ListenCallback callback = new ListenCallback(looper);
TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(callback, PhoneStateListener.LISTEN_SERVICE_STATE);
Looper.loop();
telephonyManager.listen(callback, PhoneStateListener.LISTEN_NONE);
set(callback.isConnected());
}
private Looper initializeLooper() {
Looper looper = Looper.myLooper();
if (looper == null) {
Looper.prepare();
}
return Looper.myLooper();
}
public synchronized boolean get() {
while (!complete) {
try {
wait();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
return result;
}
private synchronized void set(boolean result) {
this.result = result;
this.complete = true;
notifyAll();
}
}
private static class ListenCallback extends PhoneStateListener {
private final Looper looper;
private volatile boolean connected;
public ListenCallback(Looper looper) {
this.looper = looper;
}
@Override
public void onServiceStateChanged(ServiceState serviceState) {
this.connected = (serviceState.getState() == ServiceState.STATE_IN_SERVICE);
looper.quit();
}
public boolean isConnected() {
return connected;
}
}
}

View File

@ -1,36 +0,0 @@
package org.thoughtcrime.securesms.transport;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
public abstract class BaseTransport {
protected Intent constructSentIntent(Context context, long messageId, long type,
boolean upgraded, boolean push)
{
Intent pending = new Intent(SendReceiveService.SENT_SMS_ACTION,
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
context, SmsDeliveryListener.class);
pending.putExtra("type", type);
pending.putExtra("message_id", messageId);
pending.putExtra("upgraded", upgraded);
pending.putExtra("push", push);
return pending;
}
protected Intent constructDeliveredIntent(Context context, long messageId, long type) {
Intent pending = new Intent(SendReceiveService.DELIVERED_SMS_ACTION,
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
context, SmsDeliveryListener.class);
pending.putExtra("type", type);
pending.putExtra("message_id", messageId);
return pending;
}
}

View File

@ -1,192 +0,0 @@
/**
* Copyright (C) 2013-2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.transport;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.push.TextSecureMessageSenderFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.push.PushAddress;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.SendReq;
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
public class PushTransport extends BaseTransport {
private static final String TAG = PushTransport.class.getSimpleName();
private final Context context;
private final MasterSecret masterSecret;
public PushTransport(Context context, MasterSecret masterSecret) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
}
public void deliver(SmsMessageRecord message)
throws IOException, UntrustedIdentityException
{
try {
PushAddress address = getPushAddress(message.getIndividualRecipient());
TextSecureMessageSender messageSender = TextSecureMessageSenderFactory.create(context, masterSecret);
if (message.isEndSession()) {
messageSender.sendMessage(address, new TextSecureMessage(message.getDateSent(), null,
null, null, true, true));
} else {
messageSender.sendMessage(address, new TextSecureMessage(message.getDateSent(), null,
message.getBody().getBody()));
}
context.sendBroadcast(constructSentIntent(context, message.getId(), message.getType(), true, true));
} catch (InvalidNumberException e) {
Log.w(TAG, e);
throw new IOException("Badly formatted number.");
}
}
public void deliverGroupMessage(SendReq message)
throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions
{
TextSecureMessageSender messageSender = TextSecureMessageSenderFactory.create(context, masterSecret);
byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString());
Recipients recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
List<PushAddress> addresses = getPushAddresses(recipients);
List<TextSecureAttachment> attachments = getAttachments(message);
if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) ||
MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()))
{
String content = PartParser.getMessageText(message.getBody());
if (content != null && !content.trim().isEmpty()) {
GroupContext groupContext = GroupContext.parseFrom(Base64.decode(content));
TextSecureAttachment avatar = attachments.isEmpty() ? null : attachments.get(0);
TextSecureGroup.Type type = MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()) ? TextSecureGroup.Type.QUIT : TextSecureGroup.Type.UPDATE;
TextSecureGroup group = new TextSecureGroup(type, groupId, groupContext.getName(), groupContext.getMembersList(), avatar);
TextSecureMessage groupMessage = new TextSecureMessage(message.getSentTimestamp(), group, null, null);
messageSender.sendMessage(addresses, groupMessage);
}
} else {
String body = PartParser.getMessageText(message.getBody());
TextSecureGroup group = new TextSecureGroup(groupId);
TextSecureMessage groupMessage = new TextSecureMessage(message.getSentTimestamp(), group, attachments, body);
messageSender.sendMessage(addresses, groupMessage);
}
}
public void deliver(SendReq message)
throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions
{
TextSecureMessageSender messageSender = TextSecureMessageSenderFactory.create(context, masterSecret);
String destination = message.getTo()[0].getString();
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
if (GroupUtil.isEncodedGroup(destination)) {
deliverGroupMessage(message);
return;
}
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
PushAddress address = getPushAddress(recipients.getPrimaryRecipient());
List<TextSecureAttachment> attachments = getAttachments(message);
String body = PartParser.getMessageText(message.getBody());
TextSecureMessage mediaMessage = new TextSecureMessage(message.getSentTimestamp(), attachments, body);
messageSender.sendMessage(address, mediaMessage);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
untrustedIdentities.add(e);
} catch (UnregisteredUserException e) {
Log.w(TAG, e);
unregisteredUsers.add(e);
}
if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty()) {
throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers);
}
}
private PushAddress getPushAddress(Recipient recipient) throws InvalidNumberException {
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
String relay = Directory.getInstance(context).getRelay(e164number);
return new PushAddress(recipient.getRecipientId(), e164number, 1, relay);
}
private List<PushAddress> getPushAddresses(Recipients recipients) throws InvalidNumberException {
List<PushAddress> addresses = new LinkedList<>();
for (Recipient recipient : recipients.getRecipientsList()) {
addresses.add(getPushAddress(recipient));
}
return addresses;
}
private List<TextSecureAttachment> getAttachments(SendReq message) {
List<TextSecureAttachment> attachments = new LinkedList<>();
for (int i=0;i<message.getBody().getPartsNum();i++) {
String contentType = Util.toIsoString(message.getBody().getPart(i).getContentType());
if (ContentType.isImageType(contentType) ||
ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType))
{
byte[] data = message.getBody().getPart(i).getData();
Log.w(TAG, "Adding attachment...");
attachments.add(new TextSecureAttachmentStream(new ByteArrayInputStream(data), contentType, data.length));
}
}
return attachments;
}
}

View File

@ -1,174 +0,0 @@
/**
* 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
* 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.transport;
import android.app.PendingIntent;
import android.content.Context;
import android.telephony.SmsManager;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SmsCipher;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.NoSessionException;
import java.util.ArrayList;
public class SmsTransport extends BaseTransport {
private final Context context;
private final MasterSecret masterSecret;
public SmsTransport(Context context, MasterSecret masterSecret) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
}
public void deliver(SmsMessageRecord message) throws UndeliverableMessageException,
InsecureFallbackApprovalException
{
if (!NumberUtil.isValidSmsOrEmail(message.getIndividualRecipient().getNumber())) {
throw new UndeliverableMessageException("Not a valid SMS destination! " + message.getIndividualRecipient().getNumber());
}
if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) {
deliverSecureMessage(message);
} else {
deliverPlaintextMessage(message);
}
}
private void deliverSecureMessage(SmsMessageRecord message) throws UndeliverableMessageException,
InsecureFallbackApprovalException
{
MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
if (message.isSecure() || message.isEndSession()) {
transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage);
}
ArrayList<String> messages = multipartMessageHandler.divideMessage(transportMessage);
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure());
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
Log.w("SmsTransport", "Secure divide into message parts: " + messages.size());
for (int i=0;i<messages.size();i++) {
// XXX moxie@thoughtcrime.org 1/7/11 -- There's apparently a bug where for some unknown recipients
// and messages, this will throw an NPE. I have no idea why, so I'm just catching it and marking
// the message as a failure. That way at least it doesn't repeatedly crash every time you start
// the app.
// d3sre 12/10/13 -- extended the log file to further analyse the problem
try {
SmsManager.getDefault().sendTextMessage(message.getIndividualRecipient().getNumber(), null, messages.get(i),
sentIntents.get(i),
deliveredIntents == null ? null : deliveredIntents.get(i));
} catch (NullPointerException npe) {
Log.w("SmsTransport", npe);
Log.w("SmsTransport", "Recipient: " + message.getIndividualRecipient().getNumber());
Log.w("SmsTransport", "Message Total Parts/Current: " + messages.size() + "/" + i);
Log.w("SmsTransport", "Message Part Length: " + messages.get(i).getBytes().length);
throw new UndeliverableMessageException(npe);
} catch (IllegalArgumentException iae) {
Log.w("SmsTransport", iae);
throw new UndeliverableMessageException(iae);
}
}
}
private void deliverPlaintextMessage(SmsMessageRecord message)
throws UndeliverableMessageException
{
ArrayList<String> messages = SmsManager.getDefault().divideMessage(message.getBody().getBody());
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false);
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
String recipient = message.getIndividualRecipient().getNumber();
// XXX moxie@thoughtcrime.org 1/7/11 -- There's apparently a bug where for some unknown recipients
// and messages, this will throw an NPE. I have no idea why, so I'm just catching it and marking
// the message as a failure. That way at least it doesn't repeatedly crash every time you start
// the app.
// d3sre 12/10/13 -- extended the log file to further analyse the problem
try {
SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
} catch (NullPointerException npe) {
Log.w("SmsTransport", npe);
Log.w("SmsTransport", "Recipient: " + recipient);
Log.w("SmsTransport", "Message Parts: " + messages.size());
try {
for (int i=0;i<messages.size();i++) {
SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i),
sentIntents.get(i),
deliveredIntents == null ? null : deliveredIntents.get(i));
}
} catch (NullPointerException npe2) {
Log.w("SmsTransport", npe);
throw new UndeliverableMessageException(npe2);
}
}
}
private ArrayList<PendingIntent> constructSentIntents(long messageId, long type,
ArrayList<String> messages, boolean secure)
{
ArrayList<PendingIntent> sentIntents = new ArrayList<>(messages.size());
for (String ignored : messages) {
sentIntents.add(PendingIntent.getBroadcast(context, 0,
constructSentIntent(context, messageId, type, secure, false),
0));
}
return sentIntents;
}
private ArrayList<PendingIntent> constructDeliveredIntents(long messageId, long type, ArrayList<String> messages) {
if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) {
return null;
}
ArrayList<PendingIntent> deliveredIntents = new ArrayList<>(messages.size());
for (String ignored : messages) {
deliveredIntents.add(PendingIntent.getBroadcast(context, 0,
constructDeliveredIntent(context, messageId, type),
0));
}
return deliveredIntents;
}
private OutgoingTextMessage getAsymmetricEncrypt(MasterSecret masterSecret,
OutgoingTextMessage message)
throws InsecureFallbackApprovalException
{
try {
return new SmsCipher(new TextSecureAxolotlStore(context, masterSecret)).encrypt(message);
} catch (NoSessionException e) {
throw new InsecureFallbackApprovalException(e);
}
}
}

View File

@ -1,343 +0,0 @@
/**
* 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
* 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.transport;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.DirectoryUtil;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException;
import ws.com.google.android.mms.pdu.SendReq;
public class UniversalTransport {
private static final String TAG = UniversalTransport.class.getSimpleName();
private final Context context;
private final MasterSecret masterSecret;
private final PushTransport pushTransport;
private final SmsTransport smsTransport;
private final MmsTransport mmsTransport;
public UniversalTransport(Context context, MasterSecret masterSecret) {
this.context = context;
this.masterSecret = masterSecret;
this.pushTransport = new PushTransport(context, masterSecret);
this.smsTransport = new SmsTransport(context, masterSecret);
this.mmsTransport = new MmsTransport(context, masterSecret);
}
public void deliver(SmsMessageRecord message)
throws UndeliverableMessageException, UntrustedIdentityException, RetryLaterException,
SecureFallbackApprovalException, InsecureFallbackApprovalException
{
if (message.isForcedSms()) {
smsTransport.deliver(message);
return;
}
if (!TextSecurePreferences.isPushRegistered(context)) {
deliverDirectSms(message);
return;
}
try {
Recipient recipient = message.getIndividualRecipient();
String number = Util.canonicalizeNumber(context, recipient.getNumber());
if (isPushTransport(number) && !message.isKeyExchange()) {
boolean isSmsFallbackSupported = isSmsFallbackSupported(number);
try {
Log.w(TAG, "Using PUSH as transport...");
pushTransport.deliver(message);
} catch (UnregisteredUserException uue) {
Log.w(TAG, uue);
if (isSmsFallbackSupported) fallbackOrAskApproval(message, number);
else throw new UndeliverableMessageException(uue);
} catch (IOException ioe) {
Log.w(TAG, ioe);
if (isSmsFallbackSupported) fallbackOrAskApproval(message, number);
else throw new RetryLaterException(ioe);
}
} else {
Log.w(TAG, "Using SMS as transport...");
deliverDirectSms(message);
}
} catch (InvalidNumberException e) {
Log.w(TAG, e);
deliverDirectSms(message);
}
}
public MmsSendResult deliver(SendReq mediaMessage)
throws UndeliverableMessageException, RetryLaterException, UntrustedIdentityException,
SecureFallbackApprovalException, InsecureFallbackApprovalException
{
if (MmsDatabase.Types.isForcedSms(mediaMessage.getDatabaseMessageBox())) {
return mmsTransport.deliver(mediaMessage);
}
if (Util.isEmpty(mediaMessage.getTo())) {
return deliverDirectMms(mediaMessage);
}
if (GroupUtil.isEncodedGroup(mediaMessage.getTo()[0].getString())) {
return deliverGroupMessage(mediaMessage);
}
if (!TextSecurePreferences.isPushRegistered(context)) {
return deliverDirectMms(mediaMessage);
}
if (isMultipleRecipients(mediaMessage)) {
return deliverDirectMms(mediaMessage);
}
try {
String destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString());
if (isPushTransport(destination)) {
boolean isSmsFallbackSupported = isSmsFallbackSupported(destination);
try {
Log.w(TAG, "Using GCM as transport...");
pushTransport.deliver(mediaMessage);
return new MmsSendResult("push".getBytes("UTF-8"), 0, true, true);
} catch (IOException ioe) {
Log.w(TAG, ioe);
if (isSmsFallbackSupported) return fallbackOrAskApproval(mediaMessage, destination);
else throw new RetryLaterException(ioe);
} catch (RecipientFormattingException e) {
Log.w(TAG, e);
if (isSmsFallbackSupported) return fallbackOrAskApproval(mediaMessage, destination);
else throw new UndeliverableMessageException(e);
} catch (EncapsulatedExceptions ee) {
Log.w(TAG, ee);
if (!ee.getUnregisteredUserExceptions().isEmpty()) {
if (isSmsFallbackSupported) return mmsTransport.deliver(mediaMessage);
else throw new UndeliverableMessageException(ee);
} else {
throw new UntrustedIdentityException(ee.getUntrustedIdentityExceptions().get(0));
}
}
} else {
Log.w(TAG, "Delivering media message with MMS...");
return deliverDirectMms(mediaMessage);
}
} catch (InvalidNumberException ine) {
Log.w(TAG, ine);
return deliverDirectMms(mediaMessage);
}
}
private MmsSendResult fallbackOrAskApproval(SendReq mediaMessage, String destination)
throws SecureFallbackApprovalException, UndeliverableMessageException, InsecureFallbackApprovalException
{
try {
Recipient recipient = RecipientFactory.getRecipientsFromString(context, destination, false).getPrimaryRecipient();
boolean isSmsFallbackApprovalRequired = isSmsFallbackApprovalRequired(destination);
AxolotlStore axolotlStore = new TextSecureAxolotlStore(context, masterSecret);
if (!isSmsFallbackApprovalRequired) {
Log.w(TAG, "Falling back to MMS");
DatabaseFactory.getMmsDatabase(context).markAsForcedSms(mediaMessage.getDatabaseMessageId());
return mmsTransport.deliver(mediaMessage);
} else if (!axolotlStore.containsSession(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID)) {
Log.w(TAG, "Marking message as pending insecure SMS fallback");
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
} else {
Log.w(TAG, "Marking message as pending secure SMS fallback");
throw new SecureFallbackApprovalException("Pending user approval for fallback secure to SMS");
}
} catch (RecipientFormattingException rfe) {
throw new UndeliverableMessageException(rfe);
}
}
private void fallbackOrAskApproval(SmsMessageRecord smsMessage, String destination)
throws SecureFallbackApprovalException, UndeliverableMessageException, InsecureFallbackApprovalException
{
Recipient recipient = smsMessage.getIndividualRecipient();
boolean isSmsFallbackApprovalRequired = isSmsFallbackApprovalRequired(destination);
AxolotlStore axolotlStore = new TextSecureAxolotlStore(context, masterSecret);
if (!isSmsFallbackApprovalRequired) {
Log.w(TAG, "Falling back to SMS");
DatabaseFactory.getSmsDatabase(context).markAsForcedSms(smsMessage.getId());
smsTransport.deliver(smsMessage);
} else if (!axolotlStore.containsSession(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID)) {
Log.w(TAG, "Marking message as pending insecure fallback.");
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
} else {
Log.w(TAG, "Marking message as pending secure fallback.");
throw new SecureFallbackApprovalException("Pending user approval for fallback to secure SMS");
}
}
private MmsSendResult deliverGroupMessage(SendReq mediaMessage)
throws RetryLaterException, UndeliverableMessageException
{
if (!TextSecurePreferences.isPushRegistered(context)) {
throw new UndeliverableMessageException("Not push registered!");
}
try {
pushTransport.deliver(mediaMessage);
return new MmsSendResult("push".getBytes("UTF-8"), 0, true, true);
} catch (IOException e) {
Log.w(TAG, e);
throw new RetryLaterException(e);
} catch (RecipientFormattingException | InvalidNumberException e) {
throw new UndeliverableMessageException(e);
} catch (EncapsulatedExceptions ee) {
Log.w(TAG, ee);
try {
for (UnregisteredUserException unregistered : ee.getUnregisteredUserExceptions()) {
IncomingGroupMessage quitMessage = IncomingGroupMessage.createForQuit(mediaMessage.getTo()[0].getString(), unregistered.getE164Number());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, quitMessage);
DatabaseFactory.getGroupDatabase(context).remove(GroupUtil.getDecodedId(mediaMessage.getTo()[0].getString()), unregistered.getE164Number());
}
for (UntrustedIdentityException untrusted : ee.getUntrustedIdentityExceptions()) {
IncomingIdentityUpdateMessage identityMessage = IncomingIdentityUpdateMessage.createFor(untrusted.getE164Number(), untrusted.getIdentityKey(), mediaMessage.getTo()[0].getString());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityMessage);
}
return new MmsSendResult("push".getBytes("UTF-8"), 0, true, true);
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
}
}
private void deliverDirectSms(SmsMessageRecord message)
throws InsecureFallbackApprovalException, UndeliverableMessageException
{
if (TextSecurePreferences.isDirectSmsAllowed(context)) {
smsTransport.deliver(message);
} else {
throw new UndeliverableMessageException("Direct SMS delivery is disabled!");
}
}
private MmsSendResult deliverDirectMms(SendReq message)
throws InsecureFallbackApprovalException, UndeliverableMessageException
{
if (TextSecurePreferences.isDirectSmsAllowed(context)) {
return mmsTransport.deliver(message);
} else {
throw new UndeliverableMessageException("Direct MMS delivery is disabled!");
}
}
public boolean isMultipleRecipients(SendReq mediaMessage) {
int recipientCount = 0;
if (mediaMessage.getTo() != null) {
recipientCount += mediaMessage.getTo().length;
}
if (mediaMessage.getCc() != null) {
recipientCount += mediaMessage.getCc().length;
}
if (mediaMessage.getBcc() != null) {
recipientCount += mediaMessage.getBcc().length;
}
return recipientCount > 1;
}
private boolean isSmsFallbackApprovalRequired(String destination) {
return (isSmsFallbackSupported(destination) && TextSecurePreferences.isFallbackSmsAskRequired(context));
}
private boolean isSmsFallbackSupported(String destination) {
if (GroupUtil.isEncodedGroup(destination)) {
return false;
}
if (TextSecurePreferences.isPushRegistered(context) &&
!TextSecurePreferences.isFallbackSmsAllowed(context))
{
return false;
}
Directory directory = Directory.getInstance(context);
return directory.isSmsFallbackSupported(destination);
}
private boolean isPushTransport(String destination) {
if (GroupUtil.isEncodedGroup(destination)) {
return true;
}
Directory directory = Directory.getInstance(context);
try {
return directory.isActiveNumber(destination);
} catch (NotInDirectoryException e) {
try {
PushServiceSocket socket = PushServiceSocketFactory.create(context);
String contactToken = DirectoryUtil.getDirectoryServerToken(destination);
ContactTokenDetails registeredUser = socket.getContactTokenDetails(contactToken);
if (registeredUser == null) {
registeredUser = new ContactTokenDetails();
registeredUser.setNumber(destination);
directory.setNumber(registeredUser, false);
return false;
} else {
registeredUser.setNumber(destination);
directory.setNumber(registeredUser, true);
return true;
}
} catch (IOException e1) {
Log.w(TAG, e1);
return false;
}
}
}
}