From b8e97b0bc1004515cbe55cdb24b23fed1407bd59 Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Fri, 15 Mar 2013 16:35:14 +0000
Subject: [PATCH] Removed complex premature optimisations from DB/UI
 interaction.

---
 .../briar/android/groups/GroupActivity.java   | 127 ++--------------
 .../android/groups/GroupListActivity.java     | 128 ++++++----------
 .../briar/android/groups/GroupListItem.java   |  45 +-----
 .../groups/ReadGroupMessageActivity.java      |   6 +-
 .../messages/ConversationActivity.java        |  47 ++----
 .../messages/ConversationListActivity.java    | 138 ++++++------------
 .../messages/ConversationListItem.java        |  39 +----
 7 files changed, 123 insertions(+), 407 deletions(-)

diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
index ad9f08b461..b012ac3937 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
@@ -4,13 +4,8 @@ import static android.view.Gravity.CENTER_HORIZONTAL;
 import static android.widget.LinearLayout.VERTICAL;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
-import static net.sf.briar.api.Rating.UNRATED;
 
 import java.util.Collection;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -21,7 +16,6 @@ import net.sf.briar.android.BriarService;
 import net.sf.briar.android.BriarService.BriarServiceConnection;
 import net.sf.briar.android.widgets.CommonLayoutParams;
 import net.sf.briar.android.widgets.HorizontalBorder;
-import net.sf.briar.api.Rating;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.db.DbException;
@@ -34,10 +28,7 @@ import net.sf.briar.api.db.event.MessageExpiredEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
 import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
 import net.sf.briar.api.messaging.Author;
-import net.sf.briar.api.messaging.AuthorId;
 import net.sf.briar.api.messaging.GroupId;
