diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index e5158b9efb1f20aea845751f3f58c27970102401..02a4e1d7f60a429aabcee09d51400f2aa4906d1d 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -82,19 +82,15 @@
 			android:label="@string/add_contact_title" >
 		</activity>
 		<activity
-			android:name=".android.messages.ConversationActivity"
+			android:name="net.sf.briar.android.contact.ConversationActivity"
 			android:label="@string/app_name" >
 		</activity>
 		<activity
-			android:name=".android.messages.ConversationListActivity"
-			android:label="@string/messages_title" >
-		</activity>
-		<activity
-			android:name=".android.messages.ReadPrivateMessageActivity"
+			android:name="net.sf.briar.android.contact.ReadPrivateMessageActivity"
 			android:label="@string/app_name" >
 		</activity>
 		<activity
-			android:name=".android.messages.WritePrivateMessageActivity"
+			android:name="net.sf.briar.android.contact.WritePrivateMessageActivity"
 			android:label="@string/new_message_title" >
 		</activity>
 	</application>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 00d884d357814277c9db288eab9e79b64d634a3c..00426309219cb6e6574a3931c76a376f093a219f 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -12,7 +12,6 @@
 	<string name="try_again">Wrong password, try again:</string>
 	<string name="expiry_warning">This software has expired.\nPlease install a newer version.</string>
 	<string name="contact_list_button">Contacts</string>
-	<string name="messages_button">Messages</string>
 	<string name="forums_button">Forums</string>
 	<string name="synchronize_button">Synchronize</string>
 	<string name="quit_button">Quit</string>
diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
index b9374dd7d4af57791c43d0ba84e099a61d19ee58..6495889b6a3dc6fbe17234504e996fea0784c10e 100644
--- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
+++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
@@ -26,7 +26,6 @@ import net.sf.briar.android.BriarService.BriarBinder;
 import net.sf.briar.android.BriarService.BriarServiceConnection;
 import net.sf.briar.android.contact.ContactListActivity;
 import net.sf.briar.android.groups.GroupListActivity;
-import net.sf.briar.android.messages.ConversationListActivity;
 import net.sf.briar.api.LocalAuthor;
 import net.sf.briar.api.android.DatabaseUiExecutor;
 import net.sf.briar.api.android.ReferenceManager;
@@ -315,20 +314,6 @@ public class HomeScreenActivity extends RoboActivity {
 		});
 		buttons.add(contactsButton);
 
-		Button messagesButton = new Button(this);
-		messagesButton.setLayoutParams(matchMatch);
-		messagesButton.setBackgroundResource(0);
-		messagesButton.setCompoundDrawablesWithIntrinsicBounds(0,
-				R.drawable.content_email, 0, 0);
-		messagesButton.setText(R.string.messages_button);
-		messagesButton.setOnClickListener(new OnClickListener() {
-			public void onClick(View view) {
-				startActivity(new Intent(HomeScreenActivity.this,
-						ConversationListActivity.class));
-			}
-		});
-		buttons.add(messagesButton);
-
 		Button forumsButton = new Button(this);
 		forumsButton.setLayoutParams(matchMatch);
 		forumsButton.setBackgroundResource(0);
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
index 834416420ce8f764967b6bef60d3bdfbd80f15e3..44eb0aad30e4bef22adc6ef6536ba4f67ed7b039 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
@@ -24,6 +24,7 @@ import java.util.logging.Logger;
 import javax.inject.Inject;
 
 import net.sf.briar.R;
+import net.sf.briar.android.groups.NoContactsDialog;
 import net.sf.briar.android.invitation.AddContactActivity;
 import net.sf.briar.android.util.HorizontalBorder;
 import net.sf.briar.android.util.HorizontalSpace;
@@ -33,28 +34,32 @@ import net.sf.briar.api.ContactId;
 import net.sf.briar.api.android.DatabaseUiExecutor;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.NoSuchContactException;
+import net.sf.briar.api.db.PrivateMessageHeader;
 import net.sf.briar.api.db.event.ContactAddedEvent;
 import net.sf.briar.api.db.event.ContactRemovedEvent;
 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.lifecycle.LifecycleManager;
 import net.sf.briar.api.transport.ConnectionListener;
 import net.sf.briar.api.transport.ConnectionRegistry;
-import roboguice.activity.RoboActivity;
+import roboguice.activity.RoboFragmentActivity;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.ListView;
 
