mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-20 14:54:09 +00:00
Initial Project Import
This commit is contained in:
238
src/org/thoughtcrime/securesms/service/KeyCachingService.java
Normal file
238
src/org/thoughtcrime/securesms/service/KeyCachingService.java
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.SecureSMS;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Small service that stays running to keep a key cached in memory.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class KeyCachingService extends Service {
|
||||
|
||||
public static final int NOTIFICATION_ID = 1337;
|
||||
public static final int SERVICE_RUNNING_ID = 4141;
|
||||
|
||||
public static final String KEY_PERMISSION = "org.thoughtcrime.securesms.ACCESS_SECRETS";
|
||||
public static final String NEW_KEY_EVENT = "org.thoughtcrime.securesms.service.action.NEW_KEY_EVENT";
|
||||
public static final String PASSPHRASE_EXPIRED_EVENT = "org.thoughtcrime.securesms.service.action.PASSPHRASE_EXPIRED_EVENT";
|
||||
public static final String CLEAR_KEY_ACTION = "org.thoughtcrime.securesms.service.action.CLEAR_KEY";
|
||||
public static final String ACTIVITY_START_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_START_EVENT";
|
||||
public static final String ACTIVITY_STOP_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_STOP_EVENT";
|
||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
private static final Class[] mStartForegroundSignature = new Class[] {int.class, Notification.class};
|
||||
private static final Class[] mStopForegroundSignature = new Class[] {boolean.class};
|
||||
|
||||
private PendingIntent pending;
|
||||
private NotificationManager notificationManager;
|
||||
private Method mStartForeground;
|
||||
private Method mStopForeground;
|
||||
|
||||
private Object[] mStartForegroundArgs = new Object[2];
|
||||
private Object[] mStopForegroundArgs = new Object[1];
|
||||
private int activitiesRunning = 0;
|
||||
private final IBinder binder = new KeyCachingBinder();
|
||||
|
||||
private MasterSecret masterSecret;
|
||||
|
||||
public KeyCachingService() {}
|
||||
|
||||
public synchronized MasterSecret getMasterSecret() {
|
||||
return masterSecret;
|
||||
}
|
||||
|
||||
public synchronized void setMasterSecret(MasterSecret masterSecret) {
|
||||
this.masterSecret = masterSecret;
|
||||
|
||||
foregroundService();
|
||||
broadcastNewSecret();
|
||||
startTimeoutIfAppropriate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(Intent intent, int startId) {
|
||||
if (intent.getAction() != null && intent.getAction().equals(CLEAR_KEY_ACTION))
|
||||
handleClearKey();
|
||||
else if (intent.getAction() != null && intent.getAction().equals(ACTIVITY_START_EVENT))
|
||||
handleActivityStarted();
|
||||
else if (intent.getAction() != null && intent.getAction().equals(ACTIVITY_STOP_EVENT))
|
||||
handleActivityStopped();
|
||||
else if (intent.getAction() != null && intent.getAction().equals(PASSPHRASE_EXPIRED_EVENT))
|
||||
handlePassphraseExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
pending = PendingIntent.getService(this, 0, new Intent(PASSPHRASE_EXPIRED_EVENT, null, this, KeyCachingService.class), 0);
|
||||
notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
try {
|
||||
mStartForeground = getClass().getMethod("startForeground", mStartForegroundSignature);
|
||||
mStopForeground = getClass().getMethod("stopForeground", mStopForegroundSignature);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Running on an older platform.
|
||||
mStartForeground = mStopForeground = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.e("kcs", "KCS Is Being Destroyed!");
|
||||
}
|
||||
|
||||
private void handleActivityStarted() {
|
||||
Log.w("KeyCachingService", "Incrementing activity count...");
|
||||
|
||||
AlarmManager alarmManager = (AlarmManager)this.getSystemService(ALARM_SERVICE);
|
||||
alarmManager.cancel(pending);
|
||||
activitiesRunning++;
|
||||
}
|
||||
|
||||
private void handleActivityStopped() {
|
||||
Log.w("KeyCachingService", "Decrementing activity count...");
|
||||
|
||||
activitiesRunning--;
|
||||
startTimeoutIfAppropriate();
|
||||
}
|
||||
|
||||
private void handleClearKey() {
|
||||
this.masterSecret = null;
|
||||
stopForegroundCompat(SERVICE_RUNNING_ID);
|
||||
}
|
||||
|
||||
private void handlePassphraseExpired() {
|
||||
handleClearKey();
|
||||
Intent intent = new Intent(PASSPHRASE_EXPIRED_EVENT);
|
||||
intent.setPackage(getApplicationContext().getPackageName());
|
||||
|
||||
sendBroadcast(intent, KEY_PERMISSION);
|
||||
}
|
||||
|
||||
private void startTimeoutIfAppropriate() {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean timeoutEnabled = sharedPreferences.getBoolean(ApplicationPreferencesActivity.PASSPHRASE_TIMEOUT_PREF, false);
|
||||
|
||||
if ((activitiesRunning == 0) && (this.masterSecret != null) && timeoutEnabled) {
|
||||
long timeoutMinutes = sharedPreferences.getInt(ApplicationPreferencesActivity.PASSPHRASE_TIMEOUT_INTERVAL_PREF, 60 * 5);
|
||||
long timeoutMillis = timeoutMinutes * 60 * 1000;
|
||||
|
||||
Log.w("KeyCachingService", "Starting timeout: " + timeoutMillis);
|
||||
|
||||
AlarmManager alarmManager = (AlarmManager)this.getSystemService(ALARM_SERVICE);
|
||||
alarmManager.cancel(pending);
|
||||
alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeoutMillis, pending);
|
||||
}
|
||||
}
|
||||
|
||||
private void foregroundService() {
|
||||
Notification notification = new Notification(R.drawable.icon, "TextSecure Passphrase Cached", System.currentTimeMillis());
|
||||
Intent intent = new Intent(this, SecureSMS.class);
|
||||
PendingIntent launchIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
|
||||
notification.setLatestEventInfo(getApplicationContext(), "TextSecure Cached", "TextSecure Passphrase Cached", launchIntent);
|
||||
|
||||
stopForegroundCompat(SERVICE_RUNNING_ID);
|
||||
startForegroundCompat(SERVICE_RUNNING_ID, notification);
|
||||
}
|
||||
|
||||
private void broadcastNewSecret() {
|
||||
Log.w("service", "Broadcasting new secret...");
|
||||
Intent intent = new Intent(NEW_KEY_EVENT);
|
||||
intent.putExtra("master_secret", masterSecret);
|
||||
intent.setPackage(getApplicationContext().getPackageName());
|
||||
|
||||
sendBroadcast(intent, KEY_PERMISSION);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent arg0) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
public class KeyCachingBinder extends Binder {
|
||||
public KeyCachingService getService() {
|
||||
return KeyCachingService.this;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This is a wrapper around the new startForeground method, using the older
|
||||
* APIs if it is not available.
|
||||
*/
|
||||
private void startForegroundCompat(int id, Notification notification) {
|
||||
if (mStartForeground != null) {
|
||||
mStartForegroundArgs[0] = Integer.valueOf(id);
|
||||
mStartForegroundArgs[1] = notification;
|
||||
|
||||
try {
|
||||
mStartForeground.invoke(this, mStartForegroundArgs);
|
||||
} catch (InvocationTargetException e) {
|
||||
Log.w("KeyCachingService", "Unable to invoke startForeground", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.w("KeyCachingService", "Unable to invoke startForeground", e);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setForeground(true);
|
||||
notificationManager.notify(id, notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a wrapper around the new stopForeground method, using the older
|
||||
* APIs if it is not available.
|
||||
*/
|
||||
private void stopForegroundCompat(int id) {
|
||||
Log.w("KeyCachingService", "Calling stopForeground!");
|
||||
if (mStopForeground != null) {
|
||||
mStopForegroundArgs[0] = Boolean.TRUE;
|
||||
try {
|
||||
mStopForeground.invoke(this, mStopForegroundArgs);
|
||||
} catch (InvocationTargetException e) {
|
||||
Log.w("KeyCachingService", "Unable to invoke stopForeground", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.w("KeyCachingService", "Unable to invoke stopForeground", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
notificationManager.cancel(id);
|
||||
setForeground(false);
|
||||
}
|
||||
}
|
201
src/org/thoughtcrime/securesms/service/MessageNotifier.java
Normal file
201
src/org/thoughtcrime/securesms/service/MessageNotifier.java
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.SecureSMS;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
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 android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Handles posting system notifications for new messages.
|
||||
*
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MessageNotifier {
|
||||
|
||||
public static final int NOTIFICATION_ID = 1338;
|
||||
|
||||
private static String buildTickerMessage(int count, Recipients recipients) {
|
||||
Recipient recipient = recipients.getPrimaryRecipient();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append('(');
|
||||
builder.append(count);
|
||||
builder.append(')');
|
||||
builder.append(" New messages");
|
||||
|
||||
if (recipient != null) {
|
||||
builder.append(", most recent from: ");
|
||||
builder.append(recipient.getName() == null ? recipient.getNumber() : recipient.getName());
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String buildTitleMessage(int count) {
|
||||
return "(" + count + ") New Messages";
|
||||
}
|
||||
|
||||
private static String buildSubtitleMessage(Recipients recipients) {
|
||||
Recipient recipient = recipients.getPrimaryRecipient();
|
||||
|
||||
if (recipient != null) {
|
||||
return "Most recent from: " + (recipient.getName() == null ? recipient.getNumber() : recipient.getName());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Recipients getSmsRecipient(Context context, Cursor c) throws RecipientFormattingException {
|
||||
String address = c.getString(c.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
|
||||
return RecipientFactory.getRecipientsFromString(context, address);
|
||||
}
|
||||
|
||||
private static Recipients getMmsRecipient(Context context, Cursor c) throws RecipientFormattingException {
|
||||
long messageId = c.getLong(c.getColumnIndexOrThrow(MmsDatabase.ID));
|
||||
String address = DatabaseFactory.getMmsDatabase(context).getMessageRecipient(messageId);
|
||||
return RecipientFactory.getRecipientsFromString(context, address);
|
||||
}
|
||||
|
||||
private static Recipients getMostRecentRecipients(Context context, Cursor c) {
|
||||
if (c != null && c.moveToLast()) {
|
||||
try {
|
||||
String type = c.getString(c.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
|
||||
|
||||
if (type.equals("sms"))
|
||||
return getSmsRecipient(context, c);
|
||||
else
|
||||
return getMmsRecipient(context, c);
|
||||
|
||||
} catch (RecipientFormattingException e) {
|
||||
return new Recipients(new LinkedList<Recipient>());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static PendingIntent buildPendingIntent(Context context, Cursor c, Recipients recipients) {
|
||||
Intent intent = new Intent(context, SecureSMS.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
||||
Log.w("SMSNotifier", "Building pending intent...");
|
||||
if (c != null && c.getCount() == 1) {
|
||||
Log.w("SMSNotifier", "Adding extras...");
|
||||
c.moveToLast();
|
||||
long threadId = c.getLong(c.getColumnIndexOrThrow(SmsDatabase.THREAD_ID));
|
||||
Log.w("SmsNotifier", "Adding thread_id to pending intent: " + threadId);
|
||||
|
||||
if (recipients.getPrimaryRecipient() != null) {
|
||||
intent.putExtra("recipients", recipients);
|
||||
intent.putExtra("thread_id", threadId);
|
||||
}
|
||||
|
||||
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(context, 0, intent, 0);
|
||||
}
|
||||
|
||||
private static void sendNotification(Context context, NotificationManager manager, PendingIntent launchIntent,
|
||||
String ticker, String title, String subtitle, boolean signal)
|
||||
{
|
||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (!sp.getBoolean(ApplicationPreferencesActivity.NOTIFICATION_PREF, true)) return;
|
||||
|
||||
Notification notification = new Notification(R.drawable.stat_notify_sms, ticker, System.currentTimeMillis());
|
||||
String ringtone = sp.getString(ApplicationPreferencesActivity.RINGTONE_PREF, null);
|
||||
boolean vibrate = sp.getBoolean(ApplicationPreferencesActivity.VIBRATE_PREF, true);
|
||||
String ledColor = sp.getString(ApplicationPreferencesActivity.LED_COLOR_PREF, "green");
|
||||
String ledBlinkPattern = sp.getString(ApplicationPreferencesActivity.LED_BLINK_PREF, "500,2000");
|
||||
String ledBlinkPatternCustom = sp.getString(ApplicationPreferencesActivity.LED_BLINK_PREF_CUSTOM, "500,2000");
|
||||
String[] blinkPatternArray = parseBlinkPattern(ledBlinkPattern, ledBlinkPatternCustom);
|
||||
|
||||
notification.setLatestEventInfo(context, title, subtitle, launchIntent);
|
||||
notification.sound = TextUtils.isEmpty(ringtone) || !signal ? null : Uri.parse(ringtone);
|
||||
if (signal && vibrate)
|
||||
notification.defaults |= Notification.DEFAULT_VIBRATE;
|
||||
|
||||
notification.flags |= Notification.FLAG_SHOW_LIGHTS;
|
||||
notification.ledARGB = Color.parseColor(ledColor);//0xff00ff00;
|
||||
notification.ledOnMS = Integer.parseInt(blinkPatternArray[0]);
|
||||
notification.ledOffMS = Integer.parseInt(blinkPatternArray[1]);
|
||||
|
||||
manager.notify(NOTIFICATION_ID, notification);
|
||||
}
|
||||
|
||||
private static void flashNotification(Context context, NotificationManager manager) {
|
||||
sendNotification(context, manager, buildPendingIntent(context, null, null), "(1) New Messages", "(1) New Messages", null, true);
|
||||
}
|
||||
|
||||
public static void updateNotification(Context context, boolean signal) {
|
||||
NotificationManager manager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
manager.cancel(NOTIFICATION_ID);
|
||||
|
||||
Cursor c = null;
|
||||
|
||||
try {
|
||||
c = DatabaseFactory.getMmsSmsDatabase(context).getUnread();
|
||||
|
||||
if ((c == null && signal) || (!c.moveToFirst() && signal)) {flashNotification(context, manager); return;}
|
||||
else if (c == null || !c.moveToFirst()) return;
|
||||
|
||||
Recipients recipients = getMostRecentRecipients(context, c);
|
||||
String ticker = buildTickerMessage(c.getCount(), recipients);
|
||||
String title = buildTitleMessage(c.getCount());
|
||||
String subtitle = buildSubtitleMessage(recipients);
|
||||
PendingIntent launchIntent = buildPendingIntent(context, c, recipients);
|
||||
|
||||
sendNotification(context, manager, launchIntent, ticker, title, subtitle, signal);
|
||||
} finally {
|
||||
if (c != null)
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] parseBlinkPattern(String blinkPattern, String blinkPatternCustom) {
|
||||
if (blinkPattern.equals("custom"))
|
||||
blinkPattern = blinkPatternCustom;
|
||||
|
||||
return blinkPattern.split(",");
|
||||
}
|
||||
}
|
180
src/org/thoughtcrime/securesms/service/MmsDownloader.java
Normal file
180
src/org/thoughtcrime/securesms/service/MmsDownloader.java
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.mms.MmsDownloadHelper;
|
||||
import org.thoughtcrime.securesms.protocol.WirePrefix;
|
||||
|
||||
import ws.com.google.android.mms.MmsException;
|
||||
import ws.com.google.android.mms.pdu.PduParser;
|
||||
import ws.com.google.android.mms.pdu.RetrieveConf;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
public class MmsDownloader extends MmscProcessor {
|
||||
|
||||
private final LinkedList<DownloadItem> pendingMessages = new LinkedList<DownloadItem>();
|
||||
private final SendReceiveService.ToastHandler toastHandler;
|
||||
|
||||
public MmsDownloader(Context context, SendReceiveService.ToastHandler toastHandler) {
|
||||
super(context);
|
||||
this.toastHandler = toastHandler;
|
||||
}
|
||||
|
||||
private void handleDownloadMms(DownloadItem item) {
|
||||
if (!isConnectivityPossible()) {
|
||||
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_NO_CONNECTIVITY);
|
||||
toastHandler.makeToast("No connectivity available for MMS download, try again later...");
|
||||
Log.w("MmsDownloadService", "Unable to download MMS, please try again later.");
|
||||
} else {
|
||||
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_CONNECTING);
|
||||
pendingMessages.add(item);
|
||||
issueConnectivityRequest();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDownloadMmsContinued(DownloadItem item) {
|
||||
Log.w("MmsDownloadService", "Handling MMS download continuation...");
|
||||
MmsDatabase mmsDatabase;
|
||||
|
||||
if (item.getMasterSecret() == null)
|
||||
mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
else
|
||||
mmsDatabase = DatabaseFactory.getEncryptingMmsDatabase(context, item.getMasterSecret());
|
||||
|
||||
|
||||
try {
|
||||
byte[] pdu = MmsDownloadHelper.retrieveMms(context, item.getContentLocation());
|
||||
RetrieveConf retrieved = (RetrieveConf)new PduParser(pdu).parse();
|
||||
|
||||
for (int i=0;i<retrieved.getBody().getPartsNum();i++) {
|
||||
Log.w("MmsDownloader", "Sent MMS part of content-type: " + new String(retrieved.getBody().getPart(i).getContentType()));
|
||||
}
|
||||
|
||||
if (retrieved == null)
|
||||
throw new IOException("Bad retrieved PDU");
|
||||
|
||||
|
||||
if (retrieved.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieved.getSubject().getString())) {
|
||||
long messageId = mmsDatabase.insertSecureMessageReceived(retrieved, item.getContentLocation(), item.getThreadId());
|
||||
if (item.getMasterSecret() != null)
|
||||
DecryptingQueue.scheduleDecryption(context, item.getMasterSecret(), messageId, item.getThreadId(), retrieved);
|
||||
} else {
|
||||
mmsDatabase.insertMessageReceived(retrieved, item.getContentLocation(), item.getThreadId());
|
||||
}
|
||||
|
||||
mmsDatabase.delete(item.getMessageId());
|
||||
|
||||
// NotifyRespInd notifyResponse = new NotifyRespInd(PduHeaders.CURRENT_MMS_VERSION, item.getTransactionId(), PduHeaders.STATUS_RETRIEVED);
|
||||
// MmsSendHelper.sendMms(context, new PduComposer(context, notifyResponse).make());
|
||||
|
||||
if (this.pendingMessages.isEmpty())
|
||||
finishConnectivity();
|
||||
|
||||
} catch (IOException e) {
|
||||
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_SOFT_FAILURE);
|
||||
toastHandler.makeToast("Error connecting to MMS provider...");
|
||||
Log.w("MmsDownloader", e);
|
||||
} catch (MmsException e) {
|
||||
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_HARD_FAILURE);
|
||||
toastHandler.makeToast("Error downloading MMS!");
|
||||
Log.w("MmsDownloader", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleConnectivityChange() {
|
||||
if (!isConnected()) {
|
||||
if (!isConnectivityPossible() && !pendingMessages.isEmpty()) {
|
||||
DatabaseFactory.getMmsDatabase(context).markDownloadState(pendingMessages.remove().getMessageId(), MmsDatabase.Types.DOWNLOAD_NO_CONNECTIVITY);
|
||||
toastHandler.makeToast("No connectivity available for MMS download, try again later...");
|
||||
Log.w("MmsDownloadService", "Unable to download MMS, please try again later.");
|
||||
finishConnectivity();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pendingMessages.isEmpty()) handleDownloadMmsContinued(pendingMessages.remove());
|
||||
else finishConnectivity();
|
||||
}
|
||||
|
||||
public void process(MasterSecret masterSecret, Intent intent) {
|
||||
if (intent.getAction().equals(SendReceiveService.DOWNLOAD_MMS_ACTION)) {
|
||||
DownloadItem item = new DownloadItem(intent.getLongExtra("message_id", -1),
|
||||
intent.getLongExtra("thread_id", -1),
|
||||
intent.getStringExtra("content_location"),
|
||||
intent.getByteArrayExtra("transaction_id"),
|
||||
masterSecret);
|
||||
|
||||
handleDownloadMms(item);
|
||||
} else if (intent.getAction().equals(SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION)) {
|
||||
handleConnectivityChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class DownloadItem {
|
||||
private long threadId;
|
||||
private long messageId;
|
||||
private byte[] transactionId;
|
||||
private String contentLocation;
|
||||
private MasterSecret masterSecret;
|
||||
|
||||
public DownloadItem(long messageId, long threadId, String contentLocation, byte[] transactionId, MasterSecret masterSecret) {
|
||||
this.threadId = threadId;
|
||||
this.messageId = messageId;
|
||||
this.contentLocation = contentLocation;
|
||||
this.masterSecret = masterSecret;
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public long getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public String getContentLocation() {
|
||||
return contentLocation;
|
||||
}
|
||||
|
||||
public byte[] getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public MasterSecret getMasterSecret() {
|
||||
return masterSecret;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getConnectivityAction() {
|
||||
return SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION;
|
||||
}
|
||||
|
||||
|
||||
}
|
73
src/org/thoughtcrime/securesms/service/MmsListener.java
Normal file
73
src/org/thoughtcrime/securesms/service/MmsListener.java
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.protocol.WirePrefix;
|
||||
|
||||
import ws.com.google.android.mms.pdu.GenericPdu;
|
||||
import ws.com.google.android.mms.pdu.NotificationInd;
|
||||
import ws.com.google.android.mms.pdu.PduHeaders;
|
||||
import ws.com.google.android.mms.pdu.PduParser;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
public class MmsListener extends BroadcastReceiver {
|
||||
|
||||
private boolean isRelevent(Context context, Intent intent) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ECLAIR)
|
||||
return false;
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(ApplicationPreferencesActivity.ALL_MMS_PERF, true))
|
||||
return true;
|
||||
|
||||
byte[] mmsData = intent.getByteArrayExtra("data");
|
||||
PduParser parser = new PduParser(mmsData);
|
||||
GenericPdu pdu = parser.parse();
|
||||
|
||||
if (pdu.getMessageType() != PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND)
|
||||
return false;
|
||||
|
||||
NotificationInd notificationPdu = (NotificationInd)pdu;
|
||||
|
||||
if (notificationPdu.getSubject() == null)
|
||||
return false;
|
||||
|
||||
return WirePrefix.isEncryptedMmsSubject(notificationPdu.getSubject().getString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.w("MmsListener", "Got MMS broadcast...");
|
||||
|
||||
if (isRelevent(context, intent)) {
|
||||
intent.setAction(SendReceiveService.RECEIVE_MMS_ACTION);
|
||||
intent.putExtra("ResultCode", this.getResultCode());
|
||||
intent.setClass(context, SendReceiveService.class);
|
||||
|
||||
context.startService(intent);
|
||||
abortBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
69
src/org/thoughtcrime/securesms/service/MmsReceiver.java
Normal file
69
src/org/thoughtcrime/securesms/service/MmsReceiver.java
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
|
||||
import ws.com.google.android.mms.pdu.GenericPdu;
|
||||
import ws.com.google.android.mms.pdu.NotificationInd;
|
||||
import ws.com.google.android.mms.pdu.PduHeaders;
|
||||
import ws.com.google.android.mms.pdu.PduParser;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
public class MmsReceiver {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public MmsReceiver(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private void scheduleDownload(NotificationInd pdu, long messageId) {
|
||||
Intent intent = new Intent(SendReceiveService.DOWNLOAD_MMS_ACTION, null, context, SendReceiveService.class);
|
||||
intent.putExtra("content_location", new String(pdu.getContentLocation()));
|
||||
intent.putExtra("message_id", messageId);
|
||||
intent.putExtra("transaction_id", pdu.getTransactionId());
|
||||
intent.putExtra("thread_id", DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
|
||||
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public void process(MasterSecret masterSecret, Intent intent) {
|
||||
byte[] mmsData = intent.getByteArrayExtra("data");
|
||||
PduParser parser = new PduParser(mmsData);
|
||||
GenericPdu pdu = parser.parse();
|
||||
|
||||
if (pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
|
||||
MmsDatabase database;
|
||||
|
||||
if (masterSecret != null)
|
||||
database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
|
||||
else
|
||||
database = DatabaseFactory.getMmsDatabase(context);
|
||||
|
||||
long messageId = database.insertMessageReceived((NotificationInd)pdu);
|
||||
MessageNotifier.updateNotification(context, true);
|
||||
scheduleDownload((NotificationInd)pdu, messageId);
|
||||
Log.w("MmsReceiverService", "Inserted received notification...");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
202
src/org/thoughtcrime/securesms/service/MmsSender.java
Normal file
202
src/org/thoughtcrime/securesms/service/MmsSender.java
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.SessionCipher;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.mms.MmsSendHelper;
|
||||
import org.thoughtcrime.securesms.mms.PngTransport;
|
||||
import org.thoughtcrime.securesms.mms.TextTransport;
|
||||
import org.thoughtcrime.securesms.protocol.WirePrefix;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
||||
import ws.com.google.android.mms.ContentType;
|
||||
import ws.com.google.android.mms.MmsException;
|
||||
import ws.com.google.android.mms.pdu.CharacterSets;
|
||||
import ws.com.google.android.mms.pdu.EncodedStringValue;
|
||||
import ws.com.google.android.mms.pdu.PduBody;
|
||||
import ws.com.google.android.mms.pdu.PduComposer;
|
||||
import ws.com.google.android.mms.pdu.PduHeaders;
|
||||
import ws.com.google.android.mms.pdu.PduParser;
|
||||
import ws.com.google.android.mms.pdu.PduPart;
|
||||
import ws.com.google.android.mms.pdu.SendConf;
|
||||
import ws.com.google.android.mms.pdu.SendReq;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
public class MmsSender extends MmscProcessor {
|
||||
|
||||
private final LinkedList<SendReq[]> pendingMessages = new LinkedList<SendReq[]>();
|
||||
|
||||
public MmsSender(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void process(MasterSecret masterSecret, Intent intent) {
|
||||
if (intent.getAction().equals(SendReceiveService.SEND_MMS_ACTION)) {
|
||||
long messageId = intent.getLongExtra("message_id", -1);
|
||||
MmsDatabase database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
|
||||
|
||||
try {
|
||||
SendReq[] sendRequests;
|
||||
|
||||
if (messageId == -1) {
|
||||
sendRequests = database.getOutgoingMessages();
|
||||
} else {
|
||||
sendRequests = new SendReq[1];
|
||||
sendRequests[0] = database.getSendRequest(messageId);
|
||||
}
|
||||
|
||||
if (sendRequests.length > 0)
|
||||
handleSendMms(sendRequests);
|
||||
|
||||
} catch (MmsException me) {
|
||||
Log.w("MmsSender", me);
|
||||
if (messageId != -1)
|
||||
database.markAsSentFailed(messageId);
|
||||
}
|
||||
} else if (intent.getAction().equals(SendReceiveService.SEND_MMS_CONNECTIVITY_ACTION)) {
|
||||
handleConnectivityChange(masterSecret);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleConnectivityChange(MasterSecret masterSecret) {
|
||||
if (!isConnected())
|
||||
return;
|
||||
|
||||
if (!pendingMessages.isEmpty()) handleSendMmsContinued(masterSecret, pendingMessages.remove());
|
||||
else finishConnectivity();
|
||||
}
|
||||
|
||||
private void handleSendMms(SendReq[] sendRequests) {
|
||||
if (!isConnectivityPossible()) {
|
||||
for (int i=0;i<sendRequests.length;i++)
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(sendRequests[i].getDatabaseMessageId());
|
||||
} else {
|
||||
pendingMessages.add(sendRequests);
|
||||
issueConnectivityRequest();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInconsistentResponse(SendReq send, SendConf response) {
|
||||
Log.w("MmsSenderService", "Comparing: " + Hex.toString(send.getTransactionId()));
|
||||
Log.w("MmsSenderService", "With: " + Hex.toString(response.getTransactionId()));
|
||||
return !Arrays.equals(send.getTransactionId(), response.getTransactionId());
|
||||
}
|
||||
|
||||
private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipient, byte[] pduBytes) {
|
||||
synchronized (SessionCipher.CIPHER_LOCK) {
|
||||
SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, recipient, null), new TextTransport());
|
||||
return cipher.encryptMessage(pduBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private SendReq getEncryptedMms(MasterSecret masterSecret, SendReq pdu, long messageId) {
|
||||
Log.w("MmsSender", "Sending Secure MMS.");
|
||||
EncodedStringValue[] encodedRecipient = pdu.getTo();
|
||||
String recipient = encodedRecipient[0].getString();
|
||||
byte[] pduBytes = new PduComposer(context, pdu).make();
|
||||
byte[] encryptedPdu = getEncryptedPdu(masterSecret, recipient, pduBytes);
|
||||
Log.w("MmsSendeR", "Got encrypted bytes: " + encryptedPdu.length);
|
||||
PduBody body = new PduBody();
|
||||
PduPart part = new PduPart();
|
||||
|
||||
part.setContentId((System.currentTimeMillis()+"").getBytes());
|
||||
part.setContentType(ContentType.TEXT_PLAIN.getBytes());
|
||||
part.setName((System.currentTimeMillis()+"").getBytes());
|
||||
part.setData(encryptedPdu);
|
||||
body.addPart(part);
|
||||
pdu.setSubject(new EncodedStringValue(WirePrefix.calculateEncryptedMmsSubject()));
|
||||
pdu.setBody(body);
|
||||
|
||||
return pdu;
|
||||
}
|
||||
|
||||
private void sendMms(MmsDatabase db, SendReq pdu, String number, long messageId, boolean secure) {
|
||||
try {
|
||||
if (number != null && number.trim().length() != 0)
|
||||
pdu.setFrom(new EncodedStringValue(number));
|
||||
|
||||
byte[] response = MmsSendHelper.sendMms(context, new PduComposer(context, pdu).make());
|
||||
SendConf conf = (SendConf) new PduParser(response).parse();
|
||||
|
||||
for (int i=0;i<pdu.getBody().getPartsNum();i++) {
|
||||
Log.w("MmsSender", "Sent MMS part of content-type: " + new String(pdu.getBody().getPart(i).getContentType()));
|
||||
}
|
||||
|
||||
if (conf == null) {
|
||||
db.markAsSentFailed(messageId);
|
||||
Log.w("MmsSender", "No M-Send.conf received in response to send.");
|
||||
return;
|
||||
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
|
||||
Log.w("MmsSender", "Got bad response: " + conf.getResponseStatus());
|
||||
db.updateResponseStatus(messageId, conf.getResponseStatus());
|
||||
db.markAsSentFailed(messageId);
|
||||
return;
|
||||
} else if (isInconsistentResponse(pdu, conf)) {
|
||||
db.markAsSentFailed(messageId);
|
||||
Log.w("MmsSender", "Got a response for the wrong transaction?");
|
||||
return;
|
||||
} else {
|
||||
Log.w("MmsSender", "Successful send! " + messageId);
|
||||
if (!secure)
|
||||
db.markAsSent(messageId, conf.getMessageId(), conf.getResponseStatus());
|
||||
else
|
||||
db.markAsSecureSent(messageId, conf.getMessageId(), conf.getResponseStatus());
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.w("MmsSender", ioe);
|
||||
db.markAsSentFailed(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSendMmsContinued(MasterSecret masterSecret, SendReq[] requests) {
|
||||
Log.w("MmsSenderService", "Handling MMS send continuation...");
|
||||
|
||||
MmsDatabase db = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
|
||||
String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
|
||||
|
||||
for (int i=0;i<requests.length;i++) {
|
||||
SendReq request = requests[i];
|
||||
long messageId = request.getDatabaseMessageId();
|
||||
long messageBox = request.getDatabaseMessageBox();
|
||||
|
||||
if (MmsDatabase.Types.isSecureMmsBox(messageBox))
|
||||
request = getEncryptedMms(masterSecret, request, messageId);
|
||||
|
||||
sendMms(db, request, number, messageId, MmsDatabase.Types.isSecureMmsBox(messageBox));
|
||||
}
|
||||
|
||||
if (this.pendingMessages.isEmpty())
|
||||
finishConnectivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getConnectivityAction() {
|
||||
return SendReceiveService.SEND_MMS_CONNECTIVITY_ACTION;
|
||||
}
|
||||
|
||||
}
|
111
src/org/thoughtcrime/securesms/service/MmscProcessor.java
Normal file
111
src/org/thoughtcrime/securesms/service/MmscProcessor.java
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.util.Log;
|
||||
|
||||
public abstract class MmscProcessor {
|
||||
|
||||
private static final String FEATURE_ENABLE_MMS = "enableMMS";
|
||||
private static final int APN_ALREADY_ACTIVE = 0;
|
||||
public static final int TYPE_MOBILE_MMS = 2;
|
||||
|
||||
private ConnectivityManager connectivityManager;
|
||||
private ConnectivityListener connectivityListener;
|
||||
private WakeLock wakeLock;
|
||||
|
||||
protected final Context context;
|
||||
|
||||
public MmscProcessor(Context context) {
|
||||
this.context = context;
|
||||
PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
|
||||
this.connectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
this.wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MMS Connection");
|
||||
this.wakeLock.setReferenceCounted(false);
|
||||
}
|
||||
|
||||
protected boolean isConnected() {
|
||||
NetworkInfo info = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
|
||||
|
||||
Log.w("MmsService", "NetworkInfo: " + info);
|
||||
|
||||
if ((info == null) || (info.getType() != TYPE_MOBILE_MMS) || !info.isConnected())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract String getConnectivityAction();
|
||||
|
||||
protected void issueConnectivityRequest() {
|
||||
int status = connectivityManager.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
|
||||
|
||||
if (status == APN_ALREADY_ACTIVE) {
|
||||
issueConnectivityChange();
|
||||
} else if (connectivityListener == null) {
|
||||
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
connectivityListener = new ConnectivityListener();
|
||||
context.registerReceiver(connectivityListener, filter);
|
||||
|
||||
wakeLock.acquire();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isConnectivityPossible() {
|
||||
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(TYPE_MOBILE_MMS);
|
||||
Log.w("MmsService", "Got network info: " + networkInfo);
|
||||
return networkInfo != null && networkInfo.isAvailable();
|
||||
}
|
||||
|
||||
protected void finishConnectivity() {
|
||||
Log.w("MmsService", "Calling stop using network feature!");
|
||||
connectivityManager.stopUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
|
||||
|
||||
if (connectivityListener != null) {
|
||||
context.unregisterReceiver(connectivityListener);
|
||||
connectivityListener = null;
|
||||
}
|
||||
|
||||
if (this.wakeLock.isHeld())
|
||||
this.wakeLock.release();
|
||||
}
|
||||
|
||||
private void issueConnectivityChange() {
|
||||
Intent intent = new Intent(context, SendReceiveService.class);
|
||||
intent.setAction(getConnectivityAction());
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
private class ConnectivityListener extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.w("MmsService", "Dispatching connectivity change...");
|
||||
issueConnectivityChange();
|
||||
Log.w("MmsService", "Dispatched...");
|
||||
}
|
||||
}
|
||||
}
|
235
src/org/thoughtcrime/securesms/service/SendReceiveService.java
Normal file
235
src/org/thoughtcrime/securesms/service/SendReceiveService.java
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.CanonicalSessionMigrator;
|
||||
import org.thoughtcrime.securesms.util.WorkerThread;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Services that handles sending/receiving of SMS/MMS.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SendReceiveService extends Service {
|
||||
|
||||
public static final String SEND_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_SMS_ACTION";
|
||||
public static final String SENT_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SENT_SMS_ACTION";
|
||||
public static final String RECEIVE_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_SMS_ACTION";
|
||||
public static final String SEND_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_ACTION";
|
||||
public static final String SEND_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SEND_MMS_CONNECTIVITY_ACTION";
|
||||
public static final String RECEIVE_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.RECEIVE_MMS_ACTION";
|
||||
public static final String DOWNLOAD_MMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_ACTION";
|
||||
public static final String DOWNLOAD_MMS_CONNECTIVITY_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DOWNLOAD_MMS_CONNECTIVITY_ACTION";
|
||||
|
||||
private static final int SEND_SMS = 0;
|
||||
private static final int RECEIVE_SMS = 1;
|
||||
private static final int SEND_MMS = 2;
|
||||
private static final int RECEIVE_MMS = 3;
|
||||
private static final int DOWNLOAD_MMS = 4;
|
||||
|
||||
private ToastHandler toastHandler;
|
||||
|
||||
private SmsReceiver smsReceiver;
|
||||
private SmsSender smsSender;
|
||||
private MmsReceiver mmsReceiver;
|
||||
private MmsSender mmsSender;
|
||||
private MmsDownloader mmsDownloader;
|
||||
|
||||
private MasterSecret masterSecret;
|
||||
private NewKeyReceiver receiver;
|
||||
private List<Runnable> workQueue;
|
||||
private List<Runnable> pendingSecretList;
|
||||
private Thread workerThread;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
initializeHandlers();
|
||||
initializeProcessors();
|
||||
initializeAddressCanonicalization();
|
||||
initializeWorkQueue();
|
||||
initializeMasterSecret();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(Intent intent, int startId) {
|
||||
if (intent.getAction().equals(SEND_SMS_ACTION))
|
||||
scheduleSecretRequiredIntent(SEND_SMS, intent);
|
||||
else if (intent.getAction().equals(RECEIVE_SMS_ACTION))
|
||||
scheduleIntent(RECEIVE_SMS, intent);
|
||||
else if (intent.getAction().equals(SENT_SMS_ACTION))
|
||||
scheduleIntent(RECEIVE_SMS, intent);
|
||||
else if (intent.getAction().equals(SEND_MMS_ACTION) || intent.getAction().equals(SEND_MMS_CONNECTIVITY_ACTION))
|
||||
scheduleSecretRequiredIntent(SEND_MMS, intent);
|
||||
else if (intent.getAction().equals(RECEIVE_MMS_ACTION))
|
||||
scheduleIntent(RECEIVE_MMS, intent);
|
||||
else if (intent.getAction().equals(DOWNLOAD_MMS_ACTION) || intent.getAction().equals(DOWNLOAD_MMS_CONNECTIVITY_ACTION))
|
||||
scheduleSecretRequiredIntent(DOWNLOAD_MMS, intent);
|
||||
else
|
||||
Log.w("SendReceiveService", "Received intent with unknown action: " + intent.getAction());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void initializeHandlers() {
|
||||
toastHandler = new ToastHandler();
|
||||
}
|
||||
|
||||
private void initializeProcessors() {
|
||||
smsReceiver = new SmsReceiver(this);
|
||||
smsSender = new SmsSender(this);
|
||||
mmsReceiver = new MmsReceiver(this);
|
||||
mmsSender = new MmsSender(this);
|
||||
mmsDownloader = new MmsDownloader(this, toastHandler);
|
||||
}
|
||||
|
||||
private void initializeWorkQueue() {
|
||||
pendingSecretList = new LinkedList<Runnable>();
|
||||
workQueue = new LinkedList<Runnable>();
|
||||
workerThread = new WorkerThread(workQueue, "SendReceveService-WorkerThread");
|
||||
|
||||
workerThread.start();
|
||||
}
|
||||
|
||||
private void initializeMasterSecret() {
|
||||
receiver = new NewKeyReceiver();
|
||||
IntentFilter filter = new IntentFilter(KeyCachingService.NEW_KEY_EVENT);
|
||||
registerReceiver(receiver, filter, KeyCachingService.KEY_PERMISSION, null);
|
||||
|
||||
Intent bindIntent = new Intent(this, KeyCachingService.class);
|
||||
bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
private void initializeWithMasterSecret(MasterSecret masterSecret) {
|
||||
Log.w("SendReceiveService", "SendReceive service got master secret: " + masterSecret);
|
||||
|
||||
if (masterSecret != null) {
|
||||
synchronized (workQueue) {
|
||||
this.masterSecret = masterSecret;
|
||||
|
||||
Iterator<Runnable> iterator = pendingSecretList.iterator();
|
||||
while (iterator.hasNext())
|
||||
workQueue.add(iterator.next());
|
||||
|
||||
workQueue.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeAddressCanonicalization() {
|
||||
CanonicalSessionMigrator.migrateSessions(this);
|
||||
}
|
||||
|
||||
private void scheduleIntent(int what, Intent intent) {
|
||||
Runnable work = new SendReceiveWorkItem(intent, what);
|
||||
|
||||
synchronized (workQueue) {
|
||||
workQueue.add(work);
|
||||
workQueue.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleSecretRequiredIntent(int what, Intent intent) {
|
||||
Runnable work = new SendReceiveWorkItem(intent, what);
|
||||
|
||||
synchronized (workQueue) {
|
||||
if (masterSecret != null) {
|
||||
workQueue.add(work);
|
||||
workQueue.notifyAll();
|
||||
} else {
|
||||
pendingSecretList.add(work);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SendReceiveWorkItem implements Runnable {
|
||||
|
||||
private final Intent intent;
|
||||
private final int what;
|
||||
|
||||
public SendReceiveWorkItem(Intent intent, int what) {
|
||||
this.intent = intent;
|
||||
this.what = what;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
switch (what) {
|
||||
case RECEIVE_SMS: smsReceiver.process(masterSecret, intent); return;
|
||||
case SEND_SMS: smsSender.process(masterSecret, intent); return;
|
||||
case RECEIVE_MMS: mmsReceiver.process(masterSecret, intent); return;
|
||||
case SEND_MMS: mmsSender.process(masterSecret, intent); return;
|
||||
case DOWNLOAD_MMS: mmsDownloader.process(masterSecret, intent); return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ToastHandler extends Handler {
|
||||
public void makeToast(String toast) {
|
||||
Message message = this.obtainMessage();
|
||||
message.obj = toast;
|
||||
this.sendMessage(message);
|
||||
}
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
Toast.makeText(SendReceiveService.this, (String)message.obj, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
KeyCachingService keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService();
|
||||
MasterSecret masterSecret = keyCachingService.getMasterSecret();
|
||||
|
||||
initializeWithMasterSecret(masterSecret);
|
||||
|
||||
SendReceiveService.this.unbindService(this);
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName name) {}
|
||||
};
|
||||
|
||||
private class NewKeyReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.w("SendReceiveService", "Got a MasterSecret broadcast...");
|
||||
initializeWithMasterSecret((MasterSecret)intent.getParcelableExtra("master_secret"));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
98
src/org/thoughtcrime/securesms/service/SmsListener.java
Normal file
98
src/org/thoughtcrime/securesms/service/SmsListener.java
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import org.thoughtcrime.securesms.protocol.WirePrefix;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.telephony.SmsMessage;
|
||||
import android.util.Log;
|
||||
|
||||
public class SmsListener extends BroadcastReceiver {
|
||||
|
||||
private static final String SMS_RECEIVED_ACTION = "android.provider.Telephony.SMS_RECEIVED";
|
||||
|
||||
private boolean isExemption(SmsMessage message, String messageBody) {
|
||||
// Sprint Visual Voicemail
|
||||
return
|
||||
message.getOriginatingAddress().length() < 7 &&
|
||||
(messageBody.startsWith("//ANDROID:") || messageBody.startsWith("//Android:") ||
|
||||
messageBody.startsWith("//android:") || messageBody.startsWith("//BREW:"));
|
||||
}
|
||||
|
||||
private SmsMessage getSmsMessageFromIntent(Intent intent) {
|
||||
Bundle bundle = intent.getExtras();
|
||||
Object[] pdus = (Object[])bundle.get("pdus");
|
||||
|
||||
if (pdus == null || pdus.length == 0)
|
||||
return null;
|
||||
|
||||
return SmsMessage.createFromPdu((byte[])pdus[0]);
|
||||
}
|
||||
|
||||
private String getSmsMessageBodyFromIntent(Intent intent) {
|
||||
Bundle bundle = intent.getExtras();
|
||||
Object[] pdus = (Object[])bundle.get("pdus");
|
||||
StringBuilder bodyBuilder = new StringBuilder();
|
||||
|
||||
if (pdus == null)
|
||||
return null;
|
||||
|
||||
for (int i=0;i<pdus.length;i++)
|
||||
bodyBuilder.append(SmsMessage.createFromPdu((byte[])pdus[i]).getDisplayMessageBody());
|
||||
|
||||
return bodyBuilder.toString();
|
||||
}
|
||||
|
||||
private boolean isRelevent(Context context, Intent intent) {
|
||||
SmsMessage message = getSmsMessageFromIntent(intent);
|
||||
String messageBody = getSmsMessageBodyFromIntent(intent);
|
||||
|
||||
if (message == null && messageBody == null)
|
||||
return false;
|
||||
|
||||
if (isExemption(message, messageBody))
|
||||
return false;
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("pref_all_sms", true))
|
||||
return true;
|
||||
|
||||
return WirePrefix.isEncryptedMessage(messageBody) || WirePrefix.isKeyExchange(messageBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.w("SMSListener", "Got SMS broadcast...");
|
||||
|
||||
if (intent.getAction().equals(SMS_RECEIVED_ACTION) && isRelevent(context, intent)) {
|
||||
intent.setAction(SendReceiveService.RECEIVE_SMS_ACTION);
|
||||
intent.putExtra("ResultCode", this.getResultCode());
|
||||
intent.setClass(context, SendReceiveService.class);
|
||||
context.startService(intent);
|
||||
|
||||
abortBroadcast();
|
||||
} else if (intent.getAction().equals(SendReceiveService.SENT_SMS_ACTION)) {
|
||||
intent.putExtra("ResultCode", this.getResultCode());
|
||||
intent.setClass(context, SendReceiveService.class);
|
||||
context.startService(intent);
|
||||
}
|
||||
}
|
||||
}
|
185
src/org/thoughtcrime/securesms/service/SmsReceiver.java
Normal file
185
src/org/thoughtcrime/securesms/service/SmsReceiver.java
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
|
||||
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
|
||||
import org.thoughtcrime.securesms.crypto.InvalidVersionException;
|
||||
import org.thoughtcrime.securesms.crypto.KeyExchangeMessage;
|
||||
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.protocol.Prefix;
|
||||
import org.thoughtcrime.securesms.protocol.WirePrefix;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MultipartMessageHandler;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.telephony.SmsMessage;
|
||||
import android.util.Log;
|
||||
|
||||
public class SmsReceiver {
|
||||
|
||||
private MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SmsReceiver(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private String assembleSecureMessageFragments(String sender, String messageBody) {
|
||||
String localPrefix;
|
||||
|
||||
if (WirePrefix.isEncryptedMessage(messageBody)) {
|
||||
localPrefix = Prefix.ASYMMETRIC_ENCRYPT;
|
||||
} else {
|
||||
localPrefix = Prefix.KEY_EXCHANGE;
|
||||
}
|
||||
|
||||
Log.w("SMSReceiverService", "Calculated local prefix for message: " + messageBody + " - Local Prefix: " + localPrefix);
|
||||
|
||||
messageBody = messageBody.substring(WirePrefix.PREFIX_SIZE);
|
||||
|
||||
Log.w("SMSReceiverService", "Parsed off wire prefix: " + messageBody);
|
||||
|
||||
if (!multipartMessageHandler.isManualTransport(messageBody))
|
||||
return localPrefix + messageBody;
|
||||
else
|
||||
return multipartMessageHandler.processPotentialMultipartMessage(localPrefix, sender, messageBody);
|
||||
|
||||
}
|
||||
|
||||
private String assembleMessageFragments(SmsMessage[] messages) {
|
||||
StringBuilder body = new StringBuilder();
|
||||
|
||||
for (SmsMessage message : messages) {
|
||||
body.append(message.getDisplayMessageBody());
|
||||
}
|
||||
|
||||
String messageBody = body.toString();
|
||||
|
||||
if (WirePrefix.isEncryptedMessage(messageBody) || WirePrefix.isKeyExchange(messageBody)) {
|
||||
return assembleSecureMessageFragments(messages[0].getDisplayOriginatingAddress(), messageBody);
|
||||
} else {
|
||||
return messageBody;
|
||||
}
|
||||
}
|
||||
|
||||
private void storeSecureMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) {
|
||||
long messageId = DatabaseFactory.getSmsDatabase(context).insertSecureMessageReceived(message, messageBody);
|
||||
Log.w("SmsReceiver", "Inserted secure message received: " + messageId);
|
||||
if (masterSecret != null)
|
||||
DecryptingQueue.scheduleDecryption(context, masterSecret, messageId, message.getDisplayOriginatingAddress(), messageBody);
|
||||
}
|
||||
|
||||
private long storeStandardMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) {
|
||||
if (masterSecret != null) return DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageReceived(masterSecret, message, messageBody);
|
||||
else if (MasterSecretUtil.hasAsymmericMasterSecret(context)) return DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageReceived(MasterSecretUtil.getAsymmetricMasterSecret(context, null), message, messageBody);
|
||||
else return DatabaseFactory.getSmsDatabase(context).insertMessageReceived(message, messageBody);
|
||||
}
|
||||
|
||||
private void storeKeyExchangeMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(ApplicationPreferencesActivity.AUTO_KEY_EXCHANGE_PREF, true)) {
|
||||
try {
|
||||
Recipient recipient = new Recipient(null, message.getDisplayOriginatingAddress(), null);
|
||||
KeyExchangeMessage keyExchangeMessage = new KeyExchangeMessage(messageBody);
|
||||
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient);
|
||||
|
||||
Log.w("SmsReceiver", "Received key with fingerprint: " + keyExchangeMessage.getPublicKey().getFingerprint());
|
||||
|
||||
if (processor.isStale(keyExchangeMessage)) {
|
||||
messageBody = messageBody.substring(Prefix.KEY_EXCHANGE.length());
|
||||
messageBody = Prefix.STALE_KEY_EXCHANGE + messageBody;
|
||||
} else if (!processor.hasCompletedSession() || processor.hasSameSessionIdentity(keyExchangeMessage)) {
|
||||
messageBody = messageBody.substring(Prefix.KEY_EXCHANGE.length());
|
||||
messageBody = Prefix.PROCESSED_KEY_EXCHANGE + messageBody;
|
||||
long messageId = storeStandardMessage(masterSecret, message, messageBody);
|
||||
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
||||
|
||||
processor.processKeyExchangeMessage(keyExchangeMessage, threadId);
|
||||
return;
|
||||
}
|
||||
} catch (InvalidVersionException e) {
|
||||
Log.w("SmsReceiver", e);
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w("SmsReceiver", e);
|
||||
}
|
||||
}
|
||||
|
||||
storeStandardMessage(masterSecret, message, messageBody);
|
||||
}
|
||||
|
||||
private boolean storeMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) {
|
||||
if (messageBody.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) {
|
||||
storeSecureMessage(masterSecret, message, messageBody);
|
||||
} else if (messageBody.startsWith(Prefix.KEY_EXCHANGE)) {
|
||||
storeKeyExchangeMessage(masterSecret, message, messageBody);
|
||||
} else {
|
||||
storeStandardMessage(masterSecret, message, messageBody);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private SmsMessage[] parseMessages(Bundle bundle) {
|
||||
Object[] pdus = (Object[])bundle.get("pdus");
|
||||
SmsMessage[] messages = new SmsMessage[pdus.length];
|
||||
|
||||
for (int i=0;i<pdus.length;i++)
|
||||
messages[i] = SmsMessage.createFromPdu((byte[])pdus[i]);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
private void handleReceiveMessage(MasterSecret masterSecret, Intent intent) {
|
||||
Bundle bundle = intent.getExtras();
|
||||
SmsMessage[] messages = parseMessages(bundle);
|
||||
String message = assembleMessageFragments(messages);
|
||||
|
||||
if (message != null) {
|
||||
storeMessage(masterSecret, messages[0], message);
|
||||
MessageNotifier.updateNotification(context, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSentMessage(Intent intent) {
|
||||
long messageId = intent.getLongExtra("message_id", -1);
|
||||
long type = intent.getLongExtra("type", -1);
|
||||
|
||||
Log.w("SMSReceiverService", "Intent resultcode: " + intent.getIntExtra("ResultCode", 42));
|
||||
Log.w("SMSReceiverService", "Running sent callback: " + messageId + "," + type);
|
||||
|
||||
if (intent.getIntExtra("ResultCode", -31337) == Activity.RESULT_OK)
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSent(messageId, type);
|
||||
else
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
||||
}
|
||||
|
||||
public void process(MasterSecret masterSecret, Intent intent) {
|
||||
if (intent.getAction().equals(SendReceiveService.RECEIVE_SMS_ACTION))
|
||||
handleReceiveMessage(masterSecret, intent);
|
||||
else if (intent.getAction().equals(SendReceiveService.SENT_SMS_ACTION))
|
||||
handleSentMessage(intent);
|
||||
}
|
||||
}
|
183
src/org/thoughtcrime/securesms/service/SmsSender.java
Normal file
183
src/org/thoughtcrime/securesms/service/SmsSender.java
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
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;
|
||||
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;
|
||||
import org.thoughtcrime.securesms.sms.MultipartMessageHandler;
|
||||
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
|
||||
import org.thoughtcrime.securesms.util.InvalidMessageException;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.telephony.SmsManager;
|
||||
import android.util.Log;
|
||||
|
||||
public class SmsSender {
|
||||
|
||||
private final MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SmsSender(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void process(MasterSecret masterSecret, Intent intent) {
|
||||
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);
|
||||
|
||||
if (c != null && c.moveToFirst()) {
|
||||
do {
|
||||
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);
|
||||
|
||||
Log.w("SMSSenderService", "Actually delivering: " + messageId);
|
||||
|
||||
deliverTextMessage(address, messageText, messageId, type);
|
||||
} while (c.moveToNext());
|
||||
}
|
||||
} finally {
|
||||
if (c != null)
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
private String getClearTextBody(MasterCipher masterCipher, String body) {
|
||||
if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT)) {
|
||||
try {
|
||||
return masterCipher.decryptBody(body.substring(Prefix.SYMMETRIC_ENCRYPT.length()));
|
||||
} catch (InvalidMessageException e) {
|
||||
return "Error decrypting message.";
|
||||
}
|
||||
} else {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
return sentIntents;
|
||||
}
|
||||
|
||||
private void deliverGSMTransportTextMessage(String recipient, String text, long messageId, long type) {
|
||||
ArrayList<String> messages = SmsManager.getDefault().divideMessage(text);
|
||||
ArrayList<PendingIntent> sentIntents = constructSentIntents(messageId, type, messages);
|
||||
// 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 {
|
||||
SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, null);
|
||||
} 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;
|
||||
|
||||
if (isSecureMessage(type)) {
|
||||
prefix = new SecureMessageWirePrefix();
|
||||
text = text.substring(Prefix.ASYMMETRIC_ENCRYPT.length());
|
||||
} else {
|
||||
prefix = new KeyExchangeWirePrefix();
|
||||
text = text.substring(Prefix.KEY_EXCHANGE.length());
|
||||
}
|
||||
|
||||
if (!multipartMessageHandler.isManualTransport(text)) {
|
||||
deliverGSMTransportTextMessage(recipient, prefix.calculatePrefix(text) + text, messageId, type);
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayList<String> messages = multipartMessageHandler.divideMessage(recipient, text, prefix);
|
||||
ArrayList<PendingIntent> sentIntents = constructSentIntents(messageId, type, messages);
|
||||
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 {
|
||||
SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i), sentIntents.get(i), null);
|
||||
} catch (NullPointerException npe) {
|
||||
Log.w("SmsSender", npe);
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deliverTextMessage(String recipient, String text, long messageId, long type) {
|
||||
if (!isSecureMessage(type) && !isKeyExchange(text))
|
||||
deliverGSMTransportTextMessage(recipient, text, messageId, type);
|
||||
else
|
||||
deliverSecureTransportTextMessage(recipient, text, messageId, type);
|
||||
}
|
||||
|
||||
private boolean isSecureMessage(long type) {
|
||||
return type == SmsDatabase.Types.ENCRYPTING_TYPE;
|
||||
}
|
||||
|
||||
private boolean isKeyExchange(String messageText) {
|
||||
return messageText.startsWith(Prefix.KEY_EXCHANGE);
|
||||
}
|
||||
|
||||
private String getAsymmetricEncrypt(MasterSecret masterSecret, String body, String address) {
|
||||
synchronized (SessionCipher.CIPHER_LOCK) {
|
||||
SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, address, null), new SmsTransportDetails());
|
||||
return new String(cipher.encryptMessage(body.getBytes()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user