Major storage layer refactoring to set the stage for clean GCM.

1) We now try to hand out cursors at a minimum.  There has always been
   a fairly clean insertion layer that handles encrypting message bodies,
   but the process of decrypting message bodies has always been less than
   ideal.  Here we introduce a "Reader" interface that will decrypt message
   bodies when appropriate and return objects that encapsulate record state.

   No more MessageDisplayHelper.  The MmsSmsDatabase interface is also more
   sane.

2) We finally rid ourselves of the technical debt associated with TextSecure's
   initial usage of the default SMS DB.  In that world, we weren't able to use
   anything other than the default "Inbox, Outbox, Sent" types to describe a
   message, and had to overload the message content itself with a set of
   local "prefixes" to describe what it was (encrypted, asymetric encrypted,
   remote encrypted, a key exchange, procssed key exchange), and so on.

   This includes a major schema update that transforms the "type" field into
   a bitmask that describes everything that used to be encoded in a prefix,
   and prefixes have been completely eliminated from the system.

   No more Prefix.java

3) Refactoring of the MultipartMessageHandler code.  It's less of a mess, and
   hopefully more clear as to what's going on.

The next step is to remove what we can from SmsTransportDetails and genericize
that interface for a GCM equivalent.
This commit is contained in:
Moxie Marlinspike 2013-04-20 12:22:04 -07:00
parent 303d1acd45
commit 83e260436b
60 changed files with 2673 additions and 1760 deletions

View File

@ -87,8 +87,13 @@
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseUpgradeActivity"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseCreateActivity"
android:label="@string/AndroidManifest__create_passphrase"
android:windowSoftInputMode="stateUnchanged"

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fillViewport="true"
android:background="@drawable/background_pattern_repeat">
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center" >
<LinearLayout android:paddingRight="16dip"
android:paddingLeft="16dip"
android:paddingTop="10dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible"
android:orientation="vertical">
<TextView style="@style/Registration.BigLabel"
android:layout_width="fill_parent"
android:layout_marginBottom="16dip"
android:layout_marginTop="16dip"
android:gravity="center"
android:text="@string/database_upgrade_activity__updating_database"/>
<ProgressBar android:id="@+id/indeterminate_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="center"/>
<ProgressBar android:id="@+id/determinate_progress"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:indeterminate="false"
android:visibility="gone"
android:layout_gravity="center"/>
</LinearLayout>
</FrameLayout>
</ScrollView>

View File

@ -107,8 +107,11 @@
<!-- MmsDownloader -->
<string name="MmsDownloader_no_connectivity_available_for_mms_download_try_again_later">No connectivity available for MMS download, try again later...</string>
<string name="MmsDownloader_error_storing_mms">Error storing MMS!</string>
<string name="MmsDownloader_error_connecting_to_mms_provider">Error connecting to MMS provider...</string>
<string name="MmsDownloader_error_connecting_to_mms_provider">Error connecting to MMS provider...</string>
<!-- NotificationMmsMessageRecord -->
<string name="NotificationMmsMessageRecord_multimedia_message">Multimedia Message</string>
<!-- PassphraseChangeActivity -->
<string name="PassphraseChangeActivity_passphrases_dont_match_exclamation">Passphrases Don\'t Match!</string>
<string name="PassphraseChangeActivity_incorrect_old_passphrase_exclamation">Incorrect old passphrase!</string>
@ -294,8 +297,11 @@
<string name="database_migration_activity__skip">Skip</string>
<string name="database_migration_activity__import">Import</string>
<string name="database_migration_activity__this_could_take_a_moment_please_be_patient">This could take a moment. Please be patient, we\'ll notify you when the import is complete.</string>
<string name="database_migration_activity__importing">IMPORTING</string>
<string name="database_migration_activity__importing">IMPORTING</string>
<!-- database_upgrade_activity -->
<string name="database_upgrade_activity__updating_database">Updating Database...</string>
<!-- prompt_passphrase_activity -->
<string name="prompt_passphrase_activity__textsecure_passphrase">TEXTSECURE PASSPHRASE</string>
<string name="prompt_passphrase_activity__unlock">Unlock</string>
@ -499,7 +505,7 @@
<!-- Misc. piggybacking -->
<string name="PlayStoreListing">TextSecure is a security enhanced text messaging application that serves as a full replacement for the default text messaging application. Messages to other TextSecure users are encrypted over the air, and all text messages are stored in an encrypted database on the device. If your phone is lost or stolen, your messages will be safe, and communication with other TextSecure users can\'t be monitored over the air.</string>
<!-- EOF -->
</resources>

View File

@ -16,40 +16,6 @@
*/
package org.thoughtcrime.securesms;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.components.RecipientsPanel;
import org.thoughtcrime.securesms.crypto.AuthenticityCalculator;
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.KeyUtil;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.MmsSendHelper;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.Tag;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.EncryptedCharacterCalculator;
import org.thoughtcrime.securesms.util.InvalidMessageException;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.Util;
import ws.com.google.android.mms.MmsException;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
@ -81,6 +47,42 @@ import android.widget.Toast;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import org.thoughtcrime.securesms.components.RecipientsPanel;
import org.thoughtcrime.securesms.crypto.AuthenticityCalculator;
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.KeyUtil;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.MmsSendHelper;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.Tag;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.EncryptedCharacterCalculator;
import org.thoughtcrime.securesms.util.InvalidMessageException;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import ws.com.google.android.mms.MmsException;
/**
* Activity for displaying a message thread, as well as
@ -741,19 +743,28 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
if (recipients == null)
throw new RecipientFormattingException("Badly formatted");
String message = getMessage();
String body = getMessage();
long allocatedThreadId;
if (attachmentManager.isAttachmentPresent()) {
allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients,
threadId, attachmentManager.getSlideDeck(), message,
threadId, attachmentManager.getSlideDeck(), body,
forcePlaintext);
} else if (recipients.isEmailRecipient()) {
allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients,
threadId, new SlideDeck(), message, forcePlaintext);
threadId, new SlideDeck(), body, forcePlaintext);
} else {
allocatedThreadId = MessageSender.send(ConversationActivity.this, masterSecret, recipients,
threadId, message, forcePlaintext);
OutgoingTextMessage message;
if (isEncryptedConversation && !forcePlaintext) {
message = new OutgoingEncryptedMessage(recipients, body);
} else {
message = new OutgoingTextMessage(recipients, body);
}
Log.w("ConversationActivity", "Sending message...");
allocatedThreadId = MessageSender.send(ConversationActivity.this, masterSecret,
message, threadId);
}
sendComplete(recipients, allocatedThreadId);
@ -761,11 +772,11 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
Toast.makeText(ConversationActivity.this,
R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation,
Toast.LENGTH_LONG).show();
Log.w("compose", ex);
Log.w("ConversationActivity", ex);
} catch (InvalidMessageException ex) {
Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_message_is_empty_exclamation,
Toast.LENGTH_SHORT).show();
Log.w("compose", ex);
Log.w("ConversationActivity", ex);
} catch (MmsException e) {
Log.w("ComposeMessageActivity", e);
}

View File

@ -19,36 +19,18 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.database.Cursor;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MessageDisplayHelper;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord.GroupData;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.SlideDeck;
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 org.thoughtcrime.securesms.util.InvalidMessageException;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.PduHeaders;
import java.util.LinkedHashMap;
@ -66,11 +48,8 @@ public class ConversationAdapter extends CursorAdapter {
private final LinkedHashMap<String,MessageRecord> messageRecordCache;
private final Handler failedIconClickHandler;
private final long threadId;
private final Context context;
private final Recipients recipients;
private final MasterSecret masterSecret;
private final MasterCipher masterCipher;
private final LayoutInflater inflater;
public ConversationAdapter(Recipients recipients, long threadId, Context context,
@ -78,10 +57,7 @@ public class ConversationAdapter extends CursorAdapter {
{
super(context, null);
this.context = context;
this.recipients = recipients;
this.threadId = threadId;
this.masterSecret = masterSecret;
this.masterCipher = new MasterCipher(masterSecret);
this.failedIconClickHandler = failedIconClickHandler;
this.messageRecordCache = initializeCache();
this.inflater = (LayoutInflater)context
@ -123,7 +99,7 @@ public class ConversationAdapter extends CursorAdapter {
}
private int getItemViewType(Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID));
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
@ -131,150 +107,18 @@ public class ConversationAdapter extends CursorAdapter {
else return 1;
}
private MediaMmsMessageRecord getMediaMmsMessageRecord(long messageId, Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID));
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsDatabase.DATE_SENT));
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsDatabase.DATE_RECEIVED));
long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
Recipient recipient = getIndividualRecipientFor(null);
GroupData groupData = null;
SlideDeck slideDeck;
try {
MultimediaMessagePdu pdu = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret).getMediaMessage(messageId);
slideDeck = new SlideDeck(context, masterSecret, pdu.getBody());
if (recipients != null && !recipients.isSingleRecipient()) {
int groupSize = pdu.getTo().length;
int groupSent = MmsDatabase.Types.isFailedMmsBox(box) ? 0 : groupSize;
int groupSendFailed = groupSize - groupSent;
if (groupSize <= 1) {
groupSize = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.GROUP_SIZE));
groupSent = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.MMS_GROUP_SENT_COUNT));
groupSendFailed = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.MMS_GROUP_SEND_FAILED_COUNT));
}
Log.w("ConversationAdapter", "MMS GroupSize: " + groupSize + " , GroupSent: " + groupSent + " , GroupSendFailed: " + groupSendFailed);
groupData = new MessageRecord.GroupData(groupSize, groupSent, groupSendFailed);
}
} catch (MmsException me) {
Log.w("ConversationAdapter", me);
slideDeck = null;
}
return new MediaMmsMessageRecord(context, id, recipients, recipient,
dateSent, dateReceived, threadId,
slideDeck, box, groupData);
}
private NotificationMmsMessageRecord getNotificationMmsMessageRecord(long messageId, Cursor cursor) {
Recipient recipient = getIndividualRecipientFor(null);
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID));
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsDatabase.DATE_SENT));
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsDatabase.DATE_RECEIVED));
NotificationInd notification;
try {
notification = DatabaseFactory.getMmsDatabase(context).getNotificationMessage(messageId);
} catch (MmsException me) {
Log.w("ConversationAdapter", me);
notification = new NotificationInd(new PduHeaders());
}
return new NotificationMmsMessageRecord(id, recipients, recipient,
dateSent, dateReceived, threadId,
notification.getContentLocation(),
notification.getMessageSize(),
notification.getExpiry(),
notification.getStatus(),
notification.getTransactionId());
}
private SmsMessageRecord getSmsMessageRecord(long messageId, Cursor cursor) {
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsDatabase.DATE_RECEIVED));
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsDatabase.DATE_SENT));
long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE));
int status = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.STATUS));
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
Recipient recipient = getIndividualRecipientFor(address);
MessageRecord.GroupData groupData = null;
if (recipients != null && !recipients.isSingleRecipient()) {
int groupSize = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.GROUP_SIZE));
int groupSent = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.SMS_GROUP_SENT_COUNT));
int groupSendFailed = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.SMS_GROUP_SEND_FAILED_COUNT));
Log.w("ConversationAdapter", "GroupSize: " + groupSize + " , GroupSent: " + groupSent + " , GroupSendFailed: " + groupSendFailed);
groupData = new MessageRecord.GroupData(groupSize, groupSent, groupSendFailed);
}
SmsMessageRecord messageRecord = new SmsMessageRecord(context, messageId, recipients,
recipient, dateSent, dateReceived,
type, threadId, status, groupData);
if (body == null) {
body = "";
}
try {
String decryptedBody = MessageDisplayHelper.getDecryptedMessageBody(masterCipher, body);
messageRecord.setBody(decryptedBody);
} catch (InvalidMessageException ime) {
Log.w("ConversationAdapter", ime);
messageRecord.setBody(context.getString(R.string.MessageDisplayHelper_decryption_error_local_message_corrupted_mac_doesn_t_match_potential_tampering_question));
messageRecord.setEmphasis(true);
}
return messageRecord;
}
private MessageRecord getMessageRecord(long messageId, Cursor cursor, String type) {
if (messageRecordCache.containsKey(type + messageId))
return messageRecordCache.get(type + messageId);
MessageRecord messageRecord;
MmsSmsDatabase.Reader reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor, masterSecret);
if (type.equals("mms")) {
long mmsType = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_TYPE));
if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
messageRecord = getNotificationMmsMessageRecord(messageId, cursor);
} else {
messageRecord = getMediaMmsMessageRecord(messageId, cursor);
}
} else {
messageRecord = getSmsMessageRecord(messageId, cursor);
}
MessageRecord messageRecord = reader.getCurrent();
messageRecordCache.put(type + messageId, messageRecord);
return messageRecord;
}
private Recipient getIndividualRecipientFor(String address) {
Recipient recipient;
try {
if (address == null) recipient = recipients.getPrimaryRecipient();
else recipient = RecipientFactory.getRecipientsFromString(context, address, false).getPrimaryRecipient();
if (recipient == null) recipient = new Recipient("Unknown", "Unknown", null,
ContactPhotoFactory.getDefaultContactPhoto(context));
} catch (RecipientFormattingException e) {
Log.w("ConversationAdapter", e);
recipient = new Recipient("Unknown", "Unknown", null,
ContactPhotoFactory.getDefaultContactPhoto(context));
}
return recipient;
}
@Override
protected void onContentChanged() {
super.onContentChanged();
@ -293,6 +137,4 @@ public class ConversationAdapter extends CursorAdapter {
}
};
}
}

View File

@ -90,7 +90,7 @@ public class ConversationFragment extends SherlockListFragment
}
private void handleCopyMessage(MessageRecord message) {
String body = message.getBody();
String body = message.getDisplayBody().toString();
if (body == null) return;
ClipboardManager clipboard = (ClipboardManager)getActivity()
@ -153,7 +153,7 @@ public class ConversationFragment extends SherlockListFragment
private void handleForwardMessage(MessageRecord message) {
Intent composeIntent = new Intent(getActivity(), ConversationActivity.class);
composeIntent.putExtra("forwarded_message", message.getBody());
composeIntent.putExtra("forwarded_message", message.getDisplayBody().toString());
composeIntent.putExtra("master_secret", masterSecret);
startActivity(composeIntent);
}

View File

@ -28,10 +28,7 @@ import android.os.Handler;
import android.os.Message;
import android.provider.Contacts.Intents;
import android.provider.ContactsContract.QuickContact;
import android.text.Spannable;
import android.text.format.DateUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
@ -51,7 +48,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord.GroupData;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.protocol.Tag;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService;
@ -156,20 +152,7 @@ public class ConversationItem extends LinearLayout {
/// MessageRecord Attribute Parsers
private void setBodyText(MessageRecord messageRecord) {
String body = messageRecord.getBody();
if (messageRecord.isKeyExchange() && messageRecord.isOutgoing()) body = "\n" + getContext().getString(R.string.ConversationItem_key_exchange_message);
else if (messageRecord.isProcessedKeyExchange() && !messageRecord.isOutgoing()) body = "\n" + getContext().getString(R.string.ConversationItem_received_and_processed_key_exchange_message);
else if (messageRecord.isStaleKeyExchange()) body = "\n" + getContext().getString(R.string.ConversationItem_error_received_stale_key_exchange_message);
else if (messageRecord.isKeyExchange() && !messageRecord.isOutgoing()) body = "\n" + getContext().getString(R.string.ConversationItem_received_key_exchange_message_click_to_process);
else if (messageRecord.isOutgoing() && Tag.isTagged(body)) body = Tag.stripTag(body);
bodyText.setText(body, TextView.BufferType.SPANNABLE);
if (messageRecord.isKeyExchange() || messageRecord.getEmphasis()) {
((Spannable)bodyText.getText()).setSpan(new ForegroundColorSpan(context.getResources().getColor(android.R.color.darker_gray)), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
((Spannable)bodyText.getText()).setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
bodyText.setText(messageRecord.getDisplayBody(), TextView.BufferType.SPANNABLE);
}
private void setContactPhoto(MessageRecord messageRecord) {
@ -237,11 +220,11 @@ public class ConversationItem extends LinearLayout {
dateText.setText(messageSize + "\n" + expires);
if (MmsDatabase.Types.isDisplayDownloadButton(messageRecord.getStatus())) {
if (MmsDatabase.Status.isDisplayDownloadButton(messageRecord.getStatus())) {
mmsDownloadButton.setVisibility(View.VISIBLE);
mmsDownloadingLabel.setVisibility(View.GONE);
} else {
mmsDownloadingLabel.setText(MmsDatabase.Types.getLabelForStatus(context, messageRecord.getStatus()));
mmsDownloadingLabel.setText(MmsDatabase.Status.getLabelForStatus(context, messageRecord.getStatus()));
mmsDownloadButton.setVisibility(View.GONE);
mmsDownloadingLabel.setVisibility(View.VISIBLE);
}

View File

@ -24,11 +24,10 @@ import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.CursorAdapter;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.Collections;
import java.util.HashSet;
@ -41,16 +40,18 @@ import java.util.Set;
*/
public class ConversationListAdapter extends CursorAdapter implements AbsListView.RecyclerListener {
private final MasterSecret masterSecret;
private final Context context;
private final LayoutInflater inflater;
private final Set<Long> batchSet = Collections.synchronizedSet(new HashSet<Long>());
private boolean batchMode = false;
public ConversationListAdapter(Context context, Cursor cursor) {
public ConversationListAdapter(Context context, Cursor cursor, MasterSecret masterSecret) {
super(context, cursor);
this.context = context;
this.inflater = LayoutInflater.from(context);
this.masterSecret = masterSecret;
this.context = context;
this.inflater = LayoutInflater.from(context);
}
@Override
@ -60,27 +61,12 @@ public class ConversationListAdapter extends CursorAdapter implements AbsListVie
@Override
public void bindView(View view, Context context, Cursor cursor) {
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID));
String recipientId = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_IDS));
Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientId, true);
if (masterSecret != null) {
ThreadDatabase.Reader reader = DatabaseFactory.getThreadDatabase(context).readerFor(cursor, masterSecret);
ThreadRecord record = reader.getCurrent();
long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE));
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
long read = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.READ));
ThreadRecord thread = new ThreadRecord(context, recipients, date, count, read == 1, threadId);
setBody(cursor, thread);
((ConversationListItem)view).set(thread, batchSet, batchMode);
}
protected void filterBody(ThreadRecord thread, String body) {
if (body == null) body = "(No subject)";
thread.setBody(body);
}
protected void setBody(Cursor cursor, ThreadRecord thread) {
filterBody(thread, cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET)));
((ConversationListItem)view).set(record, batchSet, batchMode);
}
}
public void addToBatchSet(long threadId) {

View File

@ -139,12 +139,7 @@ public class ConversationListFragment extends SherlockListFragment
}
private void initializeListAdapter() {
if (this.masterSecret == null) {
this.setListAdapter(new ConversationListAdapter(getActivity(), null));
} else {
this.setListAdapter(new DecryptingConversationListAdapter(getActivity(), null, masterSecret));
}
this.setListAdapter(new ConversationListAdapter(getActivity(), null, masterSecret));
getListView().setRecyclerListener((ConversationListAdapter)getListAdapter());
getLoaderManager().restartLoader(0, null, this);
}

View File

@ -105,17 +105,7 @@ public class ConversationListItem extends RelativeLayout
this.recipients.addListener(this);
this.fromView.setText(formatFrom(recipients, count, read));
if (thread.isKeyExchange())
this.subjectView.setText(R.string.ConversationListItem_key_exchange_message,
TextView.BufferType.SPANNABLE);
else
this.subjectView.setText(thread.getBody(), TextView.BufferType.SPANNABLE);
if (thread.getEmphasis())
((Spannable)this.subjectView.getText()).setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0,
this.subjectView.getText().length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
this.subjectView.setText(thread.getDisplayBody(), TextView.BufferType.SPANNABLE);
if (thread.getDate() > 0)
this.dateView.setText(DateUtils.getRelativeTimeSpanString(getContext(), thread.getDate(), false));

View File

@ -0,0 +1,148 @@
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import java.util.SortedSet;
import java.util.TreeSet;
public class DatabaseUpgradeActivity extends Activity {
public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 46;
private static final String LAST_VERSION_CODE = "last_version_code";
private static final SortedSet<Integer> UPGRADE_VERSIONS = new TreeSet<Integer>() {{
add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION);
}};
private MasterSecret masterSecret;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret");
if (needsDatabaseUpgrade()) {
Log.w("DatabaseUpgradeActivity", "Upgrading...");
setContentView(R.layout.database_upgrade_activity);
ProgressBar indeterminateProgress = (ProgressBar)findViewById(R.id.indeterminate_progress);
ProgressBar determinateProgress = (ProgressBar)findViewById(R.id.determinate_progress);
new DatabaseUpgradeTask(indeterminateProgress, determinateProgress).execute(getLastSeenVersion());
} else {
updateLastSeenVersion();
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
finish();
}
}
private boolean needsDatabaseUpgrade() {
try {
int currentVersionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
int lastSeenVersion = getLastSeenVersion();
Log.w("DatabaseUpgradeActivity", "LastSeenVersion: " + lastSeenVersion);
if (lastSeenVersion >= currentVersionCode)
return false;
for (int version : UPGRADE_VERSIONS) {
Log.w("DatabaseUpgradeActivity", "Comparing: " + version);
if (lastSeenVersion < version)
return true;
}
return false;
} catch (PackageManager.NameNotFoundException e) {
throw new AssertionError(e);
}
}
private int getLastSeenVersion() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
return preferences.getInt(LAST_VERSION_CODE, 0);
}
private void updateLastSeenVersion() {
try {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
int currentVersionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
preferences.edit().putInt(LAST_VERSION_CODE, currentVersionCode).commit();
} catch (PackageManager.NameNotFoundException e) {
throw new AssertionError(e);
}
}
public static boolean isUpdate(Context context) {
try {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
int currentVersionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
int previousVersionCode = preferences.getInt(LAST_VERSION_CODE, 0);
return previousVersionCode < currentVersionCode;
} catch (PackageManager.NameNotFoundException e) {
throw new AssertionError(e);
}
}
public interface DatabaseUpgradeListener {
public void setProgress(int progress, int total);
}
private class DatabaseUpgradeTask extends AsyncTask<Integer, Double, Void>
implements DatabaseUpgradeListener
{
private final ProgressBar indeterminateProgress;
private final ProgressBar determinateProgress;
public DatabaseUpgradeTask(ProgressBar indeterminateProgress, ProgressBar determinateProgress) {
this.indeterminateProgress = indeterminateProgress;
this.determinateProgress = determinateProgress;
}
@Override
protected Void doInBackground(Integer... params) {
Log.w("DatabaseUpgradeActivity", "Running background upgrade..");
DatabaseFactory.getInstance(DatabaseUpgradeActivity.this)
.onApplicationLevelUpgrade(masterSecret, params[0], this);
return null;
}
@Override
protected void onProgressUpdate(Double... update) {
indeterminateProgress.setVisibility(View.GONE);
determinateProgress.setVisibility(View.VISIBLE);
double scaler = update[0];
determinateProgress.setProgress((int)Math.floor(determinateProgress.getMax() * scaler));
}
@Override
protected void onPostExecute(Void result) {
updateLastSeenVersion();
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
finish();
}
@Override
public void setProgress(int progress, int total) {
publishProgress(((double)progress / (double)total));
}
}
}

View File

@ -1,59 +0,0 @@
/**
* 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;
import android.content.Context;
import android.database.Cursor;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MessageDisplayHelper;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.util.InvalidMessageException;
/**
* A ConversationListAdapter that decrypts encrypted message bodies.
*
* @author Moxie Marlinspike
*/
public class DecryptingConversationListAdapter extends ConversationListAdapter {
private final MasterCipher bodyCipher;
private final Context context;
public DecryptingConversationListAdapter(Context context, Cursor cursor, MasterSecret masterSecret) {
super(context, cursor);
this.bodyCipher = new MasterCipher(masterSecret);
this.context = context.getApplicationContext();
}
@Override
protected void setBody(Cursor cursor, ThreadRecord thread) {
String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET));
if (body == null || body.equals("")) body = "(No subject)";
try {
String decryptedBody = MessageDisplayHelper.getDecryptedMessageBody(bodyCipher, body);
thread.setBody(decryptedBody);
} catch (InvalidMessageException ime) {
thread.setBody(context.getString(R.string.MessageDisplayHelper_decryption_error_local_message_corrupted_mac_doesn_t_match_potential_tampering_question));
thread.setEmphasis(true);
}
}
}

