Transition the outbound pipeline to JobManager jobs.

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

View File

@ -36,13 +36,16 @@ public class JobManager implements RequirementListener {
private final Executor eventExecutor = Executors.newSingleThreadExecutor();
private final AtomicBoolean hasLoadedEncrypted = new AtomicBoolean(false);
private final PersistentStorage persistentStorage;
private final PersistentStorage persistentStorage;
private final List<RequirementProvider> requirementProviders;
public JobManager(Context context, String name,
List<RequirementProvider> 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));

View File

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

View File

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

View File

@ -64,7 +64,10 @@ import org.thoughtcrime.securesms.components.EmojiToggle;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.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<OutgoingEndSessionMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingEndSessionMessage... messages) {
return MessageSender.send(context, masterSecret, messages[0], threadId, false);
}
sendComplete(recipients, allocatedThreadId, allocatedThreadId != self.threadId);
@Override
protected void onPostExecute(Long result) {
sendComplete(result);
}
}.execute(endSessionMessage);
}
}
});
@ -468,9 +474,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} catch (IOException e) {
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<Draft> getDraftsForCurrentState() {
List<Draft> drafts = new LinkedList<Draft>();
List<Draft> 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<OutgoingMediaMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingMediaMessage... messages) {
return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms);
}
@Override
protected void onPostExecute(Long result) {
sendComplete(result);
}
}.execute(outgoingMessage);
}
private long sendTextMessage(boolean forcePlaintext, boolean forceSms)
private void sendTextMessage(boolean forcePlaintext, final boolean forceSms)
throws InvalidMessageException
{
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<OutgoingTextMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingTextMessage... messages) {
return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms);
}
@Override
protected void onPostExecute(Long result) {
sendComplete(result);
}
}.execute(message);
}

View File

@ -6,6 +6,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.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<MessageRecord, Void, Void>() {
@Override
protected Void doInBackground(MessageRecord... messageRecords) {
MessageSender.resend(context, masterSecret, messageRecords[0]);
return null;
}
}.execute(message);
}
private void handleSaveAttachment(final MediaMmsMessageRecord message) {

View File

@ -46,10 +46,11 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.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);
}
});

View File

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

View File

@ -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<Long, Recipients> handlePushOperation(byte[] groupId, String groupName, byte[] avatar,
Set<String> 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<Long, Recipients>(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);
}
}

View File

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

View File

