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.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.NetworkOrServiceRequirement; 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.libaxolotl.NoSessionException; import java.util.ArrayList; public class SmsSendJob extends MasterSecretJob { private static final String TAG = SmsSendJob.class.getSimpleName(); private final long messageId; public SmsSendJob(Context context, long messageId, String name) { super(context, constructParameters(context, name)); this.messageId = messageId; } @Override public void onAdded() { } @Override public void onRun(MasterSecret masterSecret) throws NoSuchMessageException { 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 boolean onShouldRetryThrowable(Exception throwable) { return false; } @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); } 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 messages = multipartMessageHandler.divideMessage(transportMessage); ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, message.isSecure()); ArrayList deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages); Log.w("SmsTransport", "Secure divide into message parts: " + messages.size()); for (int i=0;i messages = SmsManager.getDefault().divideMessage(message.getBody().getBody()); ArrayList sentIntents = constructSentIntents(message.getId(), message.getType(), messages, false); ArrayList deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages); String recipient = message.getIndividualRecipient().getNumber(); // NOTE 11/04/14 -- There's apparently a bug where for some unknown recipients // and messages, this will throw an NPE. We have no idea why, so we're just // catching it and marking the message as a failure. That way at least it doesn't // repeatedly crash every time you start the app. try { SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents); } catch (NullPointerException npe) { Log.w(TAG, npe); Log.w(TAG, "Recipient: " + recipient); Log.w(TAG, "Message Parts: " + messages.size()); try { for (int i=0;i constructSentIntents(long messageId, long type, ArrayList messages, boolean secure) { ArrayList sentIntents = new ArrayList<>(messages.size()); for (String ignored : messages) { sentIntents.add(PendingIntent.getBroadcast(context, 0, constructSentIntent(context, messageId, type, secure, false), 0)); } return sentIntents; } private ArrayList constructDeliveredIntents(long messageId, long type, ArrayList messages) { if (!TextSecurePreferences.isSmsDeliveryReportsEnabled(context)) { return null; } ArrayList deliveredIntents = new ArrayList<>(messages.size()); for (String ignored : messages) { deliveredIntents.add(PendingIntent.getBroadcast(context, 0, constructDeliveredIntent(context, messageId, type), 0)); } return deliveredIntents; } private Intent constructSentIntent(Context context, long messageId, long type, boolean upgraded, boolean push) { Intent pending = new Intent(SmsDeliveryListener.SENT_SMS_ACTION, Uri.parse("custom://" + messageId + System.currentTimeMillis()), context, SmsDeliveryListener.class); pending.putExtra("type", type); pending.putExtra("message_id", messageId); pending.putExtra("upgraded", upgraded); pending.putExtra("push", push); return pending; } private 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; } 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(); } }