View File

@ -17,6 +17,7 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity {
private static final int STATE_PROMPT_PASSPHRASE = 2;
private static final int STATE_IMPORT_DATABASE = 3;
private static final int STATE_CONVERSATION_OR_LIST = 4;
private static final int STATE_UPGRADE_DATABASE = 5;
private MasterSecret masterSecret = null;
private boolean isVisible = false;
@ -71,6 +72,7 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity {
case STATE_PROMPT_PASSPHRASE: handlePromptPassphrase(); break;
case STATE_IMPORT_DATABASE: handleImportDatabase(); break;
case STATE_CONVERSATION_OR_LIST: handleDisplayConversationOrList(); break;
case STATE_UPGRADE_DATABASE: handleUpgradeDatabase(); break;
}
}
@ -93,6 +95,15 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity {
finish();
}
private void handleUpgradeDatabase() {
Intent intent = new Intent(this, DatabaseUpgradeActivity.class);
intent.putExtra("master_secret", masterSecret);
intent.putExtra("next_intent", getConversationListIntent());
startActivity(intent);
finish();
}
private void handleDisplayConversationOrList() {
// Intent intent = new Intent(this, RegistrationActivity.class);
// startActivity(intent);
@ -141,6 +152,9 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity {
if (!ApplicationMigrationService.isDatabaseImported(this))
return STATE_IMPORT_DATABASE;
if (DatabaseUpgradeActivity.isUpdate(this))
return STATE_UPGRADE_DATABASE;
return STATE_CONVERSATION_OR_LIST;
}

View File

@ -21,12 +21,12 @@ import android.database.Cursor;
import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingMmsDatabase;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.mms.TextTransport;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
@ -35,15 +35,15 @@ import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.WorkerThread;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
import ws.com.google.android.mms.pdu.PduParser;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
/**
* A work queue for processing a number of encryption operations.
*
@ -52,15 +52,16 @@ import java.util.List;
public class DecryptingQueue {
private static List<Runnable> workQueue = new LinkedList<Runnable>();
private static Thread workerThread;
private static final List<Runnable> workQueue = new LinkedList<Runnable>();
static {
workerThread = new WorkerThread(workQueue, "Async Decryption Thread");
Thread workerThread = new WorkerThread(workQueue, "Async Decryption Thread");
workerThread.start();
}
public static void scheduleDecryption(Context context, MasterSecret masterSecret, long messageId, long threadId, MultimediaMessagePdu mms) {
public static void scheduleDecryption(Context context, MasterSecret masterSecret,
long messageId, long threadId, MultimediaMessagePdu mms)
{
MmsDecryptionItem runnable = new MmsDecryptionItem(context, masterSecret, messageId, threadId, mms);
synchronized (workQueue) {
workQueue.add(runnable);
@ -68,8 +69,12 @@ public class DecryptingQueue {
}
}
public static void scheduleDecryption(Context context, MasterSecret masterSecret, long messageId, String originator, String body) {
DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, body, originator);
public static void scheduleDecryption(Context context, MasterSecret masterSecret,
long messageId, String originator, String body,
boolean isSecureMessage)
{
DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId,
originator, body, isSecureMessage);
synchronized (workQueue) {
workQueue.add(runnable);
workQueue.notifyAll();
@ -77,47 +82,50 @@ public class DecryptingQueue {
}
public static void schedulePendingDecrypts(Context context, MasterSecret masterSecret) {
Cursor cursor = null;
Log.w("DecryptingQueue", "Processing pending decrypts...");
try {
cursor = DatabaseFactory.getSmsDatabase(context).getDecryptInProgressMessages();
if (cursor == null || cursor.getCount() == 0 || !cursor.moveToFirst())
return;
EncryptingSmsDatabase.Reader reader = null;
SmsMessageRecord record;
do {
scheduleDecryptFromCursor(context, masterSecret, cursor);
} while (cursor.moveToNext());
try {
reader = DatabaseFactory.getEncryptingSmsDatabase(context).getDecryptInProgressMessages(masterSecret);
while ((record = reader.getNext()) != null) {
scheduleDecryptFromCursor(context, masterSecret, record);
}
} finally {
if (cursor != null)
cursor.close();
if (reader != null)
reader.close();
}
}
public static void scheduleRogueMessages(Context context, MasterSecret masterSecret, Recipient recipient) {
Cursor cursor = null;
SmsDatabase.Reader reader = null;
SmsMessageRecord record;
try {
cursor = DatabaseFactory.getSmsDatabase(context).getEncryptedRogueMessages(recipient);
if (cursor == null || cursor.getCount() == 0 || !cursor.moveToFirst())
return;
Cursor cursor = DatabaseFactory.getSmsDatabase(context).getEncryptedRogueMessages(recipient);
reader = DatabaseFactory.getEncryptingSmsDatabase(context).readerFor(masterSecret, cursor);
do {
DatabaseFactory.getSmsDatabase(context).markAsDecrypting(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
scheduleDecryptFromCursor(context, masterSecret, cursor);
} while (cursor.moveToNext());
while ((record = reader.getNext()) != null) {
DatabaseFactory.getSmsDatabase(context).markAsDecrypting(record.getId());
scheduleDecryptFromCursor(context, masterSecret, record);
}
} finally {
if (cursor != null)
cursor.close();
if (reader != null)
reader.close();
}
}
private static void scheduleDecryptFromCursor(Context context, MasterSecret masterSecret, Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
String originator = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
private static void scheduleDecryptFromCursor(Context context, MasterSecret masterSecret,
SmsMessageRecord record)
{
long messageId = record.getId();
String body = record.getBody();
String originator = record.getIndividualRecipient().getNumber();
boolean isSecureMessage = record.isSecure();
scheduleDecryption(context, masterSecret, id, originator, body);
scheduleDecryption(context, masterSecret, messageId, originator, body, isSecureMessage);
}
private static class MmsDecryptionItem implements Runnable {
@ -127,7 +135,9 @@ public class DecryptingQueue {
private MasterSecret masterSecret;
private MultimediaMessagePdu pdu;
public MmsDecryptionItem(Context context, MasterSecret masterSecret, long messageId, long threadId, MultimediaMessagePdu pdu) {
public MmsDecryptionItem(Context context, MasterSecret masterSecret,
long messageId, long threadId, MultimediaMessagePdu pdu)
{
this.context = context;
this.masterSecret = masterSecret;
this.messageId = messageId;
@ -148,7 +158,7 @@ public class DecryptingQueue {
@Override
public void run() {
EncryptingMmsDatabase database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
try {
String messageFrom = pdu.getFrom().getString();
@ -178,7 +188,7 @@ public class DecryptingQueue {
MultimediaMessagePdu plaintextPdu = (MultimediaMessagePdu)new PduParser(plaintextPduBytes).parse();
Log.w("DecryptingQueue", "Successfully decrypted MMS!");
database.insertSecureDecryptedMessageReceived(plaintextPdu, threadId);
database.insertSecureDecryptedMessageInbox(masterSecret, plaintextPdu, threadId);
database.delete(messageId);
} catch (RecipientFormattingException rfe) {
Log.w("DecryptingQueue", rfe);
@ -196,18 +206,22 @@ public class DecryptingQueue {
private static class DecryptionWorkItem implements Runnable {
private long messageId;
private Context context;
private MasterSecret masterSecret;
private String body;
private String originator;
private final long messageId;
private final Context context;
private final MasterSecret masterSecret;
private final String body;
private final String originator;
private final boolean isSecureMessage;
public DecryptionWorkItem(Context context, MasterSecret masterSecret, long messageId, String body, String originator) {
public DecryptionWorkItem(Context context, MasterSecret masterSecret, long messageId,
String originator, String body, boolean isSecureMessage)
{
this.context = context;
this.messageId = messageId;
this.masterSecret = masterSecret;
this.body = body;
this.originator = originator;
this.isSecureMessage = isSecureMessage;
}
private void handleRemoteAsymmetricEncrypt() {
@ -240,7 +254,7 @@ public class DecryptingQueue {
}
}
database.updateSecureMessageBody(masterSecret, messageId, plaintextBody);
database.updateMessageBody(masterSecret, messageId, plaintextBody);
MessageNotifier.updateNotification(context, masterSecret);
}
@ -250,8 +264,7 @@ public class DecryptingQueue {
try {
AsymmetricMasterCipher asymmetricMasterCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret));
String encryptedBody = body.substring(Prefix.ASYMMETRIC_LOCAL_ENCRYPT.length());
plaintextBody = asymmetricMasterCipher.decryptBody(encryptedBody);
plaintextBody = asymmetricMasterCipher.decryptBody(body);
} catch (InvalidMessageException ime) {
Log.w("DecryptionQueue", ime);
database.markAsDecryptFailed(messageId);
@ -268,10 +281,11 @@ public class DecryptingQueue {
@Override
public void run() {
if (body.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) handleRemoteAsymmetricEncrypt();
else if (body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT)) handleLocalAsymmetricEncrypt();
if (isSecureMessage) {
handleRemoteAsymmetricEncrypt();
} else {
handleLocalAsymmetricEncrypt();
}
}
}
}

View File

@ -24,10 +24,8 @@ import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.keys.LocalKeyRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender;
import java.util.LinkedList;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
public class KeyExchangeInitiator {
@ -51,14 +49,13 @@ public class KeyExchangeInitiator {
}
private static void initiateKeyExchange(Context context, MasterSecret masterSecret, Recipient recipient) {
LocalKeyRecord record = KeyUtil.initializeRecordFor(recipient, context, masterSecret);
KeyExchangeMessage message = new KeyExchangeMessage(context, masterSecret, 1, record, 0);
LocalKeyRecord record = KeyUtil.initializeRecordFor(recipient, context, masterSecret);
KeyExchangeMessage message = new KeyExchangeMessage(context, masterSecret, 1, record, 0);
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, message.serialize());
Log.w("SendKeyActivity", "Sending public key: " + record.getCurrentKeyPair().getPublicKey().getFingerprint());
LinkedList<Recipient> list = new LinkedList<Recipient>();
list.add(recipient);
MessageSender.send(context, masterSecret, new Recipients(list), -1, message.serialize(), true);
MessageSender.send(context, masterSecret, textMessage, -1);
}
private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret, Recipient recipient) {

View File

@ -16,18 +16,17 @@
*/
package org.thoughtcrime.securesms.crypto;
import java.io.IOException;
import android.content.Context;
import android.preference.PreferenceManager;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.database.keys.LocalKeyRecord;
import org.thoughtcrime.securesms.protocol.Message;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Conversions;
import android.content.Context;
import android.preference.PreferenceManager;
import android.util.Log;
import java.io.IOException;
/**
* A class for constructing and parsing key exchange messages.
@ -78,15 +77,14 @@ public class KeyExchangeMessage {
keyExchangeBytes = IdentityKeyUtil.getSignedKeyExchange(context, masterSecret, keyExchangeBytes);
if (messageVersion < 1)
this.serialized = Prefix.KEY_EXCHANGE + Base64.encodeBytes(keyExchangeBytes);
this.serialized = Base64.encodeBytes(keyExchangeBytes);
else
this.serialized = Prefix.KEY_EXCHANGE + Base64.encodeBytesWithoutPadding(keyExchangeBytes);
this.serialized = Base64.encodeBytesWithoutPadding(keyExchangeBytes);
}
public KeyExchangeMessage(String messageBody) throws InvalidVersionException, InvalidKeyException {
try {
String keyString = messageBody.substring(Prefix.KEY_EXCHANGE.length());
byte[] keyBytes = Base64.decode(keyString);
byte[] keyBytes = Base64.decode(messageBody);
this.messageVersion = Conversions.highBitsToInt(keyBytes[0]);
this.supportedVersion = Conversions.lowBitsToInt(keyBytes[0]);
this.serialized = messageBody;
@ -95,7 +93,7 @@ public class KeyExchangeMessage {
throw new InvalidVersionException("Key exchange with version: " + messageVersion + " but we only support: " + Message.SUPPORTED_VERSION);
if (messageVersion >= 1)
keyBytes = Base64.decodeWithoutPadding(keyString);
keyBytes = Base64.decodeWithoutPadding(messageBody);
this.publicKey = new PublicKey(keyBytes, 1);

View File

@ -26,14 +26,11 @@ import org.thoughtcrime.securesms.database.keys.RemoteKeyRecord;
import org.thoughtcrime.securesms.database.keys.SessionRecord;
import org.thoughtcrime.securesms.protocol.Message;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.thoughtcrime.securesms.util.Conversions;
import java.util.LinkedList;
import java.util.List;
/**
* This class processes key exchange interactions.
*
@ -103,14 +100,12 @@ public class KeyExchangeProcessor {
message.getPublicKey().setId(initiateKeyId);
if (needsResponseFromUs()) {
List<Recipient> recipients = new LinkedList<Recipient>();
recipients.add(recipient);
localKeyRecord = KeyUtil.initializeRecordFor(recipient, context, masterSecret);
KeyExchangeMessage ourMessage = new KeyExchangeMessage(context, masterSecret, Math.min(Message.SUPPORTED_VERSION, message.getMaxVersion()), localKeyRecord, initiateKeyId);
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, ourMessage.serialize());
Log.w("KeyExchangeProcessor", "Responding with key exchange message fingerprint: " + ourMessage.getPublicKey().getFingerprint());
Log.w("KeyExchangeProcessor", "Which has a local key record fingerprint: " + localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprint());
MessageSender.send(context, masterSecret, new Recipients(recipients), threadId, ourMessage.serialize(), true);
MessageSender.send(context, masterSecret, textMessage, threadId);
}
remoteKeyRecord.setCurrentRemoteKey(message.getPublicKey());
@ -118,7 +113,7 @@ public class KeyExchangeProcessor {
remoteKeyRecord.save();
sessionRecord.setSessionId(localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprintBytes(),
remoteKeyRecord.getCurrentRemoteKey().getFingerprintBytes());
remoteKeyRecord.getCurrentRemoteKey().getFingerprintBytes());
sessionRecord.setIdentityKey(message.getIdentityKey());
sessionRecord.setSessionVersion(Math.min(Message.SUPPORTED_VERSION, message.getMaxVersion()));

View File

@ -1,70 +0,0 @@
/**
* 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.crypto;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.util.InvalidMessageException;
import java.lang.ref.SoftReference;
import java.util.LinkedHashMap;
public class MessageDisplayHelper {
private static final int MAX_CACHE_SIZE = 2000;
private static final LinkedHashMap<String,SoftReference<String>> decryptedBodyCache = new LinkedHashMap<String,SoftReference<String>>() {
@Override
protected boolean removeEldestEntry(Entry<String,SoftReference<String>> eldest) {
return this.size() > MAX_CACHE_SIZE;
}
};
private static String checkCacheForBody(String body) {
synchronized (decryptedBodyCache) {
if (decryptedBodyCache.containsKey(body)) {
String decryptedBody = decryptedBodyCache.get(body).get();
if (decryptedBody != null) {
return decryptedBody;
} else {
decryptedBodyCache.remove(body);
return null;
}
}
return null;
}
}
public static String getDecryptedMessageBody(MasterCipher bodyCipher, String body) throws InvalidMessageException {
if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT)) {
String cacheResult = checkCacheForBody(body);
if (cacheResult != null)
return cacheResult;
String decryptedBody = bodyCipher.decryptBody(body.substring(Prefix.SYMMETRIC_ENCRYPT.length()));
synchronized (decryptedBodyCache) {
decryptedBodyCache.put(body, new SoftReference<String>(decryptedBody));
}
return decryptedBody;
}
return body;
}
}

View File

@ -17,25 +17,31 @@
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.util.InvalidMessageException;
public class DatabaseFactory {
private static final int INTRODUCED_IDENTITIES_VERSION = 2;
private static final int INTRODUCED_INDEXES_VERSION = 3;
private static final int INTRODUCED_DATE_SENT_VERSION = 4;
private static final int INTRODUCED_DRAFTS_VERSION = 5;
private static final int DATABASE_VERSION = 5;
private static final int INTRODUCED_IDENTITIES_VERSION = 2;
private static final int INTRODUCED_INDEXES_VERSION = 3;
private static final int INTRODUCED_DATE_SENT_VERSION = 4;
private static final int INTRODUCED_DRAFTS_VERSION = 5;
private static final int INTRODUCED_NEW_TYPES_VERSION = 6;
private static final int DATABASE_VERSION = 6;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
private static DatabaseFactory instance;
private static EncryptingMmsDatabase encryptingMmsInstance;
private static EncryptingPartDatabase encryptingPartInstance;
private final DatabaseHelper databaseHelper;
@ -84,17 +90,6 @@ public class DatabaseFactory {
return getInstance(context).encryptingSms;
}
public static EncryptingMmsDatabase getEncryptingMmsDatabase(Context context, MasterSecret masterSecret) {
synchronized (lock) {
if (encryptingMmsInstance == null) {
DatabaseFactory factory = getInstance(context);
encryptingMmsInstance = new EncryptingMmsDatabase(context, factory.databaseHelper, masterSecret);
}
return encryptingMmsInstance;
}
}
public static PartDatabase getPartDatabase(Context context) {
return getInstance(context).part;
}
@ -142,6 +137,67 @@ public class DatabaseFactory {
instance = null;
}
public void onApplicationLevelUpgrade(MasterSecret masterSecret, int fromVersion,
DatabaseUpgradeActivity.DatabaseUpgradeListener listener)
{
if (fromVersion < DatabaseUpgradeActivity.NO_MORE_KEY_EXCHANGE_PREFIX_VERSION) {
String KEY_EXCHANGE = "?TextSecureKeyExchange";
String PROCESSED_KEY_EXCHANGE = "?TextSecureKeyExchangd";
String STALE_KEY_EXCHANGE = "?TextSecureKeyExchangs";
MasterCipher masterCipher = new MasterCipher(masterSecret);
int count = 0;
SQLiteDatabase db = databaseHelper.getWritableDatabase();
Cursor cursor = db.query("sms",
new String[] {"_id", "type", "body"},
"type & " + 0x80000000 + " != 0",
null, null, null, null);
if (cursor != null)
count = cursor.getCount();
db.beginTransaction();
while (cursor != null && cursor.moveToNext()) {
listener.setProgress(cursor.getPosition(), count);
try {
String body = masterCipher.decryptBody(cursor.getString(cursor.getColumnIndexOrThrow("body")));
long type = cursor.getLong(cursor.getColumnIndexOrThrow("type"));
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
if (body.startsWith(KEY_EXCHANGE)) {
body = body.substring(KEY_EXCHANGE.length());
body = masterCipher.encryptBody(body);
type |= 0x8000;
db.execSQL("UPDATE sms SET body = ?, type = ? WHERE _id = ?",
new String[] {body, type+"", id+""});
} else if (body.startsWith(PROCESSED_KEY_EXCHANGE)) {
body = body.substring(PROCESSED_KEY_EXCHANGE.length());
body = masterCipher.encryptBody(body);
type |= 0x2000;
db.execSQL("UPDATE sms SET body = ?, type = ? WHERE _id = ?",
new String[] {body, type+"", id+""});
} else if (body.startsWith(STALE_KEY_EXCHANGE)) {
body = body.substring(STALE_KEY_EXCHANGE.length());
body = masterCipher.encryptBody(body);
type |= 0x4000;
db.execSQL("UPDATE sms SET body = ?, type = ? WHERE _id = ?",
new String[] {body, type+"", id+""});
}
} catch (InvalidMessageException e) {
Log.w("DatabaseFactory", e);
}
}
db.setTransactionSuccessful();
db.endTransaction();
}
}
private static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, String name, CursorFactory factory, int version) {
@ -164,46 +220,193 @@ public class DatabaseFactory {
executeStatements(db, ThreadDatabase.CREATE_INDEXS);
executeStatements(db, MmsAddressDatabase.CREATE_INDEXS);
executeStatements(db, DraftDatabase.CREATE_INDEXS);
// db.execSQL(CanonicalAddress.CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < INTRODUCED_IDENTITIES_VERSION) {
db.execSQL(IdentityDatabase.CREATE_TABLE);
db.execSQL("CREATE TABLE identities (_id INTEGER PRIMARY KEY, key TEXT UNIQUE, name TEXT UNIQUE, mac TEXT);");
}
if (oldVersion < INTRODUCED_INDEXES_VERSION) {
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
executeStatements(db, PartDatabase.CREATE_INDEXS);
executeStatements(db, ThreadDatabase.CREATE_INDEXS);
executeStatements(db, MmsAddressDatabase.CREATE_INDEXS);
executeStatements(db, new String[] {
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON sms (thread_id);",
"CREATE INDEX IF NOT EXISTS sms_read_index ON sms (read);",
"CREATE INDEX IF NOT EXISTS sms_read_and_thread_id_index ON sms (read,thread_id);",
"CREATE INDEX IF NOT EXISTS sms_type_index ON sms (type);"
});
executeStatements(db, new String[] {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON mms (thread_id);",
"CREATE INDEX IF NOT EXISTS mms_read_index ON mms (read);",
"CREATE INDEX IF NOT EXISTS mms_read_and_thread_id_index ON mms (read,thread_id);",
"CREATE INDEX IF NOT EXISTS mms_message_box_index ON mms (msg_box);"
});
executeStatements(db, new String[] {
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON part (mid);"
});
executeStatements(db, new String[] {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON thread (recipient_ids);",
});
executeStatements(db, new String[] {
"CREATE INDEX IF NOT EXISTS mms_addresses_mms_id_index ON mms_addresses (mms_id);",
});
}
if (oldVersion < INTRODUCED_DATE_SENT_VERSION) {
db.beginTransaction();
db.execSQL("ALTER TABLE " + SmsDatabase.TABLE_NAME +
" ADD COLUMN " + SmsDatabase.DATE_SENT + " INTEGER;");
db.execSQL("UPDATE " + SmsDatabase.TABLE_NAME +
" SET " + SmsDatabase.DATE_SENT + " = " + SmsDatabase.DATE_RECEIVED + ";");
db.execSQL("ALTER TABLE sms ADD COLUMN date_sent INTEGER;");
db.execSQL("UPDATE sms SET date_sent = date;");
db.execSQL("ALTER TABLE " + MmsDatabase.TABLE_NAME +
" ADD COLUMN " + MmsDatabase.DATE_RECEIVED + " INTEGER;");
db.execSQL("UPDATE " + MmsDatabase.TABLE_NAME +
" SET " + MmsDatabase.DATE_RECEIVED + " = " + MmsDatabase.DATE_SENT + ";");
db.execSQL("ALTER TABLE mms ADD COLUMN date_received INTEGER;");
db.execSQL("UPDATE mms SET date_received = date;");
db.setTransactionSuccessful();
db.endTransaction();
}
if (oldVersion < INTRODUCED_DRAFTS_VERSION) {
db.beginTransaction();
db.execSQL(DraftDatabase.CREATE_TABLE);
executeStatements(db, DraftDatabase.CREATE_INDEXS);
db.execSQL("CREATE TABLE drafts (_id INTEGER PRIMARY KEY, thread_id INTEGER, type TEXT, value TEXT);");
executeStatements(db, new String[] {
"CREATE INDEX IF NOT EXISTS draft_thread_index ON drafts (thread_id);",
});
db.setTransactionSuccessful();
db.endTransaction();
}
if (oldVersion < INTRODUCED_NEW_TYPES_VERSION) {
String KEY_EXCHANGE = "?TextSecureKeyExchange";
String SYMMETRIC_ENCRYPT = "?TextSecureLocalEncrypt";
String ASYMMETRIC_ENCRYPT = "?TextSecureAsymmetricEncrypt";
String ASYMMETRIC_LOCAL_ENCRYPT = "?TextSecureAsymmetricLocalEncrypt";
String PROCESSED_KEY_EXCHANGE = "?TextSecureKeyExchangd";
String STALE_KEY_EXCHANGE = "?TextSecureKeyExchangs";
db.beginTransaction();
// SMS Updates
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {20L+"", 1L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {21L+"", 43L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {22L+"", 4L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {23L+"", 2L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {24L+"", 5L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {(21L | 0x800000L)+"", 42L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {(23L | 0x800000L)+"", 44L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {(20L | 0x800000L)+"", 45L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {(20L | 0x800000L | 0x10000000L)+"", 46L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {(20L | 0x800000L | 0x20000000L)+"", 47L+""});
db.execSQL("UPDATE sms SET type = ? WHERE type = ?", new String[] {(20L | 0x800000L | 0x08000000L)+"", 48L+""});
Cursor cursor = db.query("sms", null,"body LIKE ?", new String[] {SYMMETRIC_ENCRYPT + "%"},
null, null, null);
updateSmsBodyAndType(db, cursor, SYMMETRIC_ENCRYPT, 0x80000000L);
cursor = db.query("sms", null,"body LIKE ?", new String[] {ASYMMETRIC_LOCAL_ENCRYPT + "%"},
null, null, null);
updateSmsBodyAndType(db, cursor, ASYMMETRIC_LOCAL_ENCRYPT, 0x40000000L);
cursor = db.query("sms", null,"body LIKE ?", new String[] {ASYMMETRIC_ENCRYPT + "%"},
null, null, null);
updateSmsBodyAndType(db, cursor, ASYMMETRIC_ENCRYPT, 0L);
cursor = db.query("sms", null,"body LIKE ?", new String[] {KEY_EXCHANGE + "%"},
null, null, null);
updateSmsBodyAndType(db, cursor, KEY_EXCHANGE, 0x8000L);
cursor = db.query("sms", null,"body LIKE ?", new String[] {PROCESSED_KEY_EXCHANGE + "%"},
null, null, null);
updateSmsBodyAndType(db, cursor, PROCESSED_KEY_EXCHANGE, 0x8000L | 0x2000L);
cursor = db.query("sms", null,"body LIKE ?", new String[] {STALE_KEY_EXCHANGE + "%"},
null, null, null);
updateSmsBodyAndType(db, cursor, STALE_KEY_EXCHANGE, 0x8000L | 0x4000L);
// MMS Updates
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {20L+"", 1+""});
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {23L+"", 2+""});
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {21L+"", 4+""});
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {24L+"", 12+""});
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(21L | 0x800000L) +"", 5+""});
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(23L | 0x800000L) +"", 6+""});
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x800000L | 0x20000000L) +"", 7+""});
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x800000L) +"", 8+""});
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x800000L | 0x08000000L) +"", 9+""});
db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x800000L | 0x10000000L) +"", 10+""});
// Thread Updates
db.execSQL("ALTER TABLE thread ADD COLUMN snippet_type INTEGER;");
cursor = db.query("thread", null,"snippet LIKE ?",
new String[] {SYMMETRIC_ENCRYPT + "%"}, null, null, null);
updateThreadSnippetAndType(db, cursor, SYMMETRIC_ENCRYPT, 0x80000000L);
cursor = db.query("thread", null,"snippet LIKE ?",
new String[] {KEY_EXCHANGE + "%"}, null, null, null);
updateThreadSnippetAndType(db, cursor, KEY_EXCHANGE, 0x8000L);
cursor = db.query("thread", null,"snippet LIKE ?",
new String[] {STALE_KEY_EXCHANGE + "%"}, null, null, null);
updateThreadSnippetAndType(db, cursor, STALE_KEY_EXCHANGE, 0x8000L | 0x4000L);
cursor = db.query("thread", null,"snippet LIKE ?",
new String[] {PROCESSED_KEY_EXCHANGE + "%"}, null, null, null);
updateThreadSnippetAndType(db, cursor, KEY_EXCHANGE, 0x8000L | 0x2000L);
db.setTransactionSuccessful();
db.endTransaction();
}
}
private void updateSmsBodyAndType(SQLiteDatabase db, Cursor cursor, String prefix, long typeMask)
{
while (cursor != null && cursor.moveToNext()) {
String body = cursor.getString(cursor.getColumnIndexOrThrow("body"));
long type = cursor.getLong(cursor.getColumnIndexOrThrow("type"));
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
if (body.startsWith(prefix)) {
body = body.substring(prefix.length());
type |= typeMask;
db.execSQL("UPDATE sms SET type = ?, body = ? WHERE _id = ?",
new String[]{type+"", body, id+""});
}
}
if (cursor != null)
cursor.close();
}
private void updateThreadSnippetAndType(SQLiteDatabase db, Cursor cursor, String prefix, long typeMask)
{
while (cursor != null && cursor.moveToNext()) {
String snippet = cursor.getString(cursor.getColumnIndexOrThrow("snippet"));
long type = cursor.getLong(cursor.getColumnIndexOrThrow("snippet_type"));
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
if (snippet.startsWith(prefix)) {
snippet = snippet.substring(prefix.length());
type |= typeMask;
db.execSQL("UPDATE thread SET snippet_type = ?, snippet = ? WHERE _id = ?",
new String[]{type+"", snippet, id+""});
}
}
if (cursor != null)
cursor.close();
}
private void executeStatements(SQLiteDatabase db, String[] statements) {

View File

@ -1,38 +0,0 @@
/**
* 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.database;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class EncryptingMmsDatabase extends MmsDatabase {
private final MasterSecret masterSecret;
public EncryptingMmsDatabase(Context context, SQLiteOpenHelper databaseHelper, MasterSecret masterSecret) {
super(context, databaseHelper);
this.masterSecret = masterSecret;
}
@Override
protected PartDatabase getPartDatabase() {
return DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
}
}

View File

@ -17,62 +17,175 @@
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.sms.TextMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.InvalidMessageException;
import java.lang.ref.SoftReference;
import java.util.LinkedHashMap;
import java.util.List;
public class EncryptingSmsDatabase extends SmsDatabase {
private final PlaintextCache plaintextCache = new PlaintextCache();
public EncryptingSmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
private String getAsymmetricEncryptedBody(AsymmetricMasterSecret masterSecret, String body) {
AsymmetricMasterCipher bodyCipher = new AsymmetricMasterCipher(masterSecret);
return Prefix.ASYMMETRIC_LOCAL_ENCRYPT + bodyCipher.encryptBody(body);
return bodyCipher.encryptBody(body);
}
private String getEncryptedBody(MasterSecret masterSecret, String body) {
MasterCipher bodyCipher = new MasterCipher(masterSecret);
return Prefix.SYMMETRIC_ENCRYPT + bodyCipher.encryptBody(body);
String ciphertext = bodyCipher.encryptBody(body);
plaintextCache.put(ciphertext, body);
return ciphertext;
}
private long insertMessageSent(MasterSecret masterSecret, String address, long threadId, String body, long date, int type) {
String encryptedBody = getEncryptedBody(masterSecret, body);
return insertMessageSent(address, threadId, encryptedBody, date, type);
public List<Long> insertMessageOutbox(MasterSecret masterSecret, long threadId,
OutgoingTextMessage message)
{
long type = Types.BASE_OUTBOX_TYPE;
message = message.withBody(getEncryptedBody(masterSecret, message.getMessageBody()));
type |= Types.ENCRYPTION_SYMMETRIC_BIT;
return insertMessageOutbox(threadId, message, type);
}
public void updateSecureMessageBody(MasterSecret masterSecret, long messageId, String body) {
String encryptedBody = getEncryptedBody(masterSecret, body);
updateMessageBodyAndType(messageId, encryptedBody, Types.SECURE_RECEIVED_TYPE);
public Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret,
IncomingTextMessage message)
{
long type = Types.BASE_INBOX_TYPE;
if (!message.isSecureMessage()) {
type |= Types.ENCRYPTION_SYMMETRIC_BIT;
message = message.withMessageBody(getEncryptedBody(masterSecret, message.getMessageBody()));
}
return insertMessageInbox(message, type);
}
public Pair<Long, Long> insertMessageInbox(AsymmetricMasterSecret masterSecret,
IncomingTextMessage message)
{
long type = Types.BASE_INBOX_TYPE;
if (message.isSecureMessage()) {
type |= Types.ENCRYPTION_REMOTE_BIT;
} else {
message = message.withMessageBody(getAsymmetricEncryptedBody(masterSecret, message.getMessageBody()));
type |= Types.ENCRYPTION_ASYMMETRIC_BIT;
}
return insertMessageInbox(message, type);
}
public void updateMessageBody(MasterSecret masterSecret, long messageId, String body) {
String encryptedBody = getEncryptedBody(masterSecret, body);
updateMessageBodyAndType(messageId, encryptedBody, Types.INBOX_TYPE);
updateMessageBodyAndType(messageId, encryptedBody, Types.ENCRYPTION_MASK,
Types.ENCRYPTION_SYMMETRIC_BIT);
}
public long insertMessageSent(MasterSecret masterSecret, String address, long threadId, String body, long date) {
return insertMessageSent(masterSecret, address, threadId, body, date, Types.ENCRYPTED_OUTBOX_TYPE);
public Reader getOutgoingMessages(MasterSecret masterSecret) {
Cursor cursor = super.getOutgoingMessages();
return new DecryptingReader(masterSecret, cursor);
}
public long insertSecureMessageSent(MasterSecret masterSecret, String address, long threadId, String body, long date) {
return insertMessageSent(masterSecret, address, threadId, body, date, Types.ENCRYPTING_TYPE);
public Reader getMessage(MasterSecret masterSecret, long messageId) {
Cursor cursor = super.getMessage(messageId);
return new DecryptingReader(masterSecret, cursor);
}
public long insertMessageReceived(MasterSecret masterSecret, TextMessage message, String body) {
String encryptedBody = getEncryptedBody(masterSecret, body);
return insertMessageReceived(message, encryptedBody);
public Reader getDecryptInProgressMessages(MasterSecret masterSecret) {
Cursor cursor = super.getDecryptInProgressMessages();
return new DecryptingReader(masterSecret, cursor);
}
public long insertMessageReceived(AsymmetricMasterSecret masterSecret, TextMessage message, String body) {
String encryptedBody = getAsymmetricEncryptedBody(masterSecret, body);
return insertSecureMessageReceived(message, encryptedBody);
public Reader readerFor(MasterSecret masterSecret, Cursor cursor) {
return new DecryptingReader(masterSecret, cursor);
}
public class DecryptingReader extends SmsDatabase.Reader {
private final MasterCipher masterCipher;
public DecryptingReader(MasterSecret masterSecret, Cursor cursor) {
super(cursor);
this.masterCipher = new MasterCipher(masterSecret);
}
@Override
protected String getBody(Cursor cursor) {
long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE));
String ciphertext = super.getBody(cursor);
try {
if (SmsDatabase.Types.isSymmetricEncryption(type)) {
String plaintext = plaintextCache.get(ciphertext);
if (plaintext != null)
return plaintext;
plaintext = masterCipher.decryptBody(ciphertext);
plaintextCache.put(ciphertext, plaintext);
return plaintext;
} else {
return ciphertext;
}
} catch (InvalidMessageException e) {
Log.w("EncryptingSmsDatabase", e);
return "Error decrypting message.";
}
}
}
private static class PlaintextCache {
private static final int MAX_CACHE_SIZE = 2000;
private static final LinkedHashMap<String,SoftReference<String>> decryptedBodyCache
= new LinkedHashMap<String,SoftReference<String>>()
{
@Override
protected boolean removeEldestEntry(Entry<String,SoftReference<String>> eldest) {
return this.size() > MAX_CACHE_SIZE;
}
};
public void put(String ciphertext, String plaintext) {
decryptedBodyCache.put(ciphertext, new SoftReference<String>(plaintext));
}
public String get(String ciphertext) {
synchronized (decryptedBodyCache) {
SoftReference<String> plaintextReference = decryptedBodyCache.get(ciphertext);
if (plaintextReference != null) {
String plaintext = plaintextReference.get();
if (plaintext != null) {
return plaintext;
} else {
decryptedBodyCache.remove(ciphertext);
return null;
}
}
return null;
}
}
}
}

View File

@ -25,12 +25,23 @@ import android.net.Uri;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.mms.SlideDeck;
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 org.thoughtcrime.securesms.util.Trimmer;
import org.thoughtcrime.securesms.util.Util;
import java.io.UnsupportedEncodingException;
import java.util.HashSet;
import java.util.Set;
import ws.com.google.android.mms.InvalidHeaderValueException;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.CharacterSets;
@ -42,19 +53,17 @@ import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.RetrieveConf;
import ws.com.google.android.mms.pdu.SendReq;
import java.io.UnsupportedEncodingException;
import java.util.HashSet;
import java.util.Set;
// XXXX Clean up MMS efficiency:
// 1) We need to be careful about how much memory we're using for parts. SoftRefereences.
// 2) How many queries do we make? calling getMediaMessageForId() from within an existing query
// seems wasteful.
public class MmsDatabase extends Database {
public class MmsDatabase extends Database implements MmsSmsColumns {
public static final String TABLE_NAME = "mms";
public static final String ID = "_id";
private static final String THREAD_ID = "thread_id";
public static final String DATE_SENT = "date";
public static final String DATE_RECEIVED = "date_received";
static final String DATE_SENT = "date";
static final String DATE_RECEIVED = "date_received";
public static final String MESSAGE_BOX = "msg_box";
private static final String READ = "read";
private static final String MESSAGE_ID = "m_id";
private static final String SUBJECT = "sub";
private static final String SUBJECT_CHARSET = "sub_cs";
@ -99,6 +108,16 @@ public class MmsDatabase extends Database {
"CREATE INDEX IF NOT EXISTS mms_message_box_index ON " + TABLE_NAME + " (" + MESSAGE_BOX + ");"
};
private static final String[] MMS_PROJECTION = new String[] {
ID, THREAD_ID, DATE_SENT + " * 1000 AS " + NORMALIZED_DATE_SENT,
DATE_RECEIVED + " * 1000 AS " + NORMALIZED_DATE_RECEIVED,
MESSAGE_BOX, READ, MESSAGE_ID, SUBJECT, SUBJECT_CHARSET, CONTENT_TYPE,
CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION,
MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS,
RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT,
DELIVERY_TIME, DELIVERY_REPORT
};
public MmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
@ -150,19 +169,32 @@ public class MmsDatabase extends Database {
}
}
public String getMessageRecipient(long messageId) {
public Recipient getMessageRecipient(long messageId) {
try {
PduHeaders headers = new PduHeaders();
MmsAddressDatabase database = DatabaseFactory.getMmsAddressDatabase(context);
database.getAddressesForId(messageId, headers);
EncodedStringValue encodedFrom = headers.getEncodedStringValue(PduHeaders.FROM);
if (encodedFrom != null)
return new String(encodedFrom.getTextString(), CharacterSets.MIMENAME_ISO_8859_1);
else
return context.getString(R.string.MmsDatabase_anonymous);
if (encodedFrom != null) {
String address = new String(encodedFrom.getTextString(), CharacterSets.MIMENAME_ISO_8859_1);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, address, false);
if (recipients == null || recipients.isEmpty()) {
return new Recipient("Unknown", "Unknown", null,
ContactPhotoFactory.getDefaultContactPhoto(context));
}
return recipients.getPrimaryRecipient();
} else {
return new Recipient("Unknown", "Unknown", null,
ContactPhotoFactory.getDefaultContactPhoto(context));
}
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
} catch (RecipientFormattingException e) {
return new Recipient("Unknown", "Unknown", null,
ContactPhotoFactory.getDefaultContactPhoto(context));
}
}
@ -174,12 +206,15 @@ public class MmsDatabase extends Database {
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
}
public void markAsSentFailed(long messageId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_SENT_FAILED);
private void updateMailboxBitmask(long id, long maskOff, long maskOn) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.execSQL("UPDATE " + TABLE_NAME +
" SET " + MESSAGE_BOX + " = (" + MESSAGE_BOX + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" +
" WHERE " + ID + " = ?", new String[] {id+""});
}
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{messageId+""});
public void markAsSentFailed(long messageId) {
updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_FAILED_TYPE);
notifyConversationListeners(getThreadIdForMessage(messageId));
}
@ -188,20 +223,9 @@ public class MmsDatabase extends Database {
ContentValues contentValues = new ContentValues();
contentValues.put(RESPONSE_STATUS, status);
contentValues.put(MESSAGE_ID, new String(mmsId));
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_SENT);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
notifyConversationListeners(getThreadIdForMessage(messageId));
}
public void markAsSecureSent(long messageId, byte[] mmsId, long status) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(RESPONSE_STATUS, status);
contentValues.put(MESSAGE_ID, new String(mmsId));
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_SECURE_SENT);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE);
notifyConversationListeners(getThreadIdForMessage(messageId));
}
@ -215,20 +239,12 @@ public class MmsDatabase extends Database {
}
public void markAsNoSession(long messageId, long threadId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_NO_SESSION_INBOX);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
updateMailboxBitmask(messageId, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_NO_SESSION_BIT);
notifyConversationListeners(threadId);
}
public void markAsDecryptFailed(long messageId, long threadId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_DECRYPT_FAILED_INBOX);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
updateMailboxBitmask(messageId, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_FAILED_BIT);
notifyConversationListeners(threadId);
}
@ -245,30 +261,35 @@ public class MmsDatabase extends Database {
return new NotificationInd(headers);
}
public MultimediaMessagePdu getMediaMessage(long messageId) throws MmsException {
public MultimediaMessagePdu getMediaMessage(long messageId)
throws MmsException
{
PduHeaders headers = getHeadersForId(messageId);
PartDatabase partDatabase = getPartDatabase();
PartDatabase partDatabase = getPartDatabase(null);
PduBody body = partDatabase.getParts(messageId, false);
return new MultimediaMessagePdu(headers, body);
}
public SendReq getSendRequest(long messageId) throws MmsException {
public SendReq getSendRequest(MasterSecret masterSecret, long messageId) throws MmsException {
PduHeaders headers = getHeadersForId(messageId);
PartDatabase partDatabase = getPartDatabase();
PartDatabase partDatabase = getPartDatabase(masterSecret);
PduBody body = partDatabase.getParts(messageId, true);
return new SendReq(headers, body, messageId, headers.getMessageBox());
}
public SendReq[] getOutgoingMessages() throws MmsException {
public SendReq[] getOutgoingMessages(MasterSecret masterSecret) throws MmsException {
MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context);
PartDatabase parts = getPartDatabase();
PartDatabase parts = getPartDatabase(masterSecret);
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, MESSAGE_BOX + " = ? OR " + MESSAGE_BOX + " = ?", new String[] {Types.MESSAGE_BOX_OUTBOX+"", Types.MESSAGE_BOX_SECURE_OUTBOX+""}, null, null, null);
cursor = database.query(TABLE_NAME, MMS_PROJECTION,
MESSAGE_BOX + " & " + Types.BASE_TYPE_MASK + " = ?",
new String[] {Types.BASE_OUTBOX_TYPE+""},
null, null, null);
if (cursor == null || cursor.getCount() == 0)
return new SendReq[0];
@ -292,36 +313,49 @@ public class MmsDatabase extends Database {
}
}
private long insertMessageReceived(MultimediaMessagePdu retrieved, String contentLocation, long threadId, long mailbox) throws MmsException {
private long insertMessageInbox(MasterSecret masterSecret, MultimediaMessagePdu retrieved,
String contentLocation, long threadId, long mailbox)
throws MmsException
{
PduHeaders headers = retrieved.getPduHeaders();
ContentValues contentValues = getContentValuesFromHeader(headers);
contentValues.put(MESSAGE_BOX, mailbox);
contentValues.put(THREAD_ID, threadId);
contentValues.put(CONTENT_LOCATION, contentLocation);
contentValues.put(STATUS, Types.DOWNLOAD_INITIALIZED);
contentValues.put(STATUS, Status.DOWNLOAD_INITIALIZED);
contentValues.put(DATE_RECEIVED, System.currentTimeMillis() / 1000);
if (!contentValues.containsKey(DATE_SENT))
contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED));
long messageId = insertMediaMessage(retrieved, contentValues);
return messageId;
return insertMediaMessage(masterSecret, retrieved, contentValues);
}
public long insertMessageReceived(RetrieveConf retrieved, String contentLocation, long threadId) throws MmsException {
return insertMessageReceived(retrieved, contentLocation, threadId, Types.MESSAGE_BOX_INBOX);
public long insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
String contentLocation, long threadId)
throws MmsException
{
return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId,
Types.BASE_INBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT);
}
public long insertSecureMessageReceived(RetrieveConf retrieved, String contentLocation, long threadId) throws MmsException {
return insertMessageReceived(retrieved, contentLocation, threadId, Types.MESSAGE_BOX_DECRYPTING_INBOX);
public long insertSecureMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
String contentLocation, long threadId)
throws MmsException
{
return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId,
Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_REMOTE_BIT);
}
public long insertSecureDecryptedMessageReceived(MultimediaMessagePdu retrieved, long threadId) throws MmsException {
return insertMessageReceived(retrieved, "", threadId, Types.MESSAGE_BOX_SECURE_INBOX);
public long insertSecureDecryptedMessageInbox(MasterSecret masterSecret, MultimediaMessagePdu retrieved, long threadId)
throws MmsException
{
return insertMessageInbox(masterSecret, retrieved, "", threadId,
Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_SYMMETRIC_BIT);
}
public long insertMessageReceived(NotificationInd notification) {
public long insertMessageInbox(NotificationInd notification) {
try {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
PduHeaders headers = notification.getPduHeaders();
@ -331,9 +365,9 @@ public class MmsDatabase extends Database {
Log.w("MmsDatabse", "Message received type: " + headers.getOctet(PduHeaders.MESSAGE_TYPE));
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_INBOX);
contentValues.put(MESSAGE_BOX, Types.BASE_INBOX_TYPE);
contentValues.put(THREAD_ID, threadId);
contentValues.put(STATUS, Types.DOWNLOAD_INITIALIZED);
contentValues.put(STATUS, Status.DOWNLOAD_INITIALIZED);
contentValues.put(DATE_RECEIVED, System.currentTimeMillis() / 1000);
if (!contentValues.containsKey(DATE_SENT))
@ -354,28 +388,38 @@ public class MmsDatabase extends Database {
}
}
public long insertMessageSent(SendReq sendRequest, long threadId, boolean isSecure) throws MmsException {
public long insertMessageOutbox(MasterSecret masterSecret, SendReq sendRequest,
long threadId, boolean isSecure)
throws MmsException
{
long type = Types.BASE_OUTBOX_TYPE;
PduHeaders headers = sendRequest.getPduHeaders();
ContentValues contentValues = getContentValuesFromHeader(headers);
if (!isSecure) contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_OUTBOX);
else contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_SECURE_OUTBOX);
if (isSecure) {
type |= Types.SECURE_MESSAGE_BIT;
}
contentValues.put(MESSAGE_BOX, type);
contentValues.put(THREAD_ID, threadId);
contentValues.put(READ, 1);
contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT));
long messageId = insertMediaMessage(sendRequest, contentValues);
long messageId = insertMediaMessage(masterSecret, sendRequest, contentValues);
Trimmer.trimThread(context, threadId);
return messageId;
}
private long insertMediaMessage(MultimediaMessagePdu message, ContentValues contentValues) throws MmsException {
private long insertMediaMessage(MasterSecret masterSecret,
MultimediaMessagePdu message,
ContentValues contentValues)
throws MmsException
{
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, null, contentValues);
PduBody body = message.getBody();
PartDatabase partsDatabase = getPartDatabase();
PartDatabase partsDatabase = getPartDatabase(masterSecret);
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
addressDatabase.insertAddressesForId(messageId, message.getPduHeaders());
@ -437,9 +481,9 @@ public class MmsDatabase extends Database {
try {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String where = THREAD_ID + " = ? AND (CASE " + MESSAGE_BOX;
String where = THREAD_ID + " = ? AND (CASE (" + MESSAGE_BOX + " & " + Types.BASE_TYPE_MASK + ") ";
for (int outgoingType : Types.OUTGOING_MAILBOX_TYPES) {
for (long outgoingType : Types.OUTGOING_MESSAGE_TYPES) {
where += " WHEN " + outgoingType + " THEN " + DATE_SENT + " < " + date;
}
@ -488,7 +532,8 @@ public class MmsDatabase extends Database {
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, ID_WHERE, new String[] {messageId+""}, null, null, null);
cursor = database.query(TABLE_NAME, MMS_PROJECTION, ID_WHERE, new String[] {messageId+""},
null, null, null);
if (cursor == null || !cursor.moveToFirst())
throw new MmsException("No headers available at ID: " + messageId);
@ -528,12 +573,14 @@ public class MmsDatabase extends Database {
phb.addOctet(REPORT_ALLOWED, PduHeaders.REPORT_ALLOWED);
phb.addOctet(RETRIEVE_STATUS, PduHeaders.RETRIEVE_STATUS);
phb.addOctet(STATUS, PduHeaders.STATUS);
phb.addLong(DATE_SENT, PduHeaders.DATE);
phb.addLong(NORMALIZED_DATE_SENT, PduHeaders.DATE);
phb.addLong(DELIVERY_TIME, PduHeaders.DELIVERY_TIME);
phb.addLong(EXPIRY, PduHeaders.EXPIRY);
phb.addLong(MESSAGE_SIZE, PduHeaders.MESSAGE_SIZE);
return phb.getHeaders();
headers.setLongInteger(headers.getLongInteger(PduHeaders.DATE) / 1000L, PduHeaders.DATE);
return headers;
}
private ContentValues getContentValuesFromHeader(PduHeaders headers) {
@ -567,70 +614,36 @@ public class MmsDatabase extends Database {
}
protected PartDatabase getPartDatabase() {
return DatabaseFactory.getPartDatabase(context);
protected PartDatabase getPartDatabase(MasterSecret masterSecret) {
if (masterSecret == null)
return DatabaseFactory.getPartDatabase(context);
else
return DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
}
public static class Types {
public static final String MMS_ERROR_TYPE = "err_type";
public static final int MESSAGE_BOX_INBOX = 1;
public static final int MESSAGE_BOX_SENT = 2;
public static final int MESSAGE_BOX_DRAFTS = 3;
public static final int MESSAGE_BOX_OUTBOX = 4;
public static final int MESSAGE_BOX_SECURE_OUTBOX = 5;
public static final int MESSAGE_BOX_SECURE_SENT = 6;
public static final int MESSAGE_BOX_DECRYPTING_INBOX = 7;
public static final int MESSAGE_BOX_SECURE_INBOX = 8;
public static final int MESSAGE_BOX_NO_SESSION_INBOX = 9;
public static final int MESSAGE_BOX_DECRYPT_FAILED_INBOX = 10;
public static final int MESSAGE_BOX_SENT_FAILED = 12;
public Reader readerFor(MasterSecret masterSecret, Cursor cursor) {
return new Reader(masterSecret, cursor);
}
public static class Status {
public static final int DOWNLOAD_INITIALIZED = 1;
public static final int DOWNLOAD_NO_CONNECTIVITY = 2;
public static final int DOWNLOAD_CONNECTING = 3;
public static final int DOWNLOAD_SOFT_FAILURE = 4;
public static final int DOWNLOAD_HARD_FAILURE = 5;
public static final int[] OUTGOING_MAILBOX_TYPES = {Types.MESSAGE_BOX_OUTBOX,
Types.MESSAGE_BOX_SENT,
Types.MESSAGE_BOX_SECURE_OUTBOX,
Types.MESSAGE_BOX_SENT_FAILED,
Types.MESSAGE_BOX_SECURE_SENT};
public static boolean isSecureMmsBox(long mailbox) {
return mailbox == Types.MESSAGE_BOX_SECURE_OUTBOX || mailbox == Types.MESSAGE_BOX_SECURE_SENT || mailbox == Types.MESSAGE_BOX_SECURE_INBOX;
}
public static boolean isOutgoingMmsBox(long mailbox) {
for (int outgoingMailboxType : OUTGOING_MAILBOX_TYPES) {
if (mailbox == outgoingMailboxType)
return true;
}
return false;
}
public static boolean isPendingMmsBox(long mailbox) {
return mailbox == Types.MESSAGE_BOX_OUTBOX || mailbox == MESSAGE_BOX_SECURE_OUTBOX;
}
public static boolean isFailedMmsBox(long mailbox) {
return mailbox == Types.MESSAGE_BOX_SENT_FAILED;
}
public static boolean isDisplayDownloadButton(int status) {
return status == DOWNLOAD_INITIALIZED || status == DOWNLOAD_NO_CONNECTIVITY || status == DOWNLOAD_SOFT_FAILURE;
return
status == DOWNLOAD_INITIALIZED ||
status == DOWNLOAD_NO_CONNECTIVITY ||
status == DOWNLOAD_SOFT_FAILURE;
}
public static String getLabelForStatus(Context context, int status) {
Log.w("MmsDatabase", "Getting label for status: " + status);
switch (status) {
case DOWNLOAD_CONNECTING: return context.getString(R.string.MmsDatabase_connecting_to_mms_server);
case DOWNLOAD_INITIALIZED: return context.getString(R.string.MmsDatabase_downloading_mms);
case DOWNLOAD_HARD_FAILURE: return context.getString(R.string.MmsDatabase_mms_download_failed);
case DOWNLOAD_CONNECTING: return context.getString(R.string.MmsDatabase_connecting_to_mms_server);
case DOWNLOAD_INITIALIZED: return context.getString(R.string.MmsDatabase_downloading_mms);
case DOWNLOAD_HARD_FAILURE: return context.getString(R.string.MmsDatabase_mms_download_failed);
}
return context.getString(R.string.MmsDatabase_downloading);
@ -640,4 +653,112 @@ public class MmsDatabase extends Database {
return status == DOWNLOAD_HARD_FAILURE;
}
}
public class Reader {
private final Cursor cursor;
private final MasterSecret masterSecret;
public Reader(MasterSecret masterSecret, Cursor cursor) {
this.cursor = cursor;
this.masterSecret = masterSecret;
}
public MessageRecord getNext() {
if (cursor == null || !cursor.moveToNext())
return null;
return getCurrent();
}
public MessageRecord getCurrent() {
long mmsType = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_TYPE));
if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
return getNotificationMmsMessageRecord(cursor);
} else {
return getMediaMmsMessageRecord(cursor);
}
}
private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID));
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_SENT));
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED));
long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID));
Recipient recipient = getMessageRecipient(id);
MessageRecord.GroupData groupData = null;
SlideDeck slideDeck;
try {
MultimediaMessagePdu pdu = getMediaMessage(id);
slideDeck = getSlideDeck(masterSecret, pdu);
if (cursor.getColumnIndex(MmsSmsDatabase.GROUP_SIZE) != -1) {
int groupSize = pdu.getTo().length;
int groupSent = MmsDatabase.Types.isFailedMessageType(box) ? 0 : groupSize;
int groupSendFailed = groupSize - groupSent;
if (groupSize <= 1) {
groupSize = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.GROUP_SIZE));
groupSent = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.MMS_GROUP_SENT_COUNT));
groupSendFailed = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.MMS_GROUP_SEND_FAILED_COUNT));
}
Log.w("ConversationAdapter", "MMS GroupSize: " + groupSize + " , GroupSent: " + groupSent + " , GroupSendFailed: " + groupSendFailed);
groupData = new MessageRecord.GroupData(groupSize, groupSent, groupSendFailed);
}
} catch (MmsException me) {
Log.w("ConversationAdapter", me);
slideDeck = null;
}
return new MediaMmsMessageRecord(context, id, new Recipients(recipient), recipient,
dateSent, dateReceived, threadId,
slideDeck, box, groupData);
}
protected SlideDeck getSlideDeck(MasterSecret masterSecret, MultimediaMessagePdu pdu) {
if (masterSecret == null)
return null;
return new SlideDeck(context, masterSecret, pdu.getBody());
}
private NotificationMmsMessageRecord getNotificationMmsMessageRecord(Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID));
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_SENT));
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID));
long mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
Recipient recipient = getMessageRecipient(id);
NotificationInd notification;
try {
notification = getNotificationMessage(id);
} catch (MmsException me) {
Log.w("ConversationAdapter", me);
notification = new NotificationInd(new PduHeaders());
}
return new NotificationMmsMessageRecord(context, id, new Recipients(recipient), recipient,
dateSent, dateReceived, threadId,
notification.getContentLocation(),
notification.getMessageSize(),
notification.getExpiry(),
notification.getStatus(),
notification.getTransactionId(),
mailbox);
}
public void close() {
cursor.close();
}
}
}

View File

@ -0,0 +1,170 @@
package org.thoughtcrime.securesms.database;
public interface MmsSmsColumns {
public static final String ID = "_id";
public static final String NORMALIZED_DATE_SENT = "date_sent";
public static final String NORMALIZED_DATE_RECEIVED = "date_received";
public static final String THREAD_ID = "thread_id";
public static final String READ = "read";
public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF;
// Base Types
protected static final long BASE_TYPE_MASK = 0xFF;
protected static final long BASE_INBOX_TYPE = 20;
protected static final long BASE_OUTBOX_TYPE = 21;
protected static final long BASE_SENDING_TYPE = 22;
protected static final long BASE_SENT_TYPE = 23;
protected static final long BASE_SENT_FAILED_TYPE = 24;
protected static final long[] OUTGOING_MESSAGE_TYPES = {BASE_OUTBOX_TYPE, BASE_SENT_TYPE,
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE};
// Key Exchange Information
protected static final long KEY_EXCHANGE_BIT = 0x8000;
protected static final long KEY_EXCHANGE_STALE_BIT = 0x4000;
protected static final long KEY_EXCHANGE_PROCESSED_BIT = 0x2000;
// Secure Message Information
protected static final long SECURE_MESSAGE_BIT = 0x800000;
// Encrypted Storage Information
protected static final long ENCRYPTION_MASK = 0xFF000000;
protected static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000;
protected static final long ENCRYPTION_ASYMMETRIC_BIT = 0x40000000;
protected static final long ENCRYPTION_REMOTE_BIT = 0x20000000;
protected static final long ENCRYPTION_REMOTE_FAILED_BIT = 0x10000000;
protected static final long ENCRYPTION_REMOTE_NO_SESSION_BIT = 0x08000000;
public static boolean isFailedMessageType(long type) {
return (type & BASE_TYPE_MASK) == BASE_SENT_FAILED_TYPE;
}
public static boolean isOutgoingMessageType(long type) {
for (long outgoingType : OUTGOING_MESSAGE_TYPES) {
if ((type & BASE_TYPE_MASK) == outgoingType)
return true;
}
return false;
}
public static boolean isPendingMessageType(long type) {
return
(type & BASE_TYPE_MASK) == BASE_OUTBOX_TYPE ||
(type & BASE_TYPE_MASK) == BASE_SENDING_TYPE;
}
public static boolean isSecureType(long type) {
return (type & SECURE_MESSAGE_BIT) != 0;
}
public static boolean isKeyExchangeType(long type) {
return (type & KEY_EXCHANGE_BIT) != 0;
}
public static boolean isStaleKeyExchange(long type) {
return (type & KEY_EXCHANGE_STALE_BIT) != 0;
}
public static boolean isProcessedKeyExchange(long type) {
return (type & KEY_EXCHANGE_PROCESSED_BIT) != 0;
}
public static boolean isSymmetricEncryption(long type) {
return (type & ENCRYPTION_SYMMETRIC_BIT) != 0;
}
public static boolean isFailedDecryptType(long type) {
return (type & ENCRYPTION_REMOTE_FAILED_BIT) != 0;
}
public static boolean isDecryptInProgressType(long type) {
return
(type & ENCRYPTION_REMOTE_BIT) != 0 ||
(type & ENCRYPTION_ASYMMETRIC_BIT) != 0;
}
public static boolean isNoRemoteSessionType(long type) {
return (type & ENCRYPTION_REMOTE_NO_SESSION_BIT) != 0;
}
public static long translateFromSystemBaseType(long theirType) {
// public static final int NONE_TYPE = 0;
// public static final int INBOX_TYPE = 1;
// public static final int SENT_TYPE = 2;
// public static final int SENT_PENDING = 4;
// public static final int FAILED_TYPE = 5;
switch ((int)theirType) {
case 1: return BASE_INBOX_TYPE;
case 2: return BASE_SENT_TYPE;
case 4: return BASE_OUTBOX_TYPE;
case 5: return BASE_SENT_FAILED_TYPE;
}
return BASE_INBOX_TYPE;
}
//
//
//
// public static final int NONE_TYPE = 0;
// public static final int INBOX_TYPE = 1;
// public static final int SENT_TYPE = 2;
// public static final int SENT_PENDING = 4;
// public static final int FAILED_TYPE = 5;
//
// public static final int OUTBOX_TYPE = 43; // Messages are stored local encrypted and need delivery.
//
//
// public static final int ENCRYPTING_TYPE = 42; // Messages are stored local encrypted and need async encryption and delivery.
// public static final int SECURE_SENT_TYPE = 44; // Messages were sent with async encryption.
// public static final int SECURE_RECEIVED_TYPE = 45; // Messages were received with async decryption.
// public static final int FAILED_DECRYPT_TYPE = 46; // Messages were received with async encryption and failed to decrypt.
// public static final int DECRYPTING_TYPE = 47; // Messages are in the process of being asymmetricaly decrypted.
// public static final int NO_SESSION_TYPE = 48; // Messages were received with async encryption but there is no session yet.
//
// public static final int OUTGOING_KEY_EXCHANGE_TYPE = 49;
// public static final int INCOMING_KEY_EXCHANGE_TYPE = 50;
// public static final int STALE_KEY_EXCHANGE_TYPE = 51;
// public static final int PROCESSED_KEY_EXCHANGE_TYPE = 52;
//
// public static final int[] OUTGOING_MESSAGE_TYPES = {SENT_TYPE, SENT_PENDING, ENCRYPTING_TYPE,
// OUTBOX_TYPE, SECURE_SENT_TYPE,
// FAILED_TYPE, OUTGOING_KEY_EXCHANGE_TYPE};
//
// public static boolean isFailedMessageType(long type) {
// return type == FAILED_TYPE;
// }
//
// public static boolean isOutgoingMessageType(long type) {
// for (int outgoingType : OUTGOING_MESSAGE_TYPES) {
// if (type == outgoingType)
// return true;
// }
//
// return false;
// }
//
// public static boolean isPendingMessageType(long type) {
// return type == SENT_PENDING || type == ENCRYPTING_TYPE || type == OUTBOX_TYPE;
// }
//
// public static boolean isSecureType(long type) {
// return
// type == SECURE_SENT_TYPE || type == ENCRYPTING_TYPE ||
// type == SECURE_RECEIVED_TYPE || type == DECRYPTING_TYPE;
// }
//
// public static boolean isKeyExchangeType(long type) {
// return type == OUTGOING_KEY_EXCHANGE_TYPE || type == INCOMING_KEY_EXCHANGE_TYPE;
// }
}
}

View File

@ -23,6 +23,9 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import java.util.HashSet;
import java.util.Set;
@ -35,53 +38,54 @@ public class MmsSmsDatabase extends Database {
public static final String MMS_GROUP_SENT_COUNT = "mms_group_sent_count";
public static final String MMS_GROUP_SEND_FAILED_COUNT = "mms_group_sent_failed_count";
public static final String DATE_SENT = "date_sent";
public static final String DATE_RECEIVED = "date_received";
public static final String MMS_TRANSPORT = "mms";
public static final String SMS_TRANSPORT = "sms";
public MmsSmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public Cursor getCollatedGroupConversation(long threadId) {
String smsCaseSecurity = "CASE " + SmsDatabase.TYPE + " " +
"WHEN " + SmsDatabase.Types.SENT_TYPE + " THEN 1 " +
"WHEN " + SmsDatabase.Types.SENT_PENDING + " THEN 1 " +
"WHEN " + SmsDatabase.Types.ENCRYPTED_OUTBOX_TYPE + " THEN 1 " +
"WHEN " + SmsDatabase.Types.FAILED_TYPE + " THEN 1 " +
"WHEN " + SmsDatabase.Types.ENCRYPTING_TYPE + " THEN 2 " +
"WHEN " + SmsDatabase.Types.SECURE_SENT_TYPE + " THEN 2 " +
String smsCaseSecurity = "CASE " + SmsDatabase.TYPE + " & " + SmsDatabase.Types.SECURE_MESSAGE_BIT + " " +
"WHEN " + SmsDatabase.Types.SECURE_MESSAGE_BIT + " THEN 1 " +
"ELSE 0 END";
String mmsCaseSecurity = "CASE " + MmsDatabase.MESSAGE_BOX + " " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_OUTBOX + " THEN 'insecure' " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SENT + " THEN 'insecure' " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SENT_FAILED + " THEN 'insecure' " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SECURE_OUTBOX + " THEN 'secure' " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SECURE_SENT + " THEN 'secure' " +
"ELSE 0 END";
String mmsCaseSecurity = "CASE " + MmsDatabase.MESSAGE_BOX + " & " + SmsDatabase.Types.SECURE_MESSAGE_BIT + " " +
"WHEN " + MmsDatabase.Types.SECURE_MESSAGE_BIT + " THEN 'secure' " +
"ELSE 'insecure' END";
String mmsGroupSentCount = "SUM(CASE " + MmsDatabase.MESSAGE_BOX + " " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SENT + " THEN 1 " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SECURE_SENT + " THEN 1 " +
String mmsGroupSentCount = "SUM(CASE " + MmsDatabase.MESSAGE_BOX + " & " + MmsDatabase.Types.BASE_TYPE_MASK + " " +
"WHEN " + MmsDatabase.Types.BASE_SENT_TYPE + " THEN 1 " +
"ELSE 0 END)";
String smsGroupSentCount = "SUM(CASE " + SmsDatabase.TYPE + " " +
"WHEN " + SmsDatabase.Types.SENT_TYPE + " THEN 1 " +
"WHEN " + SmsDatabase.Types.SECURE_SENT_TYPE + " THEN 1 " +
String smsGroupSentCount = "SUM(CASE " + SmsDatabase.TYPE + " & " + SmsDatabase.Types.BASE_TYPE_MASK + " " +
"WHEN " + SmsDatabase.Types.BASE_SENT_TYPE + " THEN 1 " +
"ELSE 0 END)";
String mmsGroupSentFailedCount = "SUM(CASE " + MmsDatabase.MESSAGE_BOX + " " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SENT_FAILED + " THEN 1 " +
String mmsGroupSentFailedCount = "SUM(CASE " + MmsDatabase.MESSAGE_BOX + " & " + MmsDatabase.Types.BASE_TYPE_MASK + " " +
"WHEN " + MmsDatabase.Types.BASE_SENT_FAILED_TYPE + " THEN 1 " +
"ELSE 0 END)";
String smsGroupSentFailedCount = "SUM(CASE " + SmsDatabase.TYPE + " " +
"WHEN " + SmsDatabase.Types.FAILED_TYPE + " THEN 1 " +
String smsGroupSentFailedCount = "SUM(CASE " + SmsDatabase.TYPE + " & " + SmsDatabase.Types.BASE_TYPE_MASK + " " +
"WHEN " + SmsDatabase.Types.BASE_SENT_FAILED_TYPE + " THEN 1 " +
"ELSE 0 END)";
String[] projection = {"_id", "body", "type", "address", "subject", "status", "normalized_date_sent AS date_sent", "normalized_date_received AS date_received", "m_type", "msg_box", "transport_type", "COUNT(_id) AS group_size", mmsGroupSentCount + " AS mms_group_sent_count", mmsGroupSentFailedCount + " AS mms_group_sent_failed_count", smsGroupSentCount + " AS sms_group_sent_count", smsGroupSentFailedCount + " AS sms_group_sent_failed_count", smsCaseSecurity + " AS sms_collate", mmsCaseSecurity + " AS mms_collate"};
String order = "normalized_date_received ASC";
String selection = "thread_id = " + threadId;
String groupBy = "normalized_date_sent / 1000, sms_collate, mms_collate";
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
MmsSmsColumns.THREAD_ID,
SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, SmsDatabase.STATUS,
MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, TRANSPORT,
"COUNT(" + MmsSmsColumns.ID + ") AS " + GROUP_SIZE,
mmsGroupSentCount + " AS " + MMS_GROUP_SENT_COUNT,
mmsGroupSentFailedCount + " AS " + MMS_GROUP_SEND_FAILED_COUNT,
smsGroupSentCount + " AS " + SMS_GROUP_SENT_COUNT,
smsGroupSentFailedCount + " AS " + SMS_GROUP_SEND_FAILED_COUNT,
smsCaseSecurity + " AS sms_collate", mmsCaseSecurity + " AS mms_collate"};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
String groupBy = MmsSmsColumns.NORMALIZED_DATE_SENT + " / 1000, sms_collate, mms_collate";
Cursor cursor = queryTables(projection, selection, order, groupBy, null);
setNotifyConverationListeners(cursor, threadId);
@ -90,12 +94,17 @@ public class MmsSmsDatabase extends Database {
}
public Cursor getConversation(long threadId) {
String[] projection = {"_id", "body", "type", "address", "subject",
"normalized_date_sent AS date_sent",
"normalized_date_received AS date_received",
"m_type", "msg_box", "status", "transport_type"};
String order = "normalized_date_received ASC";
String selection = "thread_id = " + threadId;
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
MmsSmsColumns.THREAD_ID,
SmsDatabase.ADDRESS, SmsDatabase.SUBJECT,
MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
SmsDatabase.STATUS, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
Cursor cursor = queryTables(projection, selection, order, null, null);
setNotifyConverationListeners(cursor, threadId);
@ -104,27 +113,30 @@ public class MmsSmsDatabase extends Database {
}
public Cursor getConversationSnippet(long threadId) {
String[] projection = {"_id", "body", "type", "address", "subject",
"normalized_date_sent AS date_sent",
"normalized_date_received AS date_received",
"m_type", "msg_box", "transport_type"};
String order = "normalized_date_received DESC";
String selection = "thread_id = " + threadId;
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
SmsDatabase.ADDRESS, SmsDatabase.SUBJECT,
MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
Cursor cursor = queryTables(projection, selection, order, null, "1");
return cursor;
return queryTables(projection, selection, order, null, "1");
}
public Cursor getUnread() {
String[] projection = {"_id", "body", "read", "type", "address", "subject", "thread_id",
"normalized_date_sent AS date_sent",
"normalized_date_received AS date_received",
"m_type", "msg_box", "transport_type"};
String order = "normalized_date_received ASC";
String selection = "read = 0";
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.READ, SmsDatabase.TYPE,
SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsSmsColumns.THREAD_ID,
SmsDatabase.STATUS,
MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.READ + " = 0";
Cursor cursor = queryTables(projection, selection, order, null, null);
return cursor;
return queryTables(projection, selection, order, null, null);
}
public int getConversationCount(long threadId) {
@ -135,8 +147,18 @@ public class MmsSmsDatabase extends Database {
}
private Cursor queryTables(String[] projection, String selection, String order, String groupBy, String limit) {
String[] mmsProjection = {"date * 1000 AS normalized_date_sent", "date_received * 1000 AS normalized_date_received", "_id", "body", "read", "thread_id", "type", "address", "subject", "date", "m_type", "msg_box", "status", "transport_type"};
String[] smsProjection = {"date_sent * 1 AS normalized_date_sent", "date * 1 AS normalized_date_received", "_id", "body", "read", "thread_id", "type", "address", "subject", "date", "m_type", "msg_box", "status", "transport_type"};
String[] mmsProjection = {MmsDatabase.DATE_SENT + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE,
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, TRANSPORT};
String[] smsProjection = {SmsDatabase.DATE_SENT + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE,
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, TRANSPORT};
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@ -148,28 +170,28 @@ public class MmsSmsDatabase extends Database {
smsQueryBuilder.setTables(SmsDatabase.TABLE_NAME);
Set<String> mmsColumnsPresent = new HashSet<String>();
mmsColumnsPresent.add("_id");
mmsColumnsPresent.add("m_type");
mmsColumnsPresent.add("msg_box");
mmsColumnsPresent.add("date");
mmsColumnsPresent.add("date_received");
mmsColumnsPresent.add("read");
mmsColumnsPresent.add("thread_id");
mmsColumnsPresent.add(MmsSmsColumns.ID);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX);
mmsColumnsPresent.add(MmsDatabase.DATE_SENT);
mmsColumnsPresent.add(MmsDatabase.DATE_RECEIVED);
mmsColumnsPresent.add(MmsSmsColumns.READ);
mmsColumnsPresent.add(MmsSmsColumns.THREAD_ID);
Set<String> smsColumnsPresent = new HashSet<String>();
smsColumnsPresent.add("_id");
smsColumnsPresent.add("body");
smsColumnsPresent.add("type");
smsColumnsPresent.add("address");
smsColumnsPresent.add("subject");
smsColumnsPresent.add("date_sent");
smsColumnsPresent.add("date");
smsColumnsPresent.add("read");
smsColumnsPresent.add("thread_id");
smsColumnsPresent.add("status");
smsColumnsPresent.add(MmsSmsColumns.ID);
smsColumnsPresent.add(SmsDatabase.BODY);
smsColumnsPresent.add(SmsDatabase.TYPE);
smsColumnsPresent.add(SmsDatabase.ADDRESS);
smsColumnsPresent.add(SmsDatabase.SUBJECT);
smsColumnsPresent.add(SmsDatabase.DATE_SENT);
smsColumnsPresent.add(SmsDatabase.DATE_RECEIVED);
smsColumnsPresent.add(MmsSmsColumns.READ);
smsColumnsPresent.add(MmsSmsColumns.THREAD_ID);
smsColumnsPresent.add(SmsDatabase.STATUS);
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery("transport_type", mmsProjection, mmsColumnsPresent, 2, "mms", selection, null, null, null);
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery("transport_type", smsProjection, smsColumnsPresent, 2, "sms", selection, null, null, null);
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 2, MMS_TRANSPORT, selection, null, null, null);
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 2, SMS_TRANSPORT, selection, null, null, null);
SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
String unionQuery = unionQueryBuilder.buildUnionQuery(new String[] {smsSubQuery, mmsSubQuery}, order, null);
@ -181,9 +203,50 @@ public class MmsSmsDatabase extends Database {
Log.w("MmsSmsDatabase", "Executing query: " + query);
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.rawQuery(query, null);
return cursor;
return db.rawQuery(query, null);
}
public Reader readerFor(Cursor cursor, MasterSecret masterSecret) {
return new Reader(cursor, masterSecret);
}
public Reader readerFor(Cursor cursor) {
return new Reader(cursor);
}
public class Reader {
private final Cursor cursor;
private final EncryptingSmsDatabase.Reader smsReader;
private final MmsDatabase.Reader mmsReader;
public Reader(Cursor cursor, MasterSecret masterSecret) {
this.cursor = cursor;
this.smsReader = DatabaseFactory.getEncryptingSmsDatabase(context).readerFor(masterSecret, cursor);
this.mmsReader = DatabaseFactory.getMmsDatabase(context).readerFor(masterSecret, cursor);
}
public Reader(Cursor cursor) {
this.cursor = cursor;
this.smsReader = DatabaseFactory.getSmsDatabase(context).readerFor(cursor);
this.mmsReader = DatabaseFactory.getMmsDatabase(context).readerFor(null, cursor);
}
public MessageRecord getNext() {
if (cursor == null || !cursor.moveToNext())
return null;
return getCurrent();
}
public MessageRecord getCurrent() {
String type = cursor.getString(cursor.getColumnIndexOrThrow(TRANSPORT));
if (type.equals(MmsSmsDatabase.MMS_TRANSPORT)) {
return mmsReader.getCurrent();
} else {
return smsReader.getCurrent();
}
}
}
}

View File

@ -22,14 +22,24 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
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 org.thoughtcrime.securesms.sms.TextMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.Trimmer;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@ -39,17 +49,14 @@ import java.util.Set;
* @author Moxie Marlinspike
*/
public class SmsDatabase extends Database {
public class SmsDatabase extends Database implements MmsSmsColumns {
public static final String TABLE_NAME = "sms";
public static final String ID = "_id";
public static final String THREAD_ID = "thread_id";
public static final String ADDRESS = "address";
public static final String PERSON = "person";
public static final String DATE_RECEIVED = "date";
public static final String DATE_SENT = "date_sent";
static final String DATE_RECEIVED = "date";
static final String DATE_SENT = "date_sent";
public static final String PROTOCOL = "protocol";
public static final String READ = "read";
public static final String STATUS = "status";
public static final String TYPE = "type";
public static final String REPLY_PATH_PRESENT = "reply_path_present";
@ -70,52 +77,31 @@ public class SmsDatabase extends Database {
"CREATE INDEX IF NOT EXISTS sms_type_index ON " + TABLE_NAME + " (" + TYPE + ");"
};
private static final String[] MESSAGE_PROJECTION = new String[] {
ID, THREAD_ID, ADDRESS, PERSON,
DATE_RECEIVED + " AS " + NORMALIZED_DATE_RECEIVED,
DATE_SENT + " AS " + NORMALIZED_DATE_SENT,
PROTOCOL, READ, STATUS, TYPE,
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER
};
public SmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
private void updateType(long id, long type) {
Log.w("MessageDatabase", "Updating ID: " + id + " to type: " + type);
ContentValues contentValues = new ContentValues();
contentValues.put(TYPE, type);
private void updateTypeBitmask(long id, long maskOff, long maskOn) {
Log.w("MessageDatabase", "Updating ID: " + id + " to base type: " + maskOn);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {id+""});
notifyConversationListeners(getThreadIdForMessage(id));
}
db.execSQL("UPDATE " + TABLE_NAME +
" SET " + TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" +
" WHERE " + ID + " = ?", new String[] {id+""});
private long insertMessageReceived(TextMessage message, String body, long type, long timeSent) {
List<Recipient> recipientList = new ArrayList<Recipient>(1);
recipientList.add(new Recipient(null, message.getSender(), null, null));
Recipients recipients = new Recipients(recipientList);
long threadId = getThreadIdForMessage(id);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
ContentValues values = new ContentValues(6);
values.put(ADDRESS, message.getSender());
values.put(DATE_RECEIVED, Long.valueOf(System.currentTimeMillis()));
values.put(DATE_SENT, timeSent);
values.put(PROTOCOL, message.getProtocol());
values.put(READ, Integer.valueOf(0));
if (message.getPseudoSubject().length() > 0)
values.put(SUBJECT, message.getPseudoSubject());
values.put(REPLY_PATH_PRESENT, message.isReplyPathPresent() ? 1 : 0);
values.put(SERVICE_CENTER, message.getServiceCenterAddress());
values.put(BODY, body);
values.put(TYPE, type);
values.put(THREAD_ID, threadId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, null, values);
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
notifyConversationListeners(threadId);
Trimmer.trimThread(context, threadId);
return messageId;
notifyConversationListListeners();
}
public long getThreadIdForMessage(long id) {
@ -142,7 +128,8 @@ public class SmsDatabase extends Database {
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, THREAD_ID + " = ?", new String[] {threadId+""}, null, null, null);
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, THREAD_ID + " = ?",
new String[] {threadId+""}, null, null, null);
if (cursor != null && cursor.moveToFirst())
return cursor.getInt(0);
@ -155,22 +142,19 @@ public class SmsDatabase extends Database {
}
public void markAsDecryptFailed(long id) {
updateType(id, Types.FAILED_DECRYPT_TYPE);
updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_FAILED_BIT);
}
public void markAsNoSession(long id) {
updateType(id, Types.NO_SESSION_TYPE);
updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_NO_SESSION_BIT);
}
public void markAsDecrypting(long id) {
updateType(id, Types.DECRYPT_IN_PROGRESS_TYPE);
updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_BIT);
}
public void markAsSent(long id, long type) {
if (type == Types.ENCRYPTING_TYPE)
updateType(id, Types.SECURE_SENT_TYPE);
else
updateType(id, Types.SENT_TYPE);
public void markAsSent(long id) {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE);
}
public void markStatus(long id, int status) {
@ -184,7 +168,7 @@ public class SmsDatabase extends Database {
}
public void markAsSentFailed(long id) {
updateType(id, Types.FAILED_TYPE);
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_FAILED_TYPE);
}
public void setMessagesRead(long threadId) {
@ -199,71 +183,117 @@ public class SmsDatabase extends Database {
Log.w("SmsDatabase", "setMessagesRead time: " + (end-start));
}
public void updateMessageBodyAndType(long messageId, String body, long type) {
ContentValues contentValues = new ContentValues();
contentValues.put(BODY, body);
contentValues.put(TYPE, type);
protected void updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " +
TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + ") " +
"WHERE " + ID + " = ?",
new String[] {body, messageId+""});
DatabaseFactory.getThreadDatabase(context).update(getThreadIdForMessage(messageId));
notifyConversationListeners(getThreadIdForMessage(messageId));
long threadId = getThreadIdForMessage(messageId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
notifyConversationListeners(threadId);
notifyConversationListListeners();
}
public long insertSecureMessageReceived(TextMessage message, String body) {
return insertMessageReceived(message, body, Types.DECRYPT_IN_PROGRESS_TYPE,
message.getSentTimestampMillis());
}
protected Pair<Long, Long> insertMessageInbox(IncomingTextMessage message, long type) {
if (message.isKeyExchange()) {
type |= Types.KEY_EXCHANGE_BIT;
if (((IncomingKeyExchangeMessage)message).isStale()) type |= Types.KEY_EXCHANGE_STALE_BIT;
else if (((IncomingKeyExchangeMessage)message).isProcessed()) {Log.w("SmsDatabase", "Setting processed bit..."); type |= Types.KEY_EXCHANGE_PROCESSED_BIT;}
} else if (message.isSecureMessage()) {
type |= Types.SECURE_MESSAGE_BIT;
type |= Types.ENCRYPTION_REMOTE_BIT;
}
public long insertMessageReceived(TextMessage message, String body) {
return insertMessageReceived(message, body, Types.INBOX_TYPE, message.getSentTimestampMillis());
}
Recipient recipient = new Recipient(null, message.getSender(), null, null);
Recipients recipients = new Recipients(recipient);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
public long insertMessageSent(String address, long threadId, String body, long date, long type) {
ContentValues contentValues = new ContentValues(6);
// contentValues.put(ADDRESS, NumberUtil.filterNumber(address));
contentValues.put(ADDRESS, address);
contentValues.put(THREAD_ID, threadId);
contentValues.put(BODY, body);
contentValues.put(DATE_RECEIVED, date);
contentValues.put(DATE_SENT, date);
contentValues.put(READ, 1);
contentValues.put(TYPE, type);
ContentValues values = new ContentValues(6);
values.put(ADDRESS, message.getSender());
values.put(DATE_RECEIVED, Long.valueOf(System.currentTimeMillis()));
values.put(DATE_SENT, message.getSentTimestampMillis());
values.put(PROTOCOL, message.getProtocol());
values.put(READ, Integer.valueOf(0));
if (!Util.isEmpty(message.getPseudoSubject()))
values.put(SUBJECT, message.getPseudoSubject());
values.put(REPLY_PATH_PRESENT, message.isReplyPathPresent());
values.put(SERVICE_CENTER, message.getServiceCenterAddress());
values.put(BODY, message.getMessageBody());
values.put(TYPE, type);
values.put(THREAD_ID, threadId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
long messageId = db.insert(TABLE_NAME, null, values);
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
notifyConversationListeners(threadId);
Trimmer.trimThread(context, threadId);
return messageId;
return new Pair<Long, Long>(messageId, threadId);
}
public Cursor getOutgoingMessages() {
String outgoingSelection = "(" + TYPE + " = " + Types.ENCRYPTING_TYPE + " OR " + TYPE + " = " + Types.ENCRYPTED_OUTBOX_TYPE + ")";
public Pair<Long, Long> insertMessageInbox(IncomingTextMessage message) {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE);
}
protected List<Long> insertMessageOutbox(long threadId, OutgoingTextMessage message, long type) {
if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT;
else if (message.isSecureMessage()) type |= Types.SECURE_MESSAGE_BIT;
long date = System.currentTimeMillis();
List<Long> messageIds = new LinkedList<Long>();
for (Recipient recipient : message.getRecipients().getRecipientsList()) {
ContentValues contentValues = new ContentValues(6);
contentValues.put(ADDRESS, PhoneNumberUtils.formatNumber(recipient.getNumber()));
contentValues.put(THREAD_ID, threadId);
contentValues.put(BODY, message.getMessageBody());
contentValues.put(DATE_RECEIVED, date);
contentValues.put(DATE_SENT, date);
contentValues.put(READ, 1);
contentValues.put(TYPE, type);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
messageIds.add(db.insert(TABLE_NAME, ADDRESS, contentValues));
DatabaseFactory.getThreadDatabase(context).update(threadId);
notifyConversationListeners(threadId);
Trimmer.trimThread(context, threadId);
}
return messageIds;
}
Cursor getOutgoingMessages() {
String outgoingSelection = TYPE + " & " + Types.BASE_TYPE_MASK + " = " + Types.BASE_OUTBOX_TYPE;
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, null, outgoingSelection, null, null, null, null);
return db.query(TABLE_NAME, MESSAGE_PROJECTION, outgoingSelection, null, null, null, null);
}
public Cursor getDecryptInProgressMessages() {
String where = TYPE + " = " + Types.DECRYPT_IN_PROGRESS_TYPE;
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, null, where, null, null, null, null);
String where = TYPE + " & " + (Types.ENCRYPTION_REMOTE_BIT | Types.ENCRYPTION_ASYMMETRIC_BIT) + " != 0";
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, MESSAGE_PROJECTION, where, null, null, null, null);
}
public Cursor getEncryptedRogueMessages(Recipient recipient) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String selection = TYPE + " = " + Types.NO_SESSION_TYPE + " AND PHONE_NUMBERS_EQUAL(" + ADDRESS + ", ?)";
String selection = TYPE + " & " + Types.ENCRYPTION_REMOTE_NO_SESSION_BIT + " != 0" +
" AND PHONE_NUMBERS_EQUAL(" + ADDRESS + ", ?)";
String[] args = {recipient.getNumber()};
return db.query(TABLE_NAME, null, selection, args, null, null, null);
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, MESSAGE_PROJECTION, selection, args, null, null, null);
}
public Cursor getMessage(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, null, ID_WHERE, new String[] {messageId+""}, null, null, null);
return db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId+""},
null, null, null);
}
public void deleteMessage(long messageId) {
@ -284,7 +314,7 @@ public class SmsDatabase extends Database {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = THREAD_ID + " = ? AND (CASE " + TYPE;
for (int outgoingType : Types.OUTGOING_MESSAGE_TYPES) {
for (long outgoingType : Types.OUTGOING_MESSAGE_TYPES) {
where += " WHEN " + outgoingType + " THEN " + DATE_SENT + " < " + date;
}
@ -322,10 +352,6 @@ public class SmsDatabase extends Database {
database.endTransaction();
}
/*package*/ void insertRaw(SQLiteDatabase database, ContentValues contentValues) {
database.insert(TABLE_NAME, null, contentValues);
}
/*package*/ SQLiteStatement createInsertStatement(SQLiteDatabase database) {
return database.compileStatement("INSERT INTO " + TABLE_NAME + " (" + ADDRESS + ", " +
PERSON + ", " +
@ -339,7 +365,7 @@ public class SmsDatabase extends Database {
SUBJECT + ", " +
BODY + ", " +
SERVICE_CENTER +
", THREAD_ID) " +
", " + THREAD_ID + ") " +
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
}
@ -350,45 +376,69 @@ public class SmsDatabase extends Database {
public static final int STATUS_FAILED = 64;
}
public static class Types {
public static final int INBOX_TYPE = 1;
public static final int SENT_TYPE = 2;
public static final int SENT_PENDING = 4;
public static final int FAILED_TYPE = 5;
public static final int ENCRYPTING_TYPE = 42; // Messages are stored local encrypted and need async encryption and delivery.
public static final int ENCRYPTED_OUTBOX_TYPE = 43; // Messages are stored local encrypted and need delivery.
public static final int SECURE_SENT_TYPE = 44; // Messages were sent with async encryption.
public static final int SECURE_RECEIVED_TYPE = 45; // Messages were received with async decryption.
public static final int FAILED_DECRYPT_TYPE = 46; // Messages were received with async encryption and failed to decrypt.
public static final int DECRYPT_IN_PROGRESS_TYPE = 47; // Messages are in the process of being asymmetricaly decrypted.
public static final int NO_SESSION_TYPE = 48; // Messages were received with async encryption but there is no session yet.
public static final int[] OUTGOING_MESSAGE_TYPES = {SENT_TYPE, SENT_PENDING, ENCRYPTING_TYPE,
ENCRYPTED_OUTBOX_TYPE, SECURE_SENT_TYPE,
FAILED_TYPE};
public static boolean isFailedMessageType(long type) {
return type == FAILED_TYPE;
}
public static boolean isOutgoingMessageType(long type) {
for (int outgoingType : OUTGOING_MESSAGE_TYPES) {
if (type == outgoingType)
return true;
}
return false;
}
public static boolean isPendingMessageType(long type) {
return type == SENT_PENDING || type == ENCRYPTING_TYPE || type == ENCRYPTED_OUTBOX_TYPE;
}
public static boolean isSecureType(long type) {
return type == SECURE_SENT_TYPE || type == ENCRYPTING_TYPE || type == SECURE_RECEIVED_TYPE;
}
public Reader readerFor(Cursor cursor) {
return new Reader(cursor);
}
public class Reader {
private final Cursor cursor;
public Reader(Cursor cursor) {
this.cursor = cursor;
}
public SmsMessageRecord getNext() {
if (cursor == null || !cursor.moveToNext())
return null;
return getCurrent();
}
public SmsMessageRecord getCurrent() {
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE));
long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_RECEIVED));
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.NORMALIZED_DATE_SENT));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.THREAD_ID));
int status = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.STATUS));
Recipients recipients = getRecipientsFor(address);
String body = getBody(cursor);
MessageRecord.GroupData groupData = null;
if (cursor.getColumnIndex(MmsSmsDatabase.GROUP_SIZE) != -1) {
int groupSize = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.GROUP_SIZE));
int groupSent = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.SMS_GROUP_SENT_COUNT));
int groupSendFailed = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsDatabase.SMS_GROUP_SEND_FAILED_COUNT));
Log.w("ConversationAdapter", "GroupSize: " + groupSize + " , GroupSent: " + groupSent + " , GroupSendFailed: " + groupSendFailed);
groupData = new MessageRecord.GroupData(groupSize, groupSent, groupSendFailed);
}
return new SmsMessageRecord(context, messageId, body, recipients,
recipients.getPrimaryRecipient(),
dateSent, dateReceived, type,
threadId, status, groupData);
}
private Recipients getRecipientsFor(String address) {
try {
return RecipientFactory.getRecipientsFromString(context, address, false);
} catch (RecipientFormattingException e) {
Log.w("EncryptingSmsDatabase", e);
return new Recipients(new Recipient("Unknown", "Unknown", null,
ContactPhotoFactory.getDefaultContactPhoto(context)));
}
}
protected String getBody(Cursor cursor) {
return cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
}
public void close() {
cursor.close();
}
}
}

