Show when private messages have been delivered.

This commit is contained in:
akwizgran
2014-04-03 17:22:48 +01:00
parent a4954408a8
commit 1c282a8835
11 changed files with 142 additions and 35 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -24,8 +24,10 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; 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.EventListener;
import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent; import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.messaging.Group; import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId; import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.Message; import org.briarproject.api.messaging.Message;
@@ -381,9 +384,33 @@ implements EventListener, OnClickListener, OnItemClickListener {
} else if(e instanceof MessageExpiredEvent) { } else if(e instanceof MessageExpiredEvent) {
LOG.info("Message expired, reloading"); LOG.info("Message expired, reloading");
loadHeaders(); 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) { public void onClick(View view) {
String message = content.getText().toString(); String message = content.getText().toString();
if(message.equals("")) return; if(message.equals("")) return;

View File

@@ -1,13 +1,16 @@
package org.briarproject.android.contact; package org.briarproject.android.contact;
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.LEFT; 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 android.widget.LinearLayout.VERTICAL;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP; import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import java.util.ArrayList; import java.util.ArrayList;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.android.util.ElasticHorizontalSpace;
import org.briarproject.android.util.LayoutUtils; import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.db.MessageHeader; import org.briarproject.api.db.MessageHeader;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
@@ -19,6 +22,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
@@ -60,23 +64,45 @@ class ConversationAdapter extends ArrayAdapter<ConversationItem> {
attachment.setImageResource(R.drawable.content_attachment); attachment.setImageResource(R.drawable.content_attachment);
content = attachment; content = attachment;
} }
content.setId(2);
content.setLayoutParams(MATCH_WRAP); content.setLayoutParams(MATCH_WRAP);
content.setBackgroundColor(background); content.setBackgroundColor(background);
content.setPadding(pad, pad, pad, 0); content.setPadding(pad, pad, pad, 0);
layout.addView(content); layout.addView(content);
TextView date = new TextView(ctx); if(header.isLocal()) {
date.setId(1); LinearLayout footer = new LinearLayout(ctx);
date.setLayoutParams(MATCH_WRAP); footer.setLayoutParams(MATCH_WRAP);
if(header.isLocal()) date.setGravity(RIGHT); footer.setOrientation(HORIZONTAL);
else date.setGravity(LEFT); footer.setGravity(BOTTOM);
date.setTextColor(res.getColor(R.color.private_message_date)); footer.setPadding(pad, 0, pad, pad);
date.setBackgroundColor(background); footer.setBackgroundColor(background);
date.setPadding(pad, 0, pad, pad);
long timestamp = header.getTimestamp(); footer.addView(new ElasticHorizontalSpace(ctx));
date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
layout.addView(date); 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; return layout;
} }

View File