-public class ContactListActivity extends RoboActivity
-implements OnClickListener, OnItemClickListener, DatabaseListener,
-ConnectionListener {
+public class ContactListActivity extends RoboFragmentActivity
+implements OnClickListener, DatabaseListener, ConnectionListener,
+NoContactsDialog.Listener {
 
 	private static final Logger LOG =
 			Logger.getLogger(ContactListActivity.class.getName());
@@ -63,7 +68,9 @@ ConnectionListener {
 	private ContactListAdapter adapter = null;
 	private ListView list = null;
 	private ListLoadingProgressBar loading = null;
-	private ImageButton addContactButton = null, shareButton = null;
+	private ImageButton addContactButton = null, composeButton = null;
+	private ImageButton shareButton = null;
+	private NoContactsDialog noContactsDialog = null;
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject private volatile DatabaseComponent db;
@@ -83,7 +90,7 @@ ConnectionListener {
 		// Give me all the width and all the unused height
 		list.setLayoutParams(MATCH_WRAP_1);
 		list.setAdapter(adapter);
-		list.setOnItemClickListener(this);
+		list.setOnItemClickListener(adapter);
 		layout.addView(list);
 
 		// Show a progress bar while the list is loading
@@ -106,6 +113,13 @@ ConnectionListener {
 		footer.addView(addContactButton);
 		footer.addView(new HorizontalSpace(this));
 
+		composeButton = new ImageButton(this);
+		composeButton.setBackgroundResource(0);
+		composeButton.setImageResource(R.drawable.content_new_email);
+		composeButton.setOnClickListener(this);
+		footer.addView(composeButton);
+		footer.addView(new HorizontalSpace(this));
+
 		shareButton = new ImageButton(this);
 		shareButton.setBackgroundResource(0);
 		shareButton.setImageResource(R.drawable.social_share);
@@ -115,6 +129,12 @@ ConnectionListener {
 		layout.addView(footer);
 
 		setContentView(layout);
+
+		FragmentManager fm = getSupportFragmentManager();
+		Fragment f = fm.findFragmentByTag("NoContactsDialog");
+		if(f == null) noContactsDialog = new NoContactsDialog();
+		else noContactsDialog = (NoContactsDialog) f;
+		noContactsDialog.setListener(this);
 	}
 
 	@Override
@@ -122,21 +142,33 @@ ConnectionListener {
 		super.onResume();
 		db.addListener(this);
 		connectionRegistry.addListener(this);
-		loadContacts();
+		loadHeaders();
 	}
 
-	private void loadContacts() {
+	private void loadHeaders() {
+		clearHeaders();
 		dbUiExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					lifecycleManager.waitForDatabase();
 					long now = System.currentTimeMillis();
-					Collection<Contact> contacts = db.getContacts();
 					Map<ContactId, Long> times = db.getLastConnected();
+					for(Contact c : db.getContacts()) {
+						Long lastConnected = times.get(c.getId());
+						if(lastConnected == null) continue;
+						try {
+							Collection<PrivateMessageHeader> headers =
+									db.getPrivateMessageHeaders(c.getId());
+							displayHeaders(c, lastConnected, headers);
+						} catch(NoSuchContactException e) {
+							if(LOG.isLoggable(INFO))
+								LOG.info("Contact removed");
+						}
+					}
 					long duration = System.currentTimeMillis() - now;
 					if(LOG.isLoggable(INFO))
-						LOG.info("Load took " + duration + " ms");
-					displayContacts(contacts, times);
+						LOG.info("Full load took " + duration + " ms");
+					hideProgressBar();
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -149,25 +181,54 @@ ConnectionListener {
 		});
 	}
 
-	private void displayContacts(final Collection<Contact> contacts,
-			final Map<ContactId, Long> times) {
+	private void clearHeaders() {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				list.setVisibility(GONE);
+				loading.setVisibility(VISIBLE);
+				adapter.clear();
+				adapter.notifyDataSetChanged();
+			}
+		});
+	}
+
+	private void displayHeaders(final Contact c, final long lastConnected,
+			final Collection<PrivateMessageHeader> headers) {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				list.setVisibility(VISIBLE);
 				loading.setVisibility(GONE);
-				adapter.clear();
-				for(Contact c : contacts) {
-					boolean now = connectionRegistry.isConnected(c.getId());
-					Long last = times.get(c.getId());
-					if(last != null)
-						adapter.add(new ContactListItem(c, now, last));
-				}
+				boolean connected = connectionRegistry.isConnected(c.getId());
+				// Remove the old item, if any
+				ContactListItem item = findItem(c.getId());
+				if(item != null) adapter.remove(item);
+				// Add a new item
+				adapter.add(new ContactListItem(c, connected, lastConnected,
+						headers));
 				adapter.sort(ItemComparator.INSTANCE);
 				adapter.notifyDataSetChanged();
 			}
 		});
 	}
 
