Show per-member delivery/read status on message info in groups

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2017-09-30 08:45:45 -07:00
parent 2b4064f3b7
commit 285947eb66
11 changed files with 331 additions and 109 deletions

View File

@ -6,14 +6,13 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="10dp" android:padding="16dp">
android:paddingRight="10dp">
<org.thoughtcrime.securesms.components.AvatarImageView <org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/contact_photo_image" android:id="@+id/contact_photo_image"
android:foreground="@drawable/contact_photo_background" android:foreground="@drawable/contact_photo_background"
android:layout_width="50dp" android:layout_width="40dp"
android:layout_height="50dp" android:layout_height="40dp"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_marginTop="3dp" android:layout_marginTop="3dp"
@ -28,6 +27,7 @@
android:layout_marginTop="4dip" android:layout_marginTop="4dip"
android:layout_marginBottom="4dip" android:layout_marginBottom="4dip"
android:layout_toRightOf="@id/contact_photo_image" android:layout_toRightOf="@id/contact_photo_image"
android:layout_toEndOf="@id/contact_photo_image"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:orientation="horizontal"> android:orientation="horizontal">
@ -77,7 +77,7 @@
android:drawableLeft="@drawable/ic_error_white_18dp" android:drawableLeft="@drawable/ic_error_white_18dp"
android:text="@string/message_recipients_list_item__view" android:text="@string/message_recipients_list_item__view"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="gone" />
<Button android:id="@+id/resend_button" <Button android:id="@+id/resend_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -91,8 +91,13 @@
android:drawableLeft="@drawable/ic_refresh_white_18dp" android:drawableLeft="@drawable/ic_refresh_white_18dp"
android:text="@string/message_recipients_list_item__resend" android:text="@string/message_recipients_list_item__resend"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="gone" />
<org.thoughtcrime.securesms.components.DeliveryStatusView
android:id="@+id/delivery_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"/>
</LinearLayout> </LinearLayout>

View File