-import net.sf.briar.api.messaging.Message;
-import net.sf.briar.api.messaging.MessageId;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
@@ -58,11 +49,7 @@ OnClickListener, OnItemClickListener {
 
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
-	private final Map<AuthorId, Rating> ratingCache =
-			new ConcurrentHashMap<AuthorId, Rating>();
 
-	// The following fields must only be accessed from the UI thread
-	private final Set<MessageId> messageIds = new HashSet<MessageId>();
 	private String groupName = null;
 	private GroupAdapter adapter = null;
 	private ListView list = null;
@@ -107,8 +94,6 @@ OnClickListener, OnItemClickListener {
 
 		setContentView(layout);
 
-		// Listen for messages and groups being added or removed
-		db.addListener(this);
 		// Bind to the service so we can wait for the DB to be opened
 		bindService(new Intent(BriarService.class.getName()),
 				serviceConnection, 0);
@@ -117,6 +102,7 @@ OnClickListener, OnItemClickListener {
 	@Override
 	public void onResume() {
 		super.onResume();
+		db.addListener(this);
 		loadHeaders();
 	}
 
@@ -129,8 +115,6 @@ OnClickListener, OnItemClickListener {
 					// Load the headers from the database
 					Collection<GroupMessageHeader> headers =
 							db.getMessageHeaders(groupId);
-					if(LOG.isLoggable(INFO))
-						LOG.info("Loaded " + headers.size() + " headers");
 					// Display the headers in the UI
 					displayHeaders(headers);
 				} catch(NoSuchSubscriptionException e) {
@@ -151,15 +135,8 @@ OnClickListener, OnItemClickListener {
 	private void displayHeaders(final Collection<GroupMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				ratingCache.clear();
-				messageIds.clear();
 				adapter.clear();
-				for(GroupMessageHeader h : headers) {
-					Author a = h.getAuthor();
-					if(a != null) ratingCache.put(a.getId(), h.getRating());
-					messageIds.add(h.getId());
-					adapter.add(h);
-				}
+				for(GroupMessageHeader h : headers) adapter.add(h);
 				adapter.sort(AscendingHeaderComparator.INSTANCE);
 				selectFirstUnread();
 			}
@@ -191,27 +168,27 @@ OnClickListener, OnItemClickListener {
 		}
 	}
 
+	@Override
+	public void onPause() {
+		db.removeListener(this);
+	}
+
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
-		db.removeListener(this);
 		unbindService(serviceConnection);
 	}
 
+	// FIXME: Load operations may overlap, resulting in an inconsistent view
 	public void eventOccurred(DatabaseEvent e) {
 		if(e instanceof GroupMessageAddedEvent) {
 			GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
-			Message m = g.getMessage();
-			if(m.getGroup().getId().equals(groupId))
-				loadRatingOrAddToGroup(m, g.isIncoming());
+			if(g.getMessage().getGroup().getId().equals(groupId))
+				loadHeaders();
 		} else if(e instanceof MessageExpiredEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			loadHeaders(); // FIXME: Don't reload unnecessarily
+			loadHeaders(); // FIXME: Don't reload everything
 		} else if(e instanceof RatingChangedEvent) {
-			RatingChangedEvent r = (RatingChangedEvent) e;
-			AuthorId a = r.getAuthorId();
-			ratingCache.remove(a);
-			updateRating(a, r.getRating());
+			loadHeaders();
 		} else if(e instanceof SubscriptionRemovedEvent) {
 			if(((SubscriptionRemovedEvent) e).getGroupId().equals(groupId)) {
 				if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
@@ -220,81 +197,6 @@ OnClickListener, OnItemClickListener {
 		}
 	}
 
-	private void loadRatingOrAddToGroup(Message m, boolean incoming) {
-		Author a = m.getAuthor();
-		if(a == null) {
-			addToGroup(m, UNRATED, incoming);
-		} else {
-			Rating r = ratingCache.get(a.getId());
-			if(r == null) loadRating(m, incoming);
-			else addToGroup(m, r, incoming);
-		}
-	}
-
-	private void addToGroup(final Message m, final Rating r,
-			final boolean incoming) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				if(messageIds.add(m.getId())) {
-					adapter.add(new GroupMessageHeader(m.getId(), m.getParent(),
-							m.getContentType(), m.getSubject(),
-							m.getTimestamp(),!incoming, false,
-							m.getGroup().getId(), m.getAuthor(), r));
-					adapter.sort(AscendingHeaderComparator.INSTANCE);
-					selectFirstUnread();
-				}
-			}
-		});
-	}
-
-	private void loadRating(final Message m, final boolean incoming) {
-		dbExecutor.execute(new Runnable() {
-			public void run() {
-				try {
-					// Wait for the service to be bound and started
-					serviceConnection.waitForStartup();
-					// Load the rating from the database
-					AuthorId a = m.getAuthor().getId();
-					Rating r = db.getRating(a);
-					// Cache the rating
-					ratingCache.put(a, r);
-					// Display the message
-					addToGroup(m, r, incoming);
-				} catch(DbException e) {
-					if(LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				} catch(InterruptedException e) {
-					if(LOG.isLoggable(INFO))
-						LOG.info("Interrupted while waiting for service");
-					Thread.currentThread().interrupt();
-				}
-			}
-		});
-	}
-
-	private void updateRating(final AuthorId a, final Rating r) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				boolean affected = false;
-				int count = adapter.getCount();
-				for(int i = 0; i < count; i++) {
-					GroupMessageHeader h = adapter.getItem(i);
-					Author author = h.getAuthor();
-					if(author != null && author.getId().equals(a)) {
-						adapter.remove(h);
-						adapter.insert(new GroupMessageHeader(h.getId(),
-								h.getParent(), h.getContentType(),
-								h.getSubject(), h.getTimestamp(), h.isRead(),
-								h.isStarred(), h.getGroupId(), h.getAuthor(),
-								r), i);
-						affected = true;
-					}
-				}
-				if(affected) list.invalidate();
-			}
-		});
-	}
-
 	public void onClick(View view) {
 		Intent i = new Intent(this, WriteGroupMessageActivity.class);
 		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
@@ -313,10 +215,7 @@ OnClickListener, OnItemClickListener {
 		i.putExtra("net.sf.briar.GROUP_NAME", groupName);
 		i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
 		Author author = item.getAuthor();
-		if(author == null) {
-			i.putExtra("net.sf.briar.ANONYMOUS", true);
-		} else {
-			i.putExtra("net.sf.briar.ANONYMOUS", false);
+		if(author != null) {
 			i.putExtra("net.sf.briar.AUTHOR_ID", author.getId().getBytes());
 			i.putExtra("net.sf.briar.AUTHOR_NAME", author.getName());
 			i.putExtra("net.sf.briar.RATING", item.getRating().toString());
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
index 48a977e842..e6ac1d6e72 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
@@ -14,7 +14,6 @@ import java.security.PrivateKey;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -24,7 +23,6 @@ import net.sf.briar.R;
 import net.sf.briar.android.BriarActivity;
 import net.sf.briar.android.BriarService;
 import net.sf.briar.android.BriarService.BriarServiceConnection;
-import net.sf.briar.android.DescendingHeaderComparator;
 import net.sf.briar.android.widgets.CommonLayoutParams;
 import net.sf.briar.android.widgets.HorizontalBorder;
 import net.sf.briar.api.ContactId;
@@ -102,8 +100,6 @@ implements OnClickListener, DatabaseListener {
 
 		setContentView(layout);
 
-		// Listen for messages and groups being added or removed
-		db.addListener(this);
 		// Bind to the service so we can wait for the DB to be opened
 		bindService(new Intent(BriarService.class.getName()),
 				serviceConnection, 0);
@@ -203,37 +199,31 @@ implements OnClickListener, DatabaseListener {
 	@Override
 	public void onResume() {
 		super.onResume();
-		loadGroups();
+		db.addListener(this);
+		loadHeaders();
 	}
 
-	private void loadGroups() {
+	private void loadHeaders() {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					// Wait for the service to be bound and started
 					serviceConnection.waitForStartup();
 					// Load the subscribed groups from the DB
-					Collection<Group> groups = db.getSubscriptions();
-					if(LOG.isLoggable(INFO))
-						LOG.info("Loaded " + groups.size() + " groups");
-					List<GroupListItem> items = new ArrayList<GroupListItem>();
-					for(Group g : groups) {
+					for(Group g : db.getSubscriptions()) {
 						// Filter out restricted groups
 						if(g.getPublicKey() != null) continue;
-						// Load the message headers
-						Collection<GroupMessageHeader> headers;
 						try {
-							headers = db.getMessageHeaders(g.getId());
+							// Load the headers from the database
+							Collection<GroupMessageHeader> headers =
+									db.getMessageHeaders(g.getId());
+							// Display the headers in the UI
+							displayHeaders(g, headers);
 						} catch(NoSuchSubscriptionException e) {
-							continue; // Unsubscribed since getSubscriptions()
+							if(LOG.isLoggable(INFO))
+								LOG.info("Subscription removed");
 						}
-						if(LOG.isLoggable(INFO))
-							LOG.info("Loaded " + headers.size() + " headers");
-						if(!headers.isEmpty())
-							items.add(createItem(g, headers));
 					}
-					// Display the groups in the UI
-					displayGroups(Collections.unmodifiableList(items));
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -246,25 +236,34 @@ implements OnClickListener, DatabaseListener {
 		});
 	}
 
-	private GroupListItem createItem(Group group,
-			Collection<GroupMessageHeader> headers) {
-		List<GroupMessageHeader> sort =
-				new ArrayList<GroupMessageHeader>(headers);
-		Collections.sort(sort, DescendingHeaderComparator.INSTANCE);
-		return new GroupListItem(group, sort);
-	}
-
-	private void displayGroups(final Collection<GroupListItem> items) {
+	private void displayHeaders(final Group g,
+			final Collection<GroupMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				adapter.clear();
-				for(GroupListItem i : items) adapter.add(i);
-				adapter.sort(GroupComparator.INSTANCE);
+				// Remove the old item, if any
+				GroupListItem item = findGroup(g.getId());
+				if(item != null) adapter.remove(item);
+				// Add a new item if there are any headers to display
+				if(!headers.isEmpty()) {
+					List<GroupMessageHeader> headerList =
+							new ArrayList<GroupMessageHeader>(headers);
+					adapter.add(new GroupListItem(g, headerList));
+					adapter.sort(GroupComparator.INSTANCE);
+				}
 				selectFirstUnread();
 			}
 		});
 	}
 
+	private GroupListItem findGroup(GroupId g) {
+		int count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			GroupListItem item = adapter.getItem(i);
+			if(item.getGroupId().equals(g)) return item;
+		}
+		return null; // Not found
+	}
+
 	private void selectFirstUnread() {
 		int firstUnread = -1, count = adapter.getCount();
 		for(int i = 0; i < count; i++) {
@@ -277,10 +276,14 @@ implements OnClickListener, DatabaseListener {
 		else list.setSelection(firstUnread);
 	}
 
+	@Override
+	public void onPause() {
+		db.removeListener(this);
+	}
+
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
-		db.removeListener(this);
 		unbindService(serviceConnection);
 	}
 
@@ -288,52 +291,24 @@ implements OnClickListener, DatabaseListener {
 		startActivity(new Intent(this, WriteGroupMessageActivity.class));
 	}
 
+	// FIXME: Load operations may overlap, resulting in an inconsistent view
 	public void eventOccurred(DatabaseEvent e) {
 		if(e instanceof GroupMessageAddedEvent) {
 			GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
-			addToGroup(g.getMessage(), g.isIncoming());
+			loadHeaders(g.getMessage().getGroup().getId());
 		} else if(e instanceof MessageExpiredEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			loadGroups(); // FIXME: Don't reload unnecessarily
+			loadHeaders(); // FIXME: Don't reload everything
 		} else if(e instanceof SubscriptionRemovedEvent) {
 			removeGroup(((SubscriptionRemovedEvent) e).getGroupId());
 		}
 	}
 
-	private void addToGroup(final Message m, final boolean incoming) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				GroupId g = m.getGroup().getId();
-				GroupListItem item = findGroup(g);
-				if(item == null) {
-					loadGroup(g, m, incoming);
-				} else if(item.add(m, incoming)) {
-					adapter.sort(GroupComparator.INSTANCE);
-					selectFirstUnread();
-					list.invalidate();
-				}
-			}
-		});
-	}
-
-	private GroupListItem findGroup(GroupId g) {
-		int count = adapter.getCount();
-		for(int i = 0; i < count; i++) {
-			GroupListItem item = adapter.getItem(i);
-			if(item.getGroupId().equals(g)) return item;
-		}
-		return null; // Not found
-	}
-
-	private void loadGroup(final GroupId g, final Message m,
-			final boolean incoming) {
+	private void loadHeaders(final GroupId g) {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
-					// Wait for the service to be bound and started
 					serviceConnection.waitForStartup();
-					// Load the group from the DB and display it in the UI
-					displayGroup(db.getGroup(g), m, incoming);
+					displayHeaders(db.getGroup(g), db.getMessageHeaders(g));
 				} catch(NoSuchSubscriptionException e) {
 					if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
 				} catch(DbException e) {
@@ -348,25 +323,6 @@ implements OnClickListener, DatabaseListener {
 		});
 	}
 
-	private void displayGroup(final Group g, final Message m,
-			final boolean incoming) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				// The item may have been added since loadGroup() was called
-				GroupListItem item = findGroup(g.getId());
-				if(item == null) {
-					adapter.add(new GroupListItem(g, m, incoming));
-					adapter.sort(GroupComparator.INSTANCE);
-					selectFirstUnread();
-				} else if(item.add(m, incoming)) {
-					adapter.sort(GroupComparator.INSTANCE);
-					selectFirstUnread();
-					list.invalidate();
-				}
-			}
-		});
-	}
-
 	private void removeGroup(final GroupId g) {
 		runOnUiThread(new Runnable() {
 			public void run() {
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
index 918e272d48..a7d86263ef 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
@@ -1,26 +1,20 @@
 package net.sf.briar.android.groups;
 
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 import net.sf.briar.android.DescendingHeaderComparator;
 import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.messaging.Author;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupId;
-import net.sf.briar.api.messaging.Message;
-import net.sf.briar.api.messaging.MessageId;
 
-// This class is not thread-safe
 class GroupListItem {
 
-	private final Set<MessageId> messageIds = new HashSet<MessageId>();
 	private final Group group;
-	private String authorName, subject;
-	private long timestamp;
-	private int unread;
+	private final String authorName, subject;
+	private final long timestamp;
+	private final int unread;
 
 	GroupListItem(Group group, List<GroupMessageHeader> headers) {
 		if(headers.isEmpty()) throw new IllegalArgumentException();
@@ -32,36 +26,9 @@ class GroupListItem {
 		else authorName = a.getName();
 		subject = newest.getSubject();
 		timestamp = newest.getTimestamp();
-		unread = 0;
-		for(GroupMessageHeader h : headers) {
-			if(!h.isRead()) unread++;
-			if(!messageIds.add(h.getId())) throw new IllegalArgumentException();
-		}
-	}
-
-	GroupListItem(Group group, Message first, boolean incoming) {
-		this.group = group;
-		Author a = first.getAuthor();
-		if(a == null) authorName = null;
-		else authorName = a.getName();
-		subject = first.getSubject();
-		timestamp = first.getTimestamp();
-		unread = incoming ? 1 : 0;
-		messageIds.add(first.getId());
-	}
-
-	boolean add(Message m, boolean incoming) {
-		if(!messageIds.add(m.getId())) return false;
-		if(m.getTimestamp() > timestamp) {
-			// The added message is the newest
-			Author a = m.getAuthor();
-			if(a == null) authorName = null;
-			else authorName = a.getName();
-			subject = m.getSubject();
-			timestamp = m.getTimestamp();
-		}
-		if(incoming) unread++;
-		return true;
+		int unread = 0;
+		for(GroupMessageHeader h : headers) if(!h.isRead()) unread++;
+		this.unread = unread;
 	}
 
 	GroupId getGroupId() {
diff --git a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
index 845c7b9b1e..4c03773933 100644
--- a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
@@ -90,11 +90,9 @@ implements OnClickListener {
 		id = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID");
 		if(id == null) throw new IllegalStateException();
 		messageId = new MessageId(id);
-		boolean anonymous = i.getBooleanExtra("net.sf.briar.ANONYMOUS", false);
 		String authorName = null;
-		if(!anonymous) {
-			id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
-			if(id == null) throw new IllegalStateException();
+		id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
+		if(id != null) {
 			authorId = new AuthorId(id);
 			authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME");
 			if(authorName == null) throw new IllegalStateException();
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
index 6ac5fb32fa..156c0f7c14 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
@@ -6,8 +6,6 @@ import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
 import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -29,8 +27,6 @@ import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
 import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
-import net.sf.briar.api.messaging.Message;
-import net.sf.briar.api.messaging.MessageId;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
@@ -52,8 +48,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
 
-	// The following fields must only be accessed from the UI thread
-	private Set<MessageId> messageIds = new HashSet<MessageId>();
 	private String contactName = null;
 	private ConversationAdapter adapter = null;
 	private ListView list = null;
@@ -98,8 +92,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 
 		setContentView(layout);
 
-		// Listen for messages being added or removed
-		db.addListener(this);
 		// Bind to the service so we can wait for the DB to be opened
 		bindService(new Intent(BriarService.class.getName()),
 				serviceConnection, 0);
@@ -108,6 +100,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 	@Override
 	public void onResume() {
 		super.onResume();
+		db.addListener(this);
 		loadHeaders();
 	}
 
@@ -120,8 +113,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 					// Load the headers from the database
 					Collection<PrivateMessageHeader> headers =
 							db.getPrivateMessageHeaders(contactId);
-					if(LOG.isLoggable(INFO))
-						LOG.info("Loaded " + headers.size() + " headers");
 					// Display the headers in the UI
 					displayHeaders(headers);
 				} catch(NoSuchContactException e) {
@@ -143,12 +134,8 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 			final Collection<PrivateMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				messageIds.clear();
 				adapter.clear();
-				for(PrivateMessageHeader h : headers) {
-					messageIds.add(h.getId());
-					adapter.add(h);
-				}
+				for(PrivateMessageHeader h : headers) adapter.add(h);
 				adapter.sort(AscendingHeaderComparator.INSTANCE);
 				selectFirstUnread();
 			}
@@ -180,13 +167,18 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		}
 	}
 
+	@Override
+	public void onPause() {
+		db.removeListener(this);
+	}
+	
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
-		db.removeListener(this);
 		unbindService(serviceConnection);
 	}
 
+	// FIXME: Load operations may overlap, resulting in an inconsistent view
 	public void eventOccurred(DatabaseEvent e) {
 		if(e instanceof ContactRemovedEvent) {
 			ContactRemovedEvent c = (ContactRemovedEvent) e;
@@ -195,30 +187,13 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 				finishOnUiThread();
 			}
 		} else if(e instanceof MessageExpiredEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			loadHeaders(); // FIXME: Don't reload unnecessarily
+			loadHeaders(); // FIXME: Don't reload everything
 		} else if(e instanceof PrivateMessageAddedEvent) {
-			PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e;
-			if(p.getContactId().equals(contactId))
-				addToConversation(p.getMessage(), p.isIncoming());
+			if(((PrivateMessageAddedEvent) e).getContactId().equals(contactId))
+				loadHeaders();
 		}
 	}
 
-	private void addToConversation(final Message m, final boolean incoming) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				if(messageIds.add(m.getId())) {
-					adapter.add(new PrivateMessageHeader(m.getId(),
-							m.getParent(), m.getContentType(), m.getSubject(),
-							m.getTimestamp(), !incoming, false, contactId,
-							incoming));
-					adapter.sort(AscendingHeaderComparator.INSTANCE);
-					selectFirstUnread();
-				}
-			}
-		});
-	}
-
 	public void onClick(View view) {
 		Intent i = new Intent(this, WritePrivateMessageActivity.class);
 		i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
index 4734728148..c7ab7d8a35 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
@@ -10,9 +10,7 @@ import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -89,8 +87,6 @@ implements OnClickListener, DatabaseListener {
 
 		setContentView(layout);
 
-		// Listen for messages being added or removed
-		db.addListener(this);
 		// Bind to the service so we can wait for the DB to be opened
 		bindService(new Intent(BriarService.class.getName()),
 				serviceConnection, 0);
@@ -163,6 +159,7 @@ implements OnClickListener, DatabaseListener {
 	@Override
 	public void onResume() {
 		super.onResume();
+		db.addListener(this);
 		loadHeaders();
 	}
 
@@ -173,16 +170,18 @@ implements OnClickListener, DatabaseListener {
 					// Wait for the service to be bound and started
 					serviceConnection.waitForStartup();
 					// Load the contact list from the database
-					Collection<Contact> contacts = db.getContacts();
-					if(LOG.isLoggable(INFO))
-						LOG.info("Loaded " + contacts.size() + " contacts");
-					// Load the headers from the database
-					Collection<PrivateMessageHeader> headers =
-							db.getPrivateMessageHeaders();
-					if(LOG.isLoggable(INFO))
-						LOG.info("Loaded " + headers.size() + " headers");
-					// Display the headers in the UI
-					displayHeaders(contacts, headers);
+					for(Contact c : db.getContacts()) {
+						try {
+							// Load the headers from the database
+							Collection<PrivateMessageHeader> headers =
+									db.getPrivateMessageHeaders(c.getId());
+							// Display the headers in the UI
+							displayHeaders(c, headers);
+						} catch(NoSuchContactException e) {
+							if(LOG.isLoggable(INFO))
+								LOG.info("Contact removed");
+						}
+					}
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -195,40 +194,32 @@ implements OnClickListener, DatabaseListener {
 		});
 	}
 
-	private void displayHeaders(final Collection<Contact> contacts,
+	private void displayHeaders(final Contact c,
 			final Collection<PrivateMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				adapter.clear();
-				for(ConversationListItem i : sortHeaders(contacts, headers))
-					adapter.add(i);
-				adapter.sort(ConversationComparator.INSTANCE);
+				// Remove the old item, if any
+				ConversationListItem item = findConversation(c.getId());
+				if(item != null) adapter.remove(item);
+				// Add a new item if there are any headers to display
+				if(!headers.isEmpty()) {
+					List<PrivateMessageHeader> headerList =
+							new ArrayList<PrivateMessageHeader>(headers);
+					adapter.add(new ConversationListItem(c, headerList));
+					adapter.sort(ConversationComparator.INSTANCE);
+				}
 				selectFirstUnread();
 			}
 		});
 	}
 
-	private List<ConversationListItem> sortHeaders(Collection<Contact> contacts,
-			Collection<PrivateMessageHeader> headers) {
-		// Group the headers into conversations, one per contact
-		Map<ContactId, List<PrivateMessageHeader>> map =
-				new HashMap<ContactId, List<PrivateMessageHeader>>();
-		for(Contact c : contacts)
-			map.put(c.getId(), new ArrayList<PrivateMessageHeader>());
-		for(PrivateMessageHeader h : headers) {
-			ContactId id = h.getContactId();
-			List<PrivateMessageHeader> conversation = map.get(id);
-			// Ignore header if the contact was added after db.getContacts()
-			if(conversation != null) conversation.add(h);
-		}
-		// Create a list item for each non-empty conversation
-		List<ConversationListItem> list = new ArrayList<ConversationListItem>();
-		for(Contact c : contacts) {
-			List<PrivateMessageHeader> conversation = map.get(c.getId());
-			if(!conversation.isEmpty())
-				list.add(new ConversationListItem(c, conversation));
+	private ConversationListItem findConversation(ContactId c) {
+		int count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			ConversationListItem item = adapter.getItem(i);
+			if(item.getContactId().equals(c)) return item;
 		}
-		return list;
+		return null; // Not found
 	}
 
 	private void selectFirstUnread() {
@@ -243,10 +234,14 @@ implements OnClickListener, DatabaseListener {
 		else list.setSelection(firstUnread);
 	}
 
+	@Override
+	public void onPause() {
+		db.removeListener(this);
+	}
+	
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
-		db.removeListener(this);
 		unbindService(serviceConnection);
 	}
 
@@ -254,19 +249,18 @@ implements OnClickListener, DatabaseListener {
 		startActivity(new Intent(this, WritePrivateMessageActivity.class));
 	}
 
+	// FIXME: Load operations may overlap, resulting in an inconsistent view
 	public void eventOccurred(DatabaseEvent e) {
 		if(e instanceof ContactRemovedEvent) {
-			removeContact(((ContactRemovedEvent) e).getContactId());
+			removeConversation(((ContactRemovedEvent) e).getContactId());
 		} else if(e instanceof MessageExpiredEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			loadHeaders(); // FIXME: Don't reload unnecessarily
+			loadHeaders(); // FIXME: Don't reload everything
 		} else if(e instanceof PrivateMessageAddedEvent) {
-			PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e;
-			addToConversation(p.getContactId(), p.getMessage(), p.isIncoming());
+			loadHeaders(((PrivateMessageAddedEvent) e).getContactId());
 		}
 	}
 
-	private void removeContact(final ContactId c) {
+	private void removeConversation(final ContactId c) {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				ConversationListItem item = findConversation(c);
@@ -278,40 +272,13 @@ implements OnClickListener, DatabaseListener {
 		});
 	}
 
-	private ConversationListItem findConversation(ContactId c) {
-		int count = adapter.getCount();
-		for(int i = 0; i < count; i++) {
-			ConversationListItem item = adapter.getItem(i);
-			if(item.getContactId().equals(c)) return item;
-		}
-		return null; // Not found
-	}
-
-	private void addToConversation(final ContactId c, final Message m,
-			final boolean incoming) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				ConversationListItem item = findConversation(c);
-				if(item == null) {
-					loadContact(c, m, incoming);
-				} else if(item.add(m, incoming)) {
-					adapter.sort(ConversationComparator.INSTANCE);
-					selectFirstUnread();
-					list.invalidate();
-				}
-			}
-		});
-	}
-
-	private void loadContact(final ContactId c, final Message m,
-			final boolean incoming) {
+	private void loadHeaders(final ContactId c) {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
-					// Wait for the service to be bound and started
 					serviceConnection.waitForStartup();
-					// Load the contact from the DB and display it in the UI
-					displayContact(db.getContact(c), m, incoming);
+					Contact contact = db.getContact(c);
+					displayHeaders(contact, db.getPrivateMessageHeaders(c));
 				} catch(NoSuchContactException e) {
 					if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
 				} catch(DbException e) {
@@ -326,25 +293,6 @@ implements OnClickListener, DatabaseListener {
 		});
 	}
 
