diff --git a/briar-android/res/drawable-hdpi/message_delivered.png b/briar-android/res/drawable-hdpi/message_delivered.png new file mode 100644 index 0000000000000000000000000000000000000000..6edef05a95505d6e4605621b5975f179b331c04e Binary files /dev/null and b/briar-android/res/drawable-hdpi/message_delivered.png differ diff --git a/briar-android/res/drawable-mdpi/message_delivered.png b/briar-android/res/drawable-mdpi/message_delivered.png new file mode 100644 index 0000000000000000000000000000000000000000..1f3807209e1db00dc0b3f07fa5f2fd0e2bd813ad Binary files /dev/null and b/briar-android/res/drawable-mdpi/message_delivered.png differ diff --git a/briar-android/res/drawable-xhdpi/message_delivered.png b/briar-android/res/drawable-xhdpi/message_delivered.png new file mode 100644 index 0000000000000000000000000000000000000000..a40d4d94c0fca5e85e08ce21f3ce99f8c74ceeea Binary files /dev/null and b/briar-android/res/drawable-xhdpi/message_delivered.png differ diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java index 7cde33e7db29922bc9abc2c595a61a786e8a956d..91885fcc5d9ce79acbc4d25dce4eda78ac5313ac 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java @@ -24,8 +24,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -52,6 +54,7 @@ import org.briarproject.api.event.Event; import org.briarproject.api.event.EventListener; import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.event.MessageExpiredEvent; +import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.messaging.Group; import org.briarproject.api.messaging.GroupId; import org.briarproject.api.messaging.Message; @@ -381,9 +384,33 @@ implements EventListener, OnClickListener, OnItemClickListener { } else if(e instanceof MessageExpiredEvent) { LOG.info("Message expired, reloading"); loadHeaders(); + } else if(e instanceof MessagesAckedEvent) { + MessagesAckedEvent m = (MessagesAckedEvent) e; + if(m.getContactId().equals(contactId)) { + LOG.info("Messages acked"); + markMessagesDelivered(m.getMessageIds()); + } } } + private void markMessagesDelivered(final Collection<MessageId> acked) { + runOnUiThread(new Runnable() { + public void run() { + Set<MessageId> ackedSet = new HashSet<MessageId>(acked); + boolean changed = false; + int count = adapter.getCount(); + for(int i = 0; i < count; i++) { + ConversationItem item = adapter.getItem(i); + if(ackedSet.contains(item.getHeader().getId())) { + item.setDelivered(true); + changed = true; + } + } + if(changed) adapter.notifyDataSetChanged(); + } + }); + } + public void onClick(View view) { String message = content.getText().toString(); if(message.equals("")) return; diff --git a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java index a315cf16accaec8f0063f7eef25379f68d0dd531..249392b5458e208b998ba72e125920c4bc6e0ffc 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java @@ -1,13 +1,16 @@ package org.briarproject.android.contact; +import static android.view.Gravity.BOTTOM; import static android.view.Gravity.LEFT; -import static android.view.Gravity.RIGHT; +import static android.view.View.INVISIBLE; +import static android.widget.LinearLayout.HORIZONTAL; import static android.widget.LinearLayout.VERTICAL; import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP; import java.util.ArrayList; import org.briarproject.R; +import org.briarproject.android.util.ElasticHorizontalSpace; import org.briarproject.android.util.LayoutUtils; import org.briarproject.api.db.MessageHeader; import org.briarproject.util.StringUtils; @@ -19,6 +22,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -60,23 +64,45 @@ class ConversationAdapter extends ArrayAdapter<ConversationItem> { attachment.setImageResource(R.drawable.content_attachment); content = attachment; } - content.setId(2); content.setLayoutParams(MATCH_WRAP); content.setBackgroundColor(background); content.setPadding(pad, pad, pad, 0); layout.addView(content); - TextView date = new TextView(ctx); - date.setId(1); - date.setLayoutParams(MATCH_WRAP); - if(header.isLocal()) date.setGravity(RIGHT); - else date.setGravity(LEFT); - date.setTextColor(res.getColor(R.color.private_message_date)); - date.setBackgroundColor(background); - date.setPadding(pad, 0, pad, pad); - long timestamp = header.getTimestamp(); - date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp)); - layout.addView(date); + if(header.isLocal()) { + LinearLayout footer = new LinearLayout(ctx); + footer.setLayoutParams(MATCH_WRAP); + footer.setOrientation(HORIZONTAL); + footer.setGravity(BOTTOM); + footer.setPadding(pad, 0, pad, pad); + footer.setBackgroundColor(background); + + footer.addView(new ElasticHorizontalSpace(ctx)); + + ImageView delivered = new ImageView(ctx); + delivered.setPadding(0, 0, pad, 0); + delivered.setImageResource(R.drawable.message_delivered); + if(!item.isDelivered()) delivered.setVisibility(INVISIBLE); + footer.addView(delivered); + + TextView date = new TextView(ctx); + date.setTextColor(res.getColor(R.color.private_message_date)); + long timestamp = header.getTimestamp(); + date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp)); + footer.addView(date); + + layout.addView(footer); + } else { + TextView date = new TextView(ctx); + date.setLayoutParams(MATCH_WRAP); + date.setGravity(LEFT); + date.setTextColor(res.getColor(R.color.private_message_date)); + date.setBackgroundColor(background); + date.setPadding(pad, 0, pad, pad); + long timestamp = header.getTimestamp(); + date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp)); + layout.addView(date); + } return layout; } diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java index 1982bb58ec9429431c30285b9e5fa612416ce8e1..c1959f0c5d17f96c2c635134de9ba6b68f074b35 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java @@ -7,10 +7,12 @@ class ConversationItem { private final MessageHeader header; private byte[] body; + private boolean delivered; ConversationItem(MessageHeader header) { this.header = header; body = null; + delivered = header.isDelivered(); } MessageHeader getHeader() { @@ -24,4 +26,12 @@ class ConversationItem { void setBody(byte[] body) { this.body = body; } + + boolean isDelivered() { + return delivered; + } + + void setDelivered(boolean delivered) { + this.delivered = delivered; + } } diff --git a/briar-api/src/org/briarproject/api/db/MessageHeader.java b/briar-api/src/org/briarproject/api/db/MessageHeader.java index 6fa1e223f979091b3fdb1e384e82444e40b6facc..779e4cfa0edccba03ee05ec22fda7b1fbb4de035 100644 --- a/briar-api/src/org/briarproject/api/db/MessageHeader.java +++ b/briar-api/src/org/briarproject/api/db/MessageHeader.java @@ -12,11 +12,11 @@ public class MessageHeader { private final Author.Status authorStatus; private final String contentType; private final long timestamp; - private final boolean local, read; + private final boolean local, read, delivered; public MessageHeader(MessageId id, MessageId parent, GroupId groupId, Author author, Author.Status authorStatus, String contentType, - long timestamp, boolean local, boolean read) { + long timestamp, boolean local, boolean read, boolean delivered) { this.id = id; this.parent = parent; this.groupId = groupId; @@ -26,6 +26,7 @@ public class MessageHeader { this.timestamp = timestamp; this.local = local; this.read = read; + this.delivered = delivered; } /** Returns the message's unique identifier. */ @@ -79,4 +80,12 @@ public class MessageHeader { public boolean isRead() { return read; } + + /** + * Returns true if the message has been delivered. (This only applies to + * locally generated private messages.) + */ + public boolean isDelivered() { + return delivered; + } } diff --git a/briar-api/src/org/briarproject/api/event/MessagesAckedEvent.java b/briar-api/src/org/briarproject/api/event/MessagesAckedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..cb7676455988d5a17395c8d591e7a3b5d8aeed6a --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/MessagesAckedEvent.java @@ -0,0 +1,27 @@ +package org.briarproject.api.event; + +import java.util.Collection; + +import org.briarproject.api.ContactId; +import org.briarproject.api.messaging.MessageId; + +/** An event that is broadcast when messages are acked by a contact. */ +public class MessagesAckedEvent extends Event { + + private final ContactId contactId; + private final Collection<MessageId> acked; + + public MessagesAckedEvent(ContactId contactId, + Collection<MessageId> acked ) { + this.contactId = contactId; + this.acked = acked; + } + + public ContactId getContactId() { + return contactId; + } + + public Collection<MessageId> getMessageIds() { + return acked; + } +} diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 872e55d15458c8756a4b229e0e9695304f6f59d0..ee0426ecbf7eb9b45fcafd0a74c91e28feb2179d 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -53,6 +53,7 @@ import org.briarproject.api.event.MessageExpiredEvent; import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; +import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.RemoteRetentionTimeUpdatedEvent; import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent; import org.briarproject.api.event.RemoteTransportsUpdatedEvent; @@ -1329,6 +1330,7 @@ DatabaseCleaner.Callback { } public void receiveAck(ContactId c, Ack a) throws DbException { + Collection<MessageId> acked = new ArrayList<MessageId>(); contactLock.readLock().lock(); try { messageLock.writeLock().lock(); @@ -1338,8 +1340,10 @@ DatabaseCleaner.Callback { if(!db.containsContact(txn, c)) throw new NoSuchContactException(); for(MessageId m : a.getMessageIds()) { - if(db.containsVisibleMessage(txn, c, m)) + if(db.containsVisibleMessage(txn, c, m)) { db.raiseSeenFlag(txn, c, m); + acked.add(m); + } } db.commitTransaction(txn); } catch(DbException e) { @@ -1352,6 +1356,7 @@ DatabaseCleaner.Callback { } finally { contactLock.readLock().unlock(); } + callListeners(new MessagesAckedEvent(c, acked)); } public void receiveMessage(ContactId c, Message m) throws DbException { diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index ecb1b724c7594fd62235e81a4992d28af682f024..72c35f358c81f08473d0b7206facd5130203c7ff 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -1482,14 +1482,17 @@ abstract class JdbcDatabase implements Database<Connection> { Author remoteAuthor = new Author(remoteId, remoteName, remoteKey); if(rs.next()) throw new DbException(); // Get the message headers - sql = "SELECT messageId, parentId, m.groupId, contentType," - + " timestamp, local, read" + sql = "SELECT m.messageId, parentId, m.groupId, contentType," + + " timestamp, local, read, seen" + " FROM messages AS m" + " JOIN groups AS g" + " ON m.groupId = g.groupId" + " JOIN groupVisibilities AS gv" + " ON m.groupId = gv.groupId" - + " WHERE contactId = ?" + + " JOIN statuses AS s" + + " ON m.messageId = s.messageId" + + " AND gv.contactId = s.contactId" + + " WHERE gv.contactId = ?" + " AND inbox = TRUE"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); @@ -1504,15 +1507,10 @@ abstract class JdbcDatabase implements Database<Connection> { long timestamp = rs.getLong(5); boolean local = rs.getBoolean(6); boolean read = rs.getBoolean(7); - if(local) { - headers.add(new MessageHeader(id, parent, groupId, - localAuthor, VERIFIED, contentType, timestamp, - true, read)); - } else { - headers.add(new MessageHeader(id, parent, groupId, - remoteAuthor, VERIFIED, contentType, timestamp, - false, read)); - } + boolean seen = rs.getBoolean(8); + Author author = local ? localAuthor : remoteAuthor; + headers.add(new MessageHeader(id, parent, groupId, author, + VERIFIED, contentType, timestamp, local, read, seen)); } rs.close(); ps.close(); @@ -1723,12 +1721,12 @@ abstract class JdbcDatabase implements Database<Connection> { boolean read = rs.getBoolean(9); boolean isSelf = rs.getBoolean(10); boolean isContact = rs.getBoolean(11); - Author.Status authorStatus; - if(author == null) authorStatus = ANONYMOUS; - else if(isSelf || isContact) authorStatus = VERIFIED; - else authorStatus = UNKNOWN; - headers.add(new MessageHeader(id, parent, g, author, - authorStatus, contentType, timestamp, local, read)); + Author.Status status; + if(author == null) status = ANONYMOUS; + else if(isSelf || isContact) status = VERIFIED; + else status = UNKNOWN; + headers.add(new MessageHeader(id, parent, g, author, status, + contentType, timestamp, local, read, false)); } rs.close(); ps.close(); diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java index 361e9ff3ef87de13e643e82767bc2ea99ac33372..40658bf428fdefd69adb87112fcdee0621974905 100644 --- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java +++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java @@ -1531,7 +1531,9 @@ public class H2DatabaseTest extends BriarTestCase { db.getInboxMessageHeaders(txn, contactId)); // Add a message to the inbox group - the header should be returned - db.addMessage(txn, message, true); + boolean local = true, seen = false; + db.addMessage(txn, message, local); + db.addStatus(txn, contactId, messageId, false, seen); Collection<MessageHeader> headers = db.getInboxMessageHeaders(txn, contactId); assertEquals(1, headers.size()); @@ -1542,6 +1544,9 @@ public class H2DatabaseTest extends BriarTestCase { assertEquals(localAuthor, header.getAuthor()); assertEquals(contentType, header.getContentType()); assertEquals(timestamp, header.getTimestamp()); + assertEquals(local, header.isLocal()); + assertEquals(false, header.isRead()); + assertEquals(seen, header.isDelivered()); assertFalse(header.isRead()); db.commitTransaction(txn);