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

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2015 Open Whisper Systems
*
* 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.TextView;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
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.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
@ -153,12 +156,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
@Override
public void onModified(final Recipient recipient) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
setActionBarColor(recipient.getColor());
}
});
Util.runOnMain(() -> setActionBarColor(recipient.getColor()));
}
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;
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__with;
@ -251,8 +249,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
toFromRes = R.string.message_details_header__from;
}
toFrom.setText(toFromRes);
conversationItem.bind(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(),
new HashSet<MessageRecord>(), recipient);
conversationItem.bind(masterSecret, messageRecord, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient);
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, masterSecret, messageRecord,
recipients, isPushGroup));
}
@ -319,11 +316,12 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
return false;
}
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<Recipient>> {
private WeakReference<Context> weakContext;
private MessageRecord messageRecord;
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<RecipientDeliveryStatus>> {
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.messageRecord = messageRecord;
}
@ -333,27 +331,41 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
}
@Override
public List<Recipient> doInBackground(Void... voids) {
public List<RecipientDeliveryStatus> doInBackground(Void... voids) {
Context context = getContext();
if (context == null) {
Log.w(TAG, "associated context is destroyed, finishing early");
return null;
}
List<Recipient> recipients = new LinkedList<>();
List<RecipientDeliveryStatus> recipients = new LinkedList<>();
if (!messageRecord.getRecipient().isGroupRecipient()) {
Log.w(TAG, "Recipient is not a group, resolving members immediately.");
recipients.add(messageRecord.getRecipient());
recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), -1));
} 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;
}
@Override
public void onPostExecute(List<Recipient> recipients) {
public void onPostExecute(List<RecipientDeliveryStatus> recipients) {
if (getContext() == null) {
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
return;
@ -373,5 +385,22 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
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.util.List;
public class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
private final Context context;
private final MasterSecret masterSecret;
private final MessageRecord record;
private final List<Recipient> recipients;
private final boolean isPushGroup;
private final Context context;
private final MasterSecret masterSecret;
private final MessageRecord record;
private final List<RecipientDeliveryStatus> members;
private final boolean isPushGroup;
public MessageDetailsRecipientAdapter(Context context, MasterSecret masterSecret,
MessageRecord record, List<Recipient> recipients,
boolean isPushGroup)
MessageDetailsRecipientAdapter(Context context, MasterSecret masterSecret, MessageRecord record,
List<RecipientDeliveryStatus> members, boolean isPushGroup)
{
this.context = context;
this.masterSecret = masterSecret;
this.record = record;
this.recipients = recipients;
this.isPushGroup = isPushGroup;
this.members = members;
}
@Override
public int getCount() {
return recipients.size();
return members.size();
}
@Override
public Object getItem(int position) {
return recipients.get(position);
return members.get(position);
}
@Override
public long getItemId(int position) {
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) {
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);
}
Recipient recipient = recipients.get(position);
((MessageRecipientListItem)convertView).set(masterSecret, record, recipient, isPushGroup);
RecipientDeliveryStatus member = members.get(position);
((MessageRecipientListItem)convertView).set(masterSecret, record, member, isPushGroup);
return convertView;
}
@ -70,4 +70,35 @@ public class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsLi
((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
*
* 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.TextView;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -48,13 +50,14 @@ public class MessageRecipientListItem extends RelativeLayout
{
private final static String TAG = MessageRecipientListItem.class.getSimpleName();
private Recipient recipient;
private FromTextView fromView;
private TextView errorDescription;
private TextView actionDescription;
private Button conflictButton;
private Button resendButton;
private AvatarImageView contactPhotoImage;
private RecipientDeliveryStatus member;
private FromTextView fromView;
private TextView errorDescription;
private TextView actionDescription;
private Button conflictButton;
private Button resendButton;
private AvatarImageView contactPhotoImage;
private DeliveryStatusView deliveryStatusView;
public MessageRecipientListItem(Context context) {
super(context);
@ -67,24 +70,25 @@ public class MessageRecipientListItem extends RelativeLayout
@Override
protected void onFinishInflate() {
super.onFinishInflate();
this.fromView = (FromTextView) findViewById(R.id.from);
this.errorDescription = (TextView) findViewById(R.id.error_description);
this.actionDescription = (TextView) findViewById(R.id.action_description);
this.contactPhotoImage = (AvatarImageView) findViewById(R.id.contact_photo_image);
this.conflictButton = (Button) findViewById(R.id.conflict_button);
this.resendButton = (Button) findViewById(R.id.resend_button);
this.fromView = (FromTextView) findViewById(R.id.from);
this.errorDescription = (TextView) findViewById(R.id.error_description);
this.actionDescription = (TextView) findViewById(R.id.action_description);
this.contactPhotoImage = (AvatarImageView) findViewById(R.id.contact_photo_image);
this.conflictButton = (Button) findViewById(R.id.conflict_button);
this.resendButton = (Button) findViewById(R.id.resend_button);
this.deliveryStatusView = (DeliveryStatusView) findViewById(R.id.delivery_status);
}
public void set(final MasterSecret masterSecret,
final MessageRecord record,
final Recipient recipient,
final RecipientDeliveryStatus member,
final boolean isPushGroup)
{
this.recipient = recipient;
this.member = member;
recipient.addListener(this);
fromView.setText(recipient);
contactPhotoImage.setAvatar(recipient, false);
member.getRecipient().addListener(this);
fromView.setText(member.getRecipient());
contactPhotoImage.setAvatar(member.getRecipient(), false);
setIssueIndicators(masterSecret, record, isPushGroup);
}
@ -102,12 +106,7 @@ public class MessageRecipientListItem extends RelativeLayout
conflictButton.setVisibility(View.VISIBLE);
errorText = getContext().getString(R.string.MessageDetailsRecipient_new_safety_number);
conflictButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new ConfirmIdentityDialog(getContext(), masterSecret, record, keyMismatch).show();
}
});
conflictButton.setOnClickListener(v -> new ConfirmIdentityDialog(getContext(), masterSecret, record, keyMismatch).show());
} else if (networkFailure != null || (!isPushGroup && record.isFailed())) {
resendButton.setVisibility(View.VISIBLE);
resendButton.setEnabled(true);
@ -115,17 +114,27 @@ public class MessageRecipientListItem extends RelativeLayout
conflictButton.setVisibility(View.GONE);
errorText = getContext().getString(R.string.MessageDetailsRecipient_failed_to_send);
resendButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
resendButton.setVisibility(View.GONE);
errorDescription.setVisibility(View.GONE);
actionDescription.setVisibility(View.VISIBLE);
actionDescription.setText(R.string.message_recipients_list_item__resending);
new ResendAsyncTask(masterSecret, record, networkFailure).execute();
}
resendButton.setOnClickListener(v -> {
resendButton.setVisibility(View.GONE);
errorDescription.setVisibility(View.GONE);
actionDescription.setVisibility(View.VISIBLE);
actionDescription.setText(R.string.message_recipients_list_item__resending);
new ResendAsyncTask(masterSecret, record, networkFailure).execute();
});
} 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);
conflictButton.setVisibility(View.GONE);
}
@ -137,7 +146,7 @@ public class MessageRecipientListItem extends RelativeLayout
private NetworkFailure getNetworkFailure(final MessageRecord record) {
if (record.hasNetworkFailures()) {
for (final NetworkFailure failure : record.getNetworkFailures()) {
if (failure.getAddress().equals(recipient.getAddress())) {
if (failure.getAddress().equals(member.getRecipient().getAddress())) {
return failure;
}
}
@ -148,7 +157,7 @@ public class MessageRecipientListItem extends RelativeLayout
private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) {
if (record.isIdentityMismatchFailure()) {
for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) {
if (mismatch.getAddress().equals(recipient.getAddress())) {
if (mismatch.getAddress().equals(member.getRecipient().getAddress())) {
return mismatch;
}
}
@ -157,7 +166,7 @@ public class MessageRecipientListItem extends RelativeLayout
}
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

View File

@ -107,7 +107,8 @@ public class DatabaseFactory {
private static final int PROFILE_SHARING_APPROVAL = 42;
private static final int UNSEEN_NUMBER_OFFER = 43;
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 Object lock = new Object();
@ -129,6 +130,7 @@ public class DatabaseFactory {
private final GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase;
private final ContactsDatabase contactsDatabase;
private final GroupReceiptDatabase groupReceiptDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
@ -191,21 +193,26 @@ public class DatabaseFactory {
return getInstance(context).contactsDatabase;
}
public static GroupReceiptDatabase getGroupReceiptDatabase(Context context) {
return getInstance(context).groupReceiptDatabase;
}
private DatabaseFactory(Context context) {
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms = new SmsDatabase(context, databaseHelper);
this.encryptingSms = new EncryptingSmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.attachments = new AttachmentDatabase(context, databaseHelper);
this.media = new MediaDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
this.contactsDatabase = new ContactsDatabase(context);
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms = new SmsDatabase(context, databaseHelper);
this.encryptingSms = new EncryptingSmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.attachments = new AttachmentDatabase(context, databaseHelper);
this.media = new MediaDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
this.contactsDatabase = new ContactsDatabase(context);
}
public void reset(Context context) {
@ -223,6 +230,7 @@ public class DatabaseFactory {
this.pushDatabase.reset(databaseHelper);
this.groupDatabase.reset(databaseHelper);
this.recipientDatabase.reset(databaseHelper);
this.groupReceiptDatabase.reset(databaseHelper);
old.close();
}
@ -536,6 +544,7 @@ public class DatabaseFactory {
db.execSQL(PushDatabase.CREATE_TABLE);
db.execSQL(GroupDatabase.CREATE_TABLE);
db.execSQL(RecipientDatabase.CREATE_TABLE);
db.execSQL(GroupReceiptDatabase.CREATE_TABLE);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -543,6 +552,7 @@ public class DatabaseFactory {
executeStatements(db, ThreadDatabase.CREATE_INDEXS);
executeStatements(db, DraftDatabase.CREATE_INDEXS);
executeStatements(db, GroupDatabase.CREATE_INDEXS);
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
}
@Override
@ -1328,6 +1338,11 @@ public class DatabaseFactory {
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.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.Pair;
import com.annimon.stream.Stream;
import com.google.android.mms.pdu_alt.NotificationInd;
import com.google.android.mms.pdu_alt.PduHeaders;
@ -125,7 +126,7 @@ public class MmsDatabase extends MessagingDatabase {
CONTENT_LOCATION, EXPIRY, MESSAGE_TYPE,
MESSAGE_SIZE, STATUS, TRANSACTION_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,
AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS,
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();
Cursor cursor = null;
boolean found = false;
@ -211,6 +212,7 @@ public class MmsDatabase extends MessagingDatabase {
if (ourAddress.equals(theirAddress) || theirAddress.isGroup()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
int status = deliveryReceipt ? GroupReceiptDatabase.STATUS_DELIVERED : GroupReceiptDatabase.STATUS_READ;
found = true;
@ -218,6 +220,7 @@ public class MmsDatabase extends MessagingDatabase {
columnName + " = " + columnName + " + 1 WHERE " + ID + " = ?",
new String[] {String.valueOf(id)});
DatabaseFactory.getGroupReceiptDatabase(context).update(ourAddress, id, status, timestamp);
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId);
}
@ -814,6 +817,14 @@ public class MmsDatabase extends MessagingDatabase {
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).setHasSent(threadId, true);
jobManager.add(new TrimThreadJob(context, threadId));
@ -891,6 +902,9 @@ public class MmsDatabase extends MessagingDatabase {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
attachmentDatabase.deleteAttachmentsForMessage(messageId);
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
groupReceiptDatabase.deleteRowsForMessage(messageId);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
@ -972,6 +986,7 @@ public class MmsDatabase extends MessagingDatabase {
public void deleteAllThreads() {
DatabaseFactory.getAttachmentDatabase(context).deleteAllAttachments();
DatabaseFactory.getGroupReceiptDatabase(context).deleteAllRows();
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, null, null);

View File

@ -137,14 +137,14 @@ public class MmsSmsDatabase extends Database {
return count;
}
public void incrementDeliveryReceiptCount(SyncMessageId syncMessageId) {
public void incrementDeliveryReceiptCount(SyncMessageId syncMessageId, long timestamp) {
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.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) {

View File

@ -836,7 +836,7 @@ public class PushDecryptJob extends ContextJob {
for (long timestamp : message.getTimestamps()) {
Log.w(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp));
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));
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.util.Log;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.Address;
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.NoSuchMessageException;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
@ -137,7 +141,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
{
String groupId = message.getRecipient().getAddress().toGroupString();
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();
List<Attachment> scaledAttachments = scaleAttachments(masterSecret, mediaConstraints, message.getAttachments());
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(masterSecret, scaledAttachments);
@ -181,13 +185,15 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
return addresses;
}
private List<SignalServiceAddress> getPushAddresses(List<Recipient> recipients) {
List<SignalServiceAddress> addresses = new LinkedList<>();
private List<SignalServiceAddress> getPushAddresses(List<Address> addresses) {
return Stream.of(addresses).map(this::getPushAddress).toList();
}
for (Recipient recipient : recipients) {
addresses.add(getPushAddress(recipient.getAddress()));
}
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();
return addresses;
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) {
Log.w(TAG, String.format("Received receipt: (XXXXX, %d)", envelope.getTimestamp()));
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(new SyncMessageId(Address.fromExternal(context, envelope.getSource()),
envelope.getTimestamp()));
envelope.getTimestamp()), System.currentTimeMillis());
}
private boolean isActiveNumber(@NonNull Recipient recipient) {