@ -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<Long, Long> insertMessageInbox(MasterSecret masterSecret, IncomingMediaMessage retrieved,

View File

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

View File

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

View File

@ -428,36 +428,33 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE);
}
protected List<Long> 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<Long> messageIds = new LinkedList<Long>();
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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,136 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.service;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.transport.UniversalTransport;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.SendReq;
public class MmsSender {
private final Context context;
private final SystemStateListener systemStateListener;
private final ToastHandler toastHandler;
public MmsSender(Context context, SystemStateListener systemStateListener, ToastHandler toastHandler) {
this.context = context;
this.systemStateListener = systemStateListener;
this.toastHandler = toastHandler;
}
public void process(MasterSecret masterSecret, Intent intent) {
Log.w("MmsSender", "Got intent action: " + intent.getAction());
if (SendReceiveService.SEND_MMS_ACTION.equals(intent.getAction())) {
handleSendMms(masterSecret, intent);
}
}
private void handleSendMms(MasterSecret masterSecret, Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
UniversalTransport transport = new UniversalTransport(context, masterSecret);
try {
SendReq[] messages = database.getOutgoingMessages(masterSecret, messageId);
for (SendReq message : messages) {
long threadId = database.getThreadIdForMessage(message.getDatabaseMessageId());
try {
Log.w("MmsSender", "Passing to MMS transport: " + message.getDatabaseMessageId());
database.markAsSending(message.getDatabaseMessageId());
MmsSendResult result = transport.deliver(message);
if (result.isUpgradedSecure()) database.markAsSecure(message.getDatabaseMessageId());
if (result.isPush()) database.markAsPush(message.getDatabaseMessageId());
database.markAsSent(message.getDatabaseMessageId(), result.getMessageId(),
result.getResponseStatus());
systemStateListener.unregisterForConnectivityChange();
} catch (InsecureFallbackApprovalException ifae) {
Log.w("MmsSender", ifae);
database.markAsPendingInsecureSmsFallback(message.getDatabaseMessageId());
notifyMessageDeliveryFailed(context, threads, threadId);
} catch (SecureFallbackApprovalException sfae) {
Log.w("MmsSender", sfae);
database.markAsPendingSecureSmsFallback(message.getDatabaseMessageId());
notifyMessageDeliveryFailed(context, threads, threadId);
} catch (UndeliverableMessageException e) {
Log.w("MmsSender", e);
database.markAsSentFailed(message.getDatabaseMessageId());
notifyMessageDeliveryFailed(context, threads, threadId);
} catch (UntrustedIdentityException uie) {
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
database.markAsSentFailed(messageId);
} catch (RetryLaterException e) {
Log.w("MmsSender", e);
database.markAsOutbox(message.getDatabaseMessageId());
if (systemStateListener.isConnected()) scheduleQuickRetryAlarm();
else systemStateListener.registerForConnectivityChange();
toastHandler
.obtainMessage(0, context.getString(R.string.SmsReceiver_currently_unable_to_send_your_sms_message))
.sendToTarget();
}
}
} catch (MmsException e) {
Log.w("MmsSender", e);
if (messageId != -1)
database.markAsSentFailed(messageId);
}
}
private static void notifyMessageDeliveryFailed(Context context, ThreadDatabase threads, long threadId) {
Recipients recipients = threads.getRecipientsForThreadId(threadId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
}
private void scheduleQuickRetryAlarm() {
((AlarmManager)context.getSystemService(Context.ALARM_SERVICE))
.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (30 * 1000),
PendingIntent.getService(context, 0,
new Intent(SendReceiveService.SEND_MMS_ACTION,
null, context, SendReceiveService.class),
PendingIntent.FLAG_UPDATE_CURRENT));
}
}

View File

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

View File

@ -3,19 +3,50 @@ package org.thoughtcrime.securesms.service;
import android.content.BroadcastReceiver;
import android.content.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());
}
}
}

View File

