From cafe03a70ac1e5565063e10142d0b2513f7b9358 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sat, 8 Nov 2014 11:35:58 -0800 Subject: [PATCH] Transition the outbound pipeline to JobManager jobs. --- .../whispersystems/jobqueue/JobManager.java | 17 +- .../NetworkRequirementProvider.java | 5 + .../requirements/RequirementProvider.java | 1 + .../securesms/ConversationActivity.java | 100 +++-- .../securesms/ConversationFragment.java | 14 +- .../securesms/ConversationItem.java | 20 +- .../securesms/ConversationListActivity.java | 15 +- .../securesms/GroupCreateActivity.java | 9 +- .../database/EncryptingSmsDatabase.java | 17 +- .../securesms/database/MmsDatabase.java | 54 +-- .../database/NoSuchMessageException.java | 6 + .../securesms/database/PushDatabase.java | 6 - .../securesms/database/SmsDatabase.java | 37 +- .../securesms/jobs/MmsDownloadJob.java | 10 +- .../MmsSendJob.java} | 124 ++++-- .../securesms/jobs/PushDecryptJob.java | 18 +- .../securesms/jobs/PushGroupSendJob.java | 148 +++++++ .../securesms/jobs/PushMediaSendJob.java | 150 ++++++++ .../securesms/jobs/PushSendJob.java | 98 +++++ .../securesms/jobs/PushTextSendJob.java | 148 +++++++ .../securesms/jobs/SmsDecryptJob.java | 17 +- .../securesms/jobs/SmsSendJob.java | 213 +++++++++- .../securesms/jobs/SmsSentJob.java | 109 ++++++ .../MasterSecretRequirementProvider.java | 5 + .../jobs/requirements/ServiceRequirement.java | 41 ++ .../ServiceRequirementProvider.java | 61 +++ .../thoughtcrime/securesms/mms/SlideDeck.java | 7 +- .../securesms/service/MmsSender.java | 136 ------- .../securesms/service/SendReceiveService.java | 363 ------------------ .../service/SmsDeliveryListener.java | 47 ++- .../securesms/service/SmsSender.java | 194 ---------- .../service/SystemStateListener.java | 95 ----- .../securesms/sms/MessageSender.java | 285 ++++++++++---- .../securesms/sms/TelephonyServiceState.java | 92 +++++ .../securesms/transport/BaseTransport.java | 36 -- .../securesms/transport/PushTransport.java | 192 --------- .../securesms/transport/SmsTransport.java | 174 --------- .../transport/UniversalTransport.java | 343 ----------------- 38 files changed, 1590 insertions(+), 1817 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/database/NoSuchMessageException.java rename src/org/thoughtcrime/securesms/{transport/MmsTransport.java => jobs/MmsSendJob.java} (57%) create mode 100644 src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java create mode 100644 src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java create mode 100644 src/org/thoughtcrime/securesms/jobs/PushSendJob.java create mode 100644 src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java create mode 100644 src/org/thoughtcrime/securesms/jobs/SmsSentJob.java create mode 100644 src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java create mode 100644 src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirementProvider.java delete mode 100644 src/org/thoughtcrime/securesms/service/MmsSender.java delete mode 100644 src/org/thoughtcrime/securesms/service/SendReceiveService.java delete mode 100644 src/org/thoughtcrime/securesms/service/SmsSender.java delete mode 100644 src/org/thoughtcrime/securesms/service/SystemStateListener.java create mode 100644 src/org/thoughtcrime/securesms/sms/TelephonyServiceState.java delete mode 100644 src/org/thoughtcrime/securesms/transport/BaseTransport.java delete mode 100644 src/org/thoughtcrime/securesms/transport/PushTransport.java delete mode 100644 src/org/thoughtcrime/securesms/transport/SmsTransport.java delete mode 100644 src/org/thoughtcrime/securesms/transport/UniversalTransport.java diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/JobManager.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobManager.java index 9a78907505..69d6536235 100644 --- a/jobqueue/src/main/java/org/whispersystems/jobqueue/JobManager.java +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/JobManager.java @@ -36,13 +36,16 @@ public class JobManager implements RequirementListener { private final Executor eventExecutor = Executors.newSingleThreadExecutor(); private final AtomicBoolean hasLoadedEncrypted = new AtomicBoolean(false); - private final PersistentStorage persistentStorage; + private final PersistentStorage persistentStorage; + private final List requirementProviders; public JobManager(Context context, String name, List requirementProviders, 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)); 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) { if (hasLoadedEncrypted.compareAndSet(false, true)) { eventExecutor.execute(new LoadTask(keys)); diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirementProvider.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirementProvider.java index 60d375c271..a15c58cd75 100644 --- a/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirementProvider.java +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/NetworkRequirementProvider.java @@ -46,6 +46,11 @@ public class NetworkRequirementProvider implements RequirementProvider { }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } + @Override + public String getName() { + return "network"; + } + @Override public void setListener(RequirementListener listener) { this.listener = listener; diff --git a/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementProvider.java b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementProvider.java index 8389092374..cb3dc281b4 100644 --- a/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementProvider.java +++ b/jobqueue/src/main/java/org/whispersystems/jobqueue/requirements/RequirementProvider.java @@ -17,5 +17,6 @@ package org.whispersystems.jobqueue.requirements; public interface RequirementProvider { + public String getName(); public void setListener(RequirementListener listener); } diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index c25a0afd76..618416ac6e 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -64,7 +64,10 @@ import org.thoughtcrime.securesms.components.EmojiToggle; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; 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.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DraftDatabase; 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.whispersystems.libaxolotl.InvalidMessageException; 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.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.whispersystems.textsecure.util.Util; import java.io.IOException; import java.util.LinkedList; import java.util.List; -import ws.com.google.android.mms.MmsException; - import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; @@ -420,14 +418,22 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override public void onClick(DialogInterface dialog, int which) { if (isSingleConversation()) { - ConversationActivity self = ConversationActivity.this; + final Context context = getApplicationContext(); OutgoingEndSessionMessage endSessionMessage = new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE")); - long allocatedThreadId = MessageSender.send(self, masterSecret, endSessionMessage, threadId, false); + new AsyncTask() { + @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) { Log.w(TAG, e); 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) { try { attachmentManager.setImage(imageUri); - } catch (IOException 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) { + } catch (IOException | BitmapDecodingException e) { Log.w(TAG, e); attachmentManager.clear(); 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 getDraftsForCurrentState() { - List drafts = new LinkedList(); + List drafts = new LinkedList<>(); if (!Util.isEmpty(composeText)) { drafts.add(new Draft(Draft.TEXT, composeText.getText().toString())); @@ -1032,43 +1030,38 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity }.execute(threadId); } - private void sendComplete(Recipients recipients, long threadId, boolean refreshFragment) { - attachmentManager.clear(); - composeText.setText(""); - - this.recipients = recipients; - this.threadId = threadId; + private void sendComplete(long threadId) { + boolean refreshFragment = (threadId != this.threadId); + this.threadId = threadId; ConversationFragment fragment = (ConversationFragment) getSupportFragmentManager() - .findFragmentById(R.id.fragment_content); + .findFragmentById(R.id.fragment_content); + if (refreshFragment) { fragment.reload(recipients, threadId); initializeTitleBar(); initializeSecurity(); } + fragment.scrollToBottom(); } + private void sendMessage(boolean forcePlaintext, boolean forceSms) { try { - Recipients recipients = getRecipients(); + final Recipients recipients = getRecipients(); if (recipients == null) throw new RecipientFormattingException("Badly formatted"); - long allocatedThreadId; - if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) { handleManualMmsRequired(); - return; } else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) { - allocatedThreadId = sendMediaMessage(forcePlaintext, forceSms); + sendMediaMessage(forcePlaintext, forceSms); } else { - allocatedThreadId = sendTextMessage(forcePlaintext, forceSms); + sendTextMessage(forcePlaintext, forceSms); } - - sendComplete(recipients, allocatedThreadId, allocatedThreadId != this.threadId); } catch (RecipientFormattingException ex) { Toast.makeText(ConversationActivity.this, 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.LENGTH_SHORT).show(); Log.w(TAG, ex); - } catch (MmsException e) { - Log.w(TAG, e); } } - private long sendMediaMessage(boolean forcePlaintext, boolean forceSms) - throws InvalidMessageException, MmsException + private void sendMediaMessage(boolean forcePlaintext, final boolean forceSms) + throws InvalidMessageException { + final Context context = getApplicationContext(); SlideDeck slideDeck; - if (attachmentManager.isAttachmentPresent()) slideDeck = attachmentManager.getSlideDeck(); + if (attachmentManager.isAttachmentPresent()) slideDeck = new SlideDeck(attachmentManager.getSlideDeck()); else slideDeck = new SlideDeck(); OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(this, recipients, slideDeck, @@ -1098,12 +1090,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessage); } - return MessageSender.send(this, masterSecret, outgoingMessage, threadId, forceSms); + attachmentManager.clear(); + composeText.setText(""); + + new AsyncTask() { + @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 { + final Context context = getApplicationContext(); OutgoingTextMessage message; if (isEncryptedConversation && !forcePlaintext) { @@ -1112,9 +1118,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity message = new OutgoingTextMessage(recipients, getMessage()); } - Log.w(TAG, "Sending message..."); + this.composeText.setText(""); - return MessageSender.send(ConversationActivity.this, masterSecret, message, threadId, forceSms); + new AsyncTask() { + @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); } diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index ec368a9ede..9267eada9f 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.ListFragment; @@ -241,10 +242,15 @@ public class ConversationFragment extends ListFragment startActivity(composeIntent); } - private void handleResendMessage(MessageRecord message) { - long messageId = message.getId(); - final Activity activity = getActivity(); - MessageSender.resend(activity, messageId, message.isMms()); + private void handleResendMessage(final MessageRecord message) { + final Context context = getActivity().getApplicationContext(); + new AsyncTask() { + @Override + protected Void doInBackground(MessageRecord... messageRecords) { + MessageSender.resend(context, masterSecret, messageRecords[0]); + return null; + } + }.execute(message); } private void handleSaveAttachment(final MediaMmsMessageRecord message) { diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index 33ade9222d..edd2feb3e4 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -46,10 +46,11 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; 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.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.Emoji; @@ -561,6 +562,10 @@ public class ConversationItem extends LinearLayout { } database.markAsOutbox(messageRecord.getId()); database.markAsForcedSms(messageRecord.getId()); + + ApplicationContext.getInstance(context) + .getJobManager() + .add(new MmsSendJob(context, messageRecord.getId())); } else { SmsDatabase database = DatabaseFactory.getSmsDatabase(context); if (messageRecord.isPendingInsecureSmsFallback()) { @@ -568,15 +573,12 @@ public class ConversationItem extends LinearLayout { } database.markAsOutbox(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); } }); diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index 91bf261731..56375d6cbf 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -38,18 +38,17 @@ import android.widget.AdapterView; import android.widget.ListView; 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.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.crypto.MasterSecret; public class ConversationListActivity extends PassphraseRequiredActionBarActivity @@ -72,7 +71,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit getSupportActionBar().setTitle(R.string.app_name); - initializeSenderReceiverService(); initializeResources(); initializeContactUpdatesReceiver(); @@ -247,15 +245,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit 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() { this.masterSecret = getIntent().getParcelableExtra("master_secret"); diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index b734386168..4451b541ab 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.whispersystems.textsecure.directory.Directory; import org.whispersystems.textsecure.directory.NotInDirectoryException; import org.whispersystems.textsecure.util.InvalidNumberException; +import org.whispersystems.textsecure.util.ListenableFutureTask; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; @@ -76,6 +77,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutionException; import ws.com.google.android.mms.MmsException; @@ -443,7 +445,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity { private Pair handlePushOperation(byte[] groupId, String groupName, byte[] avatar, Set e164numbers) - throws MmsException, InvalidNumberException + throws InvalidNumberException { try { @@ -460,12 +462,9 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity { OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatar); long threadId = MessageSender.send(this, masterSecret, outgoingMessage, -1, false); - return new Pair(threadId, groupRecipient); + return new Pair<>(threadId, groupRecipient); } catch (RecipientFormattingException e) { throw new AssertionError(e); - } catch (MmsException e) { - Log.w(TAG, e); - throw new MmsException(e); } } diff --git a/src/org/thoughtcrime/securesms/database/EncryptingSmsDatabase.java b/src/org/thoughtcrime/securesms/database/EncryptingSmsDatabase.java index c4285829ab..ca2cf59d2c 100644 --- a/src/org/thoughtcrime/securesms/database/EncryptingSmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/EncryptingSmsDatabase.java @@ -25,6 +25,7 @@ import android.util.Pair; import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher; import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret; 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.OutgoingTextMessage; import org.thoughtcrime.securesms.util.LRUCache; @@ -58,8 +59,8 @@ public class EncryptingSmsDatabase extends SmsDatabase { return ciphertext; } - public List insertMessageOutbox(MasterSecret masterSecret, long threadId, - OutgoingTextMessage message, boolean forceSms) + public long insertMessageOutbox(MasterSecret masterSecret, long threadId, + OutgoingTextMessage message, boolean forceSms) { long type = Types.BASE_OUTBOX_TYPE; message = message.withBody(getEncryptedBody(masterSecret, message.getMessageBody())); @@ -120,9 +121,15 @@ public class EncryptingSmsDatabase extends SmsDatabase { return new DecryptingReader(masterSecret, cursor); } - public Reader getMessage(MasterSecret masterSecret, long messageId) { - Cursor cursor = super.getMessage(messageId); - return new DecryptingReader(masterSecret, cursor); + public SmsMessageRecord getMessage(MasterSecret masterSecret, long messageId) throws NoSuchMessageException { + Cursor cursor = super.getMessage(messageId); + 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) { diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index f8271df5e8..0710f16115 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -456,39 +456,22 @@ public class MmsDatabase extends Database implements MmsSmsColumns { } } - public SendReq[] getOutgoingMessages(MasterSecret masterSecret, long messageId) - throws MmsException + public SendReq getOutgoingMessage(MasterSecret masterSecret, long messageId) + throws MmsException, NoSuchMessageException { MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context); PartDatabase partDatabase = getPartDatabase(masterSecret); SQLiteDatabase database = databaseHelper.getReadableDatabase(); - MasterCipher masterCipher = masterSecret == null ? null : new MasterCipher(masterSecret); + MasterCipher masterCipher = new MasterCipher(masterSecret); Cursor cursor = null; - - String selection; - 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; - } + String selection = ID_WHERE; + String[] selectionArgs = new String[]{String.valueOf(messageId)}; try { cursor = database.query(TABLE_NAME, MMS_PROJECTION, selection, selectionArgs, null, null, null); - if (cursor == null || cursor.getCount() == 0) - return new SendReq[0]; - - SendReq[] requests = new SendReq[cursor.getCount()]; - int i = 0; - - while (cursor.moveToNext()) { - messageId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); - + if (cursor != null && cursor.moveToNext()) { long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)); String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)); @@ -507,10 +490,10 @@ public class MmsDatabase extends Database implements MmsSmsColumns { 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 { if (cursor != null) cursor.close(); @@ -527,17 +510,20 @@ public class MmsDatabase extends Database implements MmsSmsColumns { } 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); - contentValues.put(THREAD_ID, getThreadIdForMessage(messageId)); - contentValues.put(READ, 1); - contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT)); - - return insertMediaMessage(masterSecret, request[0].getPduHeaders(), - request[0].getBody(), contentValues); + return insertMediaMessage(masterSecret, request.getPduHeaders(), + request.getBody(), contentValues); + } catch (NoSuchMessageException e) { + throw new MmsException(e); + } } private Pair insertMessageInbox(MasterSecret masterSecret, IncomingMediaMessage retrieved, diff --git a/src/org/thoughtcrime/securesms/database/NoSuchMessageException.java b/src/org/thoughtcrime/securesms/database/NoSuchMessageException.java new file mode 100644 index 0000000000..930920b01a --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/NoSuchMessageException.java @@ -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);} +} diff --git a/src/org/thoughtcrime/securesms/database/PushDatabase.java b/src/org/thoughtcrime/securesms/database/PushDatabase.java index b69c1af35a..9611970268 100644 --- a/src/org/thoughtcrime/securesms/database/PushDatabase.java +++ b/src/org/thoughtcrime/securesms/database/PushDatabase.java @@ -108,10 +108,4 @@ public class PushDatabase extends Database { this.cursor.close(); } } - - public static class NoSuchMessageException extends Exception { - public NoSuchMessageException(String s) {super(s);} - public NoSuchMessageException(Exception e) {super(e);} - } - } diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index 5b0010cf39..a3a4a4f213 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -428,36 +428,33 @@ public class SmsDatabase extends Database implements MmsSmsColumns { return insertMessageInbox(message, Types.BASE_INBOX_TYPE); } - protected List insertMessageOutbox(long threadId, OutgoingTextMessage message, - long type, boolean forceSms) + protected long insertMessageOutbox(long threadId, OutgoingTextMessage message, + long type, boolean forceSms) { if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT; else if (message.isSecureMessage()) type |= Types.SECURE_MESSAGE_BIT; else if (message.isEndSession()) type |= Types.END_SESSION_BIT; if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; - long date = System.currentTimeMillis(); - List messageIds = new LinkedList(); + long date = System.currentTimeMillis(); - for (Recipient recipient : message.getRecipients().getRecipientsList()) { - ContentValues contentValues = new ContentValues(6); - contentValues.put(ADDRESS, PhoneNumberUtils.formatNumber(recipient.getNumber())); - contentValues.put(THREAD_ID, threadId); - contentValues.put(BODY, message.getMessageBody()); - contentValues.put(DATE_RECEIVED, date); - contentValues.put(DATE_SENT, date); - contentValues.put(READ, 1); - contentValues.put(TYPE, type); + ContentValues contentValues = new ContentValues(6); + contentValues.put(ADDRESS, PhoneNumberUtils.formatNumber(message.getRecipients().getPrimaryRecipient().getNumber())); + contentValues.put(THREAD_ID, threadId); + contentValues.put(BODY, message.getMessageBody()); + contentValues.put(DATE_RECEIVED, date); + contentValues.put(DATE_SENT, date); + contentValues.put(READ, 1); + contentValues.put(TYPE, type); - SQLiteDatabase db = databaseHelper.getWritableDatabase(); - messageIds.add(db.insert(TABLE_NAME, ADDRESS, contentValues)); + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues); - DatabaseFactory.getThreadDatabase(context).update(threadId); - notifyConversationListeners(threadId); - Trimmer.trimThread(context, threadId); - } + DatabaseFactory.getThreadDatabase(context).update(threadId); + notifyConversationListeners(threadId); + Trimmer.trimThread(context, threadId); - return messageIds; + return messageId; } Cursor getMessages(int skip, int limit) { diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 66a00cbd47..8f4c5a5d93 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -57,7 +57,7 @@ public class MmsDownloadJob extends MasterSecretJob { .withPersistence() .withRequirement(new MasterSecretRequirement(context)) .withRequirement(new NetworkRequirement(context)) - .withGroupId("mms-download") + .withGroupId("mms-operation") .create()); this.messageId = messageId; @@ -170,7 +170,13 @@ public class MmsDownloadJob extends MasterSecretJob { @Override 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 diff --git a/src/org/thoughtcrime/securesms/transport/MmsTransport.java b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java similarity index 57% rename from src/org/thoughtcrime/securesms/transport/MmsTransport.java rename to src/org/thoughtcrime/securesms/jobs/MmsSendJob.java index f8c3ee17d1..8db1de800f 100644 --- a/src/org/thoughtcrime/securesms/transport/MmsTransport.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java @@ -1,21 +1,4 @@ -/** - * 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 . - */ - -package org.thoughtcrime.securesms.transport; +package org.thoughtcrime.securesms.jobs; import android.content.Context; import android.telephony.TelephonyManager; @@ -24,51 +7,108 @@ import android.util.Log; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MmsCipher; 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.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.MmsRadio; import org.thoughtcrime.securesms.mms.MmsRadioException; import org.thoughtcrime.securesms.mms.MmsSendResult; import org.thoughtcrime.securesms.mms.OutgoingMmsConnection; +import org.thoughtcrime.securesms.notifications.MessageNotifier; 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.whispersystems.jobqueue.JobParameters; +import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.libaxolotl.NoSessionException; import org.whispersystems.textsecure.util.Hex; import java.io.IOException; 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.PduComposer; import ws.com.google.android.mms.pdu.PduHeaders; import ws.com.google.android.mms.pdu.SendConf; 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 MasterSecret masterSecret; - private final MmsRadio radio; + private final long messageId; - public MmsTransport(Context context, MasterSecret masterSecret) { - this.context = context; - this.masterSecret = masterSecret; - this.radio = MmsRadio.getInstance(context); + public MmsSendJob(Context context, long messageId) { + super(context, JobParameters.newBuilder() + .withGroupId("mms-operation") + .withRequirement(new NetworkRequirement(context)) + .withRequirement(new MasterSecretRequirement(context)) + .withPersistence() + .create()); + + this.messageId = messageId; } - public MmsSendResult deliver(SendReq message) throws UndeliverableMessageException, - InsecureFallbackApprovalException + @Override + 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); + MmsRadio radio = MmsRadio.getInstance(context); + try { if (isCdmaDevice()) { Log.w(TAG, "Sending MMS directly without radio change..."); try { - return sendMms(message, false, false); + return sendMms(masterSecret, radio, message, false, false); } catch (IOException e) { Log.w(TAG, e); } @@ -78,7 +118,7 @@ public class MmsTransport { radio.connect(); try { - MmsSendResult result = sendMms(message, true, true); + MmsSendResult result = sendMms(masterSecret, radio, message, true, true); radio.disconnect(); return result; } catch (IOException e) { @@ -88,7 +128,7 @@ public class MmsTransport { Log.w(TAG, "Sending MMS with radio change and without proxy..."); try { - MmsSendResult result = sendMms(message, true, false); + MmsSendResult result = sendMms(masterSecret, radio, message, true, false); radio.disconnect(); return result; } 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 { String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number(); boolean upgradedSecure = false; if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) { - message = getEncryptedMessage(message); + message = getEncryptedMessage(masterSecret, message); 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 { MmsCipher cipher = new MmsCipher(new TextSecureAxolotlStore(context, masterSecret)); return cipher.encrypt(context, pdu); @@ -166,7 +209,7 @@ public class MmsTransport { private void validateDestination(EncodedStringValue destination) throws UndeliverableMessageException { if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) { 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); + } + + + } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index ef7df5f4cf..e648c60131 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.groups.GroupMessageProcessor; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; @@ -68,18 +69,13 @@ public class PushDecryptJob extends MasterSecretJob { } @Override - public void onRun() throws RequirementNotMetException { - try { - MasterSecret masterSecret = getMasterSecret(); - PushDatabase database = DatabaseFactory.getPushDatabase(context); - TextSecureEnvelope envelope = database.get(messageId); + public void onRun() throws RequirementNotMetException, NoSuchMessageException { + MasterSecret masterSecret = getMasterSecret(); + PushDatabase database = DatabaseFactory.getPushDatabase(context); + TextSecureEnvelope envelope = database.get(messageId); - handleMessage(masterSecret, envelope); - database.delete(messageId); - - } catch (PushDatabase.NoSuchMessageException e) { - Log.w(TAG, e); - } + handleMessage(masterSecret, envelope); + database.delete(messageId); } @Override diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java new file mode 100644 index 0000000000..35633e2e7e --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -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 addresses = getPushAddresses(recipients); + List 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 getPushAddresses(Recipients recipients) throws InvalidNumberException { + List addresses = new LinkedList<>(); + + for (Recipient recipient : recipients.getRecipientsList()) { + addresses.add(getPushAddress(recipient)); + } + + return addresses; + } + +} diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java new file mode 100644 index 0000000000..a2b08a682f --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -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 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); + } + } + + +} diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java new file mode 100644 index 0000000000..2025796a2d --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -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 getAttachments(SendReq message) { + List attachments = new LinkedList<>(); + + for (int i=0;i messages = multipartMessageHandler.divideMessage(transportMessage); + ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure()); + ArrayList deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages); + + Log.w("SmsTransport", "Secure divide into message parts: " + messages.size()); + + for (int i=0;i messages = SmsManager.getDefault().divideMessage(message.getBody().getBody()); + ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false); + ArrayList 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 constructSentIntents(long messageId, long type, + ArrayList messages, boolean secure) + { + ArrayList 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 constructDeliveredIntents(long messageId, long type, ArrayList messages) { + if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) { + return null; + } + + ArrayList 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; + } + + } diff --git a/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java b/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java new file mode 100644 index 0000000000..7bdfe32b50 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/SmsSentJob.java @@ -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); + } + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirementProvider.java b/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirementProvider.java index f4f1103d81..e33d25e76f 100644 --- a/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirementProvider.java +++ b/src/org/thoughtcrime/securesms/jobs/requirements/MasterSecretRequirementProvider.java @@ -29,6 +29,11 @@ public class MasterSecretRequirementProvider implements RequirementProvider { context.registerReceiver(newKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null); } + @Override + public String getName() { + return "master_secret"; + } + @Override public void setListener(RequirementListener listener) { this.listener = listener; diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java new file mode 100644 index 0000000000..51fc24e685 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirement.java @@ -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); + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirementProvider.java b/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirementProvider.java new file mode 100644 index 0000000000..58abd0675f --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/requirements/ServiceRequirementProvider.java @@ -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(); + } + } + } +} diff --git a/src/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/org/thoughtcrime/securesms/mms/SlideDeck.java index e9cb21c7bf..3bcb64d92a 100644 --- a/src/org/thoughtcrime/securesms/mms/SlideDeck.java +++ b/src/org/thoughtcrime/securesms/mms/SlideDeck.java @@ -33,7 +33,12 @@ import ws.com.google.android.mms.pdu.PduBody; import ws.com.google.android.mms.pdu.PduPart; public class SlideDeck { - private final List slides = new LinkedList(); + + private final List slides = new LinkedList<>(); + + public SlideDeck(SlideDeck copy) { + this.slides.addAll(copy.getSlides()); + } public SlideDeck(Context context, MasterSecret masterSecret, PduBody body) { try { diff --git a/src/org/thoughtcrime/securesms/service/MmsSender.java b/src/org/thoughtcrime/securesms/service/MmsSender.java deleted file mode 100644 index ffad1f380a..0000000000 --- a/src/org/thoughtcrime/securesms/service/MmsSender.java +++ /dev/null @@ -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 . - */ -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)); - } -} diff --git a/src/org/thoughtcrime/securesms/service/SendReceiveService.java b/src/org/thoughtcrime/securesms/service/SendReceiveService.java deleted file mode 100644 index b4bc8b9445..0000000000 --- a/src/org/thoughtcrime/securesms/service/SendReceiveService.java +++ /dev/null @@ -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 . - */ -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 workQueue; - private List 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(); - workQueue = new LinkedList(); - - 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 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(); - } - } - }; -} diff --git a/src/org/thoughtcrime/securesms/service/SmsDeliveryListener.java b/src/org/thoughtcrime/securesms/service/SmsDeliveryListener.java index 2931639bd5..cf89f0d4e1 100644 --- a/src/org/thoughtcrime/securesms/service/SmsDeliveryListener.java +++ b/src/org/thoughtcrime/securesms/service/SmsDeliveryListener.java @@ -3,19 +3,50 @@ package org.thoughtcrime.securesms.service; import android.content.BroadcastReceiver; import android.content.Context; 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 { + 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 public void onReceive(Context context, Intent intent) { - if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) { - intent.putExtra("ResultCode", this.getResultCode()); - intent.setClass(context, SendReceiveService.class); - context.startService(intent); - } else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) { - intent.putExtra("ResultCode", this.getResultCode()); - intent.setClass(context, SendReceiveService.class); - context.startService(intent); + JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); + long messageId = intent.getLongExtra("message_id", -1); + + switch (intent.getAction()) { + case SENT_SMS_ACTION: + int result = getResultCode(); + + 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()); } } } diff --git a/src/org/thoughtcrime/securesms/service/SmsSender.java b/src/org/thoughtcrime/securesms/service/SmsSender.java deleted file mode 100644 index ea1184cebd..0000000000 --- a/src/org/thoughtcrime/securesms/service/SmsSender.java +++ /dev/null @@ -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 . - */ -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)); - } - -} diff --git a/src/org/thoughtcrime/securesms/service/SystemStateListener.java b/src/org/thoughtcrime/securesms/service/SystemStateListener.java deleted file mode 100644 index 160cba10be..0000000000 --- a/src/org/thoughtcrime/securesms/service/SystemStateListener.java +++ /dev/null @@ -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); - } - } - } - } -} diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index f3b34b8898..a87d05b73d 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -17,106 +17,231 @@ package org.thoughtcrime.securesms.sms; import android.content.Context; -import android.content.Intent; import android.util.Log; import android.util.Pair; +import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; 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.push.PushServiceSocketFactory; +import org.thoughtcrime.securesms.recipients.Recipient; 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.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 java.util.List; +import java.io.IOException; import ws.com.google.android.mms.MmsException; public class MessageSender { - public static long send(Context context, MasterSecret masterSecret, - OutgoingTextMessage message, long threadId, - boolean forceSms) + private static final String TAG = MessageSender.class.getSimpleName(); + + 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) { - threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(message.getRecipients()); - } - - List messageIds = database.insertMessageOutbox(masterSecret, threadId, message, forceSms); - - if (!forceSms && isSelfSend(context, message.getRecipients())) { - for (long messageId : messageIds) { - database.markAsSent(messageId); - database.markAsPush(messageId); - - Pair messageAndThreadId = database.copyMessageInbox(messageId); - database.markAsPush(messageAndThreadId.first); - } + allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); } else { - for (long messageId : messageIds) { - 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); - } + allocatedThreadId = threadId; } - 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, - OutgoingMediaMessage message, - long threadId, boolean forceSms) + public static long send(final Context context, + final MasterSecret masterSecret, + 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 messageAndThreadId = database.copyMessageInbox(messageId); + database.markAsPush(messageAndThreadId.first); + } + + private static void sendMediaSelf(Context context, MasterSecret masterSecret, long messageId) throws MmsException { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + database.markAsSent(messageId, "self-send".getBytes(), 0); + database.markAsPush(messageId); - if (threadId == -1) { - threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(message.getRecipients(), message.getDistributionType()); - } - - 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; + long newMessageId = database.copyMessageInbox(masterSecret, messageId); + database.markAsPush(newMessageId); } - 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; - if (isMms) { - DatabaseFactory.getMmsDatabase(context).markAsSending(messageId); - intent = new Intent(SendReceiveService.SEND_MMS_ACTION, null, - context, SendReceiveService.class); - } else { - DatabaseFactory.getSmsDatabase(context).markAsSending(messageId); - intent = new Intent(SendReceiveService.SEND_SMS_ACTION, null, - context, SendReceiveService.class); + private static void sendMediaPush(Context context, Recipients recipients, long messageId) { + JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); + jobManager.add(new PushMediaSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber())); + } + + private static void sendGroupPush(Context context, Recipients recipients, long messageId) { + JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); + jobManager.add(new PushGroupSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber())); + } + + 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) { @@ -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; + } + } + } + } diff --git a/src/org/thoughtcrime/securesms/sms/TelephonyServiceState.java b/src/org/thoughtcrime/securesms/sms/TelephonyServiceState.java new file mode 100644 index 0000000000..b6cb7e5e2a --- /dev/null +++ b/src/org/thoughtcrime/securesms/sms/TelephonyServiceState.java @@ -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; + } + } +} diff --git a/src/org/thoughtcrime/securesms/transport/BaseTransport.java b/src/org/thoughtcrime/securesms/transport/BaseTransport.java deleted file mode 100644 index 0b25990710..0000000000 --- a/src/org/thoughtcrime/securesms/transport/BaseTransport.java +++ /dev/null @@ -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; - } -} diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java deleted file mode 100644 index c246be9e6c..0000000000 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ /dev/null @@ -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 . - */ - -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 addresses = getPushAddresses(recipients); - List 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 untrustedIdentities = new LinkedList<>(); - List unregisteredUsers = new LinkedList<>(); - - if (GroupUtil.isEncodedGroup(destination)) { - deliverGroupMessage(message); - return; - } - - try { - Recipients recipients = RecipientFactory.getRecipientsFromString(context, destination, false); - PushAddress address = getPushAddress(recipients.getPrimaryRecipient()); - List 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 getPushAddresses(Recipients recipients) throws InvalidNumberException { - List addresses = new LinkedList<>(); - - for (Recipient recipient : recipients.getRecipientsList()) { - addresses.add(getPushAddress(recipient)); - } - - return addresses; - } - - private List getAttachments(SendReq message) { - List attachments = new LinkedList<>(); - - for (int i=0;i. - */ - -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 messages = multipartMessageHandler.divideMessage(transportMessage); - ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure()); - ArrayList deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages); - - Log.w("SmsTransport", "Secure divide into message parts: " + messages.size()); - - for (int i=0;i messages = SmsManager.getDefault().divideMessage(message.getBody().getBody()); - ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false); - ArrayList 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 constructSentIntents(long messageId, long type, - ArrayList messages, boolean secure) - { - ArrayList 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 constructDeliveredIntents(long messageId, long type, ArrayList messages) { - if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) { - return null; - } - - ArrayList 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); - } - } -} diff --git a/src/org/thoughtcrime/securesms/transport/UniversalTransport.java b/src/org/thoughtcrime/securesms/transport/UniversalTransport.java deleted file mode 100644 index aeaa144813..0000000000 --- a/src/org/thoughtcrime/securesms/transport/UniversalTransport.java +++ /dev/null @@ -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 . - */ -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; - } - } - } -}