From 1c282a883567e1ad1488334c944808f4bcd03a84 Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Thu, 3 Apr 2014 17:22:48 +0100
Subject: [PATCH] Show when private messages have been delivered.

---
 .../res/drawable-hdpi/message_delivered.png   | Bin 0 -> 284 bytes
 .../res/drawable-mdpi/message_delivered.png   | Bin 0 -> 238 bytes
 .../res/drawable-xhdpi/message_delivered.png  | Bin 0 -> 392 bytes
 .../android/contact/ConversationActivity.java |  27 +++++++++
 .../android/contact/ConversationAdapter.java  |  52 +++++++++++++-----
 .../android/contact/ConversationItem.java     |  10 ++++
 .../briarproject/api/db/MessageHeader.java    |  13 ++++-
 .../api/event/MessagesAckedEvent.java         |  27 +++++++++
 .../db/DatabaseComponentImpl.java             |   7 ++-
 .../src/org/briarproject/db/JdbcDatabase.java |  34 ++++++------
 .../org/briarproject/db/H2DatabaseTest.java   |   7 ++-
 11 files changed, 142 insertions(+), 35 deletions(-)
 create mode 100644 briar-android/res/drawable-hdpi/message_delivered.png
 create mode 100644 briar-android/res/drawable-mdpi/message_delivered.png
 create mode 100644 briar-android/res/drawable-xhdpi/message_delivered.png
 create mode 100644 briar-api/src/org/briarproject/api/event/MessagesAckedEvent.java

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
GIT binary patch
literal 284
zcmV+%0ptFOP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00001b5ch_0Itp)
z=>Px#)=5M`R7l6|l)nxEK@f+(lTd0TDv?zvC{E!$cnF<)o@u=Uuc5$nB;@ExXpks0
zS6t5CKTdbQZg-RK+eu~y%9P0yQPU7Llki=oKzJS415&skfS-b6GyLWh3~zg`e=Dp4
zQPTpBaqyo6qNWEN0)`d-ArLifQPa%`Ujt{L83(sjFavg?W|RSb0Xm-RKa)Na6-<E*
zFt<r8@JCv;js@ZXOJE3$J=b4+3y*Ens07#onirlp2NB5~xaNhY4q6~t0Ry1wx&G@P
i3#=w;`sFGw6Pq{lbXMMsP2|u30000<MNUMnLSTZyJ!`Q5

literal 0
HcmV?d00001

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
GIT binary patch
literal 238
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|R(QHNhFF|_
zI@zA<kby|M{c86M=WdpCEocrFKj;_sfkjSXGJ8Rp_@?s0!ZOW!N)-(Xw(c)ODyBqi
z=sP=Q&mR4sd+QC(q|Wu4ReOcmj=8dU-XD(p4Z0iNDF05HTebb|#DW>62g)CC<lHJZ
zYH2TB#p37o+v`AhLgJSAR=Me(XBs3bB9|X~^jySa-%4rLCt?nqH4V)d>+VjS9U%Jc
mfbb2*&-JrXttRYT!T4~s`ej!WZF``b89ZJ6T-G@yGywpZJz$Uk

literal 0
HcmV?d00001

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
GIT binary patch
literal 392
zcmV;30eAk1P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800001b5ch_0Itp)
z=>Px$LP<nHR9M69ma$3$K@>$#B#otDVY<=MVj3&aU}3<<R?uH?e!)c1Z<yAIjXz+S
zCPBnXWC$YIOs9<~*aXtpOdu??$&A@-p>xY+hjZ_HZ{I8=B_;hAX(O82h?DH|g_7W>
zz&UVHx&a420|vlGsRlCeJtgg9_MJ*e@GT!4pgIR|6=(vjNy+DC0JsK@rX?@jfHq<g
zxC9P?Z(x5K+-pD^u?EzETP5vBgohFQ3)ocBzJwg|eFewB70}a0)IySLBW}jv^-y@8
zfi5rv)`5;TVmUH-Kc;n+oK#D|oip(aG?cV&6M8;M-jAIf?;=vtegX{#&#S~t$%7V=
zgLcO%vFm{CS(68ELN|yVXW|KX1rB1T{nrLOm7s{^(RYD1Vg<MdR)HNQZLf$07JM5j
mY2Sfu*ZYU`i<Ol0XY>OxrFzD0Tlw|?0000<MNUMnLSTYe<D|3z

literal 0
HcmV?d00001

diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
index 7cde33e7db..91885fcc5d 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 a315cf16ac..249392b545 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 1982bb58ec..c1959f0c5d 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 6fa1e223f9..779e4cfa0e 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 0000000000..cb76764559
--- /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 872e55d154..ee0426ecbf 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 ecb1b724c7..72c35f358c 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 361e9ff3ef..40658bf428 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);
-- 
GitLab