2012-10-30 00:34:14 +00:00
|
|
|
/**
|
2011-12-20 18:20:44 +00:00
|
|
|
* Copyright (C) 2011 Whisper Systems
|
2012-10-30 00:34:14 +00:00
|
|
|
*
|
2011-12-20 18:20:44 +00:00
|
|
|
* 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.
|
2012-10-30 00:34:14 +00:00
|
|
|
*
|
2011-12-20 18:20:44 +00:00
|
|
|
* 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;
|
|
|
|
|
2013-02-15 03:15:40 +00:00
|
|
|
import android.app.Activity;
|
2012-10-30 00:34:14 +00:00
|
|
|
import android.app.PendingIntent;
|
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
2013-02-15 04:42:29 +00:00
|
|
|
import android.content.IntentFilter;
|
2012-10-30 00:34:14 +00:00
|
|
|
import android.database.Cursor;
|
|
|
|
import android.net.Uri;
|
2013-01-07 05:38:36 +00:00
|
|
|
import android.preference.PreferenceManager;
|
2012-10-30 00:34:14 +00:00
|
|
|
import android.telephony.SmsManager;
|
2013-02-15 03:15:40 +00:00
|
|
|
import android.telephony.SmsMessage;
|
2012-10-30 00:34:14 +00:00
|
|
|
import android.util.Log;
|
2011-12-20 18:20:44 +00:00
|
|
|
|
2013-01-07 05:38:36 +00:00
|
|
|
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
2013-02-15 03:15:40 +00:00
|
|
|
import org.thoughtcrime.securesms.R;
|
2011-12-20 18:20:44 +00:00
|
|
|
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
|
|
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
|
|
import org.thoughtcrime.securesms.crypto.SessionCipher;
|
|
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
|
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
2013-02-15 03:15:40 +00:00
|
|
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
2011-12-20 18:20:44 +00:00
|
|
|
import org.thoughtcrime.securesms.protocol.KeyExchangeWirePrefix;
|
|
|
|
import org.thoughtcrime.securesms.protocol.Prefix;
|
|
|
|
import org.thoughtcrime.securesms.protocol.SecureMessageWirePrefix;
|
|
|
|
import org.thoughtcrime.securesms.protocol.WirePrefix;
|
|
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
2013-02-15 03:15:40 +00:00
|
|
|
import org.thoughtcrime.securesms.recipients.Recipients;
|
|
|
|
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
|
2011-12-20 18:20:44 +00:00
|
|
|
import org.thoughtcrime.securesms.sms.MultipartMessageHandler;
|
|
|
|
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
|
|
|
|
import org.thoughtcrime.securesms.util.InvalidMessageException;
|
|
|
|
|
2012-10-30 00:34:14 +00:00
|
|
|
import java.util.ArrayList;
|
2013-02-15 03:15:40 +00:00
|
|
|
import java.util.HashSet;
|
|
|
|
import java.util.Set;
|
2011-12-20 18:20:44 +00:00
|
|
|
|
|
|
|
public class SmsSender {
|
|
|
|
|
|
|
|
private final MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler();
|
2013-02-15 03:15:40 +00:00
|
|
|
private final Set<Long> pendingMessages = new HashSet<Long>();
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
private final Context context;
|
2013-02-15 03:15:40 +00:00
|
|
|
private final ToastHandler toastHandler;
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2013-02-15 03:15:40 +00:00
|
|
|
public SmsSender(Context context, ToastHandler toastHandler) {
|
|
|
|
this.context = context;
|
|
|
|
this.toastHandler = toastHandler;
|
2011-12-20 18:20:44 +00:00
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
public void process(MasterSecret masterSecret, Intent intent) {
|
2013-02-15 03:15:40 +00:00
|
|
|
if (intent.getAction().equals(SendReceiveService.SEND_SMS_ACTION)) {
|
|
|
|
handleSendMessage(masterSecret, intent);
|
|
|
|
} else if (intent.getAction().equals(SendReceiveService.SENT_SMS_ACTION)) {
|
|
|
|
handleSentMessage(intent);
|
|
|
|
} else if (intent.getAction().equals(SendReceiveService.DELIVERED_SMS_ACTION)) {
|
|
|
|
handleDeliveredMessage(intent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void handleSendMessage(MasterSecret masterSecret, Intent intent) {
|
2011-12-20 18:20:44 +00:00
|
|
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
|
|
|
long messageId = intent.getLongExtra("message_id", -1);
|
|
|
|
Cursor c = null;
|
|
|
|
|
|
|
|
Log.w("SMSSenderService", "Processing outgoing message: " + messageId);
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (messageId == -1) c = DatabaseFactory.getSmsDatabase(context).getOutgoingMessages();
|
|
|
|
else c = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
if (c != null && c.moveToFirst()) {
|
2012-10-30 00:34:14 +00:00
|
|
|
do {
|
2013-02-15 03:15:40 +00:00
|
|
|
messageId = c.getLong(c.getColumnIndexOrThrow(SmsDatabase.ID));
|
|
|
|
String body = c.getString(c.getColumnIndexOrThrow(SmsDatabase.BODY));
|
|
|
|
String address = c.getString(c.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
|
|
|
|
String messageText = getClearTextBody(masterCipher, body);
|
|
|
|
long type = c.getLong(c.getColumnIndexOrThrow(SmsDatabase.TYPE));
|
|
|
|
|
|
|
|
if (!SmsDatabase.Types.isPendingMessageType(type))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (isSecureMessage(type))
|
|
|
|
messageText = getAsymmetricEncrypt(masterSecret, messageText, address);
|
|
|
|
|
|
|
|
if (!pendingMessages.contains(messageId)) {
|
|
|
|
Log.w("SMSSenderService", "Actually delivering: " + messageId);
|
|
|
|
pendingMessages.add(messageId);
|
|
|
|
deliverTextMessage(address, messageText, messageId, type);
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
} while (c.moveToNext());
|
|
|
|
}
|
2011-12-20 18:20:44 +00:00
|
|
|
} finally {
|
|
|
|
if (c != null)
|
2012-10-30 00:34:14 +00:00
|
|
|
c.close();
|
2011-12-20 18:20:44 +00:00
|
|
|
}
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2013-02-15 03:15:40 +00:00
|
|
|
private void handleSentMessage(Intent intent) {
|
|
|
|
long messageId = intent.getLongExtra("message_id", -1);
|
|
|
|
long type = intent.getLongExtra("type", -1);
|
|
|
|
int result = intent.getIntExtra("ResultCode", -31337);
|
|
|
|
|
|
|
|
Log.w("SMSReceiverService", "Intent resultcode: " + result);
|
|
|
|
Log.w("SMSReceiverService", "Running sent callback: " + messageId + "," + type);
|
|
|
|
|
|
|
|
if (result == Activity.RESULT_OK) {
|
|
|
|
DatabaseFactory.getSmsDatabase(context).markAsSent(messageId, type);
|
2013-02-15 04:42:29 +00:00
|
|
|
unregisterForRadioChanges();
|
2013-02-15 03:15:40 +00:00
|
|
|
} else if (result == SmsManager.RESULT_ERROR_NO_SERVICE || result == SmsManager.RESULT_ERROR_RADIO_OFF) {
|
|
|
|
toastHandler
|
|
|
|
.obtainMessage(0, context.getString(R.string.SmsReceiver_currently_unable_to_send_your_sms_message))
|
|
|
|
.sendToTarget();
|
2013-02-15 04:42:29 +00:00
|
|
|
registerForRadioChanges();
|
2013-02-15 03:15:40 +00:00
|
|
|
} else {
|
|
|
|
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
|
|
|
Recipients recipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(context, threadId);
|
|
|
|
|
|
|
|
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
|
|
|
MessageNotifier.notifyMessageDeliveryFailed(context, recipients, threadId);
|
2013-02-15 04:42:29 +00:00
|
|
|
unregisterForRadioChanges();
|
2013-02-15 03:15:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pendingMessages.remove(messageId);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void handleDeliveredMessage(Intent intent) {
|
|
|
|
long messageId = intent.getLongExtra("message_id", -1);
|
|
|
|
long type = intent.getLongExtra("type", -1);
|
|
|
|
byte[] pdu = intent.getByteArrayExtra("pdu");
|
|
|
|
String format = intent.getStringExtra("format");
|
|
|
|
SmsMessage message = SmsMessage.createFromPdu(pdu);
|
|
|
|
|
|
|
|
if (message == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
DatabaseFactory.getSmsDatabase(context).markStatus(messageId, message.getStatus());
|
|
|
|
}
|
|
|
|
|
2013-02-15 04:42:29 +00:00
|
|
|
private void registerForRadioChanges() {
|
|
|
|
unregisterForRadioChanges();
|
|
|
|
|
|
|
|
IntentFilter intentFilter = new IntentFilter();
|
|
|
|
intentFilter.addAction(SystemStateListener.ACTION_SERVICE_STATE);
|
|
|
|
context.registerReceiver(SystemStateListener.getInstance(), intentFilter);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void unregisterForRadioChanges() {
|
|
|
|
try {
|
|
|
|
context.unregisterReceiver(SystemStateListener.getInstance());
|
|
|
|
} catch (IllegalArgumentException iae) {
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
private String getClearTextBody(MasterCipher masterCipher, String body) {
|
|
|
|
if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT)) {
|
|
|
|
try {
|
2012-10-30 00:34:14 +00:00
|
|
|
return masterCipher.decryptBody(body.substring(Prefix.SYMMETRIC_ENCRYPT.length()));
|
2011-12-20 18:20:44 +00:00
|
|
|
} catch (InvalidMessageException e) {
|
2012-10-30 00:34:14 +00:00
|
|
|
return "Error decrypting message.";
|
2011-12-20 18:20:44 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return body;
|
|
|
|
}
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
private ArrayList<PendingIntent> constructSentIntents(long messageId, long type, ArrayList<String> messages) {
|
|
|
|
ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(messages.size());
|
|
|
|
|
|
|
|
for (int i=0;i<messages.size();i++) {
|
|
|
|
Intent pending = new Intent(SendReceiveService.SENT_SMS_ACTION, Uri.parse("custom://" + messageId + System.currentTimeMillis()), context, SmsListener.class);
|
|
|
|
pending.putExtra("type", type);
|
|
|
|
pending.putExtra("message_id", messageId);
|
|
|
|
sentIntents.add(PendingIntent.getBroadcast(context, 0, pending, 0));
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
return sentIntents;
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2013-01-07 05:38:36 +00:00
|
|
|
private ArrayList<PendingIntent> constructDeliveredIntents(long messageId, long type, ArrayList<String> messages) {
|
|
|
|
if (!PreferenceManager.getDefaultSharedPreferences(context)
|
|
|
|
.getBoolean(ApplicationPreferencesActivity.SMS_DELIVERY_REPORT_PREF, false))
|
|
|
|
{
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
ArrayList<PendingIntent> deliveredIntents = new ArrayList<PendingIntent>(messages.size());
|
|
|
|
|
|
|
|
for (int i=0;i<messages.size();i++) {
|
|
|
|
Intent pending = new Intent(SendReceiveService.DELIVERED_SMS_ACTION, Uri.parse("custom://" + messageId + System.currentTimeMillis()), context, SmsListener.class);
|
|
|
|
pending.putExtra("type", type);
|
|
|
|
pending.putExtra("message_id", messageId);
|
|
|
|
deliveredIntents.add(PendingIntent.getBroadcast(context, 0, pending, 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
return deliveredIntents;
|
|
|
|
}
|
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
private void deliverGSMTransportTextMessage(String recipient, String text, long messageId, long type) {
|
2013-01-07 05:38:36 +00:00
|
|
|
ArrayList<String> messages = SmsManager.getDefault().divideMessage(text);
|
|
|
|
ArrayList<PendingIntent> sentIntents = constructSentIntents(messageId, type, messages);
|
|
|
|
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(messageId, type, messages);
|
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
// 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.
|
|
|
|
try {
|
2013-01-07 05:38:36 +00:00
|
|
|
SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
|
2011-12-20 18:20:44 +00:00
|
|
|
} catch (NullPointerException npe) {
|
|
|
|
Log.w("SmsSender", npe);
|
|
|
|
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void deliverSecureTransportTextMessage(String recipient, String text, long messageId, long type) {
|
|
|
|
WirePrefix prefix;
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
if (isSecureMessage(type)) {
|
|
|
|
prefix = new SecureMessageWirePrefix();
|
|
|
|
text = text.substring(Prefix.ASYMMETRIC_ENCRYPT.length());
|
|
|
|
} else {
|
|
|
|
prefix = new KeyExchangeWirePrefix();
|
|
|
|
text = text.substring(Prefix.KEY_EXCHANGE.length());
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
if (!multipartMessageHandler.isManualTransport(text)) {
|
|
|
|
deliverGSMTransportTextMessage(recipient, prefix.calculatePrefix(text) + text, messageId, type);
|
|
|
|
return;
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2013-01-07 05:38:36 +00:00
|
|
|
ArrayList<String> messages = multipartMessageHandler.divideMessage(recipient, text, prefix);
|
|
|
|
ArrayList<PendingIntent> sentIntents = constructSentIntents(messageId, type, messages);
|
|
|
|
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(messageId, type, messages);
|
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
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.
|
|
|
|
try {
|
2013-01-07 05:38:36 +00:00
|
|
|
SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i), sentIntents.get(i),
|
|
|
|
deliveredIntents == null ? null : deliveredIntents.get(i));
|
2011-12-20 18:20:44 +00:00
|
|
|
} catch (NullPointerException npe) {
|
2012-10-30 00:34:14 +00:00
|
|
|
Log.w("SmsSender", npe);
|
|
|
|
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
2013-02-27 06:57:09 +00:00
|
|
|
} catch (IllegalArgumentException iae) {
|
|
|
|
Log.w("SmsSender", iae);
|
|
|
|
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
2011-12-20 18:20:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
private void deliverTextMessage(String recipient, String text, long messageId, long type) {
|
|
|
|
if (!isSecureMessage(type) && !isKeyExchange(text))
|
|
|
|
deliverGSMTransportTextMessage(recipient, text, messageId, type);
|
|
|
|
else
|
2012-10-30 00:34:14 +00:00
|
|
|
deliverSecureTransportTextMessage(recipient, text, messageId, type);
|
2011-12-20 18:20:44 +00:00
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
private boolean isSecureMessage(long type) {
|
|
|
|
return type == SmsDatabase.Types.ENCRYPTING_TYPE;
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
private boolean isKeyExchange(String messageText) {
|
|
|
|
return messageText.startsWith(Prefix.KEY_EXCHANGE);
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
private String getAsymmetricEncrypt(MasterSecret masterSecret, String body, String address) {
|
|
|
|
synchronized (SessionCipher.CIPHER_LOCK) {
|
2012-12-24 16:40:37 +00:00
|
|
|
SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, address, null, null), new SmsTransportDetails());
|
2011-12-20 18:20:44 +00:00
|
|
|
return new String(cipher.encryptMessage(body.getBytes()));
|
|
|
|
}
|
|
|
|
}
|
2012-10-30 00:34:14 +00:00
|
|
|
|
2011-12-20 18:20:44 +00:00
|
|
|
}
|