View File

@ -25,7 +25,6 @@ import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
@ -43,7 +42,7 @@ public class SmsMigrator {
if (cursor.isNull(columnIndex)) {
statement.bindNull(index);
} else {
statement.bindString(index, encryptIfNecessary(context, masterSecret, cursor.getString(columnIndex)));
statement.bindString(index, encrypt(masterSecret, cursor.getString(columnIndex)));
}
}
@ -71,6 +70,19 @@ public class SmsMigrator {
}
}
private static void addTranslatedTypeToStatement(SQLiteStatement statement, Cursor cursor,
int index, String key)
{
int columnIndex = cursor.getColumnIndexOrThrow(key);
if (cursor.isNull(columnIndex)) {
statement.bindLong(index, SmsDatabase.Types.BASE_INBOX_TYPE | SmsDatabase.Types.ENCRYPTION_SYMMETRIC_BIT);
} else {
long theirType = cursor.getLong(columnIndex);
statement.bindLong(index, SmsDatabase.Types.translateFromSystemBaseType(theirType) | SmsDatabase.Types.ENCRYPTION_SYMMETRIC_BIT);
}
}
private static void getContentValuesForRow(Context context, MasterSecret masterSecret,
Cursor cursor, long threadId,
SQLiteStatement statement)
@ -82,7 +94,7 @@ public class SmsMigrator {
addIntToStatement(statement, cursor, 5, SmsDatabase.PROTOCOL);
addIntToStatement(statement, cursor, 6, SmsDatabase.READ);
addIntToStatement(statement, cursor, 7, SmsDatabase.STATUS);
addIntToStatement(statement, cursor, 8, SmsDatabase.TYPE);
addTranslatedTypeToStatement(statement, cursor, 8, SmsDatabase.TYPE);
addIntToStatement(statement, cursor, 9, SmsDatabase.REPLY_PATH_PRESENT);
addStringToStatement(statement, cursor, 10, SmsDatabase.SUBJECT);
addEncryptedStringToStatement(context, statement, cursor, masterSecret, 11, SmsDatabase.BODY);
@ -138,16 +150,10 @@ public class SmsMigrator {
}
}
private static String encryptIfNecessary(Context context,
MasterSecret masterSecret,
String body)
private static String encrypt(MasterSecret masterSecret, String body)
{
if (!body.startsWith(Prefix.SYMMETRIC_ENCRYPT) && !body.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
return Prefix.SYMMETRIC_ENCRYPT + masterCipher.encryptBody(body);
}
return body;
MasterCipher masterCipher = new MasterCipher(masterSecret);
return masterCipher.encryptBody(body);
}
private static void migrateConversation(Context context, MasterSecret masterSecret,

View File

@ -23,9 +23,13 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.InvalidMessageException;
import java.util.Arrays;
import java.util.HashSet;
@ -34,7 +38,7 @@ import java.util.Set;
public class ThreadDatabase extends Database {
private static final String TABLE_NAME = "thread";
static final String TABLE_NAME = "thread";
public static final String ID = "_id";
public static final String DATE = "date";
public static final String MESSAGE_COUNT = "message_count";
@ -45,11 +49,13 @@ public class ThreadDatabase extends Database {
private static final String TYPE = "type";
private static final String ERROR = "error";
private static final String HAS_ATTACHMENT = "has_attachment";
public static final String SNIPPET_TYPE = "snippet_type";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
DATE + " INTEGER DEFAULT 0, " + MESSAGE_COUNT + " INTEGER DEFAULT 0, " +
RECIPIENT_IDS + " TEXT, " + SNIPPET + " TEXT, " + SNIPPET_CHARSET + " INTEGER DEFAULT 0, " +
READ + " INTEGER DEFAULT 1, " + TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " +
HAS_ATTACHMENT + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
@ -108,11 +114,12 @@ public class ThreadDatabase extends Database {
return db.insert(TABLE_NAME, null, contentValues);
}
private void updateThread(long threadId, long count, String body, long date) {
private void updateThread(long threadId, long count, String body, long date, long type) {
ContentValues contentValues = new ContentValues(3);
contentValues.put(DATE, date - date % 1000);
contentValues.put(MESSAGE_COUNT, count);
contentValues.put(SNIPPET, body);
contentValues.put(SNIPPET_TYPE, type);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId+""});
@ -179,7 +186,7 @@ public class ThreadDatabase extends Database {
Log.w("ThreadDatabase", "Cursor count is greater than length!");
cursor.moveToPosition(cursor.getCount() - length);
long lastTweetDate = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsDatabase.DATE_RECEIVED));
long lastTweetDate = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED));
Log.w("ThreadDatabase", "Cut off tweet date: " + lastTweetDate);
@ -350,7 +357,8 @@ public class ThreadDatabase extends Database {
if (cursor != null && cursor.moveToFirst()) {
updateThread(threadId, count,
cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)),
cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsDatabase.DATE_RECEIVED)));
cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED)),
cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE)));
} else {
deleteThread(threadId);
}
@ -365,4 +373,68 @@ public class ThreadDatabase extends Database {
public static interface ProgressListener {
public void onProgress(int complete, int total);
}
public Reader readerFor(Cursor cursor, MasterSecret masterSecret) {
return new Reader(cursor, masterSecret);
}
public class Reader {
private final Cursor cursor;
private final MasterSecret masterSecret;
public Reader(Cursor cursor, MasterSecret masterSecret) {
this.cursor = cursor;
this.masterSecret = masterSecret;
}
public ThreadRecord getNext() {
if (cursor == null || !cursor.moveToNext())
return null;
return getCurrent();
}
public ThreadRecord getCurrent() {
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID));
String recipientId = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_IDS));
Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientId, true);
String body = getPlaintextBody(cursor);
long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE));
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
long read = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.READ));
long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
return new ThreadRecord(context, body, recipients, date, count, read == 1, threadId, type);
}
private String getPlaintextBody(Cursor cursor) {
long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
String ciphertextBody = cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET));
if (masterSecret == null)
return ciphertextBody;
try {
if (MmsSmsColumns.Types.isSymmetricEncryption(type)) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
return masterCipher.decryptBody(ciphertextBody);
} else {
return ciphertextBody;
}
} catch (InvalidMessageException e) {
Log.w("ThreadDatabase", e);
return "Error decrypting message.";
}
}
protected String getBody(Cursor cursor) {
return cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
}
public void close() {
cursor.close();
}
}
}

