2014-11-03 15:16:04 -08:00
|
|
|
package org.thoughtcrime.securesms.jobs;
|
|
|
|
|
2014-11-08 11:35:58 -08:00
|
|
|
import android.app.PendingIntent;
|
2014-11-03 15:16:04 -08:00
|
|
|
import android.content.Context;
|
2014-11-08 11:35:58 -08:00
|
|
|
import android.content.Intent;
|
|
|
|
import android.net.Uri;
|
|
|
|
import android.telephony.SmsManager;
|
|
|
|
import android.util.Log;
|
2014-11-03 15:16:04 -08:00
|
|
|
|
|
|
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
2014-11-08 11:35:58 -08:00
|
|
|
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;
|
2014-12-24 12:40:28 -08:00
|
|
|
import org.thoughtcrime.securesms.jobs.requirements.NetworkOrServiceRequirement;
|
2014-11-08 11:35:58 -08:00
|
|
|
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;
|
2014-11-03 15:16:04 -08:00
|
|
|
import org.whispersystems.jobqueue.JobParameters;
|
2014-11-08 11:35:58 -08:00
|
|
|
import org.whispersystems.libaxolotl.NoSessionException;
|
2014-11-03 15:16:04 -08:00
|
|
|
|
2014-11-08 11:35:58 -08:00
|
|
|
import java.util.ArrayList;
|
2014-11-03 15:16:04 -08:00
|
|
|
|
2014-11-08 11:35:58 -08:00
|
|
|
public class SmsSendJob extends MasterSecretJob {
|
|
|
|
|
|
|
|
private static final String TAG = SmsSendJob.class.getSimpleName();
|
2014-11-03 15:16:04 -08:00
|
|
|
|
|
|
|
private final long messageId;
|
|
|
|
|
2014-11-08 11:35:58 -08:00
|
|
|
public SmsSendJob(Context context, long messageId, String name) {
|
2014-12-24 12:40:28 -08:00
|
|
|
super(context, constructParameters(context, name));
|
2014-11-03 15:16:04 -08:00
|
|
|
this.messageId = messageId;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onAdded() {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2014-11-11 19:57:53 -08:00
|
|
|
public void onRun(MasterSecret masterSecret) throws NoSuchMessageException {
|
|
|
|
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
|
|
|
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
|
2014-11-03 15:16:04 -08:00
|
|
|
|
2014-11-08 11:35:58 -08:00
|
|
|
try {
|
|
|
|
Log.w(TAG, "Sending message: " + messageId);
|
2014-11-03 15:16:04 -08:00
|
|
|
|
2014-11-08 11:35:58 -08:00
|
|
|
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());
|
|
|
|
}
|
2014-11-03 15:16:04 -08:00
|
|
|
}
|
|
|
|
|
2014-11-11 19:57:53 -08:00
|
|
|
@Override
|
2014-11-11 21:11:57 -08:00
|
|
|
public boolean onShouldRetryThrowable(Exception throwable) {
|
2014-11-11 19:57:53 -08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-11-03 15:16:04 -08:00
|
|
|
@Override
|
|
|
|
public void onCanceled() {
|
2014-11-08 11:35:58 -08:00
|
|
|
Log.w(TAG, "onCanceled()");
|
|
|
|
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
|
|
|
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
|
2014-11-03 15:16:04 -08:00
|
|
|
|
2014-11-08 11:35:58 -08:00
|
|
|
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
|
|
|
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
|
2014-11-03 15:16:04 -08:00
|
|
|
}
|
|
|
|
|
2014-11-08 11:35:58 -08:00
|
|
|
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();
|
2014-12-24 12:40:28 -08:00
|
|
|
OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
|
2014-11-08 11:35:58 -08:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2014-12-24 12:40:28 -08:00
|
|
|
private Intent constructDeliveredIntent(Context context, long messageId, long type) {
|
2014-11-08 11:35:58 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2014-12-24 12:40:28 -08:00
|
|
|
private static JobParameters constructParameters(Context context, String name) {
|
|
|
|
JobParameters.Builder builder = JobParameters.newBuilder()
|
|
|
|
.withPersistence()
|
|
|
|
.withRequirement(new MasterSecretRequirement(context))
|
|
|
|
.withGroupId(name);
|
|
|
|
|
|
|
|
if (TextSecurePreferences.isWifiSmsEnabled(context)) {
|
|
|
|
builder.withRequirement(new NetworkOrServiceRequirement(context));
|
|
|
|
} else {
|
|
|
|
builder.withRequirement(new ServiceRequirement(context));
|
|
|
|
}
|
|
|
|
|
|
|
|
return builder.create();
|
|
|
|
}
|
|
|
|
|
2014-11-08 11:35:58 -08:00
|
|
|
|
2014-11-03 15:16:04 -08:00
|
|
|
}
|