+	private void hideProgressBar() {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				list.setVisibility(VISIBLE);
+				loading.setVisibility(GONE);
+			}
+		});
+	}
+
+	private ContactListItem findItem(ContactId c) {
+		int count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			ContactListItem item = adapter.getItem(i);
+			if(item.getContactId().equals(c)) return item;
+		}
+		return null; // Not found
+	}
+
 	@Override
 	public void onPause() {
 		super.onPause();
@@ -178,6 +239,13 @@ ConnectionListener {
 	public void onClick(View view) {
 		if(view == addContactButton) {
 			startActivity(new Intent(this, AddContactActivity.class));
+		} else if(view == composeButton) {
+			if(adapter.isEmpty()) {
+				FragmentManager fm = getSupportFragmentManager();
+				noContactsDialog.show(fm, "NoContactsDialog");
+			} else {
+				startActivity(new Intent(this, WritePrivateMessageActivity.class));
+			}
 		} else if(view == shareButton) {
 			String apkPath = getPackageCodePath();
 			Intent i = new Intent(ACTION_SEND);
@@ -188,14 +256,73 @@ ConnectionListener {
 		}
 	}
 
-	public void onItemClick(AdapterView<?> parent, View view, int position,
-			long id) {
-		// FIXME: Hook this up to an activity
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof ContactAddedEvent) {
+			loadHeaders();
+		} else if(e instanceof ContactRemovedEvent) {
+			// Reload the conversation, expecting NoSuchContactException
+			if(LOG.isLoggable(INFO)) LOG.info("Contact removed, reloading");
+			reloadHeaders(((ContactRemovedEvent) e).getContactId());
+		} else if(e instanceof MessageExpiredEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
+			loadHeaders();
+		} else if(e instanceof PrivateMessageAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+			reloadHeaders(((PrivateMessageAddedEvent) e).getContactId());
+		}
 	}
 
-	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof ContactAddedEvent) loadContacts();
-		else if(e instanceof ContactRemovedEvent) loadContacts();
+	private void reloadHeaders(final ContactId c) {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					lifecycleManager.waitForDatabase();
+					long now = System.currentTimeMillis();
+					Collection<PrivateMessageHeader> headers =
+							db.getPrivateMessageHeaders(c);
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Partial load took " + duration + " ms");
+					updateItem(c, headers);
+				} catch(NoSuchContactException e) {
+					if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
+					removeItem(c);
+				} 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 database");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	private void updateItem(final ContactId c,
+			final Collection<PrivateMessageHeader> headers) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				ContactListItem item = findItem(c);
+				if(item == null) return;
+				// Replace the item with a new item containing the new headers
+				adapter.remove(item);
+				item = new ContactListItem(item.getContact(),
+						item.isConnected(), item.getLastConnected(), headers);
+				adapter.add(item);
+				adapter.sort(ItemComparator.INSTANCE);
+				adapter.notifyDataSetChanged();
+			}
+		});
+	}
+
+	private void removeItem(final ContactId c) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				ContactListItem item = findItem(c);
+				if(item != null) adapter.remove(item);
+			}
+		});
 	}
 
 	public void contactConnected(ContactId c) {
@@ -225,6 +352,12 @@ ConnectionListener {
 		});
 	}
 
+	public void contactCreationSelected() {
+		startActivity(new Intent(this, AddContactActivity.class));
+	}
+
+	public void contactCreationCancelled() {}
+
 	private static class ItemComparator implements Comparator<ContactListItem> {
 
 		private static final ItemComparator INSTANCE = new ItemComparator();
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
index 3ff0f06dad794ebdd11a6baf4b43d359656fbe10..2c3a988268cd7da93809aabecfad30d02ad89495 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
@@ -8,17 +8,21 @@ import java.util.ArrayList;
 
 import net.sf.briar.R;
 import android.content.Context;
+import android.content.Intent;
 import android.content.res.Resources;
 import android.text.Html;
 import android.text.format.DateUtils;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
-class ContactListAdapter extends ArrayAdapter<ContactListItem> {
+class ContactListAdapter extends ArrayAdapter<ContactListItem>
+implements OnItemClickListener {
 
 	ContactListAdapter(Context ctx) {
 		super(ctx, android.R.layout.simple_expandable_list_item_1,
@@ -32,6 +36,9 @@ class ContactListAdapter extends ArrayAdapter<ContactListItem> {
 		LinearLayout layout = new LinearLayout(ctx);
 		layout.setOrientation(HORIZONTAL);
 		layout.setGravity(CENTER_VERTICAL);
+		Resources res = ctx.getResources();
+		if(item.getUnreadCount() > 0)
+			layout.setBackgroundColor(res.getColor(R.color.unread_background));
 
 		ImageView bulb = new ImageView(ctx);
 		bulb.setPadding(5, 5, 5, 5);
@@ -46,7 +53,10 @@ class ContactListAdapter extends ArrayAdapter<ContactListItem> {
 		name.setTextSize(18);
 		name.setMaxLines(1);
 		name.setPadding(0, 10, 10, 10);
-		name.setText(item.getContactName());
+		int unread = item.getUnreadCount();
+		String contactName = item.getContactName();
+		if(unread > 0) name.setText(contactName + " (" + unread + ")");
+		else name.setText(contactName);
 		layout.addView(name);
 
 		TextView connected = new TextView(ctx);
@@ -55,7 +65,6 @@ class ContactListAdapter extends ArrayAdapter<ContactListItem> {
 		if(item.isConnected()) {
 			connected.setText(R.string.contact_connected);
 		} else {
-			Resources res = ctx.getResources();
 			String format = res.getString(R.string.format_last_connected);
 			long then = item.getLastConnected();
 			CharSequence ago = DateUtils.getRelativeTimeSpanString(then);
@@ -65,4 +74,15 @@ class ContactListAdapter extends ArrayAdapter<ContactListItem> {
 
 		return layout;
 	}
+
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		ContactListItem item = getItem(position);
+		Intent i = new Intent(getContext(), ConversationActivity.class);
+		i.putExtra("net.sf.briar.CONTACT_ID", item.getContactId().getInt());
+		i.putExtra("net.sf.briar.CONTACT_NAME", item.getContactName());
+		i.putExtra("net.sf.briar.LOCAL_AUTHOR_ID",
+				item.getLocalAuthorId().getBytes());
+		getContext().startActivity(i);
+	}
 }
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
index 72b9d17e679c1bb1fca9e072d24a021a5da0e969..5787c1158849f0c1acbad1857ccc6c80d61b02a8 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
@@ -1,7 +1,15 @@
 package net.sf.briar.android.contact;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import net.sf.briar.android.DescendingHeaderComparator;
+import net.sf.briar.api.AuthorId;
 import net.sf.briar.api.Contact;
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.db.PrivateMessageHeader;
 
 // This class is not thread-safe
 class ContactListItem {
@@ -9,11 +17,32 @@ class ContactListItem {
 	private final Contact contact;
 	private boolean connected;
 	private long lastConnected;
+	private final boolean empty;
+	private final long timestamp;
+	private final int unread;
 
-	ContactListItem(Contact contact, boolean connected, long lastConnected) {
+	ContactListItem(Contact contact, boolean connected, long lastConnected,
+			Collection<PrivateMessageHeader> headers) {
 		this.contact = contact;
 		this.connected = connected;
 		this.lastConnected = lastConnected;
+		empty = headers.isEmpty();
+		if(empty) {
+			timestamp = 0;
+			unread = 0;
+		} else {
+			List<PrivateMessageHeader> list =
+					new ArrayList<PrivateMessageHeader>(headers);
+			Collections.sort(list, DescendingHeaderComparator.INSTANCE);
+			timestamp = list.get(0).getTimestamp();
+			int unread = 0;
+			for(PrivateMessageHeader h : list) if(!h.isRead()) unread++;
+			this.unread = unread;
+		}
+	}
+
+	Contact getContact() {
+		return contact;
 	}
 
 	ContactId getContactId() {
@@ -39,4 +68,20 @@ class ContactListItem {
 	void setConnected(boolean connected) {
 		this.connected = connected;
 	}
+
+	AuthorId getLocalAuthorId() {
+		return contact.getLocalAuthorId();
+	}
+
+	boolean isEmpty() {
+		return empty;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	int getUnreadCount() {
+		return unread;
+	}
 }
\ No newline at end of file
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java b/briar-android/src/net/sf/briar/android/contact/ConversationActivity.java
similarity index 99%
rename from briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
rename to briar-android/src/net/sf/briar/android/contact/ConversationActivity.java
index 2dce8e6dd4dab80272fd550c24a4cd3f4c0222ca..a0fe1020c10af2ae34362ade21cf256ef2686ed8 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
+++ b/briar-android/src/net/sf/briar/android/contact/ConversationActivity.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.messages;
+package net.sf.briar.android.contact;
 
 import static android.view.Gravity.CENTER_HORIZONTAL;
 import static android.view.View.GONE;
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java b/briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java
similarity index 97%
rename from briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
rename to briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java
index 863aa88c31e79a5e6fb3ad264748bf30dd042d09..3497efd1034bf429dae68880c20e01670909b5e1 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
+++ b/briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.messages;
+package net.sf.briar.android.contact;
 
 import static android.widget.LinearLayout.HORIZONTAL;
 import static java.text.DateFormat.SHORT;
diff --git a/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java
similarity index 99%
rename from briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
rename to briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java
index c47b30603b174950f87c3b1ec4f4d6fc71366383..677b12db52346307ddd5d0b80b83488c713e225d 100644
--- a/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.messages;
+package net.sf.briar.android.contact;
 
 import static android.view.Gravity.CENTER;
 import static android.view.Gravity.CENTER_VERTICAL;
diff --git a/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java
similarity index 97%
rename from briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
rename to briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java
index dfa935cc00afdcd6001f13d2169a22fa1c982d3c..51e6720860cd710d2459952189d2d7bec61763d0 100644
--- a/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.messages;
+package net.sf.briar.android.contact;
 
 import static android.text.InputType.TYPE_CLASS_TEXT;
 import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
@@ -19,9 +19,6 @@ import java.util.logging.Logger;
 import javax.inject.Inject;
 
 import net.sf.briar.R;
-import net.sf.briar.android.contact.ContactItem;
-import net.sf.briar.android.contact.ContactItemComparator;
-import net.sf.briar.android.contact.ContactSpinnerAdapter;
 import net.sf.briar.android.invitation.AddContactActivity;
 import net.sf.briar.android.util.HorizontalSpace;
 import net.sf.briar.api.AuthorId;
diff --git a/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java
index e3d12fcc130889c5cc81aaeca6c55fa0b658cb6f..6241ba2f378dd0bb9c9d88f01a9f5dfcf7440d66 100644
--- a/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java
@@ -19,7 +19,6 @@ import javax.inject.Inject;
 import net.sf.briar.R;
 import net.sf.briar.android.contact.SelectContactsDialog;
 import net.sf.briar.android.invitation.AddContactActivity;
-import net.sf.briar.android.messages.NoContactsDialog;
 import net.sf.briar.api.Contact;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.android.DatabaseUiExecutor;
diff --git a/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java b/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java
index b6d0dff45b06972ff468ec95f489d6bbcc0781f1..dd7bb39812fb381428c3d3d15ee869abd1323329 100644
--- a/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java
@@ -24,7 +24,6 @@ import javax.inject.Inject;
 import net.sf.briar.R;
 import net.sf.briar.android.contact.SelectContactsDialog;
 import net.sf.briar.android.invitation.AddContactActivity;
-import net.sf.briar.android.messages.NoContactsDialog;
 import net.sf.briar.api.Contact;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.android.DatabaseUiExecutor;
diff --git a/briar-android/src/net/sf/briar/android/messages/NoContactsDialog.java b/briar-android/src/net/sf/briar/android/groups/NoContactsDialog.java
similarity index 96%
rename from briar-android/src/net/sf/briar/android/messages/NoContactsDialog.java
rename to briar-android/src/net/sf/briar/android/groups/NoContactsDialog.java
index ddef88e455e78a3f08f37b8b859d6f0e6becf659..dcd39facb81e4f1363b413b3d5da068379f2653d 100644
--- a/briar-android/src/net/sf/briar/android/messages/NoContactsDialog.java
+++ b/briar-android/src/net/sf/briar/android/groups/NoContactsDialog.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.messages;
+package net.sf.briar.android.groups;
 
 import net.sf.briar.R;
 import android.app.AlertDialog;
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
deleted file mode 100644
index 719e33419bd1348a26617500fce4000640d01a8f..0000000000000000000000000000000000000000
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
+++ /dev/null
@@ -1,290 +0,0 @@
-package net.sf.briar.android.messages;
-
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-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.android.util.CommonLayoutParams.MATCH_MATCH;
-import static net.sf.briar.android.util.CommonLayoutParams.MATCH_WRAP_1;
-
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
-import net.sf.briar.R;
-import net.sf.briar.android.invitation.AddContactActivity;
-import net.sf.briar.android.util.HorizontalBorder;
-import net.sf.briar.android.util.ListLoadingProgressBar;
-import net.sf.briar.api.Contact;
-import net.sf.briar.api.ContactId;
-import net.sf.briar.api.android.DatabaseUiExecutor;
-import net.sf.briar.api.db.DatabaseComponent;
-import net.sf.briar.api.db.DbException;
-import net.sf.briar.api.db.NoSuchContactException;
-import net.sf.briar.api.db.PrivateMessageHeader;
-import net.sf.briar.api.db.event.ContactRemovedEvent;
-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.lifecycle.LifecycleManager;
-import roboguice.activity.RoboFragmentActivity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-
-public class ConversationListActivity extends RoboFragmentActivity
-implements OnClickListener, DatabaseListener, NoContactsDialog.Listener {
-
-	private static final Logger LOG =
-			Logger.getLogger(ConversationListActivity.class.getName());
-
-	private ConversationListAdapter adapter = null;
-	private ListView list = null;
-	private ListLoadingProgressBar loading = null;
-	private NoContactsDialog noContactsDialog = null;
-
-	// Fields that are accessed from background threads must be volatile
-	@Inject private volatile DatabaseComponent db;
-	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
-	@Inject private volatile LifecycleManager lifecycleManager;
-
-	@Override
-	public void onCreate(Bundle state) {
-		super.onCreate(state);
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(MATCH_MATCH);
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-
-		adapter = new ConversationListAdapter(this);
-		list = new ListView(this);
-		// Give me all the width and all the unused height
-		list.setLayoutParams(MATCH_WRAP_1);
-		list.setAdapter(adapter);
-		list.setOnItemClickListener(adapter);
-		layout.addView(list);
-
-		// Show a progress bar while the list is loading
-		list.setVisibility(GONE);
-		loading = new ListLoadingProgressBar(this);
-		layout.addView(loading);
-
-		layout.addView(new HorizontalBorder(this));
-
-		ImageButton composeButton = new ImageButton(this);
-		composeButton.setBackgroundResource(0);
-		composeButton.setImageResource(R.drawable.content_new_email);
-		composeButton.setOnClickListener(this);
-		layout.addView(composeButton);
-
-		setContentView(layout);
-
-		FragmentManager fm = getSupportFragmentManager();
-		Fragment f = fm.findFragmentByTag("NoContactsDialog");
-		if(f == null) noContactsDialog = new NoContactsDialog();
-		else noContactsDialog = (NoContactsDialog) f;
-		noContactsDialog.setListener(this);
-	}
-
-	@Override
-	public void onResume() {
-		super.onResume();
-		db.addListener(this);
-		loadHeaders();
-	}
-
-	private void loadHeaders() {
-		clearHeaders();
-		dbUiExecutor.execute(new Runnable() {
-			public void run() {
-				try {
-					lifecycleManager.waitForDatabase();
-					long now = System.currentTimeMillis();
-					for(Contact c : db.getContacts()) {
-						try {
-							Collection<PrivateMessageHeader> headers =
-									db.getPrivateMessageHeaders(c.getId());
-							displayHeaders(c, headers);
-						} catch(NoSuchContactException e) {
-							if(LOG.isLoggable(INFO))
-								LOG.info("Contact removed");
-						}
-					}
-					long duration = System.currentTimeMillis() - now;
-					if(LOG.isLoggable(INFO))
-						LOG.info("Full load took " + duration + " ms");
-					hideProgressBar();
-				} 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 database");
-					Thread.currentThread().interrupt();
-				}
-			}
-		});
-	}
-
-	private void clearHeaders() {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				list.setVisibility(GONE);
-				loading.setVisibility(VISIBLE);
-				adapter.clear();
-				adapter.notifyDataSetChanged();
-			}
-		});
-	}
-
-	private void displayHeaders(final Contact c,
-			final Collection<PrivateMessageHeader> headers) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				list.setVisibility(VISIBLE);
-				loading.setVisibility(GONE);
-				// Remove the old item, if any
-				ConversationListItem item = findConversation(c.getId());
-				if(item != null) adapter.remove(item);
-				// Add a new item
-				adapter.add(new ConversationListItem(c, headers));
-				adapter.sort(ItemComparator.INSTANCE);
-				adapter.notifyDataSetChanged();
-				selectFirstUnread();
-			}
-		});
-	}
-
-	private void hideProgressBar() {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				list.setVisibility(VISIBLE);
-				loading.setVisibility(GONE);
-			}
-		});
-	}
-
-	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 selectFirstUnread() {
-		int firstUnread = -1, count = adapter.getCount();
-		for(int i = 0; i < count; i++) {
-			if(adapter.getItem(i).getUnreadCount() > 0) {
-				firstUnread = i;
-				break;
-			}
-		}
-		if(firstUnread == -1) list.setSelection(count - 1);
-		else list.setSelection(firstUnread);
-	}
-
-	@Override
-	public void onPause() {
-		super.onPause();
-		db.removeListener(this);
-	}
-
-	public void onClick(View view) {
-		if(adapter.isEmpty()) {
-			FragmentManager fm = getSupportFragmentManager();
-			noContactsDialog.show(fm, "NoContactsDialog");
-		} else {
-			startActivity(new Intent(this, WritePrivateMessageActivity.class));
-		}
-	}
-
-	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof ContactRemovedEvent) {
-			// Reload the conversation, expecting NoSuchContactException
-			if(LOG.isLoggable(INFO)) LOG.info("Contact removed, reloading");
-			loadHeaders(((ContactRemovedEvent) e).getContactId());
-		} else if(e instanceof MessageExpiredEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
-			loadHeaders();
-		} else if(e instanceof PrivateMessageAddedEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
-			loadHeaders(((PrivateMessageAddedEvent) e).getContactId());
-		}
-	}
-
-	private void loadHeaders(final ContactId c) {
-		dbUiExecutor.execute(new Runnable() {
-			public void run() {
-				try {
-					lifecycleManager.waitForDatabase();
-					long now = System.currentTimeMillis();
-					Contact contact = db.getContact(c);
-					Collection<PrivateMessageHeader> headers =
-							db.getPrivateMessageHeaders(c);
-					long duration = System.currentTimeMillis() - now;
-					if(LOG.isLoggable(INFO))
-						LOG.info("Partial load took " + duration + " ms");
-					displayHeaders(contact, headers);
-				} catch(NoSuchContactException e) {
-					if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
-					removeConversation(c);
-				} 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 database");
-					Thread.currentThread().interrupt();
-				}
-			}
-		});
-	}
-
-	private void removeConversation(final ContactId c) {
-		runOnUiThread(new Runnable() {
-			public void run() {
-				ConversationListItem item = findConversation(c);
-				if(item != null) {
-					adapter.remove(item);
-					selectFirstUnread();
-				}
-			}
-		});
-	}
-
-	public void contactCreationSelected() {
-		startActivity(new Intent(this, AddContactActivity.class));
-	}
-
-	public void contactCreationCancelled() {}
-
-	private static class ItemComparator
-	implements Comparator<ConversationListItem> {
-
-		private static final ItemComparator INSTANCE = new ItemComparator();
-
-		public int compare(ConversationListItem a, ConversationListItem b) {
-			// The item with the newest message comes first
-			long aTime = a.getTimestamp(), bTime = b.getTimestamp();
-			if(aTime > bTime) return -1;
-			if(aTime < bTime) return 1;
-			// Break ties by contact name
-			String aName = a.getContactName(), bName = b.getContactName();
-			return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
-		}
-	}
-}
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
deleted file mode 100644
index e9ce83be28407f81c6786c9f16045e8fc35bde5a..0000000000000000000000000000000000000000
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package net.sf.briar.android.messages;
-
-import static android.widget.LinearLayout.HORIZONTAL;
-import static java.text.DateFormat.SHORT;
-import static net.sf.briar.android.util.CommonLayoutParams.WRAP_WRAP_1;
-
-import java.util.ArrayList;
-
-import net.sf.briar.R;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.text.format.DateUtils;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ArrayAdapter;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-class ConversationListAdapter extends ArrayAdapter<ConversationListItem>
-implements OnItemClickListener {
-
-	ConversationListAdapter(Context ctx) {
-		super(ctx, android.R.layout.simple_expandable_list_item_1,
-				new ArrayList<ConversationListItem>());
-	}
-
-	@Override
-	public View getView(int position, View convertView, ViewGroup parent) {
-		ConversationListItem item = getItem(position);
-		Context ctx = getContext();
-		Resources res = ctx.getResources();
-
-		LinearLayout layout = new LinearLayout(ctx);
-		layout.setOrientation(HORIZONTAL);
-		if(item.getUnreadCount() > 0)
-			layout.setBackgroundColor(res.getColor(R.color.unread_background));
-
-		TextView name = new TextView(ctx);
-		// Give me all the unused width
-		name.setLayoutParams(WRAP_WRAP_1);
-		name.setTextSize(18);
-		name.setMaxLines(1);
-		name.setPadding(10, 10, 10, 10);
-		int unread = item.getUnreadCount();
-		String contactName = item.getContactName();
-		if(unread > 0) name.setText(contactName + " (" + unread + ")");
-		else name.setText(contactName);
-		layout.addView(name);
-
-		if(item.isEmpty()) {
-			TextView noMessages = new TextView(ctx);
-			noMessages.setTextSize(14);
-			noMessages.setPadding(10, 0, 10, 10);
-			noMessages.setTextColor(res.getColor(R.color.no_messages));
-			noMessages.setText(R.string.no_messages);
-			layout.addView(noMessages);
-		} else {
-			TextView date = new TextView(ctx);
-			date.setTextSize(14);
-			date.setPadding(0, 10, 10, 10);
-			long then = item.getTimestamp(), now = System.currentTimeMillis();
-			date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
-			layout.addView(date);
-		}
-
-		return layout;
-	}
-
-	public void onItemClick(AdapterView<?> parent, View view, int position,
-			long id) {
-		ConversationListItem item = getItem(position);
-		Intent i = new Intent(getContext(), ConversationActivity.class);
-		i.putExtra("net.sf.briar.CONTACT_ID", item.getContactId().getInt());
-		i.putExtra("net.sf.briar.CONTACT_NAME", item.getContactName());
-		i.putExtra("net.sf.briar.LOCAL_AUTHOR_ID",
-				item.getLocalAuthorId().getBytes());
-		getContext().startActivity(i);
-	}
-}
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
deleted file mode 100644
index 231560a50af83a6d5123268d25d06ac26cd27e39..0000000000000000000000000000000000000000
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package net.sf.briar.android.messages;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-import net.sf.briar.android.DescendingHeaderComparator;
-import net.sf.briar.api.AuthorId;
-import net.sf.briar.api.Contact;
-import net.sf.briar.api.ContactId;
-import net.sf.briar.api.db.PrivateMessageHeader;
-
-class ConversationListItem {
-
-	private final Contact contact;
-	private final boolean empty;
-	private final long timestamp;
-	private final int unread;
-
-	ConversationListItem(Contact contact,
-			Collection<PrivateMessageHeader> headers) {
-		this.contact = contact;
-		empty = headers.isEmpty();
-		if(empty) {
-			timestamp = 0;
-			unread = 0;
-		} else {
-			List<PrivateMessageHeader> list =
-					new ArrayList<PrivateMessageHeader>(headers);
-			Collections.sort(list, DescendingHeaderComparator.INSTANCE);
-			timestamp = list.get(0).getTimestamp();
-			int unread = 0;
-			for(PrivateMessageHeader h : list) if(!h.isRead()) unread++;
-			this.unread = unread;
-		}
-	}
-
-	ContactId getContactId() {
-		return contact.getId();
-	}
-
-	String getContactName() {
-		return contact.getAuthor().getName();
-	}
-
-	AuthorId getLocalAuthorId() {
-		return contact.getLocalAuthorId();
-	}
-
-	boolean isEmpty() {
-		return empty;
-	}
-
-	long getTimestamp() {
-		return timestamp;
-	}
-
-	int getUnreadCount() {
-		return unread;
-	}
-}