@ -1,194 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.service;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.transport.UniversalTransport;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
public class SmsSender {
private final Context context;
private final SystemStateListener systemStateListener;
private final ToastHandler toastHandler;
public SmsSender(Context context, SystemStateListener systemStateListener, ToastHandler toastHandler) {
this.context = context;
this.systemStateListener = systemStateListener;
this.toastHandler = toastHandler;
}
public void process(MasterSecret masterSecret, Intent intent) {
if (SendReceiveService.SEND_SMS_ACTION.equals(intent.getAction())) {
handleSendMessage(masterSecret, intent);
} else if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) {
handleSentMessage(masterSecret, intent);
} else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) {
handleDeliveredMessage(intent);
}
}
private void handleSendMessage(MasterSecret masterSecret, Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
UniversalTransport transport = new UniversalTransport(context, masterSecret);
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
EncryptingSmsDatabase.Reader reader = null;
SmsMessageRecord record;
Log.w("SmsSender", "Sending message: " + messageId);
try {
if (messageId != -1) reader = database.getMessage(masterSecret, messageId);
else reader = database.getOutgoingMessages(masterSecret);
while (reader != null && (record = reader.getNext()) != null) {
try {
database.markAsSending(record.getId());
transport.deliver(record);
} catch (InsecureFallbackApprovalException ifae) {
Log.w("SmsSender", ifae);
DatabaseFactory.getSmsDatabase(context).markAsPendingInsecureSmsFallback(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (SecureFallbackApprovalException sfae) {
Log.w("SmsSender", sfae);
DatabaseFactory.getSmsDatabase(context).markAsPendingSecureSmsFallback(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (UntrustedIdentityException e) {
Log.w("SmsSender", e);
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(e.getE164Number(), e.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId());
} catch (UndeliverableMessageException ude) {
Log.w("SmsSender", ude);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId());
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (RetryLaterException rle) {
Log.w("SmsSender", rle);
DatabaseFactory.getSmsDatabase(context).markAsOutbox(record.getId());
if (systemStateListener.isConnected()) scheduleQuickRetryAlarm();
else systemStateListener.registerForConnectivityChange();
}
}
} finally {
if (reader != null)
reader.close();
}
}
private void handleSentMessage(MasterSecret masterSecret, Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
int result = intent.getIntExtra("ResultCode", -31337);
boolean upgraded = intent.getBooleanExtra("upgraded", false);
boolean push = intent.getBooleanExtra("push", false);
Log.w("SMSReceiverService", "Intent resultcode: " + result);
Log.w("SMSReceiverService", "Running sent callback: " + messageId);
if (result == Activity.RESULT_OK) {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
Cursor cursor = database.getMessage(messageId);
SmsDatabase.Reader reader = database.readerFor(cursor);
if (push) database.markAsPush(messageId);
if (upgraded) database.markAsSecure(messageId);
database.markAsSent(messageId);
SmsMessageRecord record = reader.getNext();
if (record != null && record.isEndSession()) {
Log.w("SmsSender", "Ending session...");
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
sessionStore.deleteAllSessions(record.getIndividualRecipient().getRecipientId());
SecurityEvent.broadcastSecurityUpdateEvent(context, record.getThreadId());
}
unregisterForRadioChanges();
} else if (result == SmsManager.RESULT_ERROR_NO_SERVICE || result == SmsManager.RESULT_ERROR_RADIO_OFF) {
DatabaseFactory.getSmsDatabase(context).markAsOutbox(messageId);
toastHandler
.obtainMessage(0, context.getString(R.string.SmsReceiver_currently_unable_to_send_your_sms_message))
.sendToTarget();
registerForRadioChanges();
} else {
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
unregisterForRadioChanges();
}
}
private void handleDeliveredMessage(Intent intent) {
long messageId = intent.getLongExtra("message_id", -1);
byte[] pdu = intent.getByteArrayExtra("pdu");
SmsMessage message = SmsMessage.createFromPdu(pdu);
if (message == null) {
return;
}
DatabaseFactory.getSmsDatabase(context).markStatus(messageId, message.getStatus());
}
private void registerForRadioChanges() {
if (systemStateListener.isConnected()) systemStateListener.registerForRadioChange();
else systemStateListener.registerForConnectivityChange();
}
private void unregisterForRadioChanges() {
systemStateListener.unregisterForConnectivityChange();
}
private void scheduleQuickRetryAlarm() {
((AlarmManager)context.getSystemService(Context.ALARM_SERVICE))
.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (30 * 1000),
PendingIntent.getService(context, 0,
new Intent(SendReceiveService.SEND_SMS_ACTION,
null, context, SendReceiveService.class),
PendingIntent.FLAG_UPDATE_CURRENT));
}
}

View File

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

View File

@ -17,106 +17,231 @@
package org.thoughtcrime.securesms.sms;
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<Long> messageIds = database.insertMessageOutbox(masterSecret, threadId, message, forceSms);
if (!forceSms && isSelfSend(context, message.getRecipients())) {
for (long messageId : messageIds) {
database.markAsSent(messageId);
database.markAsPush(messageId);
Pair<Long, Long> messageAndThreadId = database.copyMessageInbox(messageId);
database.markAsPush(messageAndThreadId.first);
}
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<Long, Long> 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;
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,174 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.transport;
import android.app.PendingIntent;
import android.content.Context;
import android.telephony.SmsManager;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SmsCipher;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.NoSessionException;
import java.util.ArrayList;
public class SmsTransport extends BaseTransport {
private final Context context;
private final MasterSecret masterSecret;
public SmsTransport(Context context, MasterSecret masterSecret) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
}
public void deliver(SmsMessageRecord message) throws UndeliverableMessageException,
InsecureFallbackApprovalException
{
if (!NumberUtil.isValidSmsOrEmail(message.getIndividualRecipient().getNumber())) {
throw new UndeliverableMessageException("Not a valid SMS destination! " + message.getIndividualRecipient().getNumber());
}
if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) {
deliverSecureMessage(message);
} else {
deliverPlaintextMessage(message);
}
}
private void deliverSecureMessage(SmsMessageRecord message) throws UndeliverableMessageException,
InsecureFallbackApprovalException
{
MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
if (message.isSecure() || message.isEndSession()) {
transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage);
}
ArrayList<String> messages = multipartMessageHandler.divideMessage(transportMessage);
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure());
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
Log.w("SmsTransport", "Secure divide into message parts: " + messages.size());
for (int i=0;i<messages.size();i++) {
// XXX moxie@thoughtcrime.org 1/7/11 -- There's apparently a bug where for some unknown recipients
// and messages, this will throw an NPE. I have no idea why, so I'm just catching it and marking
// the message as a failure. That way at least it doesn't repeatedly crash every time you start
// the app.
// d3sre 12/10/13 -- extended the log file to further analyse the problem
try {
SmsManager.getDefault().sendTextMessage(message.getIndividualRecipient().getNumber(), null, messages.get(i),
sentIntents.get(i),
deliveredIntents == null ? null : deliveredIntents.get(i));
} catch (NullPointerException npe) {
Log.w("SmsTransport", npe);
Log.w("SmsTransport", "Recipient: " + message.getIndividualRecipient().getNumber());
Log.w("SmsTransport", "Message Total Parts/Current: " + messages.size() + "/" + i);
Log.w("SmsTransport", "Message Part Length: " + messages.get(i).getBytes().length);
throw new UndeliverableMessageException(npe);
} catch (IllegalArgumentException iae) {
Log.w("SmsTransport", iae);
throw new UndeliverableMessageException(iae);
}
}
}
private void deliverPlaintextMessage(SmsMessageRecord message)
throws UndeliverableMessageException
{
ArrayList<String> messages = SmsManager.getDefault().divideMessage(message.getBody().getBody());
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false);
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
String recipient = message.getIndividualRecipient().getNumber();
// XXX moxie@thoughtcrime.org 1/7/11 -- There's apparently a bug where for some unknown recipients
// and messages, this will throw an NPE. I have no idea why, so I'm just catching it and marking
// the message as a failure. That way at least it doesn't repeatedly crash every time you start
// the app.
// d3sre 12/10/13 -- extended the log file to further analyse the problem
try {
SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
} catch (NullPointerException npe) {
Log.w("SmsTransport", npe);
Log.w("SmsTransport", "Recipient: " + recipient);
Log.w("SmsTransport", "Message Parts: " + messages.size());
try {
for (int i=0;i<messages.size();i++) {
SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i),
sentIntents.get(i),
deliveredIntents == null ? null : deliveredIntents.get(i));
}
} catch (NullPointerException npe2) {
Log.w("SmsTransport", npe);
throw new UndeliverableMessageException(npe2);
}
}
}
private ArrayList<PendingIntent> constructSentIntents(long messageId, long type,
ArrayList<String> messages, boolean secure)
{
ArrayList<PendingIntent> sentIntents = new ArrayList<>(messages.size());
for (String ignored : messages) {
sentIntents.add(PendingIntent.getBroadcast(context, 0,
constructSentIntent(context, messageId, type, secure, false),
0));
}
return sentIntents;
}
private ArrayList<PendingIntent> constructDeliveredIntents(long messageId, long type, ArrayList<String> messages) {
if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) {
return null;
}
ArrayList<PendingIntent> deliveredIntents = new ArrayList<>(messages.size());
for (String ignored : messages) {
deliveredIntents.add(PendingIntent.getBroadcast(context, 0,
constructDeliveredIntent(context, messageId, type),
0));
}
return deliveredIntents;
}
private OutgoingTextMessage getAsymmetricEncrypt(MasterSecret masterSecret,
OutgoingTextMessage message)
throws InsecureFallbackApprovalException
{
try {
return new SmsCipher(new TextSecureAxolotlStore(context, masterSecret)).encrypt(message);
} catch (NoSessionException e) {
throw new InsecureFallbackApprovalException(e);
}
}
}

View File

@ -1,343 +0,0 @@
/**
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.transport;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.textsecure.crypto.UntrustedIdentityException;
import org.whispersystems.textsecure.directory.Directory;
import org.whispersystems.textsecure.directory.NotInDirectoryException;
import org.whispersystems.textsecure.push.ContactTokenDetails;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.textsecure.storage.RecipientDevice;
import org.whispersystems.textsecure.util.DirectoryUtil;
import org.whispersystems.textsecure.util.InvalidNumberException;
import java.io.IOException;
import ws.com.google.android.mms.pdu.SendReq;
public class UniversalTransport {
private static final String TAG = UniversalTransport.class.getSimpleName();
private final Context context;
private final MasterSecret masterSecret;
private final PushTransport pushTransport;
private final SmsTransport smsTransport;
private final MmsTransport mmsTransport;
public UniversalTransport(Context context, MasterSecret masterSecret) {
this.context = context;
this.masterSecret = masterSecret;
this.pushTransport = new PushTransport(context, masterSecret);
this.smsTransport = new SmsTransport(context, masterSecret);
this.mmsTransport = new MmsTransport(context, masterSecret);
}
public void deliver(SmsMessageRecord message)
throws UndeliverableMessageException, UntrustedIdentityException, RetryLaterException,
SecureFallbackApprovalException, InsecureFallbackApprovalException
{
if (message.isForcedSms()) {
smsTransport.deliver(message);
return;
}
if (!TextSecurePreferences.isPushRegistered(context)) {
deliverDirectSms(message);
return;
}
try {
Recipient recipient = message.getIndividualRecipient();
String number = Util.canonicalizeNumber(context, recipient.getNumber());
if (isPushTransport(number) && !message.isKeyExchange()) {
boolean isSmsFallbackSupported = isSmsFallbackSupported(number);
try {
Log.w(TAG, "Using PUSH as transport...");
pushTransport.deliver(message);
} catch (UnregisteredUserException uue) {
Log.w(TAG, uue);
if (isSmsFallbackSupported) fallbackOrAskApproval(message, number);
else throw new UndeliverableMessageException(uue);
} catch (IOException ioe) {
Log.w(TAG, ioe);
if (isSmsFallbackSupported) fallbackOrAskApproval(message, number);
else throw new RetryLaterException(ioe);
}
} else {
Log.w(TAG, "Using SMS as transport...");
deliverDirectSms(message);
}
} catch (InvalidNumberException e) {
Log.w(TAG, e);
deliverDirectSms(message);
}
}
public MmsSendResult deliver(SendReq mediaMessage)
throws UndeliverableMessageException, RetryLaterException, UntrustedIdentityException,
SecureFallbackApprovalException, InsecureFallbackApprovalException
{
if (MmsDatabase.Types.isForcedSms(mediaMessage.getDatabaseMessageBox())) {
return mmsTransport.deliver(mediaMessage);
}
if (Util.isEmpty(mediaMessage.getTo())) {
return deliverDirectMms(mediaMessage);
}
if (GroupUtil.isEncodedGroup(mediaMessage.getTo()[0].getString())) {
return deliverGroupMessage(mediaMessage);
}
if (!TextSecurePreferences.isPushRegistered(context)) {
return deliverDirectMms(mediaMessage);
}
if (isMultipleRecipients(mediaMessage)) {
return deliverDirectMms(mediaMessage);
}
try {
String destination = Util.canonicalizeNumber(context, mediaMessage.getTo()[0].getString());
if (isPushTransport(destination)) {
boolean isSmsFallbackSupported = isSmsFallbackSupported(destination);
try {
Log.w(TAG, "Using GCM as transport...");
pushTransport.deliver(mediaMessage);
return new MmsSendResult("push".getBytes("UTF-8"), 0, true, true);
} catch (IOException ioe) {
Log.w(TAG, ioe);
if (isSmsFallbackSupported) return fallbackOrAskApproval(mediaMessage, destination);
else throw new RetryLaterException(ioe);
} catch (RecipientFormattingException e) {
Log.w(TAG, e);
if (isSmsFallbackSupported) return fallbackOrAskApproval(mediaMessage, destination);
else throw new UndeliverableMessageException(e);
} catch (EncapsulatedExceptions ee) {
Log.w(TAG, ee);
if (!ee.getUnregisteredUserExceptions().isEmpty()) {
if (isSmsFallbackSupported) return mmsTransport.deliver(mediaMessage);
else throw new UndeliverableMessageException(ee);
} else {
throw new UntrustedIdentityException(ee.getUntrustedIdentityExceptions().get(0));
}
}
} else {
Log.w(TAG, "Delivering media message with MMS...");
return deliverDirectMms(mediaMessage);
}
} catch (InvalidNumberException ine) {
Log.w(TAG, ine);
return deliverDirectMms(mediaMessage);
}
}
private MmsSendResult fallbackOrAskApproval(SendReq mediaMessage, String destination)
throws SecureFallbackApprovalException, UndeliverableMessageException, InsecureFallbackApprovalException
{
try {
Recipient recipient = RecipientFactory.getRecipientsFromString(context, destination, false).getPrimaryRecipient();
boolean isSmsFallbackApprovalRequired = isSmsFallbackApprovalRequired(destination);
AxolotlStore axolotlStore = new TextSecureAxolotlStore(context, masterSecret);
if (!isSmsFallbackApprovalRequired) {
Log.w(TAG, "Falling back to MMS");
DatabaseFactory.getMmsDatabase(context).markAsForcedSms(mediaMessage.getDatabaseMessageId());
return mmsTransport.deliver(mediaMessage);
} else if (!axolotlStore.containsSession(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID)) {
Log.w(TAG, "Marking message as pending insecure SMS fallback");
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
} else {
Log.w(TAG, "Marking message as pending secure SMS fallback");
throw new SecureFallbackApprovalException("Pending user approval for fallback secure to SMS");
}
} catch (RecipientFormattingException rfe) {
throw new UndeliverableMessageException(rfe);
}
}
private void fallbackOrAskApproval(SmsMessageRecord smsMessage, String destination)
throws SecureFallbackApprovalException, UndeliverableMessageException, InsecureFallbackApprovalException
{
Recipient recipient = smsMessage.getIndividualRecipient();
boolean isSmsFallbackApprovalRequired = isSmsFallbackApprovalRequired(destination);
AxolotlStore axolotlStore = new TextSecureAxolotlStore(context, masterSecret);
if (!isSmsFallbackApprovalRequired) {
Log.w(TAG, "Falling back to SMS");
DatabaseFactory.getSmsDatabase(context).markAsForcedSms(smsMessage.getId());
smsTransport.deliver(smsMessage);
} else if (!axolotlStore.containsSession(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID)) {
Log.w(TAG, "Marking message as pending insecure fallback.");
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
} else {
Log.w(TAG, "Marking message as pending secure fallback.");
throw new SecureFallbackApprovalException("Pending user approval for fallback to secure SMS");
}
}
private MmsSendResult deliverGroupMessage(SendReq mediaMessage)
throws RetryLaterException, UndeliverableMessageException
{
if (!TextSecurePreferences.isPushRegistered(context)) {
throw new UndeliverableMessageException("Not push registered!");
}
try {
pushTransport.deliver(mediaMessage);
return new MmsSendResult("push".getBytes("UTF-8"), 0, true, true);
} catch (IOException e) {
Log.w(TAG, e);
throw new RetryLaterException(e);
} catch (RecipientFormattingException | InvalidNumberException e) {
throw new UndeliverableMessageException(e);
} catch (EncapsulatedExceptions ee) {
Log.w(TAG, ee);
try {
for (UnregisteredUserException unregistered : ee.getUnregisteredUserExceptions()) {
IncomingGroupMessage quitMessage = IncomingGroupMessage.createForQuit(mediaMessage.getTo()[0].getString(), unregistered.getE164Number());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, quitMessage);
DatabaseFactory.getGroupDatabase(context).remove(GroupUtil.getDecodedId(mediaMessage.getTo()[0].getString()), unregistered.getE164Number());
}
for (UntrustedIdentityException untrusted : ee.getUntrustedIdentityExceptions()) {
IncomingIdentityUpdateMessage identityMessage = IncomingIdentityUpdateMessage.createFor(untrusted.getE164Number(), untrusted.getIdentityKey(), mediaMessage.getTo()[0].getString());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityMessage);
}
return new MmsSendResult("push".getBytes("UTF-8"), 0, true, true);
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
}
}
private void deliverDirectSms(SmsMessageRecord message)
throws InsecureFallbackApprovalException, UndeliverableMessageException
{
if (TextSecurePreferences.isDirectSmsAllowed(context)) {
smsTransport.deliver(message);
} else {
throw new UndeliverableMessageException("Direct SMS delivery is disabled!");
}
}
private MmsSendResult deliverDirectMms(SendReq message)
throws InsecureFallbackApprovalException, UndeliverableMessageException
{
if (TextSecurePreferences.isDirectSmsAllowed(context)) {
return mmsTransport.deliver(message);
} else {
throw new UndeliverableMessageException("Direct MMS delivery is disabled!");
}
}
public boolean isMultipleRecipients(SendReq mediaMessage) {
int recipientCount = 0;
if (mediaMessage.getTo() != null) {
recipientCount += mediaMessage.getTo().length;
}
if (mediaMessage.getCc() != null) {
recipientCount += mediaMessage.getCc().length;
}
if (mediaMessage.getBcc() != null) {
recipientCount += mediaMessage.getBcc().length;
}
return recipientCount > 1;
}
private boolean isSmsFallbackApprovalRequired(String destination) {
return (isSmsFallbackSupported(destination) && TextSecurePreferences.isFallbackSmsAskRequired(context));
}
private boolean isSmsFallbackSupported(String destination) {
if (GroupUtil.isEncodedGroup(destination)) {
return false;
}
if (TextSecurePreferences.isPushRegistered(context) &&
!TextSecurePreferences.isFallbackSmsAllowed(context))
{
return false;
}
Directory directory = Directory.getInstance(context);
return directory.isSmsFallbackSupported(destination);
}
private boolean isPushTransport(String destination) {
if (GroupUtil.isEncodedGroup(destination)) {
return true;
}
Directory directory = Directory.getInstance(context);
try {
return directory.isActiveNumber(destination);
} catch (NotInDirectoryException e) {
try {
PushServiceSocket socket = PushServiceSocketFactory.create(context);
String contactToken = DirectoryUtil.getDirectoryServerToken(destination);
ContactTokenDetails registeredUser = socket.getContactTokenDetails(contactToken);
if (registeredUser == null) {
registeredUser = new ContactTokenDetails();
registeredUser.setNumber(destination);
directory.setNumber(registeredUser, false);
return false;
} else {
registeredUser.setNumber(destination);
directory.setNumber(registeredUser, true);
return true;
}
} catch (IOException e1) {
Log.w(TAG, e1);
return false;
}
}
}
}