View File

@ -16,7 +16,10 @@
*/
package org.thoughtcrime.securesms.database.model;
import org.thoughtcrime.securesms.protocol.Prefix;
import android.content.Context;
import android.text.SpannableString;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.recipients.Recipients;
/**
@ -29,56 +32,33 @@ import org.thoughtcrime.securesms.recipients.Recipients;
public abstract class DisplayRecord {
protected final Context context;
private final Recipients recipients;
private final long dateSent;
private final long dateReceived;
private final long threadId;
protected final long type;
private String body;
protected boolean emphasis;
protected boolean keyExchange;
protected boolean processedKeyExchange;
protected boolean staleKeyExchange;
private final String body;
public DisplayRecord(Recipients recipients, long dateSent, long dateReceived, long threadId) {
public DisplayRecord(Context context, String body, Recipients recipients, long dateSent,
long dateReceived, long threadId, long type)
{
this.context = context.getApplicationContext();
this.threadId = threadId;
this.recipients = recipients;
this.dateSent = dateSent;
this.dateReceived = dateReceived;
this.emphasis = false;
}
public void setEmphasis(boolean emphasis) {
this.emphasis = emphasis;
}
public boolean getEmphasis() {
return emphasis;
}
public void setBody(String body) {
if (body.startsWith(Prefix.KEY_EXCHANGE)) {
this.keyExchange = true;
this.emphasis = true;
this.body = body;
} else if (body.startsWith(Prefix.PROCESSED_KEY_EXCHANGE)) {
this.processedKeyExchange = true;
this.emphasis = true;
this.body = body;
} else if (body.startsWith(Prefix.STALE_KEY_EXCHANGE)) {
this.staleKeyExchange = true;
this.emphasis = true;
this.body = body;
} else {
this.body = body;
this.emphasis = false;
}
this.type = type;
this.body = body;
}
public String getBody() {
return body;
}
public abstract SpannableString getDisplayBody();
public Recipients getRecipients() {
return recipients;
}
@ -96,7 +76,6 @@ public abstract class DisplayRecord {
}
public boolean isKeyExchange() {
return keyExchange || processedKeyExchange || staleKeyExchange;
return SmsDatabase.Types.isKeyExchangeType(type);
}
}

View File

@ -17,6 +17,7 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.text.SpannableString;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsDatabase;
@ -25,9 +26,6 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.Iterator;
import java.util.List;
/**
* Represents the message record model for MMS messages that contain
* media (ie: they've been downloaded).
@ -38,40 +36,21 @@ import java.util.List;
public class MediaMmsMessageRecord extends MessageRecord {
private final Context context;
private final SlideDeck slideDeck;
private final long mailbox;
public MediaMmsMessageRecord(Context context, long id, Recipients recipients,
Recipient individualRecipient, long dateSent, long dateReceived,
long threadId, SlideDeck slideDeck, long mailbox,
GroupData groupData)
{
super(id, recipients, individualRecipient, dateSent, dateReceived,
threadId, DELIVERY_STATUS_NONE, groupData);
super(context, id, getBodyFromSlidesIfAvailable(slideDeck), recipients,
individualRecipient, dateSent, dateReceived,
threadId, DELIVERY_STATUS_NONE, mailbox,
groupData);
this.context = context.getApplicationContext();
this.slideDeck = slideDeck;
this.mailbox = mailbox;
setBodyIfTextAvailable(context);
}
@Override
public boolean isOutgoing() {
return MmsDatabase.Types.isOutgoingMmsBox(mailbox);
}
@Override
public boolean isPending() {
return MmsDatabase.Types.isPendingMmsBox(mailbox);
}
@Override
public boolean isFailed() {
return MmsDatabase.Types.isFailedMmsBox(mailbox);
}
@Override
public boolean isSecure() {
return MmsDatabase.Types.isSecureMmsBox(mailbox);
}
public SlideDeck getSlideDeck() {
@ -83,39 +62,29 @@ public class MediaMmsMessageRecord extends MessageRecord {
return true;
}
private void setBodyIfTextAvailable(Context context) {
switch ((int)mailbox) {
case MmsDatabase.Types.MESSAGE_BOX_DECRYPTING_INBOX:
setBody(context.getString(R.string.MmsMessageRecord_decrypting_mms_please_wait));
setEmphasis(true);
return;
case MmsDatabase.Types.MESSAGE_BOX_DECRYPT_FAILED_INBOX:
setBody(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message));
setEmphasis(true);
return;
case MmsDatabase.Types.MESSAGE_BOX_NO_SESSION_INBOX:
setBody(context
.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session));
setEmphasis(true);
return;
@Override
public SpannableString getDisplayBody() {
if (MmsDatabase.Types.isDecryptInProgressType(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_decrypting_mms_please_wait));
} else if (MmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message));
} else if (MmsDatabase.Types.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session));
}
setBodyFromSlidesIfTextAvailable();
return super.getDisplayBody();
}
private void setBodyFromSlidesIfTextAvailable() {
private static String getBodyFromSlidesIfAvailable(SlideDeck slideDeck) {
if (slideDeck == null)
return;
List<Slide> slides = slideDeck.getSlides();
Iterator<Slide> i = slides.iterator();
while (i.hasNext()) {
Slide slide = i.next();
return "";
for (Slide slide : slideDeck.getSlides()) {
if (slide.hasText())
setBody(slide.getText());
return slide.getText();
}
return "";
}
}

View File

@ -16,6 +16,14 @@
*/
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
@ -39,29 +47,44 @@ public abstract class MessageRecord extends DisplayRecord {
private final int deliveryStatus;
private final GroupData groupData;
public MessageRecord(long id, Recipients recipients,
public MessageRecord(Context context, long id, String body, Recipients recipients,
Recipient individualRecipient,
long dateSent, long dateReceived,
long threadId, int deliveryStatus,
GroupData groupData)
long type, GroupData groupData)
{
super(recipients, dateSent, dateReceived, threadId);
super(context, body, recipients, dateSent, dateReceived, threadId, type);
this.id = id;
this.individualRecipient = individualRecipient;
this.deliveryStatus = deliveryStatus;
this.groupData = groupData;
}
public abstract boolean isOutgoing();
public abstract boolean isFailed();
public abstract boolean isSecure();
public abstract boolean isPending();
public abstract boolean isMms();
public boolean isFailed() {
return
MmsSmsColumns.Types.isFailedMessageType(type) ||
getDeliveryStatus() == DELIVERY_STATUS_FAILED;
}
public boolean isOutgoing() {
return MmsSmsColumns.Types.isOutgoingMessageType(type);
}
public boolean isPending() {
return MmsSmsColumns.Types.isPendingMessageType(type) || isGroupDeliveryPending();
}
public boolean isSecure() {
return MmsSmsColumns.Types.isSecureType(type);
}
@Override
public SpannableString getDisplayBody() {
return new SpannableString(getBody());
}
public long getId() {
return id;
}
@ -75,11 +98,11 @@ public abstract class MessageRecord extends DisplayRecord {
}
public boolean isStaleKeyExchange() {
return this.staleKeyExchange;
return SmsDatabase.Types.isStaleKeyExchange(type);
}
public boolean isProcessedKeyExchange() {
return this.processedKeyExchange;
return SmsDatabase.Types.isProcessedKeyExchange(type);
}
public Recipient getIndividualRecipient() {
@ -98,6 +121,14 @@ public abstract class MessageRecord extends DisplayRecord {
return false;
}
protected SpannableString emphasisAdded(String sequence) {
SpannableString spannable = new SpannableString(sequence);
spannable.setSpan(new ForegroundColorSpan(context.getResources().getColor(android.R.color.darker_gray)), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
}
public static class GroupData {
public final int groupSize;
public final int groupSentCount;
@ -109,6 +140,4 @@ public abstract class MessageRecord extends DisplayRecord {
this.groupSendFailedCount = groupSendFailedCount;
}
}
}

View File

@ -16,6 +16,10 @@
*/
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.text.SpannableString;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
@ -36,22 +40,20 @@ public class NotificationMmsMessageRecord extends MessageRecord {
private final int status;
private final byte[] transactionId;
public NotificationMmsMessageRecord(long id, Recipients recipients, Recipient individualRecipient,
public NotificationMmsMessageRecord(Context context, long id, Recipients recipients,
Recipient individualRecipient,
long dateSent, long dateReceived, long threadId,
byte[] contentLocation, long messageSize, long expiry,
int status, byte[] transactionId)
int status, byte[] transactionId, long mailbox)
{
super(id, recipients, individualRecipient, dateSent, dateReceived,
threadId, DELIVERY_STATUS_NONE, null);
super(context, id, "", recipients, individualRecipient, dateSent, dateReceived,
threadId, DELIVERY_STATUS_NONE, mailbox, null);
this.contentLocation = contentLocation;
this.messageSize = messageSize;
this.expiry = expiry;
this.status = status;
this.transactionId = transactionId;
setBody("Multimedia Message");
setEmphasis(true);
}
public byte[] getTransactionId() {
@ -81,7 +83,7 @@ public class NotificationMmsMessageRecord extends MessageRecord {
@Override
public boolean isFailed() {
return MmsDatabase.Types.isHardError(status);
return MmsDatabase.Status.isHardError(status);
}
@Override
@ -99,4 +101,8 @@ public class NotificationMmsMessageRecord extends MessageRecord {
return true;
}
@Override
public SpannableString getDisplayBody() {
return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_multimedia_message));
}
}

View File

@ -18,10 +18,11 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.text.SpannableString;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.protocol.Tag;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
@ -34,20 +35,15 @@ import org.thoughtcrime.securesms.recipients.Recipients;
public class SmsMessageRecord extends MessageRecord {
private final Context context;
private final long type;
public SmsMessageRecord(Context context, long id,
Recipients recipients,
String body, Recipients recipients,
Recipient individualRecipient,
long dateSent, long dateReceived,
long type, long threadId,
int status, GroupData groupData)
{
super(id, recipients, individualRecipient, dateSent, dateReceived,
threadId, getGenericDeliveryStatus(status), groupData);
this.context = context.getApplicationContext();
this.type = type;
super(context, id, body, recipients, individualRecipient, dateSent, dateReceived,
threadId, getGenericDeliveryStatus(status), type, groupData);
}
public long getType() {
@ -55,45 +51,28 @@ public class SmsMessageRecord extends MessageRecord {
}
@Override
public void setBody(String body) {
if (this.type == SmsDatabase.Types.FAILED_DECRYPT_TYPE) {
super.setBody(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
setEmphasis(true);
} else if (this.type == SmsDatabase.Types.DECRYPT_IN_PROGRESS_TYPE ||
(type == 0 && body.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) ||
(type == 0 && body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT)))
{
super.setBody(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait));
setEmphasis(true);
} else if (type == SmsDatabase.Types.NO_SESSION_TYPE) {
super.setBody(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
setEmphasis(true);
public SpannableString getDisplayBody() {
if (isProcessedKeyExchange()) {
return emphasisAdded(context.getString(R.string.ConversationItem_received_and_processed_key_exchange_message));
} else if (isStaleKeyExchange()) {
return emphasisAdded(context.getString(R.string.ConversationItem_error_received_stale_key_exchange_message));
} else if (isKeyExchange() && isOutgoing()) {
return emphasisAdded(context.getString(R.string.ConversationListAdapter_key_exchange_message));
} else if (isKeyExchange() && !isOutgoing()) {
return emphasisAdded(context.getString(R.string.ConversationItem_received_key_exchange_message_click_to_process));
} else if (isOutgoing() && Tag.isTagged(getBody())) {
return new SpannableString(Tag.stripTag(getBody()));
} else if (SmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (SmsDatabase.Types.isDecryptInProgressType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait));
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
} else {
super.setBody(body);
return super.getDisplayBody();
}
}
@Override
public boolean isFailed() {
return SmsDatabase.Types.isFailedMessageType(getType()) ||
getDeliveryStatus() == DELIVERY_STATUS_FAILED;
}
@Override
public boolean isOutgoing() {
return SmsDatabase.Types.isOutgoingMessageType(getType());
}
@Override
public boolean isPending() {
return SmsDatabase.Types.isPendingMessageType(getType()) || isGroupDeliveryPending();
}
@Override
public boolean isSecure() {
return SmsDatabase.Types.isSecureType(getType());
}
@Override
public boolean isMms() {
return false;

View File

@ -17,10 +17,14 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Util;
/**
* The message record model which represents thread heading messages.
@ -34,32 +38,42 @@ public class ThreadRecord extends DisplayRecord {
private final long count;
private final boolean read;
public ThreadRecord(Context context, Recipients recipients,
long date, long count,
boolean read, long threadId)
public ThreadRecord(Context context, String body, Recipients recipients, long date,
long count, boolean read, long threadId, long type)
{
super(recipients, date, date, threadId);
super(context, body, recipients, date, date, threadId, type);
this.context = context.getApplicationContext();
this.count = count;
this.read = read;
}
@Override
public void setBody(String body) {
if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT) ||
body.startsWith(Prefix.ASYMMETRIC_ENCRYPT) ||
body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT))
{
super.setBody(context.getString(R.string.ConversationListAdapter_encrypted_message_enter_passphrase));
setEmphasis(true);
} else if (body.startsWith(Prefix.KEY_EXCHANGE)) {
super.setBody(context.getString(R.string.ConversationListAdapter_key_exchange_message));
setEmphasis(true);
public SpannableString getDisplayBody() {
if (SmsDatabase.Types.isDecryptInProgressType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait));
} else if (isKeyExchange()) {
return emphasisAdded(context.getString(R.string.ConversationListItem_key_exchange_message));
} else if (SmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
} else {
super.setBody(body);
if (Util.isEmpty(getBody())) {
return new SpannableString(context.getString(R.string.MessageNotifier_no_subject));
} else {
return new SpannableString(getBody());
}
}
}
private SpannableString emphasisAdded(String sequence) {
SpannableString spannable = new SpannableString(sequence);
spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0,
sequence.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
}
public long getCount() {
return count;
}

View File

@ -11,7 +11,7 @@ import com.google.thoughtcrimegson.Gson;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.service.RegistrationService;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.sms.TextMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
@ -56,9 +56,9 @@ public class GcmIntentService extends GCMBaseIntentService {
if (Util.isEmpty(data))
return;
IncomingGcmMessage message = new Gson().fromJson(data, IncomingGcmMessage.class);
ArrayList<TextMessage> messages = new ArrayList<TextMessage>();
messages.add(new TextMessage(message));
IncomingGcmMessage message = new Gson().fromJson(data, IncomingGcmMessage.class);
ArrayList<IncomingTextMessage> messages = new ArrayList<IncomingTextMessage>();
messages.add(new IncomingTextMessage(message));
Intent receivedIntent = new Intent(context, SendReceiveService.class);
receivedIntent.setAction(SendReceiveService.RECEIVE_SMS_ACTION);

View File

@ -16,16 +16,15 @@
*/
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import android.content.Context;
import android.util.Log;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.PduBody;
@ -33,25 +32,25 @@ import ws.com.google.android.mms.pdu.PduBody;
public class SlideDeck {
private final List<Slide> slides = new LinkedList<Slide>();
public SlideDeck(Context context, MasterSecret masterSecret, PduBody body) {
try {
for (int i=0;i<body.getPartsNum();i++) {
String contentType = new String(body.getPart(i).getContentType(), CharacterSets.MIMENAME_ISO_8859_1);
if (ContentType.isImageType(contentType))
slides.add(new ImageSlide(context, masterSecret, body.getPart(i)));
else if (ContentType.isVideoType(contentType))
slides.add(new VideoSlide(context, body.getPart(i)));
else if (ContentType.isAudioType(contentType))
slides.add(new AudioSlide(context, body.getPart(i)));
else if (ContentType.isTextType(contentType))
slides.add(new TextSlide(context, masterSecret, body.getPart(i)));
String contentType = new String(body.getPart(i).getContentType(), CharacterSets.MIMENAME_ISO_8859_1);
if (ContentType.isImageType(contentType))
slides.add(new ImageSlide(context, masterSecret, body.getPart(i)));
else if (ContentType.isVideoType(contentType))
slides.add(new VideoSlide(context, body.getPart(i)));
else if (ContentType.isAudioType(contentType))
slides.add(new AudioSlide(context, body.getPart(i)));
else if (ContentType.isTextType(contentType))
slides.add(new TextSlide(context, masterSecret, body.getPart(i)));
}
} catch (UnsupportedEncodingException uee) {
throw new AssertionError(uee);
}
}
public SlideDeck() {
}
@ -61,10 +60,10 @@ public class SlideDeck {
public PduBody toPduBody() {
PduBody body = new PduBody();
Iterator<Slide> iterator = slides.iterator();
while (iterator.hasNext())
body.addPart(iterator.next().getPart());
for (Slide slide : slides) {
body.addPart(slide.getPart());
}
return body;
}

View File

@ -32,28 +32,21 @@ import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.BigTextStyle;
import android.support.v4.app.NotificationCompat.InboxStyle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RoutingActivity;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MessageDisplayHelper;
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.protocol.Prefix;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.InvalidMessageException;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.util.List;
@ -246,12 +239,23 @@ public class MessageNotifier {
Cursor cursor)
{
NotificationState notificationState = new NotificationState();
MessageRecord record;
MmsSmsDatabase.Reader reader;
while (cursor.moveToNext()) {
Recipients recipients = getRecipients(context, cursor);
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.THREAD_ID));
CharSequence body = getBody(context, masterSecret, cursor);
Uri image = null;
if (masterSecret == null) reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor);
else reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor, masterSecret);
while ((record = reader.getNext()) != null) {
Recipients recipients = record.getRecipients();
long threadId = record.getThreadId();
SpannableString body = record.getDisplayBody();
Uri image = null;
// XXXX This is so fucked up. FIX ME!
if (body.toString().equals(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait))) {
body = new SpannableString(context.getString(R.string.MessageNotifier_encrypted_message));
body.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
notificationState.addNotification(new NotificationItem(recipients, threadId, body, image));
}
@ -259,75 +263,6 @@ public class MessageNotifier {
return notificationState;
}
private static CharSequence getBody(Context context, MasterSecret masterSecret, Cursor cursor) {
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
if (body == null) {
return context.getString(R.string.MessageNotifier_no_subject);
}
if (masterSecret != null) {
try {
body = MessageDisplayHelper.getDecryptedMessageBody(new MasterCipher(masterSecret), body);
} catch (InvalidMessageException e) {
Log.w("MessageNotifier", e);
return Util.getItalicizedString(context.getString(R.string.MessageNotifier_corrupted_ciphertext));
}
}
if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT) ||
body.startsWith(Prefix.ASYMMETRIC_ENCRYPT) ||
body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT))
{
return Util.getItalicizedString(context.getString(R.string.MessageNotifier_encrypted_message));
} else if (body.startsWith(Prefix.KEY_EXCHANGE) ||
body.startsWith(Prefix.PROCESSED_KEY_EXCHANGE))
{
return Util.getItalicizedString(context.getString(R.string.MessageNotifier_key_exchange));
}
return body;
}
private static Recipients getSmsRecipient(Context context, Cursor cursor)
throws RecipientFormattingException
{
String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
return RecipientFactory.getRecipientsFromString(context, address, false);
}
private static Recipients getMmsRecipient(Context context, Cursor cursor)
throws RecipientFormattingException
{
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID));
String address = DatabaseFactory.getMmsDatabase(context).getMessageRecipient(messageId);
return RecipientFactory.getRecipientsFromString(context, address, false);
}
private static Recipients getRecipients(Context context, Cursor cursor) {
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
Recipients recipients = null;
try {
if (type.equals("sms")) {
recipients = getSmsRecipient(context, cursor);
} else {
recipients = getMmsRecipient(context, cursor);
}
} catch (RecipientFormattingException e) {
Log.w("MessageNotifier", e);
return new Recipients(new Recipient("Unknown", "Unknown", null,
ContactPhotoFactory.getDefaultContactPhoto(context)));
}
if (recipients == null || recipients.isEmpty()) {
recipients = new Recipients(new Recipient("Unknown", "Unknown", null,
ContactPhotoFactory.getDefaultContactPhoto(context)));
}
return recipients;
}
private static void setNotificationAlarms(Context context,
NotificationCompat.Builder builder,
boolean signal)

View File

@ -1,36 +0,0 @@
/**
* 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.protocol;
/**
* Prefixes for identifying encrypted message types. In hindsight, seems
* like these could have been DB columns or a single DB column with a bitmask
* or something.
*
* @author Moxie Marlinspike
*/
public class Prefix {
public static final String KEY_EXCHANGE = "?TextSecureKeyExchange";
public static final String SYMMETRIC_ENCRYPT = "?TextSecureLocalEncrypt";
public static final String ASYMMETRIC_ENCRYPT = "?TextSecureAsymmetricEncrypt";
public static final String ASYMMETRIC_LOCAL_ENCRYPT = "?TextSecureAsymmetricLocalEncrypt";
public static final String PROCESSED_KEY_EXCHANGE = "?TextSecureKeyExchangd";
public static final String STALE_KEY_EXCHANGE = "?TextSecureKeyExchangs";
}

View File

@ -74,11 +74,11 @@ public class KeyCachingService extends Service {
foregroundService();
broadcastNewSecret();
startTimeoutIfAppropriate();
DecryptingQueue.schedulePendingDecrypts(this, masterSecret);
new Thread() {
@Override
public void run() {
DecryptingQueue.schedulePendingDecrypts(KeyCachingService.this, masterSecret);
MessageNotifier.updateNotification(KeyCachingService.this, masterSecret);
}
}.start();

View File

@ -63,12 +63,12 @@ public class MmsDownloader extends MmscProcessor {
private void handleDownloadMmsAction(DownloadItem item) {
if (!isConnectivityPossible()) {
Log.w("MmsDownloader", "No MMS connectivity available!");
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_NO_CONNECTIVITY);
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY);
toastHandler.makeToast(context.getString(R.string.MmsDownloader_no_connectivity_available_for_mms_download_try_again_later));
return;
}
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_CONNECTING);
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_CONNECTING);
if (item.useMmsRadioMode()) downloadMmsWithRadioChange(item);
else downloadMms(item);
@ -82,13 +82,7 @@ public class MmsDownloader extends MmscProcessor {
private void downloadMms(DownloadItem item) {
Log.w("MmsDownloadService", "Handling actual MMS download...");
MmsDatabase mmsDatabase;
if (item.getMasterSecret() == null) {
mmsDatabase = DatabaseFactory.getMmsDatabase(context);
} else {
mmsDatabase = DatabaseFactory.getEncryptingMmsDatabase(context, item.getMasterSecret());
}
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
try {
RetrieveConf retrieved = MmsDownloadHelper.retrieveMms(context, item.getContentLocation(),
@ -115,12 +109,12 @@ public class MmsDownloader extends MmscProcessor {
Log.w("MmsDownloadeR", "Falling back to radio mode and proxy...");
scheduleDownloadWithRadioModeAndProxy(item);
} else {
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_SOFT_FAILURE);
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE);
toastHandler.makeToast(context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider));
}
} catch (MmsException e) {
Log.w("MmsDownloader", e);
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_HARD_FAILURE);
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_HARD_FAILURE);
toastHandler.makeToast(context.getString(R.string.MmsDownloader_error_storing_mms));
}
}
@ -129,13 +123,16 @@ public class MmsDownloader extends MmscProcessor {
throws MmsException
{
if (retrieved.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieved.getSubject().getString())) {
long messageId = mmsDatabase.insertSecureMessageReceived(retrieved, item.getContentLocation(), item.getThreadId());
long messageId = mmsDatabase.insertSecureMessageInbox(item.getMasterSecret(), 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.insertMessageInbox(item.getMasterSecret(), retrieved, item.getContentLocation(),
item.getThreadId());
}
mmsDatabase.delete(item.getMessageId());
@ -158,7 +155,7 @@ public class MmsDownloader extends MmscProcessor {
pendingMessages.clear();
for (DownloadItem item : downloadItems) {
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Types.DOWNLOAD_NO_CONNECTIVITY);
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY);
}
toastHandler.makeToast(context

View File

@ -54,15 +54,9 @@ public class MmsReceiver {
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);
long threadId = database.getThreadIdForMessage(messageId);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
long messageId = database.insertMessageInbox((NotificationInd)pdu);
long threadId = database.getThreadIdForMessage(messageId);
MessageNotifier.updateNotification(context, masterSecret, threadId);
scheduleDownload((NotificationInd)pdu, messageId, threadId);

View File

@ -65,7 +65,7 @@ public class MmsSender extends MmscProcessor {
if (intent.getAction().equals(SendReceiveService.SEND_MMS_ACTION)) {
long messageId = intent.getLongExtra("message_id", -1);
boolean isCdma = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA;
MmsDatabase database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
try {
List<SendReq> sendRequests = getOutgoingMessages(masterSecret, messageId);
@ -107,14 +107,14 @@ public class MmsSender extends MmscProcessor {
private void sendMmsMessage(SendItem item) {
Log.w("MmsSender", "Sending MMS SendItem...");
MmsDatabase db = DatabaseFactory.getEncryptingMmsDatabase(context, item.masterSecret);
MmsDatabase db = DatabaseFactory.getMmsDatabase(context);
String number = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number();
long messageId = item.request.getDatabaseMessageId();
long messageBox = item.request.getDatabaseMessageBox();
SendReq request = item.request;
if (MmsDatabase.Types.isSecureMmsBox(messageBox)) {
if (MmsDatabase.Types.isSecureType(messageBox)) {
request = getEncryptedMms(item.masterSecret, request, messageId);
}
@ -151,11 +151,7 @@ public class MmsSender extends MmscProcessor {
return;
} else {
Log.w("MmsSender", "Successful send! " + messageId);
if (!MmsDatabase.Types.isSecureMmsBox(messageBox)) {
db.markAsSent(messageId, conf.getMessageId(), conf.getResponseStatus());
} else {
db.markAsSecureSent(messageId, conf.getMessageId(), conf.getResponseStatus());
}
db.markAsSent(messageId, conf.getMessageId(), conf.getResponseStatus());
}
} catch (IOException ioe) {
Log.w("MmsSender", ioe);
@ -168,14 +164,14 @@ public class MmsSender extends MmscProcessor {
private List<SendReq> getOutgoingMessages(MasterSecret masterSecret, long messageId)
throws MmsException
{
MmsDatabase database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
List<SendReq> sendRequests;
if (messageId == -1) {
sendRequests = Arrays.asList(database.getOutgoingMessages());
sendRequests = Arrays.asList(database.getOutgoingMessages(masterSecret));
} else {
sendRequests = new ArrayList<SendReq>(1);
sendRequests.add(database.getSendRequest(messageId));
sendRequests.add(database.getSendRequest(masterSecret, messageId));
}
return sendRequests;

View File

@ -27,7 +27,7 @@ import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.sms.TextMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import java.util.ArrayList;
@ -77,12 +77,12 @@ public class SmsListener extends BroadcastReceiver {
return bodyBuilder.toString();
}
private ArrayList<TextMessage> getAsTextMessages(Intent intent) {
private ArrayList<IncomingTextMessage> getAsTextMessages(Intent intent) {
Object[] pdus = (Object[])intent.getExtras().get("pdus");
ArrayList<TextMessage> messages = new ArrayList<TextMessage>(pdus.length);
ArrayList<IncomingTextMessage> messages = new ArrayList<IncomingTextMessage>(pdus.length);
for (int i=0;i<pdus.length;i++)
messages.add(new TextMessage(SmsMessage.createFromPdu((byte[])pdus[i])));
messages.add(new IncomingTextMessage(SmsMessage.createFromPdu((byte[])pdus[i])));
return messages;
}

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.Intent;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
@ -30,14 +31,16 @@ 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.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.MultipartMessageHandler;
import org.thoughtcrime.securesms.sms.TextMessage;
import java.util.ArrayList;
import java.util.List;
public class SmsReceiver {
@ -49,80 +52,63 @@ public class SmsReceiver {
this.context = context;
}
private String assembleSecureMessageFragments(String sender, String messageBody) {
String localPrefix;
if (WirePrefix.isEncryptedMessage(messageBody)) {
localPrefix = Prefix.ASYMMETRIC_ENCRYPT;
private IncomingTextMessage assembleMessageFragments(List<IncomingTextMessage> messages) {
IncomingTextMessage message = new IncomingTextMessage(messages);
if (WirePrefix.isEncryptedMessage(message.getMessageBody()) || WirePrefix.isKeyExchange(message.getMessageBody())) {
return multipartMessageHandler.processPotentialMultipartMessage(message);
} else {
localPrefix = Prefix.KEY_EXCHANGE;
return message;
}
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(TextMessage[] messages) {
StringBuilder body = new StringBuilder();
private Pair<Long, Long> storeSecureMessage(MasterSecret masterSecret, IncomingTextMessage message) {
Pair<Long, Long> messageAndThreadId = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageInbox(masterSecret, message);
for (TextMessage message : messages) {
body.append(message.getMessage());
if (masterSecret != null) {
DecryptingQueue.scheduleDecryption(context, masterSecret, messageAndThreadId.first,
message.getSender(), message.getMessageBody(),
message.isSecureMessage());
}
String messageBody = body.toString();
return messageAndThreadId;
}
if (WirePrefix.isEncryptedMessage(messageBody) || WirePrefix.isKeyExchange(messageBody)) {
return assembleSecureMessageFragments(messages[0].getSender(), messageBody);
private Pair<Long, Long> storeStandardMessage(MasterSecret masterSecret, IncomingTextMessage message) {
EncryptingSmsDatabase encryptingDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsDatabase plaintextDatabase = DatabaseFactory.getSmsDatabase(context);
if (masterSecret != null) {
return encryptingDatabase.insertMessageInbox(masterSecret, message);
} else if (MasterSecretUtil.hasAsymmericMasterSecret(context)) {
return encryptingDatabase.insertMessageInbox(MasterSecretUtil.getAsymmetricMasterSecret(context, null), message);
} else {
return messageBody;
return plaintextDatabase.insertMessageInbox(message);
}
}
private long storeSecureMessage(MasterSecret masterSecret, TextMessage 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.getSender(), messageBody);
return messageId;
}
private long storeStandardMessage(MasterSecret masterSecret, TextMessage 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 long storeKeyExchangeMessage(MasterSecret masterSecret, TextMessage message, String messageBody) {
private Pair<Long, Long> storeKeyExchangeMessage(MasterSecret masterSecret,
IncomingKeyExchangeMessage message)
{
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(ApplicationPreferencesActivity.AUTO_KEY_EXCHANGE_PREF, true)) {
try {
Recipient recipient = new Recipient(null, message.getSender(), null, null);
KeyExchangeMessage keyExchangeMessage = new KeyExchangeMessage(messageBody);
KeyExchangeMessage keyExchangeMessage = new KeyExchangeMessage(message.getMessageBody());
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;
message.setStale(true);
} 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);
message.setProcessed(true);
processor.processKeyExchangeMessage(keyExchangeMessage, threadId);
return messageId;
Pair<Long, Long> messageAndThreadId = storeStandardMessage(masterSecret, message);
processor.processKeyExchangeMessage(keyExchangeMessage, messageAndThreadId.second);
return messageAndThreadId;
}
} catch (InvalidVersionException e) {
Log.w("SmsReceiver", e);
@ -131,41 +117,22 @@ public class SmsReceiver {
}
}
return storeStandardMessage(masterSecret, message, messageBody);
return storeStandardMessage(masterSecret, message);
}
private long storeMessage(MasterSecret masterSecret, TextMessage message, String messageBody) {
if (messageBody.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) {
return storeSecureMessage(masterSecret, message, messageBody);
} else if (messageBody.startsWith(Prefix.KEY_EXCHANGE)) {
return storeKeyExchangeMessage(masterSecret, message, messageBody);
} else {
return storeStandardMessage(masterSecret, message, messageBody);
}
private Pair<Long, Long> storeMessage(MasterSecret masterSecret, IncomingTextMessage message) {
if (message.isSecureMessage()) return storeSecureMessage(masterSecret, message);
else if (message.isKeyExchange()) return storeKeyExchangeMessage(masterSecret, (IncomingKeyExchangeMessage)message);
else return storeStandardMessage(masterSecret, message);
}
// 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) {
ArrayList<TextMessage> messagesList = intent.getExtras().getParcelableArrayList("text_messages");
TextMessage[] messages = messagesList.toArray(new TextMessage[0]);
// Bundle bundle = intent.getExtras();
// SmsMessage[] messages = parseMessages(bundle);
String message = assembleMessageFragments(messages);
List<IncomingTextMessage> messagesList = intent.getExtras().getParcelableArrayList("text_messages");
IncomingTextMessage message = assembleMessageFragments(messagesList);
if (message != null) {
long messageId = storeMessage(masterSecret, messages[0], message);
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
MessageNotifier.updateNotification(context, masterSecret, threadId);
Pair<Long, Long> messageAndThreadId = storeMessage(masterSecret, message);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
}

View File

@ -17,45 +17,30 @@
package org.thoughtcrime.securesms.service;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
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.gcm.OptimizingTransport;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
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.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService.ToastHandler;
import org.thoughtcrime.securesms.sms.MultipartMessageHandler;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import org.thoughtcrime.securesms.util.InvalidMessageException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.transport.UniversalTransport;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
public class SmsSender {
private final MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler();
private final Set<Long> pendingMessages = new HashSet<Long>();
private final Set<Long> pendingMessages = new HashSet<Long>();
private final Context context;
private final ToastHandler toastHandler;
@ -76,53 +61,43 @@ public class SmsSender {
}
private void handleSendMessage(MasterSecret masterSecret, Intent intent) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
long messageId = intent.getLongExtra("message_id", -1);
Cursor c = null;
long messageId = intent.getLongExtra("message_id", -1);
UniversalTransport transport = new UniversalTransport(context, masterSecret);
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Log.w("SMSSenderService", "Processing outgoing message: " + messageId);
EncryptingSmsDatabase.Reader reader = null;
SmsMessageRecord record;
Log.w("SmsSender", "Sending message: " + messageId);
try {
if (messageId == -1) c = DatabaseFactory.getSmsDatabase(context).getOutgoingMessages();
else c = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
if (messageId != -1) reader = database.getMessage(masterSecret, messageId);
else reader = database.getOutgoingMessages(masterSecret);
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);
if (!pendingMessages.contains(messageId)) {
Log.w("SMSSenderService", "Actually delivering: " + messageId);
pendingMessages.add(messageId);
deliverTextMessage(address, messageText, messageId, type);
}
} while (c.moveToNext());
while (reader != null && (record = reader.getNext()) != null) {
if (!pendingMessages.contains(record.getId())) {
pendingMessages.add(record.getId());
transport.deliver(record);
}
}
} catch (UndeliverableMessageException ude) {
Log.w("SmsSender", ude);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
} finally {
if (c != null)
c.close();
if (reader != null)
reader.close();
}
}
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);
Log.w("SMSReceiverService", "Running sent callback: " + messageId);
if (result == Activity.RESULT_OK) {
DatabaseFactory.getSmsDatabase(context).markAsSent(messageId, type);
DatabaseFactory.getSmsDatabase(context).markAsSent(messageId);
unregisterForRadioChanges();
} else if (result == SmsManager.RESULT_ERROR_NO_SERVICE || result == SmsManager.RESULT_ERROR_RADIO_OFF) {
toastHandler
@ -143,9 +118,7 @@ public class SmsSender {
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) {
@ -167,133 +140,7 @@ public class SmsSender {
try {
context.unregisterReceiver(SystemStateListener.getInstance());
} catch (IllegalArgumentException iae) {
Log.w("SmsSender", iae);
}
}
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 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;
}
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);
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(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 {
OptimizingTransport.sendMultipartTextMessage(context, recipient, messages, sentIntents, deliveredIntents);
//
// SmsManager.getDefault().sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents);
} 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);
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(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 {
OptimizingTransport.sendTextMessage(context, recipient, messages.get(i), sentIntents.get(i),
deliveredIntents == null ? null : deliveredIntents.get(i));
// SmsManager.getDefault().sendTextMessage(recipient, null, messages.get(i), sentIntents.get(i),
// deliveredIntents == null ? null : deliveredIntents.get(i));
} catch (NullPointerException npe) {
Log.w("SmsSender", npe);
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
} catch (IllegalArgumentException iae) {
Log.w("SmsSender", iae);
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, null), new SmsTransportDetails());
return new String(cipher.encryptMessage(body.getBytes()));
}
}
}

View File

@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.sms;
public class IncomingEncryptedMessage extends IncomingTextMessage {
IncomingEncryptedMessage(IncomingTextMessage base, String newBody) {
super(base, newBody);
}
@Override
public IncomingTextMessage withMessageBody(String body) {
return new IncomingEncryptedMessage(this, body);
}
@Override
public boolean isSecureMessage() {
return true;
}
}

View File

@ -0,0 +1,42 @@
package org.thoughtcrime.securesms.sms;
public class IncomingKeyExchangeMessage extends IncomingTextMessage {
private boolean isStale;
private boolean isProcessed;
IncomingKeyExchangeMessage(IncomingTextMessage base, String newBody) {
super(base, newBody);
if (base instanceof IncomingKeyExchangeMessage) {
this.isStale = ((IncomingKeyExchangeMessage)base).isStale;
this.isProcessed = ((IncomingKeyExchangeMessage)base).isProcessed;
}
}
@Override
public IncomingTextMessage withMessageBody(String messageBody) {
return new IncomingKeyExchangeMessage(this, messageBody);
}
public boolean isStale() {
return isStale;
}
public boolean isProcessed() {
return isProcessed;
}
public void setStale(boolean isStale) {
this.isStale = isStale;
}
public void setProcessed(boolean isProcessed) {
this.isProcessed = isProcessed;
}
@Override
public boolean isKeyExchange() {
return true;
}
}

View File

@ -6,17 +6,19 @@ import android.telephony.SmsMessage;
import org.thoughtcrime.securesms.gcm.IncomingGcmMessage;
public class TextMessage implements Parcelable {
import java.util.List;
public static final Parcelable.Creator<TextMessage> CREATOR = new Parcelable.Creator<TextMessage>() {
public class IncomingTextMessage implements Parcelable {
public static final Parcelable.Creator<IncomingTextMessage> CREATOR = new Parcelable.Creator<IncomingTextMessage>() {
@Override
public TextMessage createFromParcel(Parcel in) {
return new TextMessage(in);
public IncomingTextMessage createFromParcel(Parcel in) {
return new IncomingTextMessage(in);
}
@Override
public TextMessage[] newArray(int size) {
return new TextMessage[size];
public IncomingTextMessage[] newArray(int size) {
return new IncomingTextMessage[size];
}
};
@ -28,7 +30,7 @@ public class TextMessage implements Parcelable {
private final String pseudoSubject;
private final long sentTimestampMillis;
public TextMessage(SmsMessage message) {
public IncomingTextMessage(SmsMessage message) {
this.message = message.getDisplayMessageBody();
this.sender = message.getDisplayOriginatingAddress();
this.protocol = message.getProtocolIdentifier();
@ -38,7 +40,7 @@ public class TextMessage implements Parcelable {
this.sentTimestampMillis = message.getTimestampMillis();
}
public TextMessage(IncomingGcmMessage message) {
public IncomingTextMessage(IncomingGcmMessage message) {
this.message = message.getMessageText();
this.sender = message.getSource();
this.protocol = 31337;
@ -48,7 +50,7 @@ public class TextMessage implements Parcelable {
this.sentTimestampMillis = message.getTimestampMillis();
}
public TextMessage(Parcel in) {
public IncomingTextMessage(Parcel in) {
this.message = in.readString();
this.sender = in.readString();
this.protocol = in.readInt();
@ -58,6 +60,32 @@ public class TextMessage implements Parcelable {
this.sentTimestampMillis = in.readLong();
}
public IncomingTextMessage(IncomingTextMessage base, String newBody) {
this.message = newBody;
this.sender = base.getSender();
this.protocol = base.getProtocol();
this.serviceCenterAddress = base.getServiceCenterAddress();
this.replyPathPresent = base.isReplyPathPresent();
this.pseudoSubject = base.getPseudoSubject();
this.sentTimestampMillis = base.getSentTimestampMillis();
}
public IncomingTextMessage(List<IncomingTextMessage> fragments) {
StringBuilder body = new StringBuilder();
for (IncomingTextMessage message : fragments) {
body.append(message.getMessageBody());
}
this.message = body.toString();
this.sender = fragments.get(0).getSender();
this.protocol = fragments.get(0).getProtocol();
this.serviceCenterAddress = fragments.get(0).getServiceCenterAddress();
this.replyPathPresent = fragments.get(0).isReplyPathPresent();
this.pseudoSubject = fragments.get(0).getPseudoSubject();
this.sentTimestampMillis = fragments.get(0).getSentTimestampMillis();
}
public long getSentTimestampMillis() {
return sentTimestampMillis;
}
@ -66,10 +94,14 @@ public class TextMessage implements Parcelable {
return pseudoSubject;
}
public String getMessage() {
public String getMessageBody() {
return message;
}
public IncomingTextMessage withMessageBody(String message) {
return new IncomingTextMessage(this, message);
}
public String getSender() {
return sender;
}
@ -86,6 +118,14 @@ public class TextMessage implements Parcelable {
return replyPathPresent;
}
public boolean isKeyExchange() {
return false;
}
public boolean isSecureMessage() {
return false;
}
@Override
public int describeContents() {
return 0;

View File

@ -18,10 +18,8 @@ package org.thoughtcrime.securesms.sms;
import android.content.Context;
import android.content.Intent;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.KeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.mms.SlideDeck;
@ -30,6 +28,8 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.SendReceiveService;
import java.util.List;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.EncodedStringValue;
@ -70,31 +70,17 @@ public class MessageSender {
return threadId;
}
public static long send(Context context, MasterSecret masterSecret, Recipients recipients,
long threadId, String message, boolean forcePlaintext)
public static long send(Context context, MasterSecret masterSecret,
OutgoingTextMessage message, long threadId)
{
if (threadId == -1)
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(message.getRecipients());
long date = System.currentTimeMillis();
List<Long> messageIds = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageOutbox(masterSecret, threadId, message);
for (Recipient recipient : recipients.getRecipientsList()) {
boolean isSecure = KeyUtil.isSessionFor(context, recipient) && !forcePlaintext;
long messageId;
if (!isSecure) {
messageId = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageSent(masterSecret,
PhoneNumberUtils.formatNumber(recipient.getNumber()),
threadId, message, date);
} else {
messageId = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertSecureMessageSent(masterSecret,
PhoneNumberUtils.formatNumber(recipient.getNumber()),
threadId, message, date);
}
for (long messageId : messageIds) {
Log.w("SMSSender", "Got message id for new message: " + messageId);
Intent intent = new Intent(SendReceiveService.SEND_SMS_ACTION, null,
@ -115,8 +101,8 @@ public class MessageSender {
sendRequest.setTo(encodedNumbers);
long messageId = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret)
.insertMessageSent(sendRequest, threadId, secure);
long messageId = DatabaseFactory.getMmsDatabase(context)
.insertMessageOutbox(masterSecret, sendRequest, threadId, secure);
Intent intent = new Intent(SendReceiveService.SEND_MMS_ACTION, null,
context, SendReceiveService.class);

View File

@ -18,10 +18,7 @@ package org.thoughtcrime.securesms.sms;
import android.util.Log;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.Hex;
import java.io.IOException;
import java.util.ArrayList;
@ -29,138 +26,59 @@ import java.util.HashMap;
public class MultipartMessageHandler {
private static final int VERSION_OFFSET = 0;
private static final int MULTIPART_OFFSET = 1;
private static final int IDENTIFIER_OFFSET = 2;
private final HashMap<String, MultipartTransportMessageFragments> partialMessages =
new HashMap<String, MultipartTransportMessageFragments>();
private static final int MULTIPART_SUPPORTED_AFTER_VERSION = 1;
private final HashMap<String, Integer> idMap = new HashMap<String, Integer>();
private final HashMap<String, byte[][]> partialMessages = new HashMap<String, byte[][]>();
private final HashMap<String, Integer> idMap = new HashMap<String, Integer>();
private String spliceMessage(String prefix, byte[][] messageParts) {
Log.w("MultipartMessageHandler", "Have complete message fragments, splicing...");
int totalMessageLength = 0;
for (int i=0;i<messageParts.length;i++) {
totalMessageLength += messageParts[i].length;
}
byte[] totalMessage = new byte[totalMessageLength];
int totalMessageOffset = 0;
for (int i=0;i<messageParts.length;i++) {
System.arraycopy(messageParts[i], 0, totalMessage, totalMessageOffset, messageParts[i].length);
totalMessageOffset += messageParts[i].length;
}
return prefix + Base64.encodeBytesWithoutPadding(totalMessage);
}
private boolean isComplete(byte[][] partialMessages) {
for (int i=0;i<partialMessages.length;i++)
if (partialMessages[i] == null) return false;
Log.w("MultipartMessageHandler", "Buffer complete!");
return true;
}
private byte[][] findOrAllocateMultipartBuffer(String sender, int identifier, int count) {
String key = sender + identifier;
Log.w("MultipartMessageHandler", "Getting multipart buffer...");
if (partialMessages.containsKey(key)) {
Log.w("MultipartMessageHandler", "Returning existing multipart buffer...");
return partialMessages.get(key);
} else {
Log.w("MultipartMessageHandler", "Creating new multipart buffer: " + count);
byte[][] multipartBuffer = new byte[count][];
partialMessages.put(key, multipartBuffer);
return multipartBuffer;
}
}
private byte[] stripMultipartTransportLayer(int index, byte[] decodedMessage) {
byte[] strippedMessage = new byte[decodedMessage.length - (index == 0 ? 2 : 3)];
int copyDestinationIndex = 0;
int copyDestinationLength = strippedMessage.length;
if (index == 0) {
strippedMessage[0] = decodedMessage[0];
copyDestinationIndex++;
copyDestinationLength--;
}
System.arraycopy(decodedMessage, 3, strippedMessage, copyDestinationIndex, copyDestinationLength);
return strippedMessage;
}
private String processMultipartMessage(String prefix, int index, int count, String sender, int identifier, byte[] decodedMessage) {
private IncomingTextMessage processMultipartMessage(MultipartTransportMessage message) {
Log.w("MultipartMessageHandler", "Processing multipart message...");
decodedMessage = stripMultipartTransportLayer(index, decodedMessage);
byte[][] messageParts = findOrAllocateMultipartBuffer(sender, identifier, count);
messageParts[index] = decodedMessage;
MultipartTransportMessageFragments container = partialMessages.get(message.getKey());
Log.w("MultipartMessageHandler", "Filled buffer at index: " + index);
if (container == null) {
container = new MultipartTransportMessageFragments(message.getMultipartCount());
partialMessages.put(message.getKey(), container);
}
if (!isComplete(messageParts))
container.add(message);
Log.w("MultipartMessageHandler", "Filled buffer at index: " + message.getMultipartIndex());
if (!container.isComplete())
return null;
partialMessages.remove(sender+identifier);
return spliceMessage(prefix, messageParts);
}
partialMessages.remove(message.getKey());
String strippedMessage = Base64.encodeBytesWithoutPadding(container.getJoined());
private String processSinglePartMessage(String prefix, byte[] decodedMessage) {
Log.w("MultipartMessageHandler", "Processing single part message...");
decodedMessage[MULTIPART_OFFSET] = decodedMessage[VERSION_OFFSET];
return prefix + Base64.encodeBytesWithoutPadding(decodedMessage, 1, decodedMessage.length-1);
}
public String processPotentialMultipartMessage(String prefix, String sender, String message) {
try {
byte[] decodedMessage = Base64.decodeWithoutPadding(message);
int currentVersion = Conversions.highBitsToInt(decodedMessage[VERSION_OFFSET]);
Log.w("MultipartMessageHandler", "Decoded message with version: " + currentVersion);
Log.w("MultipartMessageHandler", "Decoded message: " + Hex.toString(decodedMessage));
if (currentVersion < MULTIPART_SUPPORTED_AFTER_VERSION)
throw new AssertionError("Caller should have checked this.");
int multipartIndex = Conversions.highBitsToInt(decodedMessage[MULTIPART_OFFSET]);
int multipartCount = Conversions.lowBitsToInt(decodedMessage[MULTIPART_OFFSET]);
int identifier = decodedMessage[IDENTIFIER_OFFSET] & 0xFF;
Log.w("MultipartMessageHandler", "Multipart Info: (" + multipartIndex + "/" + multipartCount + ") ID: " + identifier);
if (multipartIndex >= multipartCount)
return message;
if (multipartCount == 1) return processSinglePartMessage(prefix, decodedMessage);
else return processMultipartMessage(prefix, multipartIndex, multipartCount, sender, identifier, decodedMessage);
} catch (IOException e) {
return message;
if (message.getWireType() == MultipartTransportMessage.WIRETYPE_KEY) {
return new IncomingKeyExchangeMessage(message.getBaseMessage(), strippedMessage);
} else {
return new IncomingEncryptedMessage(message.getBaseMessage(), strippedMessage);
}
}
private ArrayList<String> buildSingleMessage(byte[] decodedMessage, WirePrefix prefix) {
Log.w("MultipartMessageHandler", "Adding transport info to single-part message...");
private IncomingTextMessage processSinglePartMessage(MultipartTransportMessage message) {
Log.w("MultipartMessageHandler", "Processing single part message...");
String strippedMessage = Base64.encodeBytesWithoutPadding(message.getStrippedMessage());
ArrayList<String> list = new ArrayList<String>();
byte[] messageWithMultipartHeader = new byte[decodedMessage.length + 1];
System.arraycopy(decodedMessage, 0, messageWithMultipartHeader, 1, decodedMessage.length);
if (message.getWireType() == MultipartTransportMessage.WIRETYPE_KEY) {
return new IncomingKeyExchangeMessage(message.getBaseMessage(), strippedMessage);
} else {
return new IncomingEncryptedMessage(message.getBaseMessage(), strippedMessage);
}
}
messageWithMultipartHeader[0] = decodedMessage[0];
messageWithMultipartHeader[1] = Conversions.intsToByteHighAndLow(0, 1);
String encodedMessage = Base64.encodeBytesWithoutPadding(messageWithMultipartHeader);
public IncomingTextMessage processPotentialMultipartMessage(IncomingTextMessage message) {
try {
MultipartTransportMessage transportMessage = new MultipartTransportMessage(message);
list.add(prefix.calculatePrefix(encodedMessage) + encodedMessage);
Log.w("MultipartMessageHandler", "Complete fragment size: " + list.get(list.size()-1).length());
return list;
if (transportMessage.isInvalid()) return message;
else if (transportMessage.isSinglePart()) return processSinglePartMessage(transportMessage);
else return processMultipartMessage(transportMessage);
} catch (IOException e) {
Log.w("MultipartMessageHandler", e);
return message;
}
}
private byte getIdForRecipient(String recipient) {
@ -179,56 +97,10 @@ public class MultipartMessageHandler {
return id;
}
private ArrayList<String> buildMultipartMessage(String recipient, byte[] decodedMessage, WirePrefix prefix) {
Log.w("MultipartMessageHandler", "Building multipart message...");
public ArrayList<String> divideMessage(OutgoingTextMessage message) {
ArrayList<String> list = new ArrayList<String>();
byte versionByte = decodedMessage[0];
int messageOffset = 1;
int segmentIndex = 0;
int segmentCount = SmsTransportDetails.getMessageCountForBytes(decodedMessage.length);
byte id = getIdForRecipient(recipient);
while (messageOffset < decodedMessage.length-1) {
int segmentSize = Math.min(SmsTransportDetails.BASE_MAX_BYTES, decodedMessage.length-messageOffset+3);
byte[] segment = new byte[segmentSize];
segment[0] = versionByte;
segment[1] = Conversions.intsToByteHighAndLow(segmentIndex++, segmentCount);
segment[2] = id;
Log.w("MultipartMessageHandler", "Fragment: (" + segmentIndex + "/" + segmentCount +") -- ID: " + id);
System.arraycopy(decodedMessage, messageOffset, segment, 3, segmentSize-3);
messageOffset += segmentSize-3;
String encodedSegment = Base64.encodeBytesWithoutPadding(segment);
list.add(prefix.calculatePrefix(encodedSegment) + encodedSegment);
Log.w("MultipartMessageHandler", "Complete fragment size: " + list.get(list.size()-1).length());
}
return list;
}
public boolean isManualTransport(String message) {
try {
byte[] decodedMessage = Base64.decodeWithoutPadding(message);
return Conversions.highBitsToInt(decodedMessage[0]) >= MULTIPART_SUPPORTED_AFTER_VERSION;
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
}
public ArrayList<String> divideMessage(String recipient, String message, WirePrefix prefix) {
try {
byte[] decodedMessage = Base64.decodeWithoutPadding(message);
if (decodedMessage.length <= SmsTransportDetails.SINGLE_MESSAGE_MAX_BYTES)
return buildSingleMessage(decodedMessage, prefix);
else
return buildMultipartMessage(recipient, decodedMessage, prefix);
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
byte identifier = getIdForRecipient(message.getRecipients().getPrimaryRecipient().getNumber());
return MultipartTransportMessage.getEncoded(message, identifier);
}
}

View File

@ -0,0 +1,209 @@
package org.thoughtcrime.securesms.sms;
import android.util.Log;
import org.thoughtcrime.securesms.protocol.KeyExchangeWirePrefix;
import org.thoughtcrime.securesms.protocol.SecureMessageWirePrefix;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.Hex;
import java.io.IOException;
import java.util.ArrayList;
public class MultipartTransportMessage {
private static final String TAG = MultipartTransportMessage.class.getName();
private static final int MULTIPART_SUPPORTED_AFTER_VERSION = 1;
public static final int WIRETYPE_SECURE = 1;
public static final int WIRETYPE_KEY = 2;
private static final int VERSION_OFFSET = 0;
private static final int MULTIPART_OFFSET = 1;
private static final int IDENTIFIER_OFFSET = 2;
private final int wireType;
private final byte[] decodedMessage;
private final IncomingTextMessage message;
public MultipartTransportMessage(IncomingTextMessage message) throws IOException {
this.message = message;
this.wireType = WirePrefix.isEncryptedMessage(message.getMessageBody()) ? WIRETYPE_SECURE : WIRETYPE_KEY;
this.decodedMessage = Base64.decodeWithoutPadding(message.getMessageBody().substring(WirePrefix.PREFIX_SIZE));
Log.w(TAG, "Decoded message with version: " + getCurrentVersion());
Log.w(TAG, "Decoded message: " + Hex.toString(decodedMessage));
}
public int getWireType() {
return wireType;
}
public int getCurrentVersion() {
return Conversions.highBitsToInt(decodedMessage[VERSION_OFFSET]);
}
public int getMultipartIndex() {
return Conversions.highBitsToInt(decodedMessage[MULTIPART_OFFSET]);
}
public int getMultipartCount() {
if (isDeprecatedTransport())
return 1;
return Conversions.lowBitsToInt(decodedMessage[MULTIPART_OFFSET]);
}
public int getIdentifier() {
return decodedMessage[IDENTIFIER_OFFSET] & 0xFF;
}
public boolean isDeprecatedTransport() {
return getCurrentVersion() < MULTIPART_SUPPORTED_AFTER_VERSION;
}
public boolean isInvalid() {
return getMultipartIndex() >= getMultipartCount();
}
public boolean isSinglePart() {
return getMultipartCount() == 1;
}
public byte[] getStrippedMessage() {
if (isDeprecatedTransport()) return getStrippedMessageForDeprecatedTransport();
else if (getMultipartCount() == 1) return getStrippedMessageForSinglePart();
else return getStrippedMessageForMultiPart();
}
/*
* We're dealing with a message that isn't using the multipart transport.
*
*/
private byte[] getStrippedMessageForDeprecatedTransport() {
return decodedMessage;
}
/*
* We're dealing with a transport message that is of the format:
* Version (1 byte)
* Index_And_Count (1 byte)
* Message (remainder)
*
* The version byte was stolen off the message, so we strip Index_And_Count byte out,
* put the version byte back on the front of the message, and return.
*/
private byte[] getStrippedMessageForSinglePart() {
byte[] stripped = new byte[decodedMessage.length - 1];
System.arraycopy(decodedMessage, 1, stripped, 0, decodedMessage.length - 1);
stripped[0] = decodedMessage[VERSION_OFFSET];
return stripped;
}
/*
* We're dealing with a transport message that is of the format:
*
* Version (1 byte)
* Index_And_Count (1 byte)
* Identifier (1 byte)
* Message (remainder)
*
* The version byte was stolen off the first byte of the message, but only for the first fragment
* of the message. So for the first fragment we strip off everything and put the version byte
* back on. For the remaining fragments, we just strip everything.
*/
private byte[] getStrippedMessageForMultiPart() {
byte[] strippedMessage = new byte[decodedMessage.length - (getMultipartIndex() == 0 ? 2 : 3)];
int copyDestinationIndex = 0;
int copyDestinationLength = strippedMessage.length;
if (getMultipartIndex() == 0) {
strippedMessage[0] = decodedMessage[0];
copyDestinationIndex++;
copyDestinationLength--;
}
System.arraycopy(decodedMessage, 3, strippedMessage, copyDestinationIndex, copyDestinationLength);
return strippedMessage;
}
public String getKey() {
return message.getSender() + getIdentifier();
}
public IncomingTextMessage getBaseMessage() {
return message;
}
public static ArrayList<String> getEncoded(OutgoingTextMessage message, byte identifier) {
try {
byte[] decoded = Base64.decodeWithoutPadding(message.getMessageBody());
int count = SmsTransportDetails.getMessageCountForBytes(decoded.length);
WirePrefix prefix;
if (message.isKeyExchange()) prefix = new KeyExchangeWirePrefix();
else prefix = new SecureMessageWirePrefix();
if (count == 1) return getSingleEncoded(decoded, prefix);
else return getMultiEncoded(decoded, prefix, count, identifier);
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static ArrayList<String> getSingleEncoded(byte[] decoded, WirePrefix prefix) {
ArrayList<String> list = new ArrayList<String>(1);
byte[] messageWithMultipartHeader = new byte[decoded.length + 1];
System.arraycopy(decoded, 0, messageWithMultipartHeader, 1, decoded.length);
messageWithMultipartHeader[VERSION_OFFSET] = decoded[VERSION_OFFSET];
messageWithMultipartHeader[MULTIPART_OFFSET] = Conversions.intsToByteHighAndLow(0, 1);
String encodedMessage = Base64.encodeBytesWithoutPadding(messageWithMultipartHeader);
list.add(prefix.calculatePrefix(encodedMessage) + encodedMessage);
Log.w(TAG, "Complete fragment size: " + list.get(list.size()-1).length());
return list;
}
private static ArrayList<String> getMultiEncoded(byte[] decoded, WirePrefix prefix,
int segmentCount, byte id)
{
ArrayList<String> list = new ArrayList<String>(segmentCount);
byte versionByte = decoded[VERSION_OFFSET];
int messageOffset = 1;
int segmentIndex = 0;
while (messageOffset < decoded.length-1) {
int segmentSize = Math.min(SmsTransportDetails.BASE_MAX_BYTES, decoded.length-messageOffset+3);
byte[] segment = new byte[segmentSize];
segment[VERSION_OFFSET] = versionByte;
segment[MULTIPART_OFFSET] = Conversions.intsToByteHighAndLow(segmentIndex++, segmentCount);
segment[IDENTIFIER_OFFSET] = id;
Log.w(TAG, "Fragment: (" + segmentIndex + "/" + segmentCount +") -- ID: " + id);
System.arraycopy(decoded, messageOffset, segment, 3, segmentSize-3);
messageOffset += segmentSize-3;
String encodedSegment = Base64.encodeBytesWithoutPadding(segment);
list.add(prefix.calculatePrefix(encodedSegment) + encodedSegment);
Log.w(TAG, "Complete fragment size: " + list.get(list.size()-1).length());
}
return list;
}
}

View File

@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.sms;
public class MultipartTransportMessageFragments {
private final byte[][] fragments;
public MultipartTransportMessageFragments(int count) {
this.fragments = new byte[count][];
}
public void add(MultipartTransportMessage fragment) {
this.fragments[fragment.getMultipartIndex()] = fragment.getStrippedMessage();
}
public boolean isComplete() {
for (int i=0;i<fragments.length;i++)
if (fragments[i] == null) return false;
return true;
}
public byte[] getJoined() {
int totalMessageLength = 0;
for (int i=0;i<fragments.length;i++) {
totalMessageLength += fragments[i].length;
}
byte[] totalMessage = new byte[totalMessageLength];
int totalMessageOffset = 0;
for (int i=0;i<fragments.length;i++) {
System.arraycopy(fragments[i], 0, totalMessage, totalMessageOffset, fragments[i].length);
totalMessageOffset += fragments[i].length;
}
return totalMessage;
}
}

View File

@ -0,0 +1,29 @@
package org.thoughtcrime.securesms.sms;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
public class OutgoingEncryptedMessage extends OutgoingTextMessage {
public OutgoingEncryptedMessage(Recipients recipients, String body) {
super(recipients, body);
}
public OutgoingEncryptedMessage(Recipient recipient, String body) {
super(recipient, body);
}
private OutgoingEncryptedMessage(OutgoingEncryptedMessage base, String body) {
super(base, body);
}
@Override
public boolean isSecureMessage() {
return true;
}
@Override
public OutgoingTextMessage withBody(String body) {
return new OutgoingEncryptedMessage(this, body);
}
}

View File

@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.sms;
import org.thoughtcrime.securesms.recipients.Recipient;
public class OutgoingKeyExchangeMessage extends OutgoingTextMessage {
public OutgoingKeyExchangeMessage(Recipient recipient, String message) {
super(recipient, message);
}
private OutgoingKeyExchangeMessage(OutgoingKeyExchangeMessage base, String body) {
super(base, body);
}
@Override
public boolean isKeyExchange() {
return true;
}
@Override
public OutgoingTextMessage withBody(String body) {
return new OutgoingKeyExchangeMessage(this, body);
}
}

View File

@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.sms;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
public class OutgoingTextMessage {
private final Recipients recipients;
private String message;
public OutgoingTextMessage(Recipient recipient, String message) {
this(new Recipients(recipient), message);
}
public OutgoingTextMessage(Recipients recipients, String message) {
this.recipients = recipients;
this.message = message;
}
protected OutgoingTextMessage(OutgoingTextMessage base, String body) {
this.recipients = base.getRecipients();
this.message = body;
}
public String getMessageBody() {
return message;
}
public Recipients getRecipients() {
return recipients;
}
public boolean isKeyExchange() {
return false;
}
public boolean isSecureMessage() {
return false;
}
public static OutgoingTextMessage from(SmsMessageRecord record) {
if (record.isSecure()) {
return new OutgoingEncryptedMessage(record.getIndividualRecipient(), record.getBody());
} else if (record.isKeyExchange()) {
return new OutgoingKeyExchangeMessage(record.getIndividualRecipient(), record.getBody());
} else {
return new OutgoingTextMessage(record.getIndividualRecipient(), record.getBody());
}
}
public OutgoingTextMessage withBody(String body) {
return new OutgoingTextMessage(this, body);
}
}

View File

@ -16,15 +16,14 @@
*/
package org.thoughtcrime.securesms.sms;
import java.io.IOException;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.SessionCipher;
import org.thoughtcrime.securesms.crypto.TransportDetails;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.util.Base64;
import android.util.Log;
import java.io.IOException;
public class SmsTransportDetails implements TransportDetails {
@ -51,14 +50,12 @@ public class SmsTransportDetails implements TransportDetails {
public byte[] encodeMessage(byte[] messageWithMac) {
String encodedMessage = Base64.encodeBytesWithoutPadding(messageWithMac);
Log.w("SmsTransportDetails", "Encoded Message Length: " + encodedMessage.length());
return (Prefix.ASYMMETRIC_ENCRYPT + encodedMessage).getBytes();
return encodedMessage.getBytes();
}
public byte[] decodeMessage(byte[] encodedMessageBytes) throws IOException {
String encodedMessage = new String(encodedMessageBytes);
encodedMessage = encodedMessage.substring(Prefix.ASYMMETRIC_ENCRYPT.length());
return Base64.decodeWithoutPadding(encodedMessage);
}
@ -67,8 +64,8 @@ public class SmsTransportDetails implements TransportDetails {
for (int i=1;i<messageWithPadding.length;i++) {
if (messageWithPadding[i] == (byte)0x00) {
paddingBeginsIndex = i;
break;
paddingBeginsIndex = i;
break;
}
}
@ -102,7 +99,7 @@ public class SmsTransportDetails implements TransportDetails {
return paddedBody;
}
public static final int getMessageCountForBytes(int bytes) {
public static int getMessageCountForBytes(int bytes) {
if (bytes <= SINGLE_MESSAGE_MAX_BYTES)
return 1;

View File

@ -0,0 +1,44 @@
package org.thoughtcrime.securesms.transport;
import org.thoughtcrime.securesms.crypto.TransportDetails;
import org.thoughtcrime.securesms.util.Base64;
import java.io.IOException;
public class BaseTransportDetails implements TransportDetails {
@Override
public byte[] stripPaddedMessage(byte[] messageWithPadding) {
int paddingBeginsIndex = 0;
for (int i=1;i<messageWithPadding.length;i++) {
if (messageWithPadding[i] == (byte)0x00) {
paddingBeginsIndex = i;
break;
}
}
if (paddingBeginsIndex == 0)
return messageWithPadding;
byte[] message = new byte[paddingBeginsIndex];
System.arraycopy(messageWithPadding, 0, message, 0, message.length);
return message;
}
@Override
public byte[] getPaddedMessageBody(byte[] messageBody) {
return messageBody;
}
@Override
public byte[] encodeMessage(byte[] messageWithMac) {
return Base64.encodeBytesWithoutPadding(messageWithMac).getBytes();
}
@Override
public byte[] decodeMessage(byte[] encodedMessageBytes) throws IOException {
return Base64.decodeWithoutPadding(new String(encodedMessageBytes));
}
}

View File

@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.transport;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import java.io.IOException;
public class GcmTransport {
private final Context context;
private final MasterSecret masterSecret;
public GcmTransport(Context context, MasterSecret masterSecret) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
}
public void deliver(SmsMessageRecord message) throws IOException {
}
}

View File

@ -0,0 +1,136 @@
package org.thoughtcrime.securesms.transport;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.telephony.SmsManager;
import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SessionCipher;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService;
import org.thoughtcrime.securesms.service.SmsListener;
import org.thoughtcrime.securesms.sms.MultipartMessageHandler;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
import java.util.ArrayList;
public class SmsTransport {
private final Context context;
private final MasterSecret masterSecret;
public SmsTransport(Context context, MasterSecret masterSecret) {
this.context = context.getApplicationContext();
this.masterSecret = masterSecret;
}
public void deliver(SmsMessageRecord message) throws UndeliverableMessageException {
if (message.isSecure() || message.isKeyExchange()) {
deliverSecureMessage(message);
} else {
deliverPlaintextMessage(message);
}
}
private void deliverSecureMessage(SmsMessageRecord message) throws UndeliverableMessageException {
MultipartMessageHandler multipartMessageHandler = new MultipartMessageHandler();
OutgoingTextMessage transportMessage = OutgoingTextMessage.from(message);
if (message.isSecure()) {
String encryptedMessage = getAsymmetricEncrypt(masterSecret, message.getBody(),
message.getIndividualRecipient());
transportMessage = transportMessage.withBody(encryptedMessage);
}
ArrayList<String> messages = multipartMessageHandler.divideMessage(transportMessage);
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages);
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
Log.w("SmsTransport", "Secure divide into message parts: " + messages.size());
for (int i=0;i<messages.size();i++) {
// 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(message.getIndividualRecipient().getNumber(), null, messages.get(i),
sentIntents.get(i),
deliveredIntents == null ? null : deliveredIntents.get(i));
} catch (NullPointerException npe) {
Log.w("SmsSender", npe);
throw new UndeliverableMessageException(npe);
} catch (IllegalArgumentException iae) {
Log.w("SmsSender", iae);
throw new UndeliverableMessageException(iae);
}
}
}
private void deliverPlaintextMessage(SmsMessageRecord message)
throws UndeliverableMessageException
{
ArrayList<String> messages = SmsManager.getDefault().divideMessage(message.getBody());
ArrayList<PendingIntent> sentIntents = constructSentIntents(message.getId(), message.getType(), messages);
ArrayList<PendingIntent> deliveredIntents = constructDeliveredIntents(message.getId(), message.getType(), messages);
String recipient = message.getIndividualRecipient().getNumber();
// 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, deliveredIntents);
} catch (NullPointerException npe) {
Log.w("SmsTransport", npe);
throw new UndeliverableMessageException(npe);
}
}
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 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;
}
private String getAsymmetricEncrypt(MasterSecret masterSecret, String body, Recipient recipient) {
synchronized (SessionCipher.CIPHER_LOCK) {
SessionCipher cipher = new SessionCipher(context, masterSecret, recipient, new SmsTransportDetails());
return new String(cipher.encryptMessage(body.getBytes()));
}
}
}

View File

@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.transport;
public class UndeliverableMessageException extends Throwable {
public UndeliverableMessageException() {
}
public UndeliverableMessageException(String detailMessage) {
super(detailMessage);
}
public UndeliverableMessageException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
public UndeliverableMessageException(Throwable throwable) {
super(throwable);
}
}

View File

@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.transport;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.directory.NumberFilter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.PhoneNumberFormatter;
import java.io.IOException;
public class UniversalTransport {
private final Context context;
private final GcmTransport gcmTransport;
private final SmsTransport smsTransport;
public UniversalTransport(Context context, MasterSecret masterSecret) {
this.context = context;
this.gcmTransport = new GcmTransport(context, masterSecret);
this.smsTransport = new SmsTransport(context, masterSecret);
}
public void deliver(SmsMessageRecord message) throws UndeliverableMessageException {
Recipient recipient = message.getRecipients().getPrimaryRecipient();
String number = PhoneNumberFormatter.formatNumber(context, recipient.getNumber());
if (NumberFilter.getInstance(context).containsNumber(number)) {
try {
Log.w("UniversalTransport", "Delivering with GCM...");
gcmTransport.deliver(message);
} catch (IOException ioe) {
Log.w("UniversalTransport", ioe);
smsTransport.deliver(message);
}
} else {
Log.w("UniversalTransport", "Delivering with SMS...");
smsTransport.deliver(message);
}
}
}