@@ -7,10 +7,12 @@ class ConversationItem {
private final MessageHeader header; private final MessageHeader header;
private byte[] body; private byte[] body;
private boolean delivered;
ConversationItem(MessageHeader header) { ConversationItem(MessageHeader header) {
this.header = header; this.header = header;
body = null; body = null;
delivered = header.isDelivered();
} }
MessageHeader getHeader() { MessageHeader getHeader() {
@@ -24,4 +26,12 @@ class ConversationItem {
void setBody(byte[] body) { void setBody(byte[] body) {
this.body = body; this.body = body;
} }
boolean isDelivered() {
return delivered;
}
void setDelivered(boolean delivered) {
this.delivered = delivered;
}
} }

View File

@@ -12,11 +12,11 @@ public class MessageHeader {
private final Author.Status authorStatus; private final Author.Status authorStatus;
private final String contentType; private final String contentType;
private final long timestamp; private final long timestamp;
private final boolean local, read; private final boolean local, read, delivered;
public MessageHeader(MessageId id, MessageId parent, GroupId groupId, public MessageHeader(MessageId id, MessageId parent, GroupId groupId,
Author author, Author.Status authorStatus, String contentType, 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.id = id;
this.parent = parent; this.parent = parent;
this.groupId = groupId; this.groupId = groupId;
@@ -26,6 +26,7 @@ public class MessageHeader {
this.timestamp = timestamp; this.timestamp = timestamp;
this.local = local; this.local = local;
this.read = read; this.read = read;
this.delivered = delivered;
} }
/** Returns the message's unique identifier. */ /** Returns the message's unique identifier. */
@@ -79,4 +80,12 @@ public class MessageHeader {
public boolean isRead() { public boolean isRead() {
return read; return read;
} }
/**
* Returns true if the message has been delivered. (This only applies to
* locally generated private messages.)
*/
public boolean isDelivered() {
return delivered;
}
} }

View File

@@ -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;
}
}

View File

@@ -53,6 +53,7 @@ import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageRequestedEvent;
import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToAckEvent;
import org.briarproject.api.event.MessageToRequestEvent; import org.briarproject.api.event.MessageToRequestEvent;
import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.event.RemoteRetentionTimeUpdatedEvent; import org.briarproject.api.event.RemoteRetentionTimeUpdatedEvent;
import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent; import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
import org.briarproject.api.event.RemoteTransportsUpdatedEvent; import org.briarproject.api.event.RemoteTransportsUpdatedEvent;
@@ -1329,6 +1330,7 @@ DatabaseCleaner.Callback {
} }
public void receiveAck(ContactId c, Ack a) throws DbException { public void receiveAck(ContactId c, Ack a) throws DbException {
Collection<MessageId> acked = new ArrayList<MessageId>();
contactLock.readLock().lock(); contactLock.readLock().lock();
try { try {
messageLock.writeLock().lock(); messageLock.writeLock().lock();
@@ -1338,8 +1340,10 @@ DatabaseCleaner.Callback {
if(!db.containsContact(txn, c)) if(!db.containsContact(txn, c))
throw new NoSuchContactException(); throw new NoSuchContactException();
for(MessageId m : a.getMessageIds()) { for(MessageId m : a.getMessageIds()) {
if(db.containsVisibleMessage(txn, c, m)) if(db.containsVisibleMessage(txn, c, m)) {
db.raiseSeenFlag(txn, c, m); db.raiseSeenFlag(txn, c, m);
acked.add(m);
}
} }
db.commitTransaction(txn); db.commitTransaction(txn);
} catch(DbException e) { } catch(DbException e) {
@@ -1352,6 +1356,7 @@ DatabaseCleaner.Callback {
} finally { } finally {
contactLock.readLock().unlock(); contactLock.readLock().unlock();
} }
callListeners(new MessagesAckedEvent(c, acked));
} }
public void receiveMessage(ContactId c, Message m) throws DbException { public void receiveMessage(ContactId c, Message m) throws DbException {

View File

@@ -1482,14 +1482,17 @@ abstract class JdbcDatabase implements Database<Connection> {
Author remoteAuthor = new Author(remoteId, remoteName, remoteKey); Author remoteAuthor = new Author(remoteId, remoteName, remoteKey);
if(rs.next()) throw new DbException(); if(rs.next()) throw new DbException();
// Get the message headers // Get the message headers
sql = "SELECT messageId, parentId, m.groupId, contentType," sql = "SELECT m.messageId, parentId, m.groupId, contentType,"
+ " timestamp, local, read" + " timestamp, local, read, seen"
+ " FROM messages AS m" + " FROM messages AS m"
+ " JOIN groups AS g" + " JOIN groups AS g"
+ " ON m.groupId = g.groupId" + " ON m.groupId = g.groupId"
+ " JOIN groupVisibilities AS gv" + " JOIN groupVisibilities AS gv"
+ " ON m.groupId = gv.groupId" + " 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"; + " AND inbox = TRUE";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt()); ps.setInt(1, c.getInt());
@@ -1504,15 +1507,10 @@ abstract class JdbcDatabase implements Database<Connection> {
long timestamp = rs.getLong(5); long timestamp = rs.getLong(5);
boolean local = rs.getBoolean(6); boolean local = rs.getBoolean(6);
boolean read = rs.getBoolean(7); boolean read = rs.getBoolean(7);
if(local) { boolean seen = rs.getBoolean(8);
headers.add(new MessageHeader(id, parent, groupId, Author author = local ? localAuthor : remoteAuthor;
localAuthor, VERIFIED, contentType, timestamp, headers.add(new MessageHeader(id, parent, groupId, author,
true, read)); VERIFIED, contentType, timestamp, local, read, seen));
} else {
headers.add(new MessageHeader(id, parent, groupId,
remoteAuthor, VERIFIED, contentType, timestamp,
false, read));
}
} }
rs.close(); rs.close();
ps.close(); ps.close();
@@ -1723,12 +1721,12 @@ abstract class JdbcDatabase implements Database<Connection> {
boolean read = rs.getBoolean(9); boolean read = rs.getBoolean(9);
boolean isSelf = rs.getBoolean(10); boolean isSelf = rs.getBoolean(10);
boolean isContact = rs.getBoolean(11); boolean isContact = rs.getBoolean(11);
Author.Status authorStatus; Author.Status status;
if(author == null) authorStatus = ANONYMOUS; if(author == null) status = ANONYMOUS;
else if(isSelf || isContact) authorStatus = VERIFIED; else if(isSelf || isContact) status = VERIFIED;
else authorStatus = UNKNOWN; else status = UNKNOWN;
headers.add(new MessageHeader(id, parent, g, author, headers.add(new MessageHeader(id, parent, g, author, status,
authorStatus, contentType, timestamp, local, read)); contentType, timestamp, local, read, false));
} }
rs.close(); rs.close();
ps.close(); ps.close();

View File

@@ -1531,7 +1531,9 @@ public class H2DatabaseTest extends BriarTestCase {
db.getInboxMessageHeaders(txn, contactId)); db.getInboxMessageHeaders(txn, contactId));
// Add a message to the inbox group - the header should be returned // 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 = Collection<MessageHeader> headers =
db.getInboxMessageHeaders(txn, contactId); db.getInboxMessageHeaders(txn, contactId);
assertEquals(1, headers.size()); assertEquals(1, headers.size());
@@ -1542,6 +1544,9 @@ public class H2DatabaseTest extends BriarTestCase {
assertEquals(localAuthor, header.getAuthor()); assertEquals(localAuthor, header.getAuthor());
assertEquals(contentType, header.getContentType()); assertEquals(contentType, header.getContentType());
assertEquals(timestamp, header.getTimestamp()); assertEquals(timestamp, header.getTimestamp());
assertEquals(local, header.isLocal());
assertEquals(false, header.isRead());
assertEquals(seen, header.isDelivered());
assertFalse(header.isRead()); assertFalse(header.isRead());
db.commitTransaction(txn); db.commitTransaction(txn);