Add collating support for group SMS/MMS messages.

1) When sending an SMS or MMS to multiple recipients, only show one
ConversationItem, but provide statistics on the number of recipients
delivered to.

2) Still break up the messages for secure and insecure messages.
master
Moxie Marlinspike 2012-10-29 16:51:42 -07:00
parent 3a8d29e279
commit 187ec95817
12 changed files with 222 additions and 83 deletions

View File

@ -77,7 +77,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Download"
android:text="@string/conversation_item_received__download"
android:visibility="gone" />
<TextView android:id="@+id/mms_label_downloading"
@ -86,21 +86,39 @@
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:gravity="center"
android:text="Downloading"
android:text="@string/conversation_item_received__downloading"
android:visibility="gone" />
</LinearLayout>
<TextView android:id="@+id/conversation_item_date"
android:autoLink="all"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="left"
android:textColor="#ffcccccc"
android:paddingTop="1dip"/>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="left">
<TextView android:id="@+id/group_message_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_gravity="left"
android:textColor="#ffcccccc"
android:visibility="gone"
android:layout_marginRight="8dip"
android:paddingTop="1dip"/>
<TextView android:id="@+id/conversation_item_date"
android:autoLink="all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_gravity="left"
android:textColor="#ffcccccc"
android:paddingTop="1dip"/>
</LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/indicators_parent"

View File

@ -99,7 +99,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Download"
android:text="@string/conversation_item_sent__download"
android:visibility="gone" />
<TextView android:id="@+id/mms_label_downloading"
@ -108,21 +108,40 @@
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:gravity="center"
android:text="Downloading"
android:text="@string/conversation_item_sent__downloading"
android:visibility="gone" />
</LinearLayout>
<TextView android:id="@+id/conversation_item_date"
android:autoLink="all"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="right"
android:textColor="#ffcccccc"
android:paddingTop="1dip"/>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="right">
<TextView android:id="@+id/group_message_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_gravity="right"
android:textColor="#ffcccccc"
android:visibility="gone"
android:layout_marginRight="8dip"
android:paddingTop="1dip"/>
<TextView android:id="@+id/conversation_item_date"
android:autoLink="all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_gravity="right"
android:textColor="#ffcccccc"
android:paddingTop="1dip"/>
</LinearLayout>
</LinearLayout>
<view xmlns:android="http://schemas.android.com/apk/res/android"

View File

@ -434,5 +434,9 @@
<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 -->
<string name="conversation_item_sent__download">Download</string>
<string name="conversation_item_sent__downloading">Downloading</string>
<string name="conversation_item_received__download">Download</string>
<string name="conversation_item_received__downloading">Downloading</string>
</resources>

View File