-	private void displayContact(final Contact c, final Message m,
-			final boolean incoming) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				// The item may have been added since loadContact() was called
-				ConversationListItem item = findConversation(c.getId());
-				if(item == null) {
-					adapter.add(new ConversationListItem(c, m, incoming));
-					adapter.sort(ConversationComparator.INSTANCE);
-					selectFirstUnread();
-				} else if(item.add(m, incoming)) {
-					adapter.sort(ConversationComparator.INSTANCE);
-					selectFirstUnread();
-					list.invalidate();
-				}
-			}
-		});
-	}
-
 	private static class ConversationComparator
 	implements Comparator<ConversationListItem> {
 
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
index edec2e49b3..eec30a91c8 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
@@ -1,25 +1,19 @@
 package net.sf.briar.android.messages;
 
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 import net.sf.briar.android.DescendingHeaderComparator;
 import net.sf.briar.api.Contact;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.db.PrivateMessageHeader;
-import net.sf.briar.api.messaging.Message;
-import net.sf.briar.api.messaging.MessageId;
 
-// This class is not thread-safe
 class ConversationListItem {
 
-	private final Set<MessageId> messageIds = new HashSet<MessageId>();
 	private final Contact contact;
-	private String subject;
-	private long timestamp;
-	private int unread;
+	private final String subject;
+	private final long timestamp;
+	private final int unread;
 
 	ConversationListItem(Contact contact, List<PrivateMessageHeader> headers) {
 		if(headers.isEmpty()) throw new IllegalArgumentException();
@@ -27,30 +21,9 @@ class ConversationListItem {
 		Collections.sort(headers, DescendingHeaderComparator.INSTANCE);
 		subject = headers.get(0).getSubject();
 		timestamp = headers.get(0).getTimestamp();
-		unread = 0;
-		for(PrivateMessageHeader h : headers) {
-			if(!h.isRead()) unread++;
-			if(!messageIds.add(h.getId())) throw new IllegalArgumentException();
-		}
-	}
-
-	ConversationListItem(Contact contact, Message first, boolean incoming) {
-		this.contact = contact;
-		subject = first.getSubject();
-		timestamp = first.getTimestamp();
-		unread = incoming ? 1 : 0;
-		messageIds.add(first.getId());
-	}
-
-	boolean add(Message m, boolean incoming) {
-		if(!messageIds.add(m.getId())) return false;
-		if(m.getTimestamp() > timestamp) {
-			// The added message is the newest
-			subject = m.getSubject();
-			timestamp = m.getTimestamp();
-		}
-		if(incoming) unread++;
-		return true;
+		int unread = 0;
+		for(PrivateMessageHeader h : headers) if(!h.isRead()) unread++;
+		this.unread = unread;
 	}
 
 	ContactId getContactId() {
-- 
GitLab