@ -1,4 +1,4 @@
/** /*
* Copyright (C) 2015 Open Whisper Systems * Copyright (C) 2015 Open Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -34,11 +34,14 @@ import android.view.ViewGroup;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
@ -153,12 +156,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onModified(final Recipient recipient) { public void onModified(final Recipient recipient) {
Util.runOnMain(new Runnable() { Util.runOnMain(() -> setActionBarColor(recipient.getColor()));
@Override
public void run() {
setActionBarColor(recipient.getColor());
}
});
} }
private void initializeResources() { private void initializeResources() {
@ -241,7 +239,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
}); });
} }
private void updateRecipients(MessageRecord messageRecord, Recipient recipient, List<Recipient> recipients) { private void updateRecipients(MessageRecord messageRecord, Recipient recipient, List<RecipientDeliveryStatus> recipients) {
final int toFromRes; final int toFromRes;
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) { if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__with; toFromRes = R.string.message_details_header__with;
@ -251,8 +249,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
toFromRes = R.string.message_details_header__from; toFromRes = R.string.message_details_header__from;
} }
toFrom.setText(toFromRes); toFrom.setText(toFromRes);
conversationItem.bind(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(), conversationItem.bind(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient);
new HashSet<MessageRecord>(), recipient);
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, masterSecret, messageRecord, recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, masterSecret, messageRecord,
recipients, isPushGroup)); recipients, isPushGroup));
} }
@ -319,11 +316,12 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
return false; return false;
} }
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<Recipient>> { private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<RecipientDeliveryStatus>> {
private WeakReference<Context> weakContext;
private MessageRecord messageRecord;
public MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) { private final WeakReference<Context> weakContext;
private final MessageRecord messageRecord;
MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
this.weakContext = new WeakReference<>(context); this.weakContext = new WeakReference<>(context);
this.messageRecord = messageRecord; this.messageRecord = messageRecord;
} }
@ -333,27 +331,41 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
} }
@Override @Override
public List<Recipient> doInBackground(Void... voids) { public List<RecipientDeliveryStatus> doInBackground(Void... voids) {
Context context = getContext(); Context context = getContext();
if (context == null) { if (context == null) {
Log.w(TAG, "associated context is destroyed, finishing early"); Log.w(TAG, "associated context is destroyed, finishing early");
return null; return null;
} }
List<Recipient> recipients = new LinkedList<>(); List<RecipientDeliveryStatus> recipients = new LinkedList<>();
if (!messageRecord.getRecipient().isGroupRecipient()) { if (!messageRecord.getRecipient().isGroupRecipient()) {
Log.w(TAG, "Recipient is not a group, resolving members immediately."); recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), -1));
recipients.add(messageRecord.getRecipient());
} else { } else {
recipients.addAll(DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().getAddress().toGroupString(), false)); List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
if (receiptInfoList.isEmpty()) {
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().getAddress().toGroupString(), false);
for (Recipient recipient : group) {
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, -1));
}
} else {
for (GroupReceiptInfo info : receiptInfoList) {
recipients.add(new RecipientDeliveryStatus(Recipient.from(context, info.getAddress(), true),
getStatusFor(info.getStatus(), messageRecord.isPending()),
info.getTimestamp()));
}
}
} }
return recipients; return recipients;
} }
@Override @Override
public void onPostExecute(List<Recipient> recipients) { public void onPostExecute(List<RecipientDeliveryStatus> recipients) {
if (getContext() == null) { if (getContext() == null) {
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early."); Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
return; return;
@ -373,5 +385,22 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
metadataContainer.setVisibility(View.VISIBLE); metadataContainer.setVisibility(View.VISIBLE);
} }
} }
private RecipientDeliveryStatus.Status getStatusFor(int deliveryReceiptCount, int readReceiptCount, boolean pending) {
if (readReceiptCount > 0) return RecipientDeliveryStatus.Status.READ;
else if (deliveryReceiptCount > 0) return RecipientDeliveryStatus.Status.DELIVERED;
else if (!pending) return RecipientDeliveryStatus.Status.SENT;
else return RecipientDeliveryStatus.Status.PENDING;
}
private RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending) {
if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ;
else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING;
else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN;
throw new AssertionError();
}
} }
} }

View File

@ -16,39 +16,38 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.List; import java.util.List;
public class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener { class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
private final Context context; private final Context context;
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
private final MessageRecord record; private final MessageRecord record;
private final List<Recipient> recipients; private final List<RecipientDeliveryStatus> members;
private final boolean isPushGroup; private final boolean isPushGroup;
public MessageDetailsRecipientAdapter(Context context, MasterSecret masterSecret, MessageDetailsRecipientAdapter(Context context, MasterSecret masterSecret, MessageRecord record,
MessageRecord record, List<Recipient> recipients, List<RecipientDeliveryStatus> members, boolean isPushGroup)
boolean isPushGroup)
{ {
this.context = context; this.context = context;
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
this.record = record; this.record = record;
this.recipients = recipients;
this.isPushGroup = isPushGroup; this.isPushGroup = isPushGroup;
this.members = members;
} }
@Override @Override
public int getCount() { public int getCount() {
return recipients.size(); return members.size();
} }
@Override @Override
public Object getItem(int position) { public Object getItem(int position) {
return recipients.get(position); return members.get(position);
} }
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
try { try {
return Conversions.byteArrayToLong(MessageDigest.getInstance("SHA1").digest(recipients.get(position).getAddress().serialize().getBytes())); return Conversions.byteArrayToLong(MessageDigest.getInstance("SHA1").digest(members.get(position).recipient.getAddress().serialize().getBytes()));
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -60,8 +59,9 @@ public class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsLi
convertView = LayoutInflater.from(context).inflate(R.layout.message_recipient_list_item, parent, false); convertView = LayoutInflater.from(context).inflate(R.layout.message_recipient_list_item, parent, false);
} }
Recipient recipient = recipients.get(position); RecipientDeliveryStatus member = members.get(position);
((MessageRecipientListItem)convertView).set(masterSecret, record, recipient, isPushGroup);
((MessageRecipientListItem)convertView).set(masterSecret, record, member, isPushGroup);
return convertView; return convertView;
} }
@ -70,4 +70,35 @@ public class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsLi
((MessageRecipientListItem)view).unbind(); ((MessageRecipientListItem)view).unbind();
} }
static class RecipientDeliveryStatus {
enum Status {
UNKNOWN, PENDING, SENT, DELIVERED, READ
}
private final Recipient recipient;
private final Status deliveryStatus;
private final long timestamp;
RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, long timestamp) {
this.recipient = recipient;
this.deliveryStatus = deliveryStatus;
this.timestamp = timestamp;
}
Status getDeliveryStatus() {
return deliveryStatus;
}
public long getTimestamp() {
return timestamp;
}
public Recipient getRecipient() {
return recipient;
}
}
} }

View File

@ -1,4 +1,4 @@
/** /*
* Copyright (C) 2014 Open Whisper Systems * Copyright (C) 2014 Open Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -25,7 +25,9 @@ import android.widget.Button;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.FromTextView; import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -48,13 +50,14 @@ public class MessageRecipientListItem extends RelativeLayout
{ {
private final static String TAG = MessageRecipientListItem.class.getSimpleName(); private final static String TAG = MessageRecipientListItem.class.getSimpleName();
private Recipient recipient; private RecipientDeliveryStatus member;
private FromTextView fromView; private FromTextView fromView;
private TextView errorDescription; private TextView errorDescription;
private TextView actionDescription; private TextView actionDescription;
private Button conflictButton; private Button conflictButton;
private Button resendButton; private Button resendButton;
private AvatarImageView contactPhotoImage; private AvatarImageView contactPhotoImage;
private DeliveryStatusView deliveryStatusView;
public MessageRecipientListItem(Context context) { public MessageRecipientListItem(Context context) {
super(context); super(context);
@ -73,18 +76,19 @@ public class MessageRecipientListItem extends RelativeLayout
this.contactPhotoImage = (AvatarImageView) findViewById(R.id.contact_photo_image); this.contactPhotoImage = (AvatarImageView) findViewById(R.id.contact_photo_image);
this.conflictButton = (Button) findViewById(R.id.conflict_button); this.conflictButton = (Button) findViewById(R.id.conflict_button);
this.resendButton = (Button) findViewById(R.id.resend_button); this.resendButton = (Button) findViewById(R.id.resend_button);
this.deliveryStatusView = (DeliveryStatusView) findViewById(R.id.delivery_status);
} }
public void set(final MasterSecret masterSecret, public void set(final MasterSecret masterSecret,
final MessageRecord record, final MessageRecord record,
final Recipient recipient, final RecipientDeliveryStatus member,
final boolean isPushGroup) final boolean isPushGroup)
{ {
this.recipient = recipient; this.member = member;
recipient.addListener(this); member.getRecipient().addListener(this);
fromView.setText(recipient); fromView.setText(member.getRecipient());
contactPhotoImage.setAvatar(recipient, false); contactPhotoImage.setAvatar(member.getRecipient(), false);
setIssueIndicators(masterSecret, record, isPushGroup); setIssueIndicators(masterSecret, record, isPushGroup);
} }
@ -102,12 +106,7 @@ public class MessageRecipientListItem extends RelativeLayout
conflictButton.setVisibility(View.VISIBLE); conflictButton.setVisibility(View.VISIBLE);
errorText = getContext().getString(R.string.MessageDetailsRecipient_new_safety_number); errorText = getContext().getString(R.string.MessageDetailsRecipient_new_safety_number);
conflictButton.setOnClickListener(new OnClickListener() { conflictButton.setOnClickListener(v -> new ConfirmIdentityDialog(getContext(), masterSecret, record, keyMismatch).show());
@Override
public void onClick(View v) {
new ConfirmIdentityDialog(getContext(), masterSecret, record, keyMismatch).show();
}
});
} else if (networkFailure != null || (!isPushGroup && record.isFailed())) { } else if (networkFailure != null || (!isPushGroup && record.isFailed())) {
resendButton.setVisibility(View.VISIBLE); resendButton.setVisibility(View.VISIBLE);
resendButton.setEnabled(true); resendButton.setEnabled(true);
@ -115,17 +114,27 @@ public class MessageRecipientListItem extends RelativeLayout
conflictButton.setVisibility(View.GONE); conflictButton.setVisibility(View.GONE);
errorText = getContext().getString(R.string.MessageDetailsRecipient_failed_to_send); errorText = getContext().getString(R.string.MessageDetailsRecipient_failed_to_send);
resendButton.setOnClickListener(new OnClickListener() { resendButton.setOnClickListener(v -> {
@Override
public void onClick(View v) {
resendButton.setVisibility(View.GONE); resendButton.setVisibility(View.GONE);
errorDescription.setVisibility(View.GONE); errorDescription.setVisibility(View.GONE);
actionDescription.setVisibility(View.VISIBLE); actionDescription.setVisibility(View.VISIBLE);
actionDescription.setText(R.string.message_recipients_list_item__resending); actionDescription.setText(R.string.message_recipients_list_item__resending);
new ResendAsyncTask(masterSecret, record, networkFailure).execute(); new ResendAsyncTask(masterSecret, record, networkFailure).execute();
}
}); });
} else { } else {
if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.PENDING || member.getDeliveryStatus() == RecipientDeliveryStatus.Status.UNKNOWN) {
deliveryStatusView.setVisibility(View.GONE);
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.READ) {
deliveryStatusView.setRead();
deliveryStatusView.setVisibility(View.VISIBLE);
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.DELIVERED) {
deliveryStatusView.setDelivered();
deliveryStatusView.setVisibility(View.VISIBLE);
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.SENT) {
deliveryStatusView.setSent();
deliveryStatusView.setVisibility(View.VISIBLE);
}
resendButton.setVisibility(View.GONE); resendButton.setVisibility(View.GONE);
conflictButton.setVisibility(View.GONE); conflictButton.setVisibility(View.GONE);
} }
@ -137,7 +146,7 @@ public class MessageRecipientListItem extends RelativeLayout
private NetworkFailure getNetworkFailure(final MessageRecord record) { private NetworkFailure getNetworkFailure(final MessageRecord record) {
if (record.hasNetworkFailures()) { if (record.hasNetworkFailures()) {
for (final NetworkFailure failure : record.getNetworkFailures()) { for (final NetworkFailure failure : record.getNetworkFailures()) {
if (failure.getAddress().equals(recipient.getAddress())) { if (failure.getAddress().equals(member.getRecipient().getAddress())) {
return failure; return failure;
} }
} }
@ -148,7 +157,7 @@ public class MessageRecipientListItem extends RelativeLayout
private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) { private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) {
if (record.isIdentityMismatchFailure()) { if (record.isIdentityMismatchFailure()) {
for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) { for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) {
if (mismatch.getAddress().equals(recipient.getAddress())) { if (mismatch.getAddress().equals(member.getRecipient().getAddress())) {
return mismatch; return mismatch;
} }
} }
@ -157,7 +166,7 @@ public class MessageRecipientListItem extends RelativeLayout
} }
public void unbind() { public void unbind() {
if (this.recipient != null) this.recipient.removeListener(this); if (this.member != null && this.member.getRecipient() != null) this.member.getRecipient().removeListener(this);
} }
@Override @Override

View File

@ -107,7 +107,8 @@ public class DatabaseFactory {
private static final int PROFILE_SHARING_APPROVAL = 42; private static final int PROFILE_SHARING_APPROVAL = 42;
private static final int UNSEEN_NUMBER_OFFER = 43; private static final int UNSEEN_NUMBER_OFFER = 43;
private static final int READ_RECEIPTS = 44; private static final int READ_RECEIPTS = 44;
private static final int DATABASE_VERSION = 44; private static final int GROUP_RECEIPT_TRACKING = 45;
private static final int DATABASE_VERSION = 45;
private static final String DATABASE_NAME = "messages.db"; private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object(); private static final Object lock = new Object();
@ -129,6 +130,7 @@ public class DatabaseFactory {
private final GroupDatabase groupDatabase; private final GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase; private final RecipientDatabase recipientDatabase;
private final ContactsDatabase contactsDatabase; private final ContactsDatabase contactsDatabase;
private final GroupReceiptDatabase groupReceiptDatabase;
public static DatabaseFactory getInstance(Context context) { public static DatabaseFactory getInstance(Context context) {
synchronized (lock) { synchronized (lock) {
@ -191,6 +193,10 @@ public class DatabaseFactory {
return getInstance(context).contactsDatabase; return getInstance(context).contactsDatabase;
} }
public static GroupReceiptDatabase getGroupReceiptDatabase(Context context) {
return getInstance(context).groupReceiptDatabase;
}
private DatabaseFactory(Context context) { private DatabaseFactory(Context context) {
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION); this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms = new SmsDatabase(context, databaseHelper); this.sms = new SmsDatabase(context, databaseHelper);
@ -205,6 +211,7 @@ public class DatabaseFactory {
this.pushDatabase = new PushDatabase(context, databaseHelper); this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper); this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientDatabase = new RecipientDatabase(context, databaseHelper); this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
this.contactsDatabase = new ContactsDatabase(context); this.contactsDatabase = new ContactsDatabase(context);
} }
@ -223,6 +230,7 @@ public class DatabaseFactory {
this.pushDatabase.reset(databaseHelper); this.pushDatabase.reset(databaseHelper);
this.groupDatabase.reset(databaseHelper); this.groupDatabase.reset(databaseHelper);
this.recipientDatabase.reset(databaseHelper); this.recipientDatabase.reset(databaseHelper);
this.groupReceiptDatabase.reset(databaseHelper);
old.close(); old.close();
} }
@ -536,6 +544,7 @@ public class DatabaseFactory {
db.execSQL(PushDatabase.CREATE_TABLE); db.execSQL(PushDatabase.CREATE_TABLE);
db.execSQL(GroupDatabase.CREATE_TABLE); db.execSQL(GroupDatabase.CREATE_TABLE);
db.execSQL(RecipientDatabase.CREATE_TABLE); db.execSQL(RecipientDatabase.CREATE_TABLE);
db.execSQL(GroupReceiptDatabase.CREATE_TABLE);
executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -543,6 +552,7 @@ public class DatabaseFactory {
executeStatements(db, ThreadDatabase.CREATE_INDEXS); executeStatements(db, ThreadDatabase.CREATE_INDEXS);
executeStatements(db, DraftDatabase.CREATE_INDEXS); executeStatements(db, DraftDatabase.CREATE_INDEXS);
executeStatements(db, GroupDatabase.CREATE_INDEXS); executeStatements(db, GroupDatabase.CREATE_INDEXS);
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
} }
@Override @Override
@ -1328,6 +1338,11 @@ public class DatabaseFactory {
db.execSQL("ALTER TABLE thread ADD COLUMN read_receipt_count INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE thread ADD COLUMN read_receipt_count INTEGER DEFAULT 0");
} }
if (oldVersion < GROUP_RECEIPT_TRACKING) {
db.execSQL("CREATE TABLE group_receipts (_id INTEGER PRIMARY KEY, mms_id INTEGER, address TEXT, status INTEGER, timestamp INTEGER)");
db.execSQL("CREATE INDEX IF NOT EXISTS group_receipt_mms_id_index ON group_receipts (mms_id)");
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
} }

View File

@ -0,0 +1,112 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import java.util.LinkedList;
import java.util.List;
public class GroupReceiptDatabase extends Database {
public static final String TABLE_NAME = "group_receipts";
private static final String ID = "_id";
private static final String MMS_ID = "mms_id";
private static final String ADDRESS = "address";
private static final String STATUS = "status";
private static final String TIMESTAMP = "timestamp";
public static final int STATUS_UNKNOWN = -1;
public static final int STATUS_UNDELIVERED = 0;
public static final int STATUS_DELIVERED = 1;
public static final int STATUS_READ = 2;
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
MMS_ID + " INTEGER, " + ADDRESS + " TEXT, " + STATUS + " INTEGER, " + TIMESTAMP + " INTEGER);";
public static final String[] CREATE_INDEXES = {
"CREATE INDEX IF NOT EXISTS group_receipt_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
};
public GroupReceiptDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public void insert(List<Address> addresses, long mmsId, int status, long timestamp) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
for (Address address : addresses) {
ContentValues values = new ContentValues(4);
values.put(MMS_ID, mmsId);
values.put(ADDRESS, address.serialize());
values.put(STATUS, status);
values.put(TIMESTAMP, timestamp);
db.insert(TABLE_NAME, null, values);
}
}
public void update(Address address, long mmsId, int status, long timestamp) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues(2);
values.put(STATUS, status);
values.put(TIMESTAMP, timestamp);
db.update(TABLE_NAME, values, MMS_ID + " = ? AND " + ADDRESS + " = ? AND " + STATUS + " < ?",
new String[] {String.valueOf(mmsId), address.serialize(), String.valueOf(status)});
}
public @NonNull List<GroupReceiptInfo> getGroupReceiptInfo(long mmsId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
List<GroupReceiptInfo> results = new LinkedList<>();
try (Cursor cursor = db.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)}, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
results.add(new GroupReceiptInfo(Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))),
cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)),
cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP))));
}
}
return results;
}
void deleteRowsForMessage(long mmsId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});
}
void deleteAllRows() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
public static class GroupReceiptInfo {
private final Address address;
private final int status;
private final long timestamp;
public GroupReceiptInfo(Address address, int status, long timestamp) {
this.address = address;
this.status = status;
this.timestamp = timestamp;
}
public Address getAddress() {
return address;
}
public int getStatus() {
return status;
}
public long getTimestamp() {
return timestamp;
}
}
}

View File

@ -28,6 +28,7 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import com.annimon.stream.Stream;
import com.google.android.mms.pdu_alt.NotificationInd; import com.google.android.mms.pdu_alt.NotificationInd;
import com.google.android.mms.pdu_alt.PduHeaders; import com.google.android.mms.pdu_alt.PduHeaders;
@ -125,7 +126,7 @@ public class MmsDatabase extends MessagingDatabase {
CONTENT_LOCATION, EXPIRY, MESSAGE_TYPE, CONTENT_LOCATION, EXPIRY, MESSAGE_TYPE,
MESSAGE_SIZE, STATUS, TRANSACTION_ID, MESSAGE_SIZE, STATUS, TRANSACTION_ID,
BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
DELIVERY_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID, DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, EXPIRES_IN, EXPIRE_STARTED, NOTIFIED,
AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS, AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS,
AttachmentDatabase.UNIQUE_ID, AttachmentDatabase.UNIQUE_ID,
@ -194,7 +195,7 @@ public class MmsDatabase extends MessagingDatabase {
} }
} }
public void incrementReceiptCount(SyncMessageId messageId, boolean deliveryReceipt, boolean readReceipt) { public void incrementReceiptCount(SyncMessageId messageId, long timestamp, boolean deliveryReceipt, boolean readReceipt) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = null; Cursor cursor = null;
boolean found = false; boolean found = false;
@ -211,6 +212,7 @@ public class MmsDatabase extends MessagingDatabase {
if (ourAddress.equals(theirAddress) || theirAddress.isGroup()) { if (ourAddress.equals(theirAddress) || theirAddress.isGroup()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
int status = deliveryReceipt ? GroupReceiptDatabase.STATUS_DELIVERED : GroupReceiptDatabase.STATUS_READ;
found = true; found = true;
@ -218,6 +220,7 @@ public class MmsDatabase extends MessagingDatabase {
columnName + " = " + columnName + " + 1 WHERE " + ID + " = ?", columnName + " = " + columnName + " + 1 WHERE " + ID + " = ?",
new String[] {String.valueOf(id)}); new String[] {String.valueOf(id)});
DatabaseFactory.getGroupReceiptDatabase(context).update(ourAddress, id, status, timestamp);
DatabaseFactory.getThreadDatabase(context).update(threadId, false); DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId); notifyConversationListeners(threadId);
} }
@ -814,6 +817,14 @@ public class MmsDatabase extends MessagingDatabase {
long messageId = insertMediaMessage(masterSecret, message.getBody(), message.getAttachments(), contentValues, insertListener); long messageId = insertMediaMessage(masterSecret, message.getBody(), message.getAttachments(), contentValues, insertListener);
if (message.getRecipient().getAddress().isGroup()) {
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().getAddress().toGroupString(), false);
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
receiptDatabase.insert(Stream.of(members).map(Recipient::getAddress).toList(),
messageId, GroupReceiptDatabase.STATUS_UNDELIVERED, message.getSentTimeMillis());
}
DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId); DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
DatabaseFactory.getThreadDatabase(context).setHasSent(threadId, true); DatabaseFactory.getThreadDatabase(context).setHasSent(threadId, true);
jobManager.add(new TrimThreadJob(context, threadId)); jobManager.add(new TrimThreadJob(context, threadId));
@ -891,6 +902,9 @@ public class MmsDatabase extends MessagingDatabase {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
attachmentDatabase.deleteAttachmentsForMessage(messageId); attachmentDatabase.deleteAttachmentsForMessage(messageId);
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
groupReceiptDatabase.deleteRowsForMessage(messageId);
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false); boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
@ -972,6 +986,7 @@ public class MmsDatabase extends MessagingDatabase {
public void deleteAllThreads() { public void deleteAllThreads() {
DatabaseFactory.getAttachmentDatabase(context).deleteAllAttachments(); DatabaseFactory.getAttachmentDatabase(context).deleteAllAttachments();
DatabaseFactory.getGroupReceiptDatabase(context).deleteAllRows();
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, null, null); database.delete(TABLE_NAME, null, null);

View File

@ -137,14 +137,14 @@ public class MmsSmsDatabase extends Database {
return count; return count;
} }
public void incrementDeliveryReceiptCount(SyncMessageId syncMessageId) { public void incrementDeliveryReceiptCount(SyncMessageId syncMessageId, long timestamp) {
DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, true, false); DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, true, false);
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, true, false); DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true, false);
} }
public void incrementReadReceiptCount(SyncMessageId syncMessageId) { public void incrementReadReceiptCount(SyncMessageId syncMessageId, long timestamp) {
DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, false, true); DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, false, true);
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, false, true); DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, false, true);
} }
private Cursor queryTables(String[] projection, String selection, String order, String limit) { private Cursor queryTables(String[] projection, String selection, String order, String limit) {

View File

@ -836,7 +836,7 @@ public class PushDecryptJob extends ContextJob {
for (long timestamp : message.getTimestamps()) { for (long timestamp : message.getTimestamps()) {
Log.w(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp)); Log.w(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp));
DatabaseFactory.getMmsSmsDatabase(context) DatabaseFactory.getMmsSmsDatabase(context)
.incrementDeliveryReceiptCount(new SyncMessageId(Address.fromExternal(context, envelope.getSource()), timestamp)); .incrementDeliveryReceiptCount(new SyncMessageId(Address.fromExternal(context, envelope.getSource()), timestamp), System.currentTimeMillis());
} }
} }
@ -848,7 +848,7 @@ public class PushDecryptJob extends ContextJob {
Log.w(TAG, String.format("Received encrypted read receipt: (XXXXX, %d)", timestamp)); Log.w(TAG, String.format("Received encrypted read receipt: (XXXXX, %d)", timestamp));
DatabaseFactory.getMmsSmsDatabase(context) DatabaseFactory.getMmsSmsDatabase(context)
.incrementReadReceiptCount(new SyncMessageId(Address.fromExternal(context, envelope.getSource()), timestamp)); .incrementReadReceiptCount(new SyncMessageId(Address.fromExternal(context, envelope.getSource()), timestamp), envelope.getTimestamp());
} }
} }
} }

View File

@ -5,11 +5,15 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.documents.NetworkFailure;
@ -137,7 +141,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
{ {
String groupId = message.getRecipient().getAddress().toGroupString(); String groupId = message.getRecipient().getAddress().toGroupString();
Optional<byte[]> profileKey = getProfileKey(message.getRecipient()); Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); List<Address> recipients = getGroupMessageRecipients(groupId, messageId);
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
List<Attachment> scaledAttachments = scaleAttachments(masterSecret, mediaConstraints, message.getAttachments()); List<Attachment> scaledAttachments = scaleAttachments(masterSecret, mediaConstraints, message.getAttachments());
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(masterSecret, scaledAttachments); List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(masterSecret, scaledAttachments);
@ -181,13 +185,15 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
return addresses; return addresses;
} }
private List<SignalServiceAddress> getPushAddresses(List<Recipient> recipients) { private List<SignalServiceAddress> getPushAddresses(List<Address> addresses) {
List<SignalServiceAddress> addresses = new LinkedList<>(); return Stream.of(addresses).map(this::getPushAddress).toList();
for (Recipient recipient : recipients) {
addresses.add(getPushAddress(recipient.getAddress()));
} }
return addresses; private @NonNull List<Address> getGroupMessageRecipients(String groupId, long messageId) {
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList();
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
return Stream.of(members).map(Recipient::getAddress).toList();
} }
} }

View File

@ -56,7 +56,7 @@ public abstract class PushReceivedJob extends ContextJob {
private void handleReceipt(SignalServiceEnvelope envelope) { private void handleReceipt(SignalServiceEnvelope envelope) {
Log.w(TAG, String.format("Received receipt: (XXXXX, %d)", envelope.getTimestamp())); Log.w(TAG, String.format("Received receipt: (XXXXX, %d)", envelope.getTimestamp()));
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(new SyncMessageId(Address.fromExternal(context, envelope.getSource()), DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(new SyncMessageId(Address.fromExternal(context, envelope.getSource()),
envelope.getTimestamp())); envelope.getTimestamp()), System.currentTimeMillis());
} }
private boolean isActiveNumber(@NonNull Recipient recipient) { private boolean isActiveNumber(@NonNull Recipient recipient) {