From 3a8d29e27926c274d85a32e01168003028c20e93 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sun, 28 Oct 2012 16:04:24 -0700 Subject: [PATCH] Major reorganization of view/model interactions. Mostly, the inheritance graph for MessageRecord/MmsMessageRecord was all messed up, and each class was overloaded for things it shouldn't have been. 1) Broke MessageRecord/MmsMessageRecord up into: DisplayRecord, ThreadRecord, MessageRecord, SmsMessageRecord, NotificationMmsMessageRecord, and MediaMmsMessageRecord. 2) Updated all the adapters/views to keep pace with that change. --- .../securesms/ConversationAdapter.java | 149 ++++++++--- .../securesms/ConversationFragment.java | 10 +- .../securesms/ConversationItem.java | 249 +++++++++--------- .../securesms/ConversationListAdapter.java | 26 +- .../securesms/ConversationListItem.java | 20 +- .../DecryptingConversationListAdapter.java | 14 +- .../contacts/ContactIdentityManagerICS.java | 1 + .../crypto/MessageDisplayHelper.java | 85 +----- .../securesms/database/MessageRecord.java | 181 ------------- .../securesms/database/MmsDatabase.java | 2 +- .../database/loaders/ConversationLoader.java | 15 +- .../database/model/DisplayRecord.java | 96 +++++++ .../MediaMmsMessageRecord.java} | 109 +++----- .../database/model/MessageRecord.java | 83 ++++++ .../model/NotificationMmsMessageRecord.java | 101 +++++++ .../database/model/SmsMessageRecord.java | 111 ++++++++ .../database/model/ThreadRecord.java | 71 +++++ .../securesms/mms/MmsFactory.java | 79 ------ .../thoughtcrime/securesms/protocol/Tag.java | 2 +- 19 files changed, 793 insertions(+), 611 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/database/MessageRecord.java create mode 100644 src/org/thoughtcrime/securesms/database/model/DisplayRecord.java rename src/org/thoughtcrime/securesms/database/{MmsMessageRecord.java => model/MediaMmsMessageRecord.java} (56%) create mode 100644 src/org/thoughtcrime/securesms/database/model/MessageRecord.java create mode 100644 src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java create mode 100644 src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java create mode 100644 src/org/thoughtcrime/securesms/database/model/ThreadRecord.java delete mode 100644 src/org/thoughtcrime/securesms/mms/MmsFactory.java diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java index e14636d5ae..e42dce9f96 100644 --- a/src/org/thoughtcrime/securesms/ConversationAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java @@ -30,17 +30,25 @@ 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.MessageRecord; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.mms.MmsFactory; +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.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.service.MessageNotifier; +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; @@ -56,6 +64,8 @@ public class ConversationAdapter extends CursorAdapter { private static final int MAX_CACHE_SIZE = 40; + + private final TouchListener touchListener = new TouchListener(); private final LinkedHashMap messageRecordCache; private final Handler failedIconClickHandler; @@ -68,7 +78,9 @@ public class ConversationAdapter extends CursorAdapter { private boolean dataChanged; - public ConversationAdapter(Recipients recipients, long threadId, Context context, MasterSecret masterSecret, Handler failedIconClickHandler) { + public ConversationAdapter(Recipients recipients, long threadId, Context context, + MasterSecret masterSecret, Handler failedIconClickHandler) + { super(context, null); this.context = context; this.recipients = recipients; @@ -85,27 +97,15 @@ public class ConversationAdapter extends CursorAdapter { MessageNotifier.updateNotification(context, false); } - private Recipient buildRecipient(String address) { - Recipient recipient; - - try { - if (address == null) recipient = recipients.getPrimaryRecipient(); - else recipient = RecipientFactory.getRecipientsFromString(context, address).getPrimaryRecipient(); - } catch (RecipientFormattingException e) { - Log.w("ConversationAdapter", e); - recipient = new Recipient("Unknown", "Unknown", null); - } - - return recipient; - } - @Override public void bindView(View view, Context context, Cursor cursor) { + ConversationItem item = (ConversationItem)view; long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID)); - String type = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.TRANSPORT)); + String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); MessageRecord messageRecord = getMessageRecord(id, cursor, type); - ((ConversationItem)view).set(masterSecret, messageRecord, failedIconClickHandler); + item.set(masterSecret, messageRecord, failedIconClickHandler); + view.setOnTouchListener(touchListener); } @@ -135,35 +135,85 @@ public class ConversationAdapter extends CursorAdapter { private int getItemViewType(Cursor cursor) { long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID)); - String type = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.TRANSPORT)); + String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); MessageRecord messageRecord = getMessageRecord(id, cursor, type); if (messageRecord.isOutgoing()) return 0; else return 1; } - private MessageRecord getNewMmsMessageRecord(long messageId, Cursor cursor) { - MessageRecord messageRecord = getNewSmsMessageRecord(messageId, cursor); - long mmsType = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_TYPE)); - long mmsBox = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)); + private MediaMmsMessageRecord getMediaMmsMessageRecord(long messageId, Cursor cursor) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID)); + long date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE)); + long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)); + Recipient recipient = getIndividualRecipientFor(null); + + SlideDeck slideDeck; try { - return MmsFactory.getMms(context, masterSecret, messageRecord, mmsType, mmsBox); + MultimediaMessagePdu pdu = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret).getMediaMessage(messageId); + slideDeck = new SlideDeck(context, masterSecret, pdu.getBody()); } catch (MmsException me) { Log.w("ConversationAdapter", me); - return messageRecord; + slideDeck = null; } + + return new MediaMmsMessageRecord(context, id, recipients, recipient, + date, threadId, slideDeck, box); } - private MessageRecord getNewSmsMessageRecord(long messageId, Cursor cursor) { - long date = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.DATE)); - long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE)); - String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS)); - Recipient recipient = buildRecipient(address); - MessageRecord messageRecord = new MessageRecord(messageId, recipients, date, type, threadId); + private NotificationMmsMessageRecord getNotificationMmsMessageRecord(long messageId, Cursor cursor) { + Recipient recipient = getIndividualRecipientFor(null); + long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID)); + long date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE)); - messageRecord.setMessageRecipient(recipient); - setBody(cursor, messageRecord); + 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, date, threadId, + notification.getContentLocation(), + notification.getMessageSize(), + notification.getExpiry(), + notification.getStatus(), + notification.getTransactionId()); + } + + private SmsMessageRecord getSmsMessageRecord(long messageId, Cursor cursor) { + long date = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.DATE)); + long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE)); + 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.SMS_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)); +// +// groupData = new MessageRecord.GroupData(groupSize, groupSent, groupSendFailed); +// } + SmsMessageRecord messageRecord = new SmsMessageRecord(context, messageId, recipients, + recipient, date, type, threadId); + + 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; } @@ -174,22 +224,35 @@ public class ConversationAdapter extends CursorAdapter { MessageRecord messageRecord; - if (type.equals("mms")) messageRecord = getNewMmsMessageRecord(messageId, cursor); - else messageRecord = getNewSmsMessageRecord(messageId, cursor); + 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); + } messageRecordCache.put(type + messageId, messageRecord); return messageRecord; } - protected void setBody(Cursor cursor, MessageRecord message) { - String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)); + private Recipient getIndividualRecipientFor(String address) { + Recipient recipient; - if (body == null) - message.setBody(""); - else - MessageDisplayHelper.setDecryptedMessageBody(context, body, message, masterCipher); + try { + if (address == null) recipient = recipients.getPrimaryRecipient(); + else recipient = RecipientFactory.getRecipientsFromString(context, address).getPrimaryRecipient(); + } catch (RecipientFormattingException e) { + Log.w("ConversationAdapter", e); + recipient = new Recipient("Unknown", "Unknown", null); + } + + return recipient; } - @Override protected void onContentChanged() { super.onContentChanged(); diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index 78b227c598..429207e6cf 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -21,8 +21,8 @@ import com.actionbarsherlock.app.SherlockListFragment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessageRecord; import org.thoughtcrime.securesms.database.loaders.ConversationLoader; +import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.recipients.Recipients; import java.sql.Date; @@ -98,9 +98,8 @@ public class ConversationFragment extends SherlockListFragment clipboard.setText(body); } - private void handleDeleteMessage(MessageRecord message) { + private void handleDeleteMessage(final MessageRecord message) { final long messageId = message.getId(); - final String transport = message.isMms() ? "mms" : "sms"; AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.ConversationFragment_confirm_message_delete); @@ -111,7 +110,7 @@ public class ConversationFragment extends SherlockListFragment builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - if (transport.equals("mms")) { + if (message.isMms()) { DatabaseFactory.getMmsDatabase(getActivity()).delete(messageId); } else { DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageId); @@ -165,7 +164,8 @@ public class ConversationFragment extends SherlockListFragment @Override public Loader onCreateLoader(int arg0, Bundle arg1) { - return new ConversationLoader(getActivity(), threadId); + return new ConversationLoader(getActivity(), threadId, + (recipients != null && !recipients.isSingleRecipient())); } @Override diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index c52b64663c..3140656e69 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -43,9 +43,10 @@ import android.widget.Toast; import org.thoughtcrime.securesms.contacts.ContactIdentityManager; import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.database.MessageRecord; import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.MmsMessageRecord; +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.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.protocol.Tag; @@ -126,90 +127,27 @@ public class ConversationItem extends LinearLayout { this.masterSecret = masterSecret; this.failedIconHandler = failedIconHandler; - // Double-dispatch back to methods below. - messageRecord.setOnConversationItem(this); + setBodyText(messageRecord); + setStatusIcons(messageRecord); + setContactPhoto(messageRecord); + setEvents(messageRecord); + + if (messageRecord instanceof NotificationMmsMessageRecord) { + setNotificationMmsAttributes((NotificationMmsMessageRecord)messageRecord); + } else if (messageRecord instanceof MediaMmsMessageRecord) { + setMediaMmsAttributes((MediaMmsMessageRecord)messageRecord); + } } public MessageRecord getMessageRecord() { return messageRecord; } - public void setMessageRecord(MessageRecord messageRecord) { - setBody(messageRecord); - setStatusIcons(messageRecord); - setEvents(messageRecord); - } - - public void setMessageRecord(MmsMessageRecord messageRecord) { - setMessageRecord((MessageRecord)messageRecord); - - if (messageRecord.isNotification()) - setMmsNotificationAttributes(messageRecord); - else - setMmsMediaAttributes(messageRecord); - } - - private void setMmsNotificationAttributes(MmsMessageRecord messageRecord) { - String messageSize = String.format(getContext() - .getString(R.string.ConversationItem_message_size_d_kb), - messageRecord.getMessageSize()); - String expires = String.format(getContext() - .getString(R.string.ConversationItem_expires_s), - DateUtils.getRelativeTimeSpanString(getContext(), - messageRecord.getExpiration(), - false)); - - dateText.setText(messageSize + "\n" + expires); - - if (MmsDatabase.Types.isDisplayDownloadButton(messageRecord.getStatus())) { - mmsDownloadButton.setVisibility(View.VISIBLE); - mmsDownloadingLabel.setVisibility(View.GONE); - } else { - mmsDownloadingLabel.setText(MmsDatabase.Types.getLabelForStatus(context, messageRecord.getStatus())); - mmsDownloadButton.setVisibility(View.GONE); - mmsDownloadingLabel.setVisibility(View.VISIBLE); - } - - if (MmsDatabase.Types.isHardError(messageRecord.getStatus())) - failedImage.setVisibility(View.VISIBLE); - } - - private void setMmsMediaAttributes(MmsMessageRecord messageRecord) { - SlideDeck slideDeck = messageRecord.getSlideDeck(); - List slides = slideDeck.getSlides(); - - Iterator iterator = slides.iterator(); - - while (iterator.hasNext()) { - Slide slide = iterator.next(); - if (slide.hasImage()) { - mmsThumbnail.setImageBitmap(slide.getThumbnail()); - mmsThumbnail.setOnClickListener(new ThumbnailClickListener(slide)); - mmsThumbnail.setOnLongClickListener(new ThumbnailSaveListener(slide)); - mmsThumbnail.setVisibility(View.VISIBLE); - return; - } - } - - mmsThumbnail.setVisibility(View.GONE); - } - public void setHandler(Handler failedIconHandler) { this.failedIconHandler = failedIconHandler; } - private void checkForAutoInitiate(MessageRecord messageRecord) { - if (AutoInitiateActivity.isValidAutoInitiateSituation(context, masterSecret, messageRecord.getRecipients().getPrimaryRecipient(), messageRecord.getBody(), messageRecord.getThreadId())) { - AutoInitiateActivity.exemptThread(context, messageRecord.getThreadId()); - Intent intent = new Intent(); - intent.setClass(context, AutoInitiateActivity.class); - intent.putExtra("threadId", messageRecord.getThreadId()); - intent.putExtra("masterSecret", masterSecret); - intent.putExtra("recipient", messageRecord.getRecipients().getPrimaryRecipient()); - - context.startActivity(intent); - } - } + /// MessageRecord Attribute Parsers private void setBodyText(MessageRecord messageRecord) { String body = messageRecord.getBody(); @@ -228,46 +166,12 @@ public class ConversationItem extends LinearLayout { } } - private void setContactPhotoForUserIdentity() { - Uri selfIdentityContact = ContactIdentityManager.getInstance(context).getSelfIdentityUri(); - - if (selfIdentityContact!= null) { - Recipient recipient = RecipientFactory.getRecipientForUri(context, selfIdentityContact); - if (recipient != null) { - contactPhoto.setImageBitmap(recipient.getContactPhoto()); - return; - } - } else { - contactPhoto.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_contact_picture)); - } - } - - private void setBodyImage(MessageRecord messageRecord) { - final Recipient recipient = messageRecord.getMessageRecipient(); - - if (!messageRecord.isOutgoing()) { - contactPhoto.setImageBitmap(recipient.getContactPhoto()); - contactPhoto.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (recipient.getContactUri() != null) { - QuickContact.showQuickContact(context, contactPhoto, recipient.getContactUri(), QuickContact.MODE_LARGE, null); - } else { - Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, Uri.fromParts("tel", recipient.getNumber(), null)); - context.startActivity(intent); - } - } - }); - } else { + private void setContactPhoto(MessageRecord messageRecord) { + if (messageRecord.isOutgoing()) { setContactPhotoForUserIdentity(); + } else { + setContactPhotoForRecipient(messageRecord.getIndividualRecipient()); } - - contactPhoto.setVisibility(View.VISIBLE); - } - - private void setBody(MessageRecord messageRecord) { - setBodyText(messageRecord); - setBodyImage(messageRecord); } private void setStatusIcons(MessageRecord messageRecord) { @@ -289,10 +193,112 @@ public class ConversationItem extends LinearLayout { private void setEvents(MessageRecord messageRecord) { setClickable(messageRecord.isKeyExchange() && !messageRecord.isOutgoing()); - if (!messageRecord.isOutgoing() && messageRecord.getRecipients().isSingleRecipient()) - checkForAutoInitiate(messageRecord); + if (!messageRecord.isOutgoing() && messageRecord.getRecipients().isSingleRecipient()) { + checkForAutoInitiate(messageRecord.getIndividualRecipient(), + messageRecord.getBody(), + messageRecord.getThreadId()); + } } + private void setNotificationMmsAttributes(NotificationMmsMessageRecord messageRecord) { + String messageSize = String.format(getContext() + .getString(R.string.ConversationItem_message_size_d_kb), + messageRecord.getMessageSize()); + String expires = String.format(getContext() + .getString(R.string.ConversationItem_expires_s), + DateUtils.getRelativeTimeSpanString(getContext(), + messageRecord.getExpiration(), + false)); + + dateText.setText(messageSize + "\n" + expires); + + if (MmsDatabase.Types.isDisplayDownloadButton(messageRecord.getStatus())) { + mmsDownloadButton.setVisibility(View.VISIBLE); + mmsDownloadingLabel.setVisibility(View.GONE); + } else { + mmsDownloadingLabel.setText(MmsDatabase.Types.getLabelForStatus(context, messageRecord.getStatus())); + mmsDownloadButton.setVisibility(View.GONE); + mmsDownloadingLabel.setVisibility(View.VISIBLE); + } + } + + private void setMediaMmsAttributes(MediaMmsMessageRecord messageRecord) { + SlideDeck slideDeck = messageRecord.getSlideDeck(); + + if (slideDeck != null) { + List slides = slideDeck.getSlides(); + + Iterator iterator = slides.iterator(); + + while (iterator.hasNext()) { + Slide slide = iterator.next(); + if (slide.hasImage()) { + mmsThumbnail.setImageBitmap(slide.getThumbnail()); + mmsThumbnail.setOnClickListener(new ThumbnailClickListener(slide)); + mmsThumbnail.setOnLongClickListener(new ThumbnailSaveListener(slide)); + mmsThumbnail.setVisibility(View.VISIBLE); + return; + } + } + } + + mmsThumbnail.setVisibility(View.GONE); + } + + /// Helper Methods + + private void checkForAutoInitiate(Recipient recipient, String body, long threadId) { + if (AutoInitiateActivity.isValidAutoInitiateSituation(context, masterSecret, recipient, + body, threadId)) + { + AutoInitiateActivity.exemptThread(context, threadId); + + Intent intent = new Intent(); + intent.setClass(context, AutoInitiateActivity.class); + intent.putExtra("threadId", threadId); + intent.putExtra("masterSecret", masterSecret); + intent.putExtra("recipient", recipient); + + context.startActivity(intent); + } + } + + + private void setContactPhotoForUserIdentity() { + Uri selfIdentityContact = ContactIdentityManager.getInstance(context).getSelfIdentityUri(); + + if (selfIdentityContact!= null) { + Recipient recipient = RecipientFactory.getRecipientForUri(context, selfIdentityContact); + if (recipient != null) { + contactPhoto.setImageBitmap(recipient.getContactPhoto()); + return; + } + } else { + contactPhoto.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_contact_picture)); + } + + contactPhoto.setVisibility(View.VISIBLE); + } + + private void setContactPhotoForRecipient(final Recipient recipient) { + contactPhoto.setImageBitmap(recipient.getContactPhoto()); + contactPhoto.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (recipient.getContactUri() != null) { + QuickContact.showQuickContact(context, contactPhoto, recipient.getContactUri(), QuickContact.MODE_LARGE, null); + } else { + Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, Uri.fromParts("tel", recipient.getNumber(), null)); + context.startActivity(intent); + } + } + }); + + contactPhoto.setVisibility(View.VISIBLE); + } + + /// Event handlers + private void handleKeyExchangeClicked() { Intent intent = new Intent(context, ReceiveKeyActivity.class); intent.putExtra("recipient", messageRecord.getRecipients().getPrimaryRecipient()); @@ -452,15 +458,16 @@ public class ConversationItem extends LinearLayout { private class MmsDownloadClickListener implements View.OnClickListener { public void onClick(View v) { - Log.w("MmsDownloadClickListener", "Content location: " + new String(((MmsMessageRecord)messageRecord).getContentLocation())); + NotificationMmsMessageRecord notificationRecord = (NotificationMmsMessageRecord)messageRecord; + Log.w("MmsDownloadClickListener", "Content location: " + new String(notificationRecord.getContentLocation())); mmsDownloadButton.setVisibility(View.GONE); mmsDownloadingLabel.setVisibility(View.VISIBLE); Intent intent = new Intent(context, SendReceiveService.class); - intent.putExtra("content_location", new String(((MmsMessageRecord)messageRecord).getContentLocation())); - intent.putExtra("message_id", ((MmsMessageRecord)messageRecord).getId()); - intent.putExtra("transaction_id", ((MmsMessageRecord)messageRecord).getTransactionId()); - intent.putExtra("thread_id", ((MmsMessageRecord)messageRecord).getThreadId()); + intent.putExtra("content_location", new String(notificationRecord.getContentLocation())); + intent.putExtra("message_id", notificationRecord.getId()); + intent.putExtra("transaction_id", notificationRecord.getTransactionId()); + intent.putExtra("thread_id", notificationRecord.getThreadId()); intent.setAction(SendReceiveService.DOWNLOAD_MMS_ACTION); context.startService(intent); } diff --git a/src/org/thoughtcrime/securesms/ConversationListAdapter.java b/src/org/thoughtcrime/securesms/ConversationListAdapter.java index c733bb37a5..adc881a1f6 100644 --- a/src/org/thoughtcrime/securesms/ConversationListAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationListAdapter.java @@ -23,9 +23,8 @@ import android.view.ViewGroup; import android.widget.CursorAdapter; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessageRecord; import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.protocol.Prefix; +import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; @@ -68,28 +67,19 @@ public class ConversationListAdapter extends CursorAdapter { long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)); long read = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.READ)); - MessageRecord message = new MessageRecord(-1, recipients, date, count, read == 1, threadId); - setBody(cursor, message); + ThreadRecord thread = new ThreadRecord(context, recipients, date, count, read == 1, threadId); + setBody(cursor, thread); - ((ConversationListItem)view).set(message, batchMode); + ((ConversationListItem)view).set(thread, batchMode); } - protected void filterBody(MessageRecord message, String body) { + protected void filterBody(ThreadRecord thread, String body) { if (body == null) body = "(No subject)"; - - if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT) || body.startsWith(Prefix.ASYMMETRIC_ENCRYPT) || body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT)) { - message.setBody(context.getString(R.string.ConversationListAdapter_encrypted_message_enter_passphrase)); - message.setEmphasis(true); - } else if (body.startsWith(Prefix.KEY_EXCHANGE)) { - message.setBody(context.getString(R.string.ConversationListAdapter_key_exchange_message)); - message.setEmphasis(true); - } else { - message.setBody(body); - } + thread.setBody(body); } - protected void setBody(Cursor cursor, MessageRecord message) { - filterBody(message, cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET))); + protected void setBody(Cursor cursor, ThreadRecord thread) { + filterBody(thread, cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET))); } public void addToBatchSet(long threadId) { diff --git a/src/org/thoughtcrime/securesms/ConversationListItem.java b/src/org/thoughtcrime/securesms/ConversationListItem.java index 77a30a5f8a..0e1e86dde5 100644 --- a/src/org/thoughtcrime/securesms/ConversationListItem.java +++ b/src/org/thoughtcrime/securesms/ConversationListItem.java @@ -33,7 +33,7 @@ import android.widget.QuickContactBadge; import android.widget.RelativeLayout; import android.widget.TextView; -import org.thoughtcrime.securesms.database.MessageRecord; +import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.recipients.Recipients; import java.util.Set; @@ -84,22 +84,22 @@ public class ConversationListItem extends RelativeLayout { super(context, attrs); } - public void set(MessageRecord message, boolean batchMode) { - this.recipients = message.getRecipients(); - this.threadId = message.getThreadId(); - this.fromView.setText(formatFrom(recipients, message.getCount(), message.getRead())); + public void set(ThreadRecord thread, boolean batchMode) { + this.recipients = thread.getRecipients(); + this.threadId = thread.getThreadId(); + this.fromView.setText(formatFrom(recipients, thread.getCount(), thread.isRead())); - if (message.isKeyExchange()) + if (thread.isKeyExchange()) this.subjectView.setText(R.string.ConversationListItem_key_exchange_message, TextView.BufferType.SPANNABLE); else - this.subjectView.setText(message.getBody(), TextView.BufferType.SPANNABLE); + this.subjectView.setText(thread.getBody(), TextView.BufferType.SPANNABLE); - if (message.getEmphasis()) + if (thread.getEmphasis()) ((Spannable)this.subjectView.getText()).setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, this.subjectView.getText().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - if (message.getDate() > 0) - this.dateView.setText(DateUtils.getRelativeTimeSpanString(getContext(), message.getDate(), false)); + if (thread.getDate() > 0) + this.dateView.setText(DateUtils.getRelativeTimeSpanString(getContext(), thread.getDate(), false)); if (selectedThreads != null) this.checkbox.setChecked(selectedThreads.contains(threadId)); diff --git a/src/org/thoughtcrime/securesms/DecryptingConversationListAdapter.java b/src/org/thoughtcrime/securesms/DecryptingConversationListAdapter.java index 63f3303c05..5d2bd60859 100644 --- a/src/org/thoughtcrime/securesms/DecryptingConversationListAdapter.java +++ b/src/org/thoughtcrime/securesms/DecryptingConversationListAdapter.java @@ -22,8 +22,9 @@ 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.MessageRecord; 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. @@ -43,9 +44,16 @@ public class DecryptingConversationListAdapter extends ConversationListAdapter { } @Override - protected void setBody(Cursor cursor, MessageRecord message) { + protected void setBody(Cursor cursor, ThreadRecord thread) { String body = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET)); if (body == null || body.equals("")) body = "(No subject)"; - MessageDisplayHelper.setDecryptedMessageBody(context, body, message, bodyCipher); + + 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); + } } } diff --git a/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerICS.java b/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerICS.java index c8652ef6b5..c06fc47982 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerICS.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactIdentityManagerICS.java @@ -48,6 +48,7 @@ class ContactIdentityManagerICS extends ContactIdentityManager { return true; } + @SuppressLint("NewApi") @Override public List getSelfIdentityRawContactIds() { List results = new LinkedList(); diff --git a/src/org/thoughtcrime/securesms/crypto/MessageDisplayHelper.java b/src/org/thoughtcrime/securesms/crypto/MessageDisplayHelper.java index 5892f6f439..229da3b7e3 100644 --- a/src/org/thoughtcrime/securesms/crypto/MessageDisplayHelper.java +++ b/src/org/thoughtcrime/securesms/crypto/MessageDisplayHelper.java @@ -16,17 +16,11 @@ */ package org.thoughtcrime.securesms.crypto; -import android.content.Context; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.MessageRecord; -import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.protocol.Prefix; import org.thoughtcrime.securesms.util.InvalidMessageException; import java.lang.ref.SoftReference; import java.util.LinkedHashMap; -import java.util.Map.Entry; public class MessageDisplayHelper { @@ -39,30 +33,6 @@ public class MessageDisplayHelper { } }; - private static boolean isUnreadableAsymmetricMessage(long type) { - return type == SmsDatabase.Types.FAILED_DECRYPT_TYPE; - } - - private static boolean isInProcessAsymmetricMessage(String body, long type) { - return type == SmsDatabase.Types.DECRYPT_IN_PROGRESS_TYPE || (type == 0 && body.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) || (type == 0 && body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT)); - } - - private static boolean isRogueAsymmetricMessage(long type) { - return type == SmsDatabase.Types.NO_SESSION_TYPE; - } - - private static boolean isKeyExchange(String body) { - return body.startsWith(Prefix.KEY_EXCHANGE); - } - - private static boolean isProcessedKeyExchange(String body) { - return body.startsWith(Prefix.PROCESSED_KEY_EXCHANGE); - } - - private static boolean isStaleKeyExchange(String body) { - return body.startsWith(Prefix.STALE_KEY_EXCHANGE); - } - private static String checkCacheForBody(String body) { if (decryptedBodyCache.containsKey(body)) { String decryptedBody = decryptedBodyCache.get(body).get(); @@ -77,50 +47,19 @@ public class MessageDisplayHelper { return null; } - public static void setDecryptedMessageBody(Context context, String body, - MessageRecord message, MasterCipher bodyCipher) - { - try { - if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT)) { - String cacheResult = checkCacheForBody(body); - if (cacheResult != null) { - body = cacheResult; - } else { - String decryptedBody = bodyCipher.decryptBody(body.substring(Prefix.SYMMETRIC_ENCRYPT.length())); - decryptedBodyCache.put(body, new SoftReference(decryptedBody)); - body = decryptedBody; - } - } + public static String getDecryptedMessageBody(MasterCipher bodyCipher, String body) throws InvalidMessageException { + if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT)) { + String cacheResult = checkCacheForBody(body); - if (isUnreadableAsymmetricMessage(message.getType())) { - message.setBody(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message)); - message.setEmphasis(true); - } else if (isInProcessAsymmetricMessage(body, message.getType())) { - message.setBody(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait)); - message.setEmphasis(true); - } else if (isRogueAsymmetricMessage(message.getType())) { - message.setBody(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session)); - message.setEmphasis(true); - } else if (isKeyExchange(body)) { - message.setKeyExchange(true); - message.setEmphasis(true); - message.setBody(body); - } else if (isProcessedKeyExchange(body)) { - message.setProcessedKeyExchange(true); - message.setEmphasis(true); - message.setBody(body); - } else if (isStaleKeyExchange(body)) { - message.setStaleKeyExchange(true); - message.setEmphasis(true); - message.setBody(body); - } else { - message.setBody(body); - message.setEmphasis(false); - } - } catch (InvalidMessageException ime) { - message.setBody(context.getString(R.string.MessageDisplayHelper_decryption_error_local_message_corrupted_mac_doesn_t_match_potential_tampering_question)); - message.setEmphasis(true); + if (cacheResult != null) + return cacheResult; + + String decryptedBody = bodyCipher.decryptBody(body.substring(Prefix.SYMMETRIC_ENCRYPT.length())); + decryptedBodyCache.put(body, new SoftReference(decryptedBody)); + + return decryptedBody; } - } + return body; + } } diff --git a/src/org/thoughtcrime/securesms/database/MessageRecord.java b/src/org/thoughtcrime/securesms/database/MessageRecord.java deleted file mode 100644 index a73488f8ed..0000000000 --- a/src/org/thoughtcrime/securesms/database/MessageRecord.java +++ /dev/null @@ -1,181 +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 . - */ -package org.thoughtcrime.securesms.database; - -import org.thoughtcrime.securesms.ConversationItem; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.Recipients; - -public class MessageRecord { - - private long id; - private long threadId; - private Recipient messageRecipient; - private Recipients recipients; - private String body; - private long date; - private long count; - private boolean read; - private long type; - private boolean emphasis; - private boolean keyExchange; - private boolean processedKeyExchange; - private boolean staleKeyExchange; - - public MessageRecord(MessageRecord copy) { - this.id = copy.id; - this.threadId = copy.threadId; - this.messageRecipient = copy.messageRecipient; - this.recipients = copy.recipients; - this.body = copy.body; - this.date = copy.date; - this.count = copy.count; - this.read = copy.read; - this.type = copy.type; - this.emphasis = copy.emphasis; - this.keyExchange = copy.keyExchange; - this.processedKeyExchange = copy.processedKeyExchange; - } - - public MessageRecord(long id, Recipients recipients, long date, long type, long threadId) { - this.id = id; - this.date = date; - this.type = type; - this.recipients = recipients; - this.threadId = threadId; - } - - public MessageRecord(long id, Recipients recipients, long date, long count, boolean read, long threadId) { - this.id = id; - this.threadId = threadId; - this.recipients = recipients; - this.date = date; - this.count = count; - this.read = read; - } - - public void setOnConversationItem(ConversationItem item) { - item.setMessageRecord(this); - } - - public boolean isMms() { - return false; - } - - public long getType() { - return type; - } - - public void setMessageRecipient(Recipient recipient) { - this.messageRecipient = recipient; - } - - public Recipient getMessageRecipient() { - return this.messageRecipient; - } - - public void setEmphasis(boolean emphasis) { - this.emphasis = emphasis; - } - - public boolean getEmphasis() { - return this.emphasis; - } - - public void setId(long id) { - this.id = id; - } - - public void setBody(String body) { - this.body = body; - } - - public long getThreadId() { - return threadId; - } - - public long getId() { - return id; - } - - public Recipients getRecipients() { - return recipients; - } - - public String getBody() { - return body; - } - - public long getDate() { - return date; - } - - public long getCount() { - return count; - } - - public boolean getRead() { - return read; - } - - public boolean isStaleKeyExchange() { - return this.staleKeyExchange; - } - - public void setStaleKeyExchange(boolean staleKeyExchange) { - this.staleKeyExchange = staleKeyExchange; - } - - public boolean isProcessedKeyExchange() { - return processedKeyExchange; - } - - public void setProcessedKeyExchange(boolean processedKeyExchange) { - this.processedKeyExchange = processedKeyExchange; - } - - public boolean isKeyExchange() { - return keyExchange || processedKeyExchange || staleKeyExchange; - } - - public void setKeyExchange(boolean keyExchange) { - this.keyExchange = keyExchange; - } - - public boolean isFailedDecryptType() { - return type == SmsDatabase.Types.FAILED_DECRYPT_TYPE; - } - - public boolean isFailed() { - return SmsDatabase.Types.isFailedMessageType(type); - } - - public boolean isOutgoing() { - return SmsDatabase.Types.isOutgoingMessageType(type); - } - - public boolean isPending() { - return SmsDatabase.Types.isPendingMessageType(type); - } - - public boolean isSecure() { - return SmsDatabase.Types.isSecureType(type); - } - - - -} diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index 737b8c005a..049ad65f36 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -49,7 +49,7 @@ public class MmsDatabase extends Database { public static final String TABLE_NAME = "mms"; public static final String ID = "_id"; private static final String THREAD_ID = "thread_id"; - private static final String DATE = "date"; + public static final String DATE = "date"; public static final String MESSAGE_BOX = "msg_box"; private static final String READ = "read"; private static final String MESSAGE_ID = "m_id"; diff --git a/src/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java b/src/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java index ecafcf7d78..c0e3bd5a85 100644 --- a/src/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java +++ b/src/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java @@ -7,17 +7,24 @@ import android.support.v4.content.CursorLoader; import org.thoughtcrime.securesms.database.DatabaseFactory; public class ConversationLoader extends CursorLoader { + private final Context context; private final long threadId; +// private final boolean isGroupConversation; - public ConversationLoader(Context context, long threadId) { + public ConversationLoader(Context context, long threadId, boolean isGroupConversation) { super(context); - this.context = context.getApplicationContext(); - this.threadId = threadId; + this.context = context.getApplicationContext(); + this.threadId = threadId; +// this.isGroupConversation = isGroupConversation; } @Override public Cursor loadInBackground() { - return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId); +// if (!isGroupConversation) { + return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId); +// } else { +// return DatabaseFactory.getMmsSmsDatabase(context).getCollatedGroupConversation(threadId); +// } } } diff --git a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java new file mode 100644 index 0000000000..8009b62158 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2012 Moxie Marlinspike + * + * 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 . + */ +package org.thoughtcrime.securesms.database.model; + +import org.thoughtcrime.securesms.protocol.Prefix; +import org.thoughtcrime.securesms.recipients.Recipients; + +/** + * The base class for all message record models. Encapsulates basic data + * shared between ThreadRecord and MessageRecord. + * + * @author Moxie Marlinspike + * + */ + +public abstract class DisplayRecord { + + private final Recipients recipients; + private final long date; + private final long threadId; + + private String body; + protected boolean emphasis; + protected boolean keyExchange; + protected boolean processedKeyExchange; + protected boolean staleKeyExchange; + + public DisplayRecord(Recipients recipients, long date, long threadId) { + this.threadId = threadId; + this.recipients = recipients; + this.date = date; + 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; + } + } + + public String getBody() { + return body; + } + + public Recipients getRecipients() { + return recipients; + } + + public long getDate() { + return date; + } + + public long getThreadId() { + return threadId; + } + + public boolean isKeyExchange() { + return keyExchange || processedKeyExchange || staleKeyExchange; + } + +} diff --git a/src/org/thoughtcrime/securesms/database/MmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java similarity index 56% rename from src/org/thoughtcrime/securesms/database/MmsMessageRecord.java rename to src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index 59616da3f5..4ea6a2e859 100644 --- a/src/org/thoughtcrime/securesms/database/MmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2011 Whisper Systems + * Copyright (C) 2012 Moxie Marlinspike * * 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 @@ -14,56 +14,44 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.database; +package org.thoughtcrime.securesms.database.model; import android.content.Context; -import org.thoughtcrime.securesms.ConversationItem; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.mms.Slide; 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; -public class MmsMessageRecord extends MessageRecord { +/** + * Represents the message record model for MMS messages that contain + * media (ie: they've been downloaded). + * + * @author Moxie Marlinspike + * + */ - private SlideDeck slideDeck; - private byte[] contentLocation; - private long messageSize; - private long expiry; - private boolean isNotification; - private long mailbox; - private int status; - private byte[] transactionId; +public class MediaMmsMessageRecord extends MessageRecord { - public MmsMessageRecord(Context context, MessageRecord record, SlideDeck slideDeck, long mailbox) { - super(record); - this.slideDeck = slideDeck; - this.isNotification = false; - this.mailbox = mailbox; + private final SlideDeck slideDeck; + private final long mailbox; + + public MediaMmsMessageRecord(Context context, long id, Recipients recipients, + Recipient individualRecipient, long date, long threadId, + SlideDeck slideDeck, long mailbox) + { + super(id, recipients, individualRecipient, date, threadId); + this.slideDeck = slideDeck; + this.mailbox = mailbox; setBodyIfTextAvailable(context); } - public MmsMessageRecord(MessageRecord record, byte[] contentLocation, long messageSize, long expiry, int status, byte[] transactionId) { - super(record); - this.contentLocation = contentLocation; - this.messageSize = messageSize; - this.expiry = expiry; - this.isNotification = true; - this.status = status; - this.transactionId = transactionId; - } - - public byte[] getTransactionId() { - return transactionId; - } - - public int getStatus() { - return this.status; - } - @Override public boolean isOutgoing() { return MmsDatabase.Types.isOutgoingMmsBox(mailbox); @@ -84,43 +72,13 @@ public class MmsMessageRecord extends MessageRecord { return MmsDatabase.Types.isSecureMmsBox(mailbox); } - // This is the double-dispatch pattern, don't refactor - // this into the base class. - @Override - public void setOnConversationItem(ConversationItem item) { - item.setMessageRecord(this); - } - - public byte[] getContentLocation() { - return contentLocation; - } - - public long getMessageSize() { - return (messageSize + 1023) / 1024; - } - - public long getExpiration() { - return expiry * 1000; - } - - public boolean isNotification() { - return isNotification; - } - public SlideDeck getSlideDeck() { return slideDeck; } - private void setBodyFromSlidesIfTextAvailable() { - List slides = slideDeck.getSlides(); - Iterator i = slides.iterator(); - - while (i.hasNext()) { - Slide slide = i.next(); - - if (slide.hasText()) - setBody(slide.getText()); - } + @Override + public boolean isMms() { + return true; } private void setBodyIfTextAvailable(Context context) { @@ -135,7 +93,7 @@ public class MmsMessageRecord extends MessageRecord { return; case MmsDatabase.Types.MESSAGE_BOX_NO_SESSION_INBOX: setBody(context - .getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session)); + .getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session)); setEmphasis(true); return; } @@ -143,9 +101,16 @@ public class MmsMessageRecord extends MessageRecord { setBodyFromSlidesIfTextAvailable(); } - @Override - public boolean isMms() { - return true; + private void setBodyFromSlidesIfTextAvailable() { + List slides = slideDeck.getSlides(); + Iterator i = slides.iterator(); + + while (i.hasNext()) { + Slide slide = i.next(); + + if (slide.hasText()) + setBody(slide.getText()); + } } } diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java new file mode 100644 index 0000000000..e383f3e5e9 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2012 Moxie Marlinpsike + * + * 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 . + */ +package org.thoughtcrime.securesms.database.model; + +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; + +/** + * The base class for message record models that are displayed in + * conversations, as opposed to models that are displayed in a thread list. + * Encapsulates the shared data between both SMS and MMS messages. + * + * @author Moxie Marlinspike + * + */ +public abstract class MessageRecord extends DisplayRecord { + + private final Recipient individualRecipient; + private final long id; + + public MessageRecord(long id, Recipients recipients, + Recipient individualRecipient, + long date, long threadId) + { + super(recipients, date, threadId); + this.id = id; + this.individualRecipient = individualRecipient; + } + + public abstract boolean isOutgoing(); + + public abstract boolean isFailed(); + + public abstract boolean isSecure(); + + public abstract boolean isPending(); + + public abstract boolean isMms(); + + public long getId() { + return id; + } + + public boolean isStaleKeyExchange() { + return this.staleKeyExchange; + } + + public boolean isProcessedKeyExchange() { + return this.processedKeyExchange; + } + + public Recipient getIndividualRecipient() { + return individualRecipient; + } + +// +// public static class GroupData { +// public final int groupSize; +// public final int groupSentCount; +// public final int groupSendFailedCount; +// +// public GroupData(int groupSize, int groupSentCount, int groupSendFailedCount) { +// this.groupSize = groupSize; +// this.groupSentCount = groupSentCount; +// this.groupSendFailedCount = groupSendFailedCount; +// } +// } + +} diff --git a/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java new file mode 100644 index 0000000000..7ea21cfbff --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -0,0 +1,101 @@ +/** + * Copyright (C) 2012 Moxie Marlinspike + * + * 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 . + */ +package org.thoughtcrime.securesms.database.model; + +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; + +/** + * Represents the message record model for MMS messages that are + * notifications (ie: they're pointers to undownloaded media). + * + * @author Moxie Marlinspike + * + */ + +public class NotificationMmsMessageRecord extends MessageRecord { + + private final byte[] contentLocation; + private final long messageSize; + private final long expiry; + private final int status; + private final byte[] transactionId; + + public NotificationMmsMessageRecord(long id, Recipients recipients, Recipient individualRecipient, + long date, long threadId, byte[] contentLocation, + long messageSize, long expiry, + int status, byte[] transactionId) + { + super(id, recipients, individualRecipient, date, threadId); + this.contentLocation = contentLocation; + this.messageSize = messageSize; + this.expiry = expiry; + this.status = status; + this.transactionId = transactionId; + + setBody("Multimedia Message"); + setEmphasis(true); + } + + public byte[] getTransactionId() { + return transactionId; + } + + public int getStatus() { + return this.status; + } + + + public byte[] getContentLocation() { + return contentLocation; + } + + public long getMessageSize() { + return (messageSize + 1023) / 1024; + } + + public long getExpiration() { + return expiry * 1000; + } + + @Override + public boolean isOutgoing() { + return false; + } + + @Override + public boolean isFailed() { + return MmsDatabase.Types.isHardError(status); + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public boolean isPending() { + return false; + } + + @Override + public boolean isMms() { + return true; + } + +} diff --git a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java new file mode 100644 index 0000000000..d4cb570317 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2012 Moxie Marlinspike + * + * 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 . + */ + +package org.thoughtcrime.securesms.database.model; + +import android.content.Context; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.protocol.Prefix; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; + +/** + * The message record model which represents standard SMS messages. + * + * @author Moxie Marlinspike + * + */ + +public class SmsMessageRecord extends MessageRecord { + + private final Context context; + private final long type; + + public SmsMessageRecord(Context context, long id, + Recipients recipients, + Recipient individualRecipient, + long date, long type, long threadId) + { + super(id, recipients, individualRecipient, date, threadId); + this.context = context.getApplicationContext(); + this.type = type; + } + + public long getType() { + return type; + } + + @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); + } else { + super.setBody(body); + } + } + + @Override + public boolean isFailed() { + return SmsDatabase.Types.isFailedMessageType(getType()); + } + + @Override + public boolean isOutgoing() { + return SmsDatabase.Types.isOutgoingMessageType(getType()); + } + + @Override + public boolean isPending() { + return SmsDatabase.Types.isPendingMessageType(getType()); + } + + @Override + public boolean isSecure() { + return SmsDatabase.Types.isSecureType(getType()); + } + + @Override + public boolean isMms() { + return false; + } + + public static class GroupData { + public final int groupSize; + public final int groupSentCount; + public final int groupSendFailedCount; + + public GroupData(int groupSize, int groupSentCount, int groupSendFailedCount) { + this.groupSize = groupSize; + this.groupSentCount = groupSentCount; + this.groupSendFailedCount = groupSendFailedCount; + } + } + + +} diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java new file mode 100644 index 0000000000..52137f954f --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2012 Moxie Marlinspike + * + * 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 . + */ +package org.thoughtcrime.securesms.database.model; + +import android.content.Context; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.protocol.Prefix; +import org.thoughtcrime.securesms.recipients.Recipients; + +/** + * The message record model which represents thread heading messages. + * + * @author Moxie Marlinspike + * + */ +public class ThreadRecord extends DisplayRecord { + + private final Context context; + private final long count; + private final boolean read; + + public ThreadRecord(Context context, Recipients recipients, + long date, long count, + boolean read, long threadId) + { + super(recipients, date, threadId); + 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); + } else { + super.setBody(body); + } + } + + public long getCount() { + return count; + } + + public boolean isRead() { + return read; + } + +} diff --git a/src/org/thoughtcrime/securesms/mms/MmsFactory.java b/src/org/thoughtcrime/securesms/mms/MmsFactory.java deleted file mode 100644 index b85618f613..0000000000 --- a/src/org/thoughtcrime/securesms/mms/MmsFactory.java +++ /dev/null @@ -1,79 +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 . - */ -package org.thoughtcrime.securesms.mms; - -import android.content.Context; -import android.util.Log; - -import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessageRecord; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.MmsMessageRecord; - -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; - -public class MmsFactory { - - public static MmsMessageRecord getMms(Context context, MasterSecret masterSecret, MessageRecord record, long mmsType, long box) throws MmsException { - Log.w("MmsFactory", "MMS Type: " + mmsType); - if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) { - return getNotificationMmsRecord(context, record); - } else { - return getMediaMmsRecord(context, masterSecret, record, box); - } - } - - private static MmsMessageRecord getNotificationMmsRecord(Context context, MessageRecord record) throws MmsException { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - NotificationInd notification = database.getNotificationMessage(record.getId()); - return new MmsMessageRecord(record, notification.getContentLocation(), notification.getMessageSize(), - notification.getExpiry(), notification.getStatus(), notification.getTransactionId()); - } - - private static MmsMessageRecord getMediaMmsRecord(Context context, MasterSecret masterSecret, MessageRecord record, long box) throws MmsException { - MmsDatabase database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret); - MultimediaMessagePdu msg = database.getMediaMessage(record.getId()); - SlideDeck slideDeck = new SlideDeck(context, masterSecret, msg.getBody()); - - return new MmsMessageRecord(context, record, slideDeck, box); - } - - // private static void setBodyIfText(SlideModel slide, MessageRecord record) { - // if ((slide != null) && slide.hasText()) { - // TextModel tm = slide.getText(); - // - // if (tm.isDrmProtected()) { - // record.setBody("DRM protected"); - // } else { - // record.setBody(tm.getText()); - // } - // } - // } - // - // private static SlideshowModel getSlideshowModel(Context context, long messageId) throws MmsException { - // LayoutManager.init(context); - // MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - // MultimediaMessagePdu msg = database.getMediaMessage(messageId); - // return SlideshowModel.createFromPduBody(context, msg.getBody()); - // } - - -} diff --git a/src/org/thoughtcrime/securesms/protocol/Tag.java b/src/org/thoughtcrime/securesms/protocol/Tag.java index 849d4f48e9..291b6ceb87 100644 --- a/src/org/thoughtcrime/securesms/protocol/Tag.java +++ b/src/org/thoughtcrime/securesms/protocol/Tag.java @@ -19,7 +19,7 @@ public class Tag { } public static boolean isTagged(String message) { - return message.matches(".*[^\\s]" + WHITESPACE_TAG + "$"); + return message != null && message.matches(".*[^\\s]" + WHITESPACE_TAG + "$"); } public static String getTaggedMessage(String message) {