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.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; 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.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 java.util.ArrayList; public class SmsSendJob extends SendJob { 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() { SmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); database.markAsSending(messageId); } @Override public void onSend(MasterSecret masterSecret) throws NoSuchMessageException { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); SmsMessageRecord record = database.getMessage(masterSecret, messageId); try { Log.w(TAG, "Sending message: " + messageId); deliver(record); } catch (UndeliverableMessageException ude) { Log.w(TAG, ude); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(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(SmsMessageRecord message) throws UndeliverableMessageException { if (!NumberUtil.isValidSmsOrEmail(message.getIndividualRecipient().getNumber())) { throw new UndeliverableMessageException("Not a valid SMS destination! " + message.getIndividualRecipient().getNumber()); } if (message.isSecure() || message.isKeyExchange() || message.isEndSession()) { throw new UndeliverableMessageException("Trying to send a secure SMS?"); } ArrayList 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(); // remove characters for visible number formatting - they break sending sms on some phones (issue #1516) // stripSeparator() to rigid? what will people try to send here? - see discussion on #3099 //recipient = PhoneNumberUtils.stripSeparators(recipient); // minimum required to fix the issue - brackets are no dialable chars... recipient = recipient.replaceAll("[\\(\\)]", ""); // 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)) .withRetryCount(15) .withGroupId(name); if (TextSecurePreferences.isWifiSmsEnabled(context)) { builder.withRequirement(new NetworkOrServiceRequirement(context)); } else { builder.withRequirement(new ServiceRequirement(context)); } return builder.create(); } }