mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Transition the outbound pipeline to JobManager jobs.
This commit is contained in:
parent
99f42e2ee1
commit
cafe03a70a
@ -37,12 +37,15 @@ public class JobManager implements RequirementListener {
|
|||||||
private final AtomicBoolean hasLoadedEncrypted = new AtomicBoolean(false);
|
private final AtomicBoolean hasLoadedEncrypted = new AtomicBoolean(false);
|
||||||
|
|
||||||
private final PersistentStorage persistentStorage;
|
private final PersistentStorage persistentStorage;
|
||||||
|
private final List<RequirementProvider> requirementProviders;
|
||||||
|
|
||||||
public JobManager(Context context, String name,
|
public JobManager(Context context, String name,
|
||||||
List<RequirementProvider> requirementProviders,
|
List<RequirementProvider> requirementProviders,
|
||||||
JobSerializer jobSerializer, int consumers)
|
JobSerializer jobSerializer, int consumers)
|
||||||
{
|
{
|
||||||
this.persistentStorage = new PersistentStorage(context, name, jobSerializer);
|
this.persistentStorage = new PersistentStorage(context, name, jobSerializer);
|
||||||
|
this.requirementProviders = requirementProviders;
|
||||||
|
|
||||||
eventExecutor.execute(new LoadTask(null));
|
eventExecutor.execute(new LoadTask(null));
|
||||||
|
|
||||||
if (requirementProviders != null && !requirementProviders.isEmpty()) {
|
if (requirementProviders != null && !requirementProviders.isEmpty()) {
|
||||||
@ -56,6 +59,16 @@ public class JobManager implements RequirementListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RequirementProvider getRequirementProvider(String name) {
|
||||||
|
for (RequirementProvider provider : requirementProviders) {
|
||||||
|
if (provider.getName().equals(name)) {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public void setEncryptionKeys(EncryptionKeys keys) {
|
public void setEncryptionKeys(EncryptionKeys keys) {
|
||||||
if (hasLoadedEncrypted.compareAndSet(false, true)) {
|
if (hasLoadedEncrypted.compareAndSet(false, true)) {
|
||||||
eventExecutor.execute(new LoadTask(keys));
|
eventExecutor.execute(new LoadTask(keys));
|
||||||
|
@ -46,6 +46,11 @@ public class NetworkRequirementProvider implements RequirementProvider {
|
|||||||
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "network";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setListener(RequirementListener listener) {
|
public void setListener(RequirementListener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
|
@ -17,5 +17,6 @@
|
|||||||
package org.whispersystems.jobqueue.requirements;
|
package org.whispersystems.jobqueue.requirements;
|
||||||
|
|
||||||
public interface RequirementProvider {
|
public interface RequirementProvider {
|
||||||
|
public String getName();
|
||||||
public void setListener(RequirementListener listener);
|
public void setListener(RequirementListener listener);
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,10 @@ import org.thoughtcrime.securesms.components.EmojiToggle;
|
|||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||||
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
|
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
|
||||||
|
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||||
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||||
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
||||||
@ -104,18 +107,13 @@ import org.thoughtcrime.securesms.util.MemoryCleaner;
|
|||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||||
import org.whispersystems.libaxolotl.state.SessionStore;
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
|
||||||
import org.whispersystems.textsecure.util.Util;
|
import org.whispersystems.textsecure.util.Util;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import ws.com.google.android.mms.MmsException;
|
|
||||||
|
|
||||||
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||||
import static org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener;
|
import static org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener;
|
||||||
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
|
import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext;
|
||||||
@ -420,14 +418,22 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
if (isSingleConversation()) {
|
if (isSingleConversation()) {
|
||||||
ConversationActivity self = ConversationActivity.this;
|
final Context context = getApplicationContext();
|
||||||
|
|
||||||
OutgoingEndSessionMessage endSessionMessage =
|
OutgoingEndSessionMessage endSessionMessage =
|
||||||
new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE"));
|
new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE"));
|
||||||
|
|
||||||
long allocatedThreadId = MessageSender.send(self, masterSecret, endSessionMessage, threadId, false);
|
new AsyncTask<OutgoingEndSessionMessage, Void, Long>() {
|
||||||
|
@Override
|
||||||
|
protected Long doInBackground(OutgoingEndSessionMessage... messages) {
|
||||||
|
return MessageSender.send(context, masterSecret, messages[0], threadId, false);
|
||||||
|
}
|
||||||
|
|
||||||
sendComplete(recipients, allocatedThreadId, allocatedThreadId != self.threadId);
|
@Override
|
||||||
|
protected void onPostExecute(Long result) {
|
||||||
|
sendComplete(result);
|
||||||
|
}
|
||||||
|
}.execute(endSessionMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -468,9 +474,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
Toast.makeText(ConversationActivity.this, "Error leaving group....", Toast.LENGTH_LONG).show();
|
Toast.makeText(ConversationActivity.this, "Error leaving group....", Toast.LENGTH_LONG).show();
|
||||||
} catch (MmsException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
Toast.makeText(ConversationActivity.this, "Error leaving group...", Toast.LENGTH_LONG).show();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -842,12 +845,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
private void addAttachmentImage(Uri imageUri) {
|
private void addAttachmentImage(Uri imageUri) {
|
||||||
try {
|
try {
|
||||||
attachmentManager.setImage(imageUri);
|
attachmentManager.setImage(imageUri);
|
||||||
} catch (IOException e) {
|
} catch (IOException | BitmapDecodingException e) {
|
||||||
Log.w(TAG, e);
|
|
||||||
attachmentManager.clear();
|
|
||||||
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
} catch (BitmapDecodingException e) {
|
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
attachmentManager.clear();
|
attachmentManager.clear();
|
||||||
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
||||||
@ -917,7 +915,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<Draft> getDraftsForCurrentState() {
|
private List<Draft> getDraftsForCurrentState() {
|
||||||
List<Draft> drafts = new LinkedList<Draft>();
|
List<Draft> drafts = new LinkedList<>();
|
||||||
|
|
||||||
if (!Util.isEmpty(composeText)) {
|
if (!Util.isEmpty(composeText)) {
|
||||||
drafts.add(new Draft(Draft.TEXT, composeText.getText().toString()));
|
drafts.add(new Draft(Draft.TEXT, composeText.getText().toString()));
|
||||||
@ -1032,43 +1030,38 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}.execute(threadId);
|
}.execute(threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendComplete(Recipients recipients, long threadId, boolean refreshFragment) {
|
private void sendComplete(long threadId) {
|
||||||
attachmentManager.clear();
|
boolean refreshFragment = (threadId != this.threadId);
|
||||||
composeText.setText("");
|
|
||||||
|
|
||||||
this.recipients = recipients;
|
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
|
|
||||||
ConversationFragment fragment = (ConversationFragment) getSupportFragmentManager()
|
ConversationFragment fragment = (ConversationFragment) getSupportFragmentManager()
|
||||||
.findFragmentById(R.id.fragment_content);
|
.findFragmentById(R.id.fragment_content);
|
||||||
|
|
||||||
if (refreshFragment) {
|
if (refreshFragment) {
|
||||||
fragment.reload(recipients, threadId);
|
fragment.reload(recipients, threadId);
|
||||||
|
|
||||||
initializeTitleBar();
|
initializeTitleBar();
|
||||||
initializeSecurity();
|
initializeSecurity();
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment.scrollToBottom();
|
fragment.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void sendMessage(boolean forcePlaintext, boolean forceSms) {
|
private void sendMessage(boolean forcePlaintext, boolean forceSms) {
|
||||||
try {
|
try {
|
||||||
Recipients recipients = getRecipients();
|
final Recipients recipients = getRecipients();
|
||||||
|
|
||||||
if (recipients == null)
|
if (recipients == null)
|
||||||
throw new RecipientFormattingException("Badly formatted");
|
throw new RecipientFormattingException("Badly formatted");
|
||||||
|
|
||||||
long allocatedThreadId;
|
|
||||||
|
|
||||||
if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) {
|
if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) {
|
||||||
handleManualMmsRequired();
|
handleManualMmsRequired();
|
||||||
return;
|
|
||||||
} else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) {
|
} else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) {
|
||||||
allocatedThreadId = sendMediaMessage(forcePlaintext, forceSms);
|
sendMediaMessage(forcePlaintext, forceSms);
|
||||||
} else {
|
} else {
|
||||||
allocatedThreadId = sendTextMessage(forcePlaintext, forceSms);
|
sendTextMessage(forcePlaintext, forceSms);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendComplete(recipients, allocatedThreadId, allocatedThreadId != this.threadId);
|
|
||||||
} catch (RecipientFormattingException ex) {
|
} catch (RecipientFormattingException ex) {
|
||||||
Toast.makeText(ConversationActivity.this,
|
Toast.makeText(ConversationActivity.this,
|
||||||
R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation,
|
R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation,
|
||||||
@ -1078,17 +1071,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_message_is_empty_exclamation,
|
Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_message_is_empty_exclamation,
|
||||||
Toast.LENGTH_SHORT).show();
|
Toast.LENGTH_SHORT).show();
|
||||||
Log.w(TAG, ex);
|
Log.w(TAG, ex);
|
||||||
} catch (MmsException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private long sendMediaMessage(boolean forcePlaintext, boolean forceSms)
|
private void sendMediaMessage(boolean forcePlaintext, final boolean forceSms)
|
||||||
throws InvalidMessageException, MmsException
|
throws InvalidMessageException
|
||||||
{
|
{
|
||||||
|
final Context context = getApplicationContext();
|
||||||
SlideDeck slideDeck;
|
SlideDeck slideDeck;
|
||||||
|
|
||||||
if (attachmentManager.isAttachmentPresent()) slideDeck = attachmentManager.getSlideDeck();
|
if (attachmentManager.isAttachmentPresent()) slideDeck = new SlideDeck(attachmentManager.getSlideDeck());
|
||||||
else slideDeck = new SlideDeck();
|
else slideDeck = new SlideDeck();
|
||||||
|
|
||||||
OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(this, recipients, slideDeck,
|
OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(this, recipients, slideDeck,
|
||||||
@ -1098,12 +1090,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessage);
|
outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MessageSender.send(this, masterSecret, outgoingMessage, threadId, forceSms);
|
attachmentManager.clear();
|
||||||
|
composeText.setText("");
|
||||||
|
|
||||||
|
new AsyncTask<OutgoingMediaMessage, Void, Long>() {
|
||||||
|
@Override
|
||||||
|
protected Long doInBackground(OutgoingMediaMessage... messages) {
|
||||||
|
return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms);
|
||||||
}
|
}
|
||||||
|
|
||||||
private long sendTextMessage(boolean forcePlaintext, boolean forceSms)
|
@Override
|
||||||
|
protected void onPostExecute(Long result) {
|
||||||
|
sendComplete(result);
|
||||||
|
}
|
||||||
|
}.execute(outgoingMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendTextMessage(boolean forcePlaintext, final boolean forceSms)
|
||||||
throws InvalidMessageException
|
throws InvalidMessageException
|
||||||
{
|
{
|
||||||
|
final Context context = getApplicationContext();
|
||||||
OutgoingTextMessage message;
|
OutgoingTextMessage message;
|
||||||
|
|
||||||
if (isEncryptedConversation && !forcePlaintext) {
|
if (isEncryptedConversation && !forcePlaintext) {
|
||||||
@ -1112,9 +1118,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
message = new OutgoingTextMessage(recipients, getMessage());
|
message = new OutgoingTextMessage(recipients, getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.w(TAG, "Sending message...");
|
this.composeText.setText("");
|
||||||
|
|
||||||
return MessageSender.send(ConversationActivity.this, masterSecret, message, threadId, forceSms);
|
new AsyncTask<OutgoingTextMessage, Void, Long>() {
|
||||||
|
@Override
|
||||||
|
protected Long doInBackground(OutgoingTextMessage... messages) {
|
||||||
|
return MessageSender.send(context, masterSecret, messages[0], threadId, forceSms);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Long result) {
|
||||||
|
sendComplete(result);
|
||||||
|
}
|
||||||
|
}.execute(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import android.content.Context;
|
|||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.v4.app.ListFragment;
|
import android.support.v4.app.ListFragment;
|
||||||
@ -241,10 +242,15 @@ public class ConversationFragment extends ListFragment
|
|||||||
startActivity(composeIntent);
|
startActivity(composeIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleResendMessage(MessageRecord message) {
|
private void handleResendMessage(final MessageRecord message) {
|
||||||
long messageId = message.getId();
|
final Context context = getActivity().getApplicationContext();
|
||||||
final Activity activity = getActivity();
|
new AsyncTask<MessageRecord, Void, Void>() {
|
||||||
MessageSender.resend(activity, messageId, message.isMms());
|
@Override
|
||||||
|
protected Void doInBackground(MessageRecord... messageRecords) {
|
||||||
|
MessageSender.resend(context, masterSecret, messageRecords[0]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}.execute(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSaveAttachment(final MediaMmsMessageRecord message) {
|
private void handleSaveAttachment(final MediaMmsMessageRecord message) {
|
||||||
|
@ -46,10 +46,11 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.service.SendReceiveService;
|
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.Dialogs;
|
import org.thoughtcrime.securesms.util.Dialogs;
|
||||||
import org.thoughtcrime.securesms.util.Emoji;
|
import org.thoughtcrime.securesms.util.Emoji;
|
||||||
@ -561,6 +562,10 @@ public class ConversationItem extends LinearLayout {
|
|||||||
}
|
}
|
||||||
database.markAsOutbox(messageRecord.getId());
|
database.markAsOutbox(messageRecord.getId());
|
||||||
database.markAsForcedSms(messageRecord.getId());
|
database.markAsForcedSms(messageRecord.getId());
|
||||||
|
|
||||||
|
ApplicationContext.getInstance(context)
|
||||||
|
.getJobManager()
|
||||||
|
.add(new MmsSendJob(context, messageRecord.getId()));
|
||||||
} else {
|
} else {
|
||||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||||
if (messageRecord.isPendingInsecureSmsFallback()) {
|
if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||||
@ -568,15 +573,12 @@ public class ConversationItem extends LinearLayout {
|
|||||||
}
|
}
|
||||||
database.markAsOutbox(messageRecord.getId());
|
database.markAsOutbox(messageRecord.getId());
|
||||||
database.markAsForcedSms(messageRecord.getId());
|
database.markAsForcedSms(messageRecord.getId());
|
||||||
|
|
||||||
|
ApplicationContext.getInstance(context)
|
||||||
|
.getJobManager()
|
||||||
|
.add(new SmsSendJob(context, messageRecord.getId(),
|
||||||
|
messageRecord.getIndividualRecipient().getNumber()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent intent = new Intent(context, SendReceiveService.class);
|
|
||||||
intent.setAction(messageRecord.isMms() ?
|
|
||||||
SendReceiveService.SEND_MMS_ACTION :
|
|
||||||
SendReceiveService.SEND_SMS_ACTION);
|
|
||||||
intent.putExtra(SendReceiveService.MASTER_SECRET_EXTRA, masterSecret);
|
|
||||||
|
|
||||||
context.startService(intent);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,18 +38,17 @@ import android.widget.AdapterView;
|
|||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.SimpleAdapter;
|
import android.widget.SimpleAdapter;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||||
|
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.thoughtcrime.securesms.service.SendReceiveService;
|
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
||||||
|
|
||||||
|
|
||||||
public class ConversationListActivity extends PassphraseRequiredActionBarActivity
|
public class ConversationListActivity extends PassphraseRequiredActionBarActivity
|
||||||
@ -72,7 +71,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
|||||||
|
|
||||||
getSupportActionBar().setTitle(R.string.app_name);
|
getSupportActionBar().setTitle(R.string.app_name);
|
||||||
|
|
||||||
initializeSenderReceiverService();
|
|
||||||
initializeResources();
|
initializeResources();
|
||||||
initializeContactUpdatesReceiver();
|
initializeContactUpdatesReceiver();
|
||||||
|
|
||||||
@ -247,15 +245,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
|||||||
true, observer);
|
true, observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeSenderReceiverService() {
|
|
||||||
Intent smsSenderIntent = new Intent(SendReceiveService.SEND_SMS_ACTION, null, this,
|
|
||||||
SendReceiveService.class);
|
|
||||||
Intent mmsSenderIntent = new Intent(SendReceiveService.SEND_MMS_ACTION, null, this,
|
|
||||||
SendReceiveService.class);
|
|
||||||
startService(smsSenderIntent);
|
|
||||||
startService(mmsSenderIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeResources() {
|
private void initializeResources() {
|
||||||
this.masterSecret = getIntent().getParcelableExtra("master_secret");
|
this.masterSecret = getIntent().getParcelableExtra("master_secret");
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|||||||
import org.whispersystems.textsecure.directory.Directory;
|
import org.whispersystems.textsecure.directory.Directory;
|
||||||
import org.whispersystems.textsecure.directory.NotInDirectoryException;
|
import org.whispersystems.textsecure.directory.NotInDirectoryException;
|
||||||
import org.whispersystems.textsecure.util.InvalidNumberException;
|
import org.whispersystems.textsecure.util.InvalidNumberException;
|
||||||
|
import org.whispersystems.textsecure.util.ListenableFutureTask;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@ -76,6 +77,7 @@ import java.util.HashSet;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import ws.com.google.android.mms.MmsException;
|
import ws.com.google.android.mms.MmsException;
|
||||||
|
|
||||||
@ -443,7 +445,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity {
|
|||||||
|
|
||||||
private Pair<Long, Recipients> handlePushOperation(byte[] groupId, String groupName, byte[] avatar,
|
private Pair<Long, Recipients> handlePushOperation(byte[] groupId, String groupName, byte[] avatar,
|
||||||
Set<String> e164numbers)
|
Set<String> e164numbers)
|
||||||
throws MmsException, InvalidNumberException
|
throws InvalidNumberException
|
||||||
{
|
{
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -460,12 +462,9 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity {
|
|||||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatar);
|
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatar);
|
||||||
long threadId = MessageSender.send(this, masterSecret, outgoingMessage, -1, false);
|
long threadId = MessageSender.send(this, masterSecret, outgoingMessage, -1, false);
|
||||||
|
|
||||||
return new Pair<Long, Recipients>(threadId, groupRecipient);
|
return new Pair<>(threadId, groupRecipient);
|
||||||
} catch (RecipientFormattingException e) {
|
} catch (RecipientFormattingException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
} catch (MmsException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
throw new MmsException(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import android.util.Pair;
|
|||||||
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
|
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
|
||||||
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
|
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
|
||||||
import org.thoughtcrime.securesms.database.model.DisplayRecord;
|
import org.thoughtcrime.securesms.database.model.DisplayRecord;
|
||||||
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||||
import org.thoughtcrime.securesms.util.LRUCache;
|
import org.thoughtcrime.securesms.util.LRUCache;
|
||||||
@ -58,7 +59,7 @@ public class EncryptingSmsDatabase extends SmsDatabase {
|
|||||||
return ciphertext;
|
return ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Long> insertMessageOutbox(MasterSecret masterSecret, long threadId,
|
public long insertMessageOutbox(MasterSecret masterSecret, long threadId,
|
||||||
OutgoingTextMessage message, boolean forceSms)
|
OutgoingTextMessage message, boolean forceSms)
|
||||||
{
|
{
|
||||||
long type = Types.BASE_OUTBOX_TYPE;
|
long type = Types.BASE_OUTBOX_TYPE;
|
||||||
@ -120,9 +121,15 @@ public class EncryptingSmsDatabase extends SmsDatabase {
|
|||||||
return new DecryptingReader(masterSecret, cursor);
|
return new DecryptingReader(masterSecret, cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Reader getMessage(MasterSecret masterSecret, long messageId) {
|
public SmsMessageRecord getMessage(MasterSecret masterSecret, long messageId) throws NoSuchMessageException {
|
||||||
Cursor cursor = super.getMessage(messageId);
|
Cursor cursor = super.getMessage(messageId);
|
||||||
return new DecryptingReader(masterSecret, cursor);
|
DecryptingReader reader = new DecryptingReader(masterSecret, cursor);
|
||||||
|
SmsMessageRecord record = reader.getNext();
|
||||||
|
|
||||||
|
reader.close();
|
||||||
|
|
||||||
|
if (record == null) throw new NoSuchMessageException("No message for ID: " + messageId);
|
||||||
|
else return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Reader getDecryptInProgressMessages(MasterSecret masterSecret) {
|
public Reader getDecryptInProgressMessages(MasterSecret masterSecret) {
|
||||||
|
@ -456,39 +456,22 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendReq[] getOutgoingMessages(MasterSecret masterSecret, long messageId)
|
public SendReq getOutgoingMessage(MasterSecret masterSecret, long messageId)
|
||||||
throws MmsException
|
throws MmsException, NoSuchMessageException
|
||||||
{
|
{
|
||||||
MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context);
|
MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context);
|
||||||
PartDatabase partDatabase = getPartDatabase(masterSecret);
|
PartDatabase partDatabase = getPartDatabase(masterSecret);
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
MasterCipher masterCipher = masterSecret == null ? null : new MasterCipher(masterSecret);
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||||
Cursor cursor = null;
|
Cursor cursor = null;
|
||||||
|
|
||||||
|
String selection = ID_WHERE;
|
||||||
String selection;
|
String[] selectionArgs = new String[]{String.valueOf(messageId)};
|
||||||
String[] selectionArgs;
|
|
||||||
|
|
||||||
if (messageId > 0) {
|
|
||||||
selection = ID_WHERE;
|
|
||||||
selectionArgs = new String[]{messageId + ""};
|
|
||||||
} else {
|
|
||||||
selection = MESSAGE_BOX + " & " + Types.BASE_TYPE_MASK + " = " + Types.BASE_OUTBOX_TYPE;
|
|
||||||
selectionArgs = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cursor = database.query(TABLE_NAME, MMS_PROJECTION, selection, selectionArgs, null, null, null);
|
cursor = database.query(TABLE_NAME, MMS_PROJECTION, selection, selectionArgs, null, null, null);
|
||||||
|
|
||||||
if (cursor == null || cursor.getCount() == 0)
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
return new SendReq[0];
|
|
||||||
|
|
||||||
SendReq[] requests = new SendReq[cursor.getCount()];
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
messageId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
|
||||||
|
|
||||||
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
|
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
|
||||||
String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
|
String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
|
||||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
|
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
|
||||||
@ -507,10 +490,10 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
|||||||
Log.w("MmsDatabase", e);
|
Log.w("MmsDatabase", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
requests[i++] = new SendReq(headers, body, messageId, outboxType, timestamp);
|
return new SendReq(headers, body, messageId, outboxType, timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return requests;
|
throw new NoSuchMessageException("No record found for id: " + messageId);
|
||||||
} finally {
|
} finally {
|
||||||
if (cursor != null)
|
if (cursor != null)
|
||||||
cursor.close();
|
cursor.close();
|
||||||
@ -527,17 +510,20 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public long copyMessageInbox(MasterSecret masterSecret, long messageId) throws MmsException {
|
public long copyMessageInbox(MasterSecret masterSecret, long messageId) throws MmsException {
|
||||||
SendReq[] request = getOutgoingMessages(masterSecret, messageId);
|
try {
|
||||||
|
SendReq request = getOutgoingMessage(masterSecret, messageId);
|
||||||
ContentValues contentValues = getContentValuesFromHeader(request[0].getPduHeaders());
|
ContentValues contentValues = getContentValuesFromHeader(request.getPduHeaders());
|
||||||
|
|
||||||
contentValues.put(MESSAGE_BOX, Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_SYMMETRIC_BIT);
|
contentValues.put(MESSAGE_BOX, Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_SYMMETRIC_BIT);
|
||||||
contentValues.put(THREAD_ID, getThreadIdForMessage(messageId));
|
contentValues.put(THREAD_ID, getThreadIdForMessage(messageId));
|
||||||
contentValues.put(READ, 1);
|
contentValues.put(READ, 1);
|
||||||
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
|
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
|
||||||
|
|
||||||
return insertMediaMessage(masterSecret, request[0].getPduHeaders(),
|
return insertMediaMessage(masterSecret, request.getPduHeaders(),
|
||||||
request[0].getBody(), contentValues);
|
request.getBody(), contentValues);
|
||||||
|
} catch (NoSuchMessageException e) {
|
||||||
|
throw new MmsException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, IncomingMediaMessage retrieved,
|
private Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, IncomingMediaMessage retrieved,
|
||||||
|
@ -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);}
|
||||||
|
}
|
@ -108,10 +108,4 @@ public class PushDatabase extends Database {
|
|||||||
this.cursor.close();
|
this.cursor.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class NoSuchMessageException extends Exception {
|
|
||||||
public NoSuchMessageException(String s) {super(s);}
|
|
||||||
public NoSuchMessageException(Exception e) {super(e);}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -428,7 +428,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
|||||||
return insertMessageInbox(message, Types.BASE_INBOX_TYPE);
|
return insertMessageInbox(message, Types.BASE_INBOX_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<Long> insertMessageOutbox(long threadId, OutgoingTextMessage message,
|
protected long insertMessageOutbox(long threadId, OutgoingTextMessage message,
|
||||||
long type, boolean forceSms)
|
long type, boolean forceSms)
|
||||||
{
|
{
|
||||||
if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT;
|
if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT;
|
||||||
@ -437,11 +437,9 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
|||||||
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
|
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
|
||||||
|
|
||||||
long date = System.currentTimeMillis();
|
long date = System.currentTimeMillis();
|
||||||
List<Long> messageIds = new LinkedList<Long>();
|
|
||||||
|
|
||||||
for (Recipient recipient : message.getRecipients().getRecipientsList()) {
|
|
||||||
ContentValues contentValues = new ContentValues(6);
|
ContentValues contentValues = new ContentValues(6);
|
||||||
contentValues.put(ADDRESS, PhoneNumberUtils.formatNumber(recipient.getNumber()));
|
contentValues.put(ADDRESS, PhoneNumberUtils.formatNumber(message.getRecipients().getPrimaryRecipient().getNumber()));
|
||||||
contentValues.put(THREAD_ID, threadId);
|
contentValues.put(THREAD_ID, threadId);
|
||||||
contentValues.put(BODY, message.getMessageBody());
|
contentValues.put(BODY, message.getMessageBody());
|
||||||
contentValues.put(DATE_RECEIVED, date);
|
contentValues.put(DATE_RECEIVED, date);
|
||||||
@ -450,14 +448,13 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
|||||||
contentValues.put(TYPE, type);
|
contentValues.put(TYPE, type);
|
||||||
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
messageIds.add(db.insert(TABLE_NAME, ADDRESS, contentValues));
|
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
|
||||||
|
|
||||||
DatabaseFactory.getThreadDatabase(context).update(threadId);
|
DatabaseFactory.getThreadDatabase(context).update(threadId);
|
||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
Trimmer.trimThread(context, threadId);
|
Trimmer.trimThread(context, threadId);
|
||||||
}
|
|
||||||
|
|
||||||
return messageIds;
|
return messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
Cursor getMessages(int skip, int limit) {
|
Cursor getMessages(int skip, int limit) {
|
||||||
|
@ -57,7 +57,7 @@ public class MmsDownloadJob extends MasterSecretJob {
|
|||||||
.withPersistence()
|
.withPersistence()
|
||||||
.withRequirement(new MasterSecretRequirement(context))
|
.withRequirement(new MasterSecretRequirement(context))
|
||||||
.withRequirement(new NetworkRequirement(context))
|
.withRequirement(new NetworkRequirement(context))
|
||||||
.withGroupId("mms-download")
|
.withGroupId("mms-operation")
|
||||||
.create());
|
.create());
|
||||||
|
|
||||||
this.messageId = messageId;
|
this.messageId = messageId;
|
||||||
@ -170,7 +170,13 @@ public class MmsDownloadJob extends MasterSecretJob {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCanceled() {
|
public void onCanceled() {
|
||||||
// TODO
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||||
|
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE);
|
||||||
|
|
||||||
|
if (automatic) {
|
||||||
|
database.markIncomingNotificationReceived(threadId);
|
||||||
|
MessageNotifier.updateNotification(context, null, threadId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,21 +1,4 @@
|
|||||||
/**
|
package org.thoughtcrime.securesms.jobs;
|
||||||
* Copyright (C) 2013 Open Whisper Systems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.transport;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.telephony.TelephonyManager;
|
import android.telephony.TelephonyManager;
|
||||||
@ -24,51 +7,108 @@ import android.util.Log;
|
|||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.MmsCipher;
|
import org.thoughtcrime.securesms.crypto.MmsCipher;
|
||||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||||
|
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
||||||
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
|
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
|
||||||
import org.thoughtcrime.securesms.mms.MmsRadio;
|
import org.thoughtcrime.securesms.mms.MmsRadio;
|
||||||
import org.thoughtcrime.securesms.mms.MmsRadioException;
|
import org.thoughtcrime.securesms.mms.MmsRadioException;
|
||||||
import org.thoughtcrime.securesms.mms.MmsSendResult;
|
import org.thoughtcrime.securesms.mms.MmsSendResult;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
|
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
|
||||||
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||||
|
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||||
|
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||||
import org.thoughtcrime.securesms.util.NumberUtil;
|
import org.thoughtcrime.securesms.util.NumberUtil;
|
||||||
|
import org.whispersystems.jobqueue.JobParameters;
|
||||||
|
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
|
||||||
import org.whispersystems.libaxolotl.NoSessionException;
|
import org.whispersystems.libaxolotl.NoSessionException;
|
||||||
import org.whispersystems.textsecure.util.Hex;
|
import org.whispersystems.textsecure.util.Hex;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import ws.com.google.android.mms.MmsException;
|
||||||
import ws.com.google.android.mms.pdu.EncodedStringValue;
|
import ws.com.google.android.mms.pdu.EncodedStringValue;
|
||||||
import ws.com.google.android.mms.pdu.PduComposer;
|
import ws.com.google.android.mms.pdu.PduComposer;
|
||||||
import ws.com.google.android.mms.pdu.PduHeaders;
|
import ws.com.google.android.mms.pdu.PduHeaders;
|
||||||
import ws.com.google.android.mms.pdu.SendConf;
|
import ws.com.google.android.mms.pdu.SendConf;
|
||||||
import ws.com.google.android.mms.pdu.SendReq;
|
import ws.com.google.android.mms.pdu.SendReq;
|
||||||
|
|
||||||
public class MmsTransport {
|
public class MmsSendJob extends MasterSecretJob {
|
||||||
|
|
||||||
private static final String TAG = MmsTransport.class.getSimpleName();
|
private static final String TAG = MmsSendJob.class.getSimpleName();
|
||||||
|
|
||||||
private final Context context;
|
private final long messageId;
|
||||||
private final MasterSecret masterSecret;
|
|
||||||
private final MmsRadio radio;
|
|
||||||
|
|
||||||
public MmsTransport(Context context, MasterSecret masterSecret) {
|
public MmsSendJob(Context context, long messageId) {
|
||||||
this.context = context;
|
super(context, JobParameters.newBuilder()
|
||||||
this.masterSecret = masterSecret;
|
.withGroupId("mms-operation")
|
||||||
this.radio = MmsRadio.getInstance(context);
|
.withRequirement(new NetworkRequirement(context))
|
||||||
|
.withRequirement(new MasterSecretRequirement(context))
|
||||||
|
.withPersistence()
|
||||||
|
.create());
|
||||||
|
|
||||||
|
this.messageId = messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MmsSendResult deliver(SendReq message) throws UndeliverableMessageException,
|
@Override
|
||||||
InsecureFallbackApprovalException
|
public void onAdded() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRun() throws RequirementNotMetException, MmsException, NoSuchMessageException {
|
||||||
|
MasterSecret masterSecret = getMasterSecret();
|
||||||
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||||
|
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
MmsSendResult result = deliver(masterSecret, message);
|
||||||
|
|
||||||
|
if (result.isUpgradedSecure()) {
|
||||||
|
database.markAsSecure(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
database.markAsSent(messageId, result.getMessageId(), result.getResponseStatus());
|
||||||
|
} catch (UndeliverableMessageException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
database.markAsSentFailed(messageId);
|
||||||
|
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||||
|
} catch (InsecureFallbackApprovalException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
database.markAsPendingInsecureSmsFallback(messageId);
|
||||||
|
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onShouldRetry(Throwable throwable) {
|
||||||
|
if (throwable instanceof RequirementNotMetException) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCanceled() {
|
||||||
|
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||||
|
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MmsSendResult deliver(MasterSecret masterSecret, SendReq message)
|
||||||
|
throws UndeliverableMessageException, InsecureFallbackApprovalException
|
||||||
{
|
{
|
||||||
|
|
||||||
validateDestinations(message);
|
validateDestinations(message);
|
||||||
|
|
||||||
|
MmsRadio radio = MmsRadio.getInstance(context);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isCdmaDevice()) {
|
if (isCdmaDevice()) {
|
||||||
Log.w(TAG, "Sending MMS directly without radio change...");
|
Log.w(TAG, "Sending MMS directly without radio change...");
|
||||||
try {
|
try {
|
||||||
return sendMms(message, false, false);
|
return sendMms(masterSecret, radio, message, false, false);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
}
|
}
|
||||||
@ -78,7 +118,7 @@ public class MmsTransport {
|
|||||||
radio.connect();
|
radio.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MmsSendResult result = sendMms(message, true, true);
|
MmsSendResult result = sendMms(masterSecret, radio, message, true, true);
|
||||||
radio.disconnect();
|
radio.disconnect();
|
||||||
return result;
|
return result;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -88,7 +128,7 @@ public class MmsTransport {
|
|||||||
Log.w(TAG, "Sending MMS with radio change and without proxy...");
|
Log.w(TAG, "Sending MMS with radio change and without proxy...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MmsSendResult result = sendMms(message, true, false);
|
MmsSendResult result = sendMms(masterSecret, radio, message, true, false);
|
||||||
radio.disconnect();
|
radio.disconnect();
|
||||||
return result;
|
return result;
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
@ -103,14 +143,15 @@ public class MmsTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MmsSendResult sendMms(SendReq message, boolean usingMmsRadio, boolean useProxy)
|
private MmsSendResult sendMms(MasterSecret masterSecret, MmsRadio radio, SendReq message,
|
||||||
|
boolean usingMmsRadio, boolean useProxy)
|
||||||
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
|
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
|
||||||
{
|
{
|
||||||
String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
|
String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
|
||||||
boolean upgradedSecure = false;
|
boolean upgradedSecure = false;
|
||||||
|
|
||||||
if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) {
|
if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) {
|
||||||
message = getEncryptedMessage(message);
|
message = getEncryptedMessage(masterSecret, message);
|
||||||
upgradedSecure = true;
|
upgradedSecure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +181,9 @@ public class MmsTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SendReq getEncryptedMessage(SendReq pdu) throws InsecureFallbackApprovalException {
|
private SendReq getEncryptedMessage(MasterSecret masterSecret, SendReq pdu)
|
||||||
|
throws InsecureFallbackApprovalException
|
||||||
|
{
|
||||||
try {
|
try {
|
||||||
MmsCipher cipher = new MmsCipher(new TextSecureAxolotlStore(context, masterSecret));
|
MmsCipher cipher = new MmsCipher(new TextSecureAxolotlStore(context, masterSecret));
|
||||||
return cipher.encrypt(context, pdu);
|
return cipher.encrypt(context, pdu);
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
|||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||||
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
|
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
|
||||||
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
||||||
@ -68,18 +69,13 @@ public class PushDecryptJob extends MasterSecretJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRun() throws RequirementNotMetException {
|
public void onRun() throws RequirementNotMetException, NoSuchMessageException {
|
||||||
try {
|
|
||||||
MasterSecret masterSecret = getMasterSecret();
|
MasterSecret masterSecret = getMasterSecret();
|
||||||
PushDatabase database = DatabaseFactory.getPushDatabase(context);
|
PushDatabase database = DatabaseFactory.getPushDatabase(context);
|
||||||
TextSecureEnvelope envelope = database.get(messageId);
|
TextSecureEnvelope envelope = database.get(messageId);
|
||||||
|
|
||||||
handleMessage(masterSecret, envelope);
|
handleMessage(masterSecret, envelope);
|
||||||
database.delete(messageId);
|
database.delete(messageId);
|
||||||
|
|
||||||
} catch (PushDatabase.NoSuchMessageException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
148
src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
Normal file
148
src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
150
src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
Normal file
150
src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
98
src/org/thoughtcrime/securesms/jobs/PushSendJob.java
Normal file
98
src/org/thoughtcrime/securesms/jobs/PushSendJob.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
148
src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java
Normal file
148
src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.crypto.SmsCipher;
|
|||||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
||||||
@ -61,15 +62,12 @@ public class SmsDecryptJob extends MasterSecretJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRun() throws RequirementNotMetException {
|
public void onRun() throws RequirementNotMetException, NoSuchMessageException {
|
||||||
MasterSecret masterSecret = getMasterSecret();
|
MasterSecret masterSecret = getMasterSecret();
|
||||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||||
SmsDatabase.Reader reader = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
reader = database.getMessage(masterSecret, messageId);
|
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
|
||||||
|
|
||||||
SmsMessageRecord record = reader.getNext();
|
|
||||||
IncomingTextMessage message = createIncomingTextMessage(masterSecret, record);
|
IncomingTextMessage message = createIncomingTextMessage(masterSecret, record);
|
||||||
long messageId = record.getId();
|
long messageId = record.getId();
|
||||||
long threadId = record.getThreadId();
|
long threadId = record.getThreadId();
|
||||||
@ -93,9 +91,6 @@ public class SmsDecryptJob extends MasterSecretJob {
|
|||||||
} catch (NoSessionException e) {
|
} catch (NoSessionException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
database.markAsNoSession(messageId);
|
database.markAsNoSession(messageId);
|
||||||
} finally {
|
|
||||||
if (reader != null)
|
|
||||||
reader.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,46 @@
|
|||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.telephony.SmsManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
import org.thoughtcrime.securesms.crypto.SmsCipher;
|
||||||
import org.whispersystems.jobqueue.EncryptionKeys;
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||||
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
||||||
|
import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirement;
|
||||||
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||||
|
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
|
||||||
|
import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler;
|
||||||
|
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||||
|
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||||
|
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||||
|
import org.thoughtcrime.securesms.util.NumberUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.jobqueue.JobParameters;
|
import org.whispersystems.jobqueue.JobParameters;
|
||||||
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
|
import org.whispersystems.libaxolotl.NoSessionException;
|
||||||
|
|
||||||
public class SmsSendJob extends ContextJob {
|
import java.util.ArrayList;
|
||||||
|
|
||||||
private transient MasterSecret masterSecret;
|
public class SmsSendJob extends MasterSecretJob {
|
||||||
|
|
||||||
|
private static final String TAG = SmsSendJob.class.getSimpleName();
|
||||||
|
|
||||||
private final long messageId;
|
private final long messageId;
|
||||||
|
|
||||||
public SmsSendJob(Context context, MasterSecret masterSecret, long messageId, String name) {
|
public SmsSendJob(Context context, long messageId, String name) {
|
||||||
super(context, JobParameters.newBuilder()
|
super(context, JobParameters.newBuilder()
|
||||||
.withPersistence()
|
.withPersistence()
|
||||||
.withEncryption(new EncryptionKeys(ParcelUtil.serialize(masterSecret)))
|
.withRequirement(new MasterSecretRequirement(context))
|
||||||
|
.withRequirement(new ServiceRequirement(context))
|
||||||
.withGroupId(name)
|
.withGroupId(name)
|
||||||
.create());
|
.create());
|
||||||
|
|
||||||
@ -30,19 +53,191 @@ public class SmsSendJob extends ContextJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRun() {
|
public void onRun() throws RequirementNotMetException, NoSuchMessageException {
|
||||||
MasterSecret masterSecret = ParcelUtil.deserialize(getEncryptionKeys().getEncoded(), MasterSecret.CREATOR);
|
MasterSecret masterSecret = getMasterSecret();
|
||||||
|
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||||
|
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log.w(TAG, "Sending message: " + messageId);
|
||||||
|
|
||||||
|
deliver(masterSecret, record);
|
||||||
|
} catch (UndeliverableMessageException ude) {
|
||||||
|
Log.w(TAG, ude);
|
||||||
|
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId());
|
||||||
|
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
|
||||||
|
} catch (InsecureFallbackApprovalException ifae) {
|
||||||
|
Log.w(TAG, ifae);
|
||||||
|
DatabaseFactory.getSmsDatabase(context).markAsPendingInsecureSmsFallback(record.getId());
|
||||||
|
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCanceled() {
|
public void onCanceled() {
|
||||||
|
Log.w(TAG, "onCanceled()");
|
||||||
|
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
||||||
|
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
|
||||||
|
|
||||||
|
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
||||||
|
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onShouldRetry(Throwable throwable) {
|
public boolean onShouldRetry(Throwable throwable) {
|
||||||
|
if (throwable instanceof RequirementNotMetException) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void deliver(MasterSecret masterSecret, SmsMessageRecord record)
|
||||||
|
throws UndeliverableMessageException, InsecureFallbackApprovalException
|
||||||
|
{
|
||||||
|
if (!NumberUtil.isValidSmsOrEmail(record.getIndividualRecipient().getNumber())) {
|
||||||
|
throw new UndeliverableMessageException("Not a valid SMS destination! " + record.getIndividualRecipient().getNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.isSecure() || record.isKeyExchange() || record.isEndSession()) {
|
||||||
|
deliverSecureMessage(masterSecret, record);
|
||||||
|
} else {
|
||||||
|
deliverPlaintextMessage(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deliverSecureMessage(MasterSecret masterSecret, SmsMessageRecord message)
|
||||||
|
throws UndeliverableMessageException, InsecureFallbackApprovalException
|
||||||
|
{
|
||||||
|
MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
|
||||||
|
OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
|
||||||
|
|
||||||
|
if (message.isSecure() || message.isEndSession()) {
|
||||||
|
transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<String> messages = multipartMessageHandler.divideMessage(transportMessage);
|
||||||
|
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure());
|
||||||
|
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
|
||||||
|
|
||||||
|
Log.w("SmsTransport", "Secure divide into message parts: " + messages.size());
|
||||||
|
|
||||||
|
for (int i=0;i<messages.size();i++) {
|
||||||
|
// NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients
|
||||||
|
// and messages, this will throw an NPE. We have no idea why, so we're just
|
||||||
|
// catching it and marking the message as a failure. That way at least it
|
||||||
|
// doesn't repeatedly crash every time you start the app.
|
||||||
|
try {
|
||||||
|
SmsManager.getDefault().sendTextMessage(message.getIndividualRecipient().getNumber(), null, messages.get(i),
|
||||||
|
sentIntents.get(i),
|
||||||
|
deliveredIntents == null ? null : deliveredIntents.get(i));
|
||||||
|
} catch (NullPointerException npe) {
|
||||||
|
Log.w(TAG, npe);
|
||||||
|
Log.w(TAG, "Recipient: " + message.getIndividualRecipient().getNumber());
|
||||||
|
Log.w(TAG, "Message Total Parts/Current: " + messages.size() + "/" + i);
|
||||||
|
Log.w(TAG, "Message Part Length: " + messages.get(i).getBytes().length);
|
||||||
|
throw new UndeliverableMessageException(npe);
|
||||||
|
} catch (IllegalArgumentException iae) {
|
||||||
|
Log.w(TAG, iae);
|
||||||
|
throw new UndeliverableMessageException(iae);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deliverPlaintextMessage(SmsMessageRecord message)
|
||||||
|
throws UndeliverableMessageException
|
||||||
|
{
|
||||||
|
ArrayList<String> messages = SmsManager.getDefault().divideMessage(message.getBody().getBody());
|
||||||
|
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false);
|
||||||
|
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
|
||||||
|
String recipient = message.getIndividualRecipient().getNumber();
|
||||||
|
|
||||||
|
// NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients
|
||||||
|
// and messages, this will throw an NPE. We have no idea why, so we're just
|
||||||
|
// catching it and marking the message as a failure. That way at least it doesn't
|
||||||
|
// repeatedly crash every time you start the app.
|
||||||
|
try {
|
||||||
|
SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
|
||||||
|
} catch (NullPointerException npe) {
|
||||||
|
Log.w(TAG, npe);
|
||||||
|
Log.w(TAG, "Recipient: " + recipient);
|
||||||
|
Log.w(TAG, "Message Parts: " + messages.size());
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int i=0;i<messages.size();i++) {
|
||||||
|
SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i),
|
||||||
|
sentIntents.get(i),
|
||||||
|
deliveredIntents == null ? null : deliveredIntents.get(i));
|
||||||
|
}
|
||||||
|
} catch (NullPointerException npe2) {
|
||||||
|
Log.w(TAG, npe);
|
||||||
|
throw new UndeliverableMessageException(npe2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OutgoingTextMessage getAsymmetricEncrypt(MasterSecret masterSecret,
|
||||||
|
OutgoingTextMessage message)
|
||||||
|
throws InsecureFallbackApprovalException
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return new SmsCipher(new TextSecureAxolotlStore(context, masterSecret)).encrypt(message);
|
||||||
|
} catch (NoSessionException e) {
|
||||||
|
throw new InsecureFallbackApprovalException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<PendingIntent> constructSentIntents(long messageId, long type,
|
||||||
|
ArrayList<String> messages, boolean secure)
|
||||||
|
{
|
||||||
|
ArrayList<PendingIntent> sentIntents = new ArrayList<>(messages.size());
|
||||||
|
|
||||||
|
for (String ignored : messages) {
|
||||||
|
sentIntents.add(PendingIntent.getBroadcast(context, 0,
|
||||||
|
constructSentIntent(context, messageId, type, secure, false),
|
||||||
|
0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sentIntents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<PendingIntent> constructDeliveredIntents(long messageId, long type, ArrayList<String> messages) {
|
||||||
|
if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<PendingIntent> deliveredIntents = new ArrayList<>(messages.size());
|
||||||
|
|
||||||
|
for (String ignored : messages) {
|
||||||
|
deliveredIntents.add(PendingIntent.getBroadcast(context, 0,
|
||||||
|
constructDeliveredIntent(context, messageId, type),
|
||||||
|
0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveredIntents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intent constructSentIntent(Context context, long messageId, long type,
|
||||||
|
boolean upgraded, boolean push)
|
||||||
|
{
|
||||||
|
Intent pending = new Intent(SmsDeliveryListener.SENT_SMS_ACTION,
|
||||||
|
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
|
||||||
|
context, SmsDeliveryListener.class);
|
||||||
|
|
||||||
|
pending.putExtra("type", type);
|
||||||
|
pending.putExtra("message_id", messageId);
|
||||||
|
pending.putExtra("upgraded", upgraded);
|
||||||
|
pending.putExtra("push", push);
|
||||||
|
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Intent constructDeliveredIntent(Context context, long messageId, long type) {
|
||||||
|
Intent pending = new Intent(SmsDeliveryListener.DELIVERED_SMS_ACTION,
|
||||||
|
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
|
||||||
|
context, SmsDeliveryListener.class);
|
||||||
|
pending.putExtra("type", type);
|
||||||
|
pending.putExtra("message_id", messageId);
|
||||||
|
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
109
src/org/thoughtcrime/securesms/jobs/SmsSentJob.java
Normal file
109
src/org/thoughtcrime/securesms/jobs/SmsSentJob.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,11 @@ public class MasterSecretRequirementProvider implements RequirementProvider {
|
|||||||
context.registerReceiver(newKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
|
context.registerReceiver(newKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "master_secret";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setListener(RequirementListener listener) {
|
public void setListener(RequirementListener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,7 +33,12 @@ import ws.com.google.android.mms.pdu.PduBody;
|
|||||||
import ws.com.google.android.mms.pdu.PduPart;
|
import ws.com.google.android.mms.pdu.PduPart;
|
||||||
|
|
||||||
public class SlideDeck {
|
public class SlideDeck {
|
||||||
private final List<Slide> slides = new LinkedList<Slide>();
|
|
||||||
|
private final List<Slide> slides = new LinkedList<>();
|
||||||
|
|
||||||
|
public SlideDeck(SlideDeck copy) {
|
||||||
|
this.slides.addAll(copy.getSlides());
|
||||||
|
}
|
||||||
|
|
||||||
public SlideDeck(Context context, MasterSecret masterSecret, PduBody body) {
|
public SlideDeck(Context context, MasterSecret masterSecret, PduBody body) {
|
||||||
try {
|
try {
|
||||||
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -3,19 +3,50 @@ package org.thoughtcrime.securesms.service;
|
|||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.telephony.SmsMessage;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
|
import org.thoughtcrime.securesms.jobs.SmsSentJob;
|
||||||
|
import org.whispersystems.jobqueue.JobManager;
|
||||||
|
|
||||||
public class SmsDeliveryListener extends BroadcastReceiver {
|
public class SmsDeliveryListener extends BroadcastReceiver {
|
||||||
|
|
||||||
|
private static final String TAG = SmsDeliveryListener.class.getSimpleName();
|
||||||
|
|
||||||
|
public static final String SENT_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SENT_SMS_ACTION";
|
||||||
|
public static final String DELIVERED_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DELIVERED_SMS_ACTION";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) {
|
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||||
intent.putExtra("ResultCode", this.getResultCode());
|
long messageId = intent.getLongExtra("message_id", -1);
|
||||||
intent.setClass(context, SendReceiveService.class);
|
|
||||||
context.startService(intent);
|
switch (intent.getAction()) {
|
||||||
} else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) {
|
case SENT_SMS_ACTION:
|
||||||
intent.putExtra("ResultCode", this.getResultCode());
|
int result = getResultCode();
|
||||||
intent.setClass(context, SendReceiveService.class);
|
|
||||||
context.startService(intent);
|
jobManager.add(new SmsSentJob(context, messageId, SENT_SMS_ACTION, result));
|
||||||
|
break;
|
||||||
|
case DELIVERED_SMS_ACTION:
|
||||||
|
byte[] pdu = intent.getByteArrayExtra("pdu");
|
||||||
|
|
||||||
|
if (pdu == null) {
|
||||||
|
Log.w(TAG, "No PDU in delivery receipt!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
SmsMessage message = SmsMessage.createFromPdu(pdu);
|
||||||
|
|
||||||
|
if (message == null) {
|
||||||
|
Log.w(TAG, "Delivery receipt failed to parse!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobManager.add(new SmsSentJob(context, messageId, DELIVERED_SMS_ACTION, message.getStatus()));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.w(TAG, "Unknown action: " + intent.getAction());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,106 +17,231 @@
|
|||||||
package org.thoughtcrime.securesms.sms;
|
package org.thoughtcrime.securesms.sms;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||||
|
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||||
import org.thoughtcrime.securesms.service.SendReceiveService;
|
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
import org.whispersystems.jobqueue.JobManager;
|
||||||
|
import org.whispersystems.textsecure.directory.Directory;
|
||||||
|
import org.whispersystems.textsecure.directory.NotInDirectoryException;
|
||||||
|
import org.whispersystems.textsecure.push.ContactTokenDetails;
|
||||||
|
import org.whispersystems.textsecure.push.PushServiceSocket;
|
||||||
|
import org.whispersystems.textsecure.util.DirectoryUtil;
|
||||||
import org.whispersystems.textsecure.util.InvalidNumberException;
|
import org.whispersystems.textsecure.util.InvalidNumberException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.io.IOException;
|
||||||
|
|
||||||
import ws.com.google.android.mms.MmsException;
|
import ws.com.google.android.mms.MmsException;
|
||||||
|
|
||||||
public class MessageSender {
|
public class MessageSender {
|
||||||
|
|
||||||
public static long send(Context context, MasterSecret masterSecret,
|
private static final String TAG = MessageSender.class.getSimpleName();
|
||||||
OutgoingTextMessage message, long threadId,
|
|
||||||
boolean forceSms)
|
public static long send(final Context context,
|
||||||
|
final MasterSecret masterSecret,
|
||||||
|
final OutgoingTextMessage message,
|
||||||
|
final long threadId,
|
||||||
|
final boolean forceSms)
|
||||||
{
|
{
|
||||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||||
|
Recipients recipients = message.getRecipients();
|
||||||
|
boolean keyExchange = message.isKeyExchange();
|
||||||
|
|
||||||
|
long allocatedThreadId;
|
||||||
|
|
||||||
if (threadId == -1) {
|
if (threadId == -1) {
|
||||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(message.getRecipients());
|
allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
|
||||||
|
} else {
|
||||||
|
allocatedThreadId = threadId;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Long> messageIds = database.insertMessageOutbox(masterSecret, threadId, message, forceSms);
|
long messageId = database.insertMessageOutbox(masterSecret, allocatedThreadId, message, forceSms);
|
||||||
|
|
||||||
|
sendTextMessage(context, recipients, forceSms, keyExchange, messageId);
|
||||||
|
|
||||||
|
return allocatedThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
if (!forceSms && isSelfSend(context, message.getRecipients())) {
|
|
||||||
for (long messageId : messageIds) {
|
|
||||||
database.markAsSent(messageId);
|
database.markAsSent(messageId);
|
||||||
database.markAsPush(messageId);
|
database.markAsPush(messageId);
|
||||||
|
|
||||||
Pair<Long, Long> messageAndThreadId = database.copyMessageInbox(messageId);
|
Pair<Long, Long> messageAndThreadId = database.copyMessageInbox(messageId);
|
||||||
database.markAsPush(messageAndThreadId.first);
|
database.markAsPush(messageAndThreadId.first);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
for (long messageId : messageIds) {
|
|
||||||
Log.w("SMSSender", "Got message id for new message: " + messageId);
|
|
||||||
|
|
||||||
Intent intent = new Intent(SendReceiveService.SEND_SMS_ACTION, null,
|
private static void sendMediaSelf(Context context, MasterSecret masterSecret, long messageId)
|
||||||
context, SendReceiveService.class);
|
|
||||||
intent.putExtra("message_id", messageId);
|
|
||||||
context.startService(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return threadId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long send(Context context, MasterSecret masterSecret,
|
|
||||||
OutgoingMediaMessage message,
|
|
||||||
long threadId, boolean forceSms)
|
|
||||||
throws MmsException
|
throws MmsException
|
||||||
{
|
{
|
||||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||||
|
|
||||||
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.markAsSent(messageId, "self-send".getBytes(), 0);
|
||||||
database.markAsPush(messageId);
|
database.markAsPush(messageId);
|
||||||
|
|
||||||
long newMessageId = database.copyMessageInbox(masterSecret, messageId);
|
long newMessageId = database.copyMessageInbox(masterSecret, messageId);
|
||||||
database.markAsPush(newMessageId);
|
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;
|
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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void resend(Context context, long messageId, boolean isMms)
|
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()));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
intent.putExtra("message_id", messageId);
|
|
||||||
context.startService(intent);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPushMediaSend(Context context, Recipients recipients) {
|
||||||
|
try {
|
||||||
|
if (!TextSecurePreferences.isPushRegistered(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipients.getRecipientsList().size() > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Recipient recipient = recipients.getPrimaryRecipient();
|
||||||
|
String destination = Util.canonicalizeNumber(context, recipient.getNumber());
|
||||||
|
|
||||||
|
return isPushDestination(context, destination);
|
||||||
|
} catch (InvalidNumberException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isGroupPushSend(Recipients recipients) {
|
||||||
|
return GroupUtil.isEncodedGroup(recipients.getPrimaryRecipient().getNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isSelfSend(Context context, Recipients recipients) {
|
private static boolean isSelfSend(Context context, Recipients recipients) {
|
||||||
@ -137,4 +262,32 @@ public class MessageSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isPushDestination(Context context, String destination) {
|
||||||
|
Directory directory = Directory.getInstance(context);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return directory.isActiveNumber(destination);
|
||||||
|
} catch (NotInDirectoryException e) {
|
||||||
|
try {
|
||||||
|
PushServiceSocket socket = PushServiceSocketFactory.create(context);
|
||||||
|
String contactToken = DirectoryUtil.getDirectoryServerToken(destination);
|
||||||
|
ContactTokenDetails registeredUser = socket.getContactTokenDetails(contactToken);
|
||||||
|
|
||||||
|
if (registeredUser == null) {
|
||||||
|
registeredUser = new ContactTokenDetails();
|
||||||
|
registeredUser.setNumber(destination);
|
||||||
|
directory.setNumber(registeredUser, false);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
registeredUser.setNumber(destination);
|
||||||
|
directory.setNumber(registeredUser, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (IOException e1) {
|
||||||
|
Log.w(TAG, e1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user