mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-24 11:08:50 +00:00
Transition the outbound pipeline to JobManager jobs.
This commit is contained in:
@@ -57,7 +57,7 @@ public class MmsDownloadJob extends MasterSecretJob {
|
||||
.withPersistence()
|
||||
.withRequirement(new MasterSecretRequirement(context))
|
||||
.withRequirement(new NetworkRequirement(context))
|
||||
.withGroupId("mms-download")
|
||||
.withGroupId("mms-operation")
|
||||
.create());
|
||||
|
||||
this.messageId = messageId;
|
||||
@@ -170,7 +170,13 @@ public class MmsDownloadJob extends MasterSecretJob {
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
// TODO
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE);
|
||||
|
||||
if (automatic) {
|
||||
database.markIncomingNotificationReceived(threadId);
|
||||
MessageNotifier.updateNotification(context, null, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
249
src/org/thoughtcrime/securesms/jobs/MmsSendJob.java
Normal file
249
src/org/thoughtcrime/securesms/jobs/MmsSendJob.java
Normal file
@@ -0,0 +1,249 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MmsCipher;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
||||
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
|
||||
import org.thoughtcrime.securesms.mms.MmsRadio;
|
||||
import org.thoughtcrime.securesms.mms.MmsRadioException;
|
||||
import org.thoughtcrime.securesms.mms.MmsSendResult;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.NumberUtil;
|
||||
import org.whispersystems.jobqueue.JobParameters;
|
||||
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
|
||||
import org.whispersystems.libaxolotl.NoSessionException;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import ws.com.google.android.mms.MmsException;
|
||||
import ws.com.google.android.mms.pdu.EncodedStringValue;
|
||||
import ws.com.google.android.mms.pdu.PduComposer;
|
||||
import ws.com.google.android.mms.pdu.PduHeaders;
|
||||
import ws.com.google.android.mms.pdu.SendConf;
|
||||
import ws.com.google.android.mms.pdu.SendReq;
|
||||
|
||||
public class MmsSendJob extends MasterSecretJob {
|
||||
|
||||
private static final String TAG = MmsSendJob.class.getSimpleName();
|
||||
|
||||
private final long messageId;
|
||||
|
||||
public MmsSendJob(Context context, long messageId) {
|
||||
super(context, JobParameters.newBuilder()
|
||||
.withGroupId("mms-operation")
|
||||
.withRequirement(new NetworkRequirement(context))
|
||||
.withRequirement(new MasterSecretRequirement(context))
|
||||
.withPersistence()
|
||||
.create());
|
||||
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws RequirementNotMetException, MmsException, NoSuchMessageException {
|
||||
MasterSecret masterSecret = getMasterSecret();
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
|
||||
|
||||
try {
|
||||
MmsSendResult result = deliver(masterSecret, message);
|
||||
|
||||
if (result.isUpgradedSecure()) {
|
||||
database.markAsSecure(messageId);
|
||||
}
|
||||
|
||||
database.markAsSent(messageId, result.getMessageId(), result.getResponseStatus());
|
||||
} catch (UndeliverableMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
} catch (InsecureFallbackApprovalException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsPendingInsecureSmsFallback(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(Throwable throwable) {
|
||||
if (throwable instanceof RequirementNotMetException) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
|
||||
public MmsSendResult deliver(MasterSecret masterSecret, SendReq message)
|
||||
throws UndeliverableMessageException, InsecureFallbackApprovalException
|
||||
{
|
||||
|
||||
validateDestinations(message);
|
||||
|
||||
MmsRadio radio = MmsRadio.getInstance(context);
|
||||
|
||||
try {
|
||||
if (isCdmaDevice()) {
|
||||
Log.w(TAG, "Sending MMS directly without radio change...");
|
||||
try {
|
||||
return sendMms(masterSecret, radio, message, false, false);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "Sending MMS with radio change and proxy...");
|
||||
radio.connect();
|
||||
|
||||
try {
|
||||
MmsSendResult result = sendMms(masterSecret, radio, message, true, true);
|
||||
radio.disconnect();
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.w(TAG, "Sending MMS with radio change and without proxy...");
|
||||
|
||||
try {
|
||||
MmsSendResult result = sendMms(masterSecret, radio, message, true, false);
|
||||
radio.disconnect();
|
||||
return result;
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
radio.disconnect();
|
||||
throw new UndeliverableMessageException(ioe);
|
||||
}
|
||||
|
||||
} catch (MmsRadioException mre) {
|
||||
Log.w(TAG, mre);
|
||||
throw new UndeliverableMessageException(mre);
|
||||
}
|
||||
}
|
||||
|
||||
private MmsSendResult sendMms(MasterSecret masterSecret, MmsRadio radio, SendReq message,
|
||||
boolean usingMmsRadio, boolean useProxy)
|
||||
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
|
||||
{
|
||||
String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
|
||||
boolean upgradedSecure = false;
|
||||
|
||||
if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) {
|
||||
message = getEncryptedMessage(masterSecret, message);
|
||||
upgradedSecure = true;
|
||||
}
|
||||
|
||||
if (number != null && number.trim().length() != 0) {
|
||||
message.setFrom(new EncodedStringValue(number));
|
||||
}
|
||||
|
||||
try {
|
||||
OutgoingMmsConnection connection = new OutgoingMmsConnection(context, radio.getApnInformation(), new PduComposer(context, message).make());
|
||||
SendConf conf = connection.send(usingMmsRadio, useProxy);
|
||||
|
||||
for (int i=0;i<message.getBody().getPartsNum();i++) {
|
||||
Log.w(TAG, "Sent MMS part of content-type: " + new String(message.getBody().getPart(i).getContentType()));
|
||||
}
|
||||
|
||||
if (conf == null) {
|
||||
throw new UndeliverableMessageException("No M-Send.conf received in response to send.");
|
||||
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
|
||||
throw new UndeliverableMessageException("Got bad response: " + conf.getResponseStatus());
|
||||
} else if (isInconsistentResponse(message, conf)) {
|
||||
throw new UndeliverableMessageException("Mismatched response!");
|
||||
} else {
|
||||
return new MmsSendResult(conf.getMessageId(), conf.getResponseStatus(), upgradedSecure, false);
|
||||
}
|
||||
} catch (ApnUnavailableException aue) {
|
||||
throw new IOException("no APN was retrievable");
|
||||
}
|
||||
}
|
||||
|
||||
private SendReq getEncryptedMessage(MasterSecret masterSecret, SendReq pdu)
|
||||
throws InsecureFallbackApprovalException
|
||||
{
|
||||
try {
|
||||
MmsCipher cipher = new MmsCipher(new TextSecureAxolotlStore(context, masterSecret));
|
||||
return cipher.encrypt(context, pdu);
|
||||
} catch (NoSessionException e) {
|
||||
throw new InsecureFallbackApprovalException(e);
|
||||
} catch (RecipientFormattingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInconsistentResponse(SendReq message, SendConf response) {
|
||||
Log.w(TAG, "Comparing: " + Hex.toString(message.getTransactionId()));
|
||||
Log.w(TAG, "With: " + Hex.toString(response.getTransactionId()));
|
||||
return !Arrays.equals(message.getTransactionId(), response.getTransactionId());
|
||||
}
|
||||
|
||||
private boolean isCdmaDevice() {
|
||||
return ((TelephonyManager)context
|
||||
.getSystemService(Context.TELEPHONY_SERVICE))
|
||||
.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
|
||||
}
|
||||
|
||||
private void validateDestination(EncodedStringValue destination) throws UndeliverableMessageException {
|
||||
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
|
||||
throw new UndeliverableMessageException("Invalid destination: " +
|
||||
(destination == null ? null : destination.getString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDestinations(SendReq message) throws UndeliverableMessageException {
|
||||
if (message.getTo() != null) {
|
||||
for (EncodedStringValue to : message.getTo()) {
|
||||
validateDestination(to);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.getCc() != null) {
|
||||
for (EncodedStringValue cc : message.getCc()) {
|
||||
validateDestination(cc);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.getBcc() != null) {
|
||||
for (EncodedStringValue bcc : message.getBcc()) {
|
||||
validateDestination(bcc);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) {
|
||||
throw new UndeliverableMessageException("No to, cc, or bcc specified!");
|
||||
}
|
||||
}
|
||||
|
||||
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.EncryptingSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
|
||||
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
||||
@@ -68,18 +69,13 @@ public class PushDecryptJob extends MasterSecretJob {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws RequirementNotMetException {
|
||||
try {
|
||||
MasterSecret masterSecret = getMasterSecret();
|
||||
PushDatabase database = DatabaseFactory.getPushDatabase(context);
|
||||
TextSecureEnvelope envelope = database.get(messageId);
|
||||
public void onRun() throws RequirementNotMetException, NoSuchMessageException {
|
||||
MasterSecret masterSecret = getMasterSecret();
|
||||
PushDatabase database = DatabaseFactory.getPushDatabase(context);
|
||||
TextSecureEnvelope envelope = database.get(messageId);
|
||||
|
||||
handleMessage(masterSecret, envelope);
|
||||
database.delete(messageId);
|
||||
|
||||
} catch (PushDatabase.NoSuchMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
handleMessage(masterSecret, envelope);
|
||||
database.delete(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
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.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
||||
@@ -61,18 +62,15 @@ public class SmsDecryptJob extends MasterSecretJob {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws RequirementNotMetException {
|
||||
public void onRun() throws RequirementNotMetException, NoSuchMessageException {
|
||||
MasterSecret masterSecret = getMasterSecret();
|
||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
SmsDatabase.Reader reader = null;
|
||||
|
||||
try {
|
||||
reader = database.getMessage(masterSecret, messageId);
|
||||
|
||||
SmsMessageRecord record = reader.getNext();
|
||||
IncomingTextMessage message = createIncomingTextMessage(masterSecret, record);
|
||||
long messageId = record.getId();
|
||||
long threadId = record.getThreadId();
|
||||
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
|
||||
IncomingTextMessage message = createIncomingTextMessage(masterSecret, record);
|
||||
long messageId = record.getId();
|
||||
long threadId = record.getThreadId();
|
||||
|
||||
if (message.isSecureMessage()) handleSecureMessage(masterSecret, messageId, message);
|
||||
else if (message.isPreKeyBundle()) handlePreKeyWhisperMessage(masterSecret, messageId, threadId, (IncomingPreKeyBundleMessage) message);
|
||||
@@ -93,9 +91,6 @@ public class SmsDecryptJob extends MasterSecretJob {
|
||||
} catch (NoSessionException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsNoSession(messageId);
|
||||
} finally {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,46 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.telephony.SmsManager;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.whispersystems.jobqueue.EncryptionKeys;
|
||||
import org.thoughtcrime.securesms.crypto.SmsCipher;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
||||
import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirement;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.service.SmsDeliveryListener;
|
||||
import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.NumberUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.jobqueue.JobParameters;
|
||||
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
|
||||
import org.whispersystems.libaxolotl.NoSessionException;
|
||||
|
||||
public class SmsSendJob extends ContextJob {
|
||||
import java.util.ArrayList;
|
||||
|
||||
private transient MasterSecret masterSecret;
|
||||
public class SmsSendJob extends MasterSecretJob {
|
||||
|
||||
private static final String TAG = SmsSendJob.class.getSimpleName();
|
||||
|
||||
private final long messageId;
|
||||
|
||||
public SmsSendJob(Context context, MasterSecret masterSecret, long messageId, String name) {
|
||||
public SmsSendJob(Context context, long messageId, String name) {
|
||||
super(context, JobParameters.newBuilder()
|
||||
.withPersistence()
|
||||
.withEncryption(new EncryptionKeys(ParcelUtil.serialize(masterSecret)))
|
||||
.withRequirement(new MasterSecretRequirement(context))
|
||||
.withRequirement(new ServiceRequirement(context))
|
||||
.withGroupId(name)
|
||||
.create());
|
||||
|
||||
@@ -30,19 +53,191 @@ public class SmsSendJob extends ContextJob {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() {
|
||||
MasterSecret masterSecret = ParcelUtil.deserialize(getEncryptionKeys().getEncoded(), MasterSecret.CREATOR);
|
||||
public void onRun() throws RequirementNotMetException, NoSuchMessageException {
|
||||
MasterSecret masterSecret = getMasterSecret();
|
||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
|
||||
|
||||
try {
|
||||
Log.w(TAG, "Sending message: " + messageId);
|
||||
|
||||
deliver(masterSecret, record);
|
||||
} catch (UndeliverableMessageException ude) {
|
||||
Log.w(TAG, ude);
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId());
|
||||
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
|
||||
} catch (InsecureFallbackApprovalException ifae) {
|
||||
Log.w(TAG, ifae);
|
||||
DatabaseFactory.getSmsDatabase(context).markAsPendingInsecureSmsFallback(record.getId());
|
||||
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Log.w(TAG, "onCanceled()");
|
||||
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
||||
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
|
||||
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
||||
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(Throwable throwable) {
|
||||
if (throwable instanceof RequirementNotMetException) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void deliver(MasterSecret masterSecret, SmsMessageRecord record)
|
||||
throws UndeliverableMessageException, InsecureFallbackApprovalException
|
||||
{
|
||||
if (!NumberUtil.isValidSmsOrEmail(record.getIndividualRecipient().getNumber())) {
|
||||
throw new UndeliverableMessageException("Not a valid SMS destination! " + record.getIndividualRecipient().getNumber());
|
||||
}
|
||||
|
||||
if (record.isSecure() || record.isKeyExchange() || record.isEndSession()) {
|
||||
deliverSecureMessage(masterSecret, record);
|
||||
} else {
|
||||
deliverPlaintextMessage(record);
|
||||
}
|
||||
}
|
||||
|
||||
private void deliverSecureMessage(MasterSecret masterSecret, SmsMessageRecord message)
|
||||
throws UndeliverableMessageException, InsecureFallbackApprovalException
|
||||
{
|
||||
MultipartSmsMessageHandler multipartMessageHandler = new MultipartSmsMessageHandler();
|
||||
OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
|
||||
|
||||
if (message.isSecure() || message.isEndSession()) {
|
||||
transportMessage = getAsymmetricEncrypt(masterSecret, transportMessage);
|
||||
}
|
||||
|
||||
ArrayList<String> messages = multipartMessageHandler.divideMessage(transportMessage);
|
||||
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure());
|
||||
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
|
||||
|
||||
Log.w("SmsTransport", "Secure divide into message parts: " + messages.size());
|
||||
|
||||
for (int i=0;i<messages.size();i++) {
|
||||
// NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients
|
||||
// and messages, this will throw an NPE. We have no idea why, so we're just
|
||||
// catching it and marking the message as a failure. That way at least it
|
||||
// doesn't repeatedly crash every time you start the app.
|
||||
try {
|
||||
SmsManager.getDefault().sendTextMessage(message.getIndividualRecipient().getNumber(), null, messages.get(i),
|
||||
sentIntents.get(i),
|
||||
deliveredIntents == null ? null : deliveredIntents.get(i));
|
||||
} catch (NullPointerException npe) {
|
||||
Log.w(TAG, npe);
|
||||
Log.w(TAG, "Recipient: " + message.getIndividualRecipient().getNumber());
|
||||
Log.w(TAG, "Message Total Parts/Current: " + messages.size() + "/" + i);
|
||||
Log.w(TAG, "Message Part Length: " + messages.get(i).getBytes().length);
|
||||
throw new UndeliverableMessageException(npe);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
Log.w(TAG, iae);
|
||||
throw new UndeliverableMessageException(iae);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deliverPlaintextMessage(SmsMessageRecord message)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
ArrayList<String> messages = SmsManager.getDefault().divideMessage(message.getBody().getBody());
|
||||
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false);
|
||||
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
|
||||
String recipient = message.getIndividualRecipient().getNumber();
|
||||
|
||||
// NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients
|
||||
// and messages, this will throw an NPE. We have no idea why, so we're just
|
||||
// catching it and marking the message as a failure. That way at least it doesn't
|
||||
// repeatedly crash every time you start the app.
|
||||
try {
|
||||
SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
|
||||
} catch (NullPointerException npe) {
|
||||
Log.w(TAG, npe);
|
||||
Log.w(TAG, "Recipient: " + recipient);
|
||||
Log.w(TAG, "Message Parts: " + messages.size());
|
||||
|
||||
try {
|
||||
for (int i=0;i<messages.size();i++) {
|
||||
SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i),
|
||||
sentIntents.get(i),
|
||||
deliveredIntents == null ? null : deliveredIntents.get(i));
|
||||
}
|
||||
} catch (NullPointerException npe2) {
|
||||
Log.w(TAG, npe);
|
||||
throw new UndeliverableMessageException(npe2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OutgoingTextMessage getAsymmetricEncrypt(MasterSecret masterSecret,
|
||||
OutgoingTextMessage message)
|
||||
throws InsecureFallbackApprovalException
|
||||
{
|
||||
try {
|
||||
return new SmsCipher(new TextSecureAxolotlStore(context, masterSecret)).encrypt(message);
|
||||
} catch (NoSessionException e) {
|
||||
throw new InsecureFallbackApprovalException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<PendingIntent> constructSentIntents(long messageId, long type,
|
||||
ArrayList<String> messages, boolean secure)
|
||||
{
|
||||
ArrayList<PendingIntent> sentIntents = new ArrayList<>(messages.size());
|
||||
|
||||
for (String ignored : messages) {
|
||||
sentIntents.add(PendingIntent.getBroadcast(context, 0,
|
||||
constructSentIntent(context, messageId, type, secure, false),
|
||||
0));
|
||||
}
|
||||
|
||||
return sentIntents;
|
||||
}
|
||||
|
||||
private ArrayList<PendingIntent> constructDeliveredIntents(long messageId, long type, ArrayList<String> messages) {
|
||||
if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ArrayList<PendingIntent> deliveredIntents = new ArrayList<>(messages.size());
|
||||
|
||||
for (String ignored : messages) {
|
||||
deliveredIntents.add(PendingIntent.getBroadcast(context, 0,
|
||||
constructDeliveredIntent(context, messageId, type),
|
||||
0));
|
||||
}
|
||||
|
||||
return deliveredIntents;
|
||||
}
|
||||
|
||||
private Intent constructSentIntent(Context context, long messageId, long type,
|
||||
boolean upgraded, boolean push)
|
||||
{
|
||||
Intent pending = new Intent(SmsDeliveryListener.SENT_SMS_ACTION,
|
||||
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
|
||||
context, SmsDeliveryListener.class);
|
||||
|
||||
pending.putExtra("type", type);
|
||||
pending.putExtra("message_id", messageId);
|
||||
pending.putExtra("upgraded", upgraded);
|
||||
pending.putExtra("push", push);
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
protected Intent constructDeliveredIntent(Context context, long messageId, long type) {
|
||||
Intent pending = new Intent(SmsDeliveryListener.DELIVERED_SMS_ACTION,
|
||||
Uri.parse("custom://" + messageId + System.currentTimeMillis()),
|
||||
context, SmsDeliveryListener.class);
|
||||
pending.putExtra("type", type);
|
||||
pending.putExtra("message_id", messageId);
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "master_secret";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener(RequirementListener 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user