@ -35,6 +35,7 @@ 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;
@ -64,8 +65,6 @@ public class ConversationAdapter extends CursorAdapter {
private static final int MAX_CACHE_SIZE = 40;
private final TouchListener touchListener = new TouchListener();
private final LinkedHashMap<String,MessageRecord> messageRecordCache;
private final Handler failedIconClickHandler;
@ -147,19 +146,36 @@ public class ConversationAdapter extends CursorAdapter {
long date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE));
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,
date, threadId, slideDeck, box);
date, threadId, slideDeck, box, groupData);
}
private NotificationMmsMessageRecord getNotificationMmsMessageRecord(long messageId, Cursor cursor) {
@ -190,17 +206,22 @@ public class ConversationAdapter extends CursorAdapter {
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);
// }
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, date, type, threadId);
recipient, date, type, threadId,
groupData);
if (body == null) {
body = "";

View File

@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.MmsDatabase;
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.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
@ -78,6 +79,7 @@ public class ConversationItem extends LinearLayout {
private TextView bodyText;
private TextView dateText;
private TextView groupStatusText;
private ImageView secureImage;
private ImageView failedImage;
private ImageView keyImage;
@ -108,6 +110,7 @@ public class ConversationItem extends LinearLayout {
this.bodyText = (TextView) findViewById(R.id.conversation_item_body);
this.dateText = (TextView) findViewById(R.id.conversation_item_date);
this.groupStatusText = (TextView) findViewById(R.id.group_message_status);
this.secureImage = (ImageView)findViewById(R.id.sms_secure_indicator);
this.failedImage = (ImageView)findViewById(R.id.sms_failed_indicator);
this.keyImage = (ImageView)findViewById(R.id.key_exchange_indicator);
@ -130,6 +133,7 @@ public class ConversationItem extends LinearLayout {
setBodyText(messageRecord);
setStatusIcons(messageRecord);
setContactPhoto(messageRecord);
setGroupMessageStatus(messageRecord);
setEvents(messageRecord);
if (messageRecord instanceof NotificationMmsMessageRecord) {
@ -200,6 +204,22 @@ public class ConversationItem extends LinearLayout {
}
}
private void setGroupMessageStatus(MessageRecord messageRecord) {
GroupData groupData = messageRecord.getGroupData();
if (groupData != null) {
String status = String.format("Sent (%d/%d)", groupData.groupSentCount, groupData.groupSize);
if (groupData.groupSendFailedCount != 0)
status = status + String.format(", Failed (%d/%d)", groupData.groupSendFailedCount, groupData.groupSize);
this.groupStatusText.setText(status);
this.groupStatusText.setVisibility(View.VISIBLE);
} else {
this.groupStatusText.setVisibility(View.GONE);
}
}
private void setNotificationMmsAttributes(NotificationMmsMessageRecord messageRecord) {
String messageSize = String.format(getContext()
.getString(R.string.ConversationItem_message_size_d_kb),

View File

@ -28,18 +28,70 @@ import java.util.Set;
public class MmsSmsDatabase extends Database {
public static final String TRANSPORT = "transport_type";
public static final String TRANSPORT = "transport_type";
public static final String GROUP_SIZE = "group_size";
public static final String SMS_GROUP_SENT_COUNT = "sms_group_sent_count";
public static final String SMS_GROUP_SEND_FAILED_COUNT = "sms_group_sent_failed_count";
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 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 " +
"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 mmsGroupSentCount = "SUM(CASE " + MmsDatabase.MESSAGE_BOX + " " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SENT + " THEN 1 " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SECURE_SENT + " 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 " +
"ELSE 0 END)";
String mmsGroupSentFailedCount = "SUM(CASE " + MmsDatabase.MESSAGE_BOX + " " +
"WHEN " + MmsDatabase.Types.MESSAGE_BOX_SENT_FAILED + " THEN 1 " +
"ELSE 0 END)";
String smsGroupSentFailedCount = "SUM(CASE " + SmsDatabase.TYPE + " " +
"WHEN " + SmsDatabase.Types.FAILED_TYPE + " THEN 1 " +
"ELSE 0 END)";
String[] projection = {"_id", "body", "type", "address", "subject", "normalized_date AS date", "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 ASC";
String selection = "thread_id = " + threadId;
String groupBy = "normalized_date / 1000, sms_collate, mms_collate";
Cursor cursor = queryTables(projection, selection, order, groupBy, null);
setNotifyConverationListeners(cursor, threadId);
return cursor;
}
public Cursor getConversation(long threadId) {
String[] projection = {"_id", "body", "type", "address", "subject", "normalized_date AS date", "m_type", "msg_box", "transport_type"};
String order = "normalized_date ASC";
String selection = "thread_id = " + threadId;
Cursor cursor = queryTables(projection, selection, order, null);
Cursor cursor = queryTables(projection, selection, order, null, null);
setNotifyConverationListeners(cursor, threadId);
return cursor;
@ -50,7 +102,7 @@ public class MmsSmsDatabase extends Database {
String order = "normalized_date DESC";
String selection = "thread_id = " + threadId;
Cursor cursor = queryTables(projection, selection, order, "1");
Cursor cursor = queryTables(projection, selection, order, null, "1");
return cursor;
}
@ -59,7 +111,7 @@ public class MmsSmsDatabase extends Database {
String order = "normalized_date ASC";
String selection = "read = 0";
Cursor cursor = queryTables(projection, selection, order, null);
Cursor cursor = queryTables(projection, selection, order, null, null);
return cursor;
}
@ -70,7 +122,7 @@ public class MmsSmsDatabase extends Database {
return count;
}
private Cursor queryTables(String[] projection, String selection, String order, String limit) {
private Cursor queryTables(String[] projection, String selection, String order, String groupBy, String limit) {
String[] mmsProjection = {"date * 1000 AS normalized_date", "_id", "body", "read", "thread_id", "type", "address", "subject", "date", "m_type", "msg_box", "transport_type"};
String[] smsProjection = {"date * 1 AS normalized_date", "_id", "body", "read", "thread_id", "type", "address", "subject", "date", "m_type", "msg_box", "transport_type"};
@ -110,7 +162,7 @@ public class MmsSmsDatabase extends Database {
SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
outerQueryBuilder.setTables("(" + unionQuery + ")");
String query = outerQueryBuilder.buildQuery(projection, null, null, null, null, null, limit);
String query = outerQueryBuilder.buildQuery(projection, null, null, groupBy, null, null, limit);
Log.w("MmsSmsDatabase", "Executing query: " + query);
SQLiteDatabase db = databaseHelper.getReadableDatabase();

View File

@ -10,21 +10,21 @@ public class ConversationLoader extends CursorLoader {
private final Context context;
private final long threadId;
// private final boolean isGroupConversation;
private final boolean isGroupConversation;
public ConversationLoader(Context context, long threadId, boolean isGroupConversation) {
super(context);
this.context = context.getApplicationContext();
this.threadId = threadId;
// this.isGroupConversation = isGroupConversation;
this.isGroupConversation = isGroupConversation;
}
@Override
public Cursor loadInBackground() {
// if (!isGroupConversation) {
if (!isGroupConversation) {
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId);
// } else {
// return DatabaseFactory.getMmsSmsDatabase(context).getCollatedGroupConversation(threadId);
// }
} else {
return DatabaseFactory.getMmsSmsDatabase(context).getCollatedGroupConversation(threadId);
}
}
}

View File

@ -43,9 +43,9 @@ public class MediaMmsMessageRecord extends MessageRecord {
public MediaMmsMessageRecord(Context context, long id, Recipients recipients,
Recipient individualRecipient, long date, long threadId,
SlideDeck slideDeck, long mailbox)
SlideDeck slideDeck, long mailbox, GroupData groupData)
{
super(id, recipients, individualRecipient, date, threadId);
super(id, recipients, individualRecipient, date, threadId, groupData);
this.slideDeck = slideDeck;
this.mailbox = mailbox;

View File

@ -31,14 +31,17 @@ public abstract class MessageRecord extends DisplayRecord {
private final Recipient individualRecipient;
private final long id;
private final GroupData groupData;
public MessageRecord(long id, Recipients recipients,
Recipient individualRecipient,
long date, long threadId)
long date, long threadId,
GroupData groupData)
{
super(recipients, date, threadId);
this.id = id;
this.individualRecipient = individualRecipient;
this.groupData = groupData;
}
public abstract boolean isOutgoing();
@ -67,17 +70,29 @@ public abstract class MessageRecord extends DisplayRecord {
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;
// }
// }
public GroupData getGroupData() {
return this.groupData;
}
protected boolean isGroupDeliveryPending() {
if (this.groupData != null) {
return groupData.groupSentCount + groupData.groupSendFailedCount < groupData.groupSize;
}
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;
}
}
}

View File

@ -41,7 +41,7 @@ public class NotificationMmsMessageRecord extends MessageRecord {
long messageSize, long expiry,
int status, byte[] transactionId)
{
super(id, recipients, individualRecipient, date, threadId);
super(id, recipients, individualRecipient, date, threadId, null);
this.contentLocation = contentLocation;
this.messageSize = messageSize;
this.expiry = expiry;

View File

@ -40,11 +40,12 @@ public class SmsMessageRecord extends MessageRecord {
public SmsMessageRecord(Context context, long id,
Recipients recipients,
Recipient individualRecipient,
long date, long type, long threadId)
long date, long type, long threadId,
GroupData groupData)
{
super(id, recipients, individualRecipient, date, threadId);
this.context = context.getApplicationContext();
this.type = type;
super(id, recipients, individualRecipient, date, threadId, groupData);
this.context = context.getApplicationContext();
this.type = type;
}
public long getType() {
@ -82,7 +83,7 @@ public class SmsMessageRecord extends MessageRecord {
@Override
public boolean isPending() {
return SmsDatabase.Types.isPendingMessageType(getType());
return SmsDatabase.Types.isPendingMessageType(getType()) || isGroupDeliveryPending();
}
@Override
@ -95,17 +96,4 @@ public class SmsMessageRecord extends MessageRecord {
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;
}
}
}

View File

@ -76,6 +76,8 @@ public class MessageSender {
if (threadId == -1)
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
long date = System.currentTimeMillis();
for (Recipient recipient : recipients.getRecipientsList()) {
boolean isSecure = KeyUtil.isSessionFor(context, recipient) && !forcePlaintext;
@ -85,12 +87,12 @@ public class MessageSender {
messageId = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageSent(masterSecret,
PhoneNumberUtils.formatNumber(recipient.getNumber()),
threadId, message, System.currentTimeMillis());
threadId, message, date);
} else {
messageId = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertSecureMessageSent(masterSecret,
PhoneNumberUtils.formatNumber(recipient.getNumber()),
threadId, message, System.currentTimeMillis());
threadId, message, date);
}
Log.w("SMSSender", "Got message id for new message: " + messageId);