diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 0b0771da6e3cb758e532496672e95d226cf837cd..e402626affe6154e434dfd3d5c4bd8ce01cefbef 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -40,6 +40,22 @@
 		    android:name=".android.contact.ContactListActivity"
 		    android:label="@string/contact_list_title" >
 		</activity>
+		<activity
+			android:name=".android.groups.GroupActivity"
+			android:label="@string/groups_title" >
+		</activity>
+		<activity
+			android:name=".android.groups.GroupListActivity"
+			android:label="@string/groups_title" >
+		</activity>
+		<activity
+			android:name=".android.groups.ReadGroupMessageActivity"
+			android:label="@string/groups_title" >
+		</activity>
+		<activity
+			android:name=".android.groups.WriteGroupMessageActivity"
+			android:label="@string/compose_group_title" >
+		</activity>
 		<activity
 			android:name=".android.invitation.AddContactActivity"
 			android:label="@string/add_contact_title" >
@@ -53,12 +69,12 @@
 			android:label="@string/messages_title" >
 		</activity>
 		<activity
-			android:name=".android.messages.ReadMessageActivity"
+			android:name="net.sf.briar.android.messages.ReadPrivateMessageActivity"
 			android:label="@string/messages_title" >
 		</activity>
 		<activity
-			android:name=".android.messages.WriteMessageActivity"
-			android:label="@string/compose_title" >
+			android:name="net.sf.briar.android.messages.WritePrivateMessageActivity"
+			android:label="@string/compose_message_title" >
 		</activity>
 	</application>
 </manifest>
diff --git a/briar-android/res/drawable-hdpi/social_new_chat.png b/briar-android/res/drawable-hdpi/social_new_chat.png
new file mode 100644
index 0000000000000000000000000000000000000000..a6a42eeb6779836698a0e2c213955518d2284373
Binary files /dev/null and b/briar-android/res/drawable-hdpi/social_new_chat.png differ
diff --git a/briar-android/res/drawable-hdpi/social_reply_all.png b/briar-android/res/drawable-hdpi/social_reply_all.png
new file mode 100644
index 0000000000000000000000000000000000000000..377f6286ce9d88bcb8e7e4bc2b331d583f5cd6c3
Binary files /dev/null and b/briar-android/res/drawable-hdpi/social_reply_all.png differ
diff --git a/briar-android/res/drawable-mdpi/social_new_chat.png b/briar-android/res/drawable-mdpi/social_new_chat.png
new file mode 100644
index 0000000000000000000000000000000000000000..e78580b8ec8bd72df2953c1cb27afbe201205552
Binary files /dev/null and b/briar-android/res/drawable-mdpi/social_new_chat.png differ
diff --git a/briar-android/res/drawable-mdpi/social_reply_all.png b/briar-android/res/drawable-mdpi/social_reply_all.png
new file mode 100644
index 0000000000000000000000000000000000000000..86334552edec456966f3b5bd4b9e00b73a180bd7
Binary files /dev/null and b/briar-android/res/drawable-mdpi/social_reply_all.png differ
diff --git a/briar-android/res/drawable-xhdpi/social_new_chat.png b/briar-android/res/drawable-xhdpi/social_new_chat.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d5d9049252fe043a5b55b80cc8b5b8b8af37b3a
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/social_new_chat.png differ
diff --git a/briar-android/res/drawable-xhdpi/social_reply_all.png b/briar-android/res/drawable-xhdpi/social_reply_all.png
new file mode 100644
index 0000000000000000000000000000000000000000..f10a492c0229009a9b358cbd8ccae910ba21cdab
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/social_reply_all.png differ
diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml
index a55aadfa90727f7aedb1be06d8a627e684a43259..866b33e5869f3fc6a2201322d71a9cf620a7502f 100644
--- a/briar-android/res/values/color.xml
+++ b/briar-android/res/values/color.xml
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-	<color name="HorizontalBorder">#CCCCCC</color>
+	<color name="horizontal_border">#CCCCCC</color>
+	<color name="anonymous_author">#999999</color>
+	<color name="pseudonymous_author">#000000</color>
 </resources>
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 92486abf3bd87f4e6a8982c4ceb6909747448989..14258e04719804e036265ed5c8b05d5a2e558144 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -11,14 +11,13 @@
 	<string name="quit_button">Quit</string>
 	<string name="contact_list_title">Contacts</string>
 	<string name="contact_connected">Connected</string>
-	<string name="contact_last_connected">Last connected &lt;br /&gt; %1$s</string>
-	<string name="search_button">Search</string>
+	<string name="format_contact_last_connected">Last connected &lt;br /&gt; %1$s</string>
 	<string name="add_contact_title">Add a Contact</string>
 	<string name="same_network">Briar can add contacts via Wi-Fi or Bluetooth.  To use Wi-Fi you must both be connected to the same network.</string>
 	<string name="wifi_not_available">Wi-Fi is not available on this device</string>
 	<string name="wifi_disabled">Wi-Fi is OFF</string>
 	<string name="wifi_disconnected">Wi-Fi is DISCONNECTED</string>
-	<string name="wifi_connected">Wi-Fi is CONNECTED to %1$s</string>
+	<string name="format_wifi_connected">Wi-Fi is CONNECTED to %1$s</string>
 	<string name="bluetooth_not_available">Bluetooth is not available on this device</string>
 	<string name="bluetooth_disabled">Bluetooth is OFF</string>
 	<string name="bluetooth_not_discoverable">Bluetooth is NOT DISCOVERABLE</string>
@@ -26,7 +25,7 @@
 	<string name="continue_button">Continue</string>
 	<string name="your_invitation_code">Your invitation code is</string>
 	<string name="enter_invitation_code">Please enter your contact\'s invitation code:</string>
-	<string name="connecting_wifi">Connecting via %1$s\u2026</string>
+	<string name="format_connecting_wifi">Connecting via %1$s\u2026</string>
 	<string name="connecting_bluetooth">Connecting via Bluetooth\u2026</string>
 	<string name="connection_failed">Connection failed</string>
 	<string name="check_same_network">Please check that you are both using the same network.</string>
@@ -41,7 +40,11 @@
 	<string name="enter_nickname">Please enter a nickname for this contact:</string>
 	<string name="done_button">Done</string>
 	<string name="messages_title">Messages</string>
-	<string name="message_from">From: %1$s</string>
-	<string name="compose_title">New Message</string>
-	<string name="message_to">To:</string>
+	<string name="format_from">From: %1$s</string>
+	<string name="format_to">To: %1$s</string>
+	<string name="compose_message_title">New Message</string>
+	<string name="to">To:</string>
+	<string name="groups_title">Groups</string>
+	<string name="anonymous">(Anonymous)</string>
+	<string name="compose_group_title">New Post</string>
 </resources>
diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
index 090ceca84df471d1e5cf04c4e977bb575ed0205a..02cea07efb922402829581a0e8f09f40b23bfc2e 100644
--- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
+++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
@@ -12,6 +12,7 @@ import net.sf.briar.R;
 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.android.widgets.CommonLayoutParams;
 import android.content.Intent;
@@ -88,7 +89,8 @@ public class HomeScreenActivity extends BriarActivity {
 			groupsButton.setText(R.string.groups_button);
 			groupsButton.setOnClickListener(new OnClickListener() {
 				public void onClick(View view) {
-					// FIXME: Hook this button up to an activity
+					startActivity(new Intent(HomeScreenActivity.this,
+							GroupListActivity.class));
 				}
 			});
 			buttons.add(groupsButton);
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java b/briar-android/src/net/sf/briar/android/contact/ContactComparator.java
index 6495bf631051e67b2e2f1d23f5e77da4844c8735..8e9207c6fc1731c4f0ef1e288d2c358885c1b13f 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactComparator.java
@@ -7,7 +7,7 @@ class ContactComparator implements Comparator<ContactListItem> {
 	static final ContactComparator INSTANCE = new ContactComparator();
 
 	public int compare(ContactListItem a, ContactListItem b) {
-		return String.CASE_INSENSITIVE_ORDER.compare(a.contact.getName(),
-				b.contact.getName());
+		return String.CASE_INSENSITIVE_ORDER.compare(a.getContactName(),
+				b.getContactName());
 	}
 }
\ No newline at end of file
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 39cb91c6c1a97215f7e3aba48e7a1e5d73fbcdf0..bb10e399cf2f6de279036871782b87118bfdbdb6 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
@@ -8,7 +8,6 @@ import java.util.ArrayList;
 import net.sf.briar.R;
 import net.sf.briar.android.widgets.CommonLayoutParams;
 import android.content.Context;
-import android.content.res.Resources;
 import android.text.Html;
 import android.text.format.DateUtils;
 import android.view.View;
@@ -46,8 +45,9 @@ implements OnItemClickListener {
 		// Give me all the unused width
 		name.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
 		name.setTextSize(18);
+		name.setMaxLines(1);
 		name.setPadding(0, 10, 10, 10);
-		name.setText(item.getName());
+		name.setText(item.getContactName());
 		layout.addView(name);
 
 		TextView connected = new TextView(ctx);
@@ -56,8 +56,8 @@ implements OnItemClickListener {
 		if(item.isConnected()) {
 			connected.setText(R.string.contact_connected);
 		} else {
-			Resources res = ctx.getResources();
-			String format = res.getString(R.string.contact_last_connected);
+			String format = ctx.getResources().getString(
+					R.string.format_contact_last_connected);
 			long then = item.getLastConnected();
 			CharSequence ago = DateUtils.getRelativeTimeSpanString(then);
 			connected.setText(Html.fromHtml(String.format(format, ago)));
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 cba805e863df13b0d69393146c8559d9b6c836d2..0b030f2f96a4dad832dc1de2b73ea2e6649fba3f 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
@@ -6,7 +6,7 @@ import net.sf.briar.api.ContactId;
 // This class is not thread-safe
 class ContactListItem {
 
-	final Contact contact;
+	private final Contact contact;
 	private boolean connected;
 
 	ContactListItem(Contact contact, boolean connected) {
@@ -18,7 +18,7 @@ class ContactListItem {
 		return contact.getId();
 	}
 
-	String getName() {
+	String getContactName() {
 		return contact.getName();
 	}
 
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..8ed287306acf942eaf298ed4440fb4441f9806ee
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
@@ -0,0 +1,226 @@
+package net.sf.briar.android.groups;
+
+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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.AscendingHeaderComparator;
+import net.sf.briar.android.BriarActivity;
+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.db.DatabaseComponent;
+import net.sf.briar.api.db.DatabaseExecutor;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.db.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.SubscriptionAddedEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.Author;
+import net.sf.briar.api.messaging.GroupId;
+import android.content.Intent;
+import android.os.Bundle;
+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;
+
+import com.google.inject.Inject;
+
+public class GroupActivity extends BriarActivity implements DatabaseListener,
+OnClickListener, OnItemClickListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(GroupActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private DatabaseComponent db;
+	@Inject @DatabaseExecutor private Executor dbExecutor;
+
+	private GroupId groupId = null;
+	private String groupName = null;
+	private GroupAdapter adapter = null;
+	private ListView list = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(id == null) throw new IllegalStateException();
+		groupId = new GroupId(id);
+		groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+		if(groupName == null) throw new IllegalStateException();
+		setTitle(groupName);
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(CommonLayoutParams.MATCH_MATCH);
+		layout.setOrientation(VERTICAL);
+		layout.setGravity(CENTER_HORIZONTAL);
+
+		adapter = new GroupAdapter(this);
+		list = new ListView(this);
+		// Give me all the width and all the unused height
+		list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
+		list.setAdapter(adapter);
+		list.setOnItemClickListener(this);
+		layout.addView(list);
+
+		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);
+
+		// 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);
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		reloadMessageHeaders();
+	}
+
+	private void reloadMessageHeaders() {
+		final DatabaseComponent db = this.db;
+		final GroupId groupId = this.groupId;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// Load the message headers from the database
+					Collection<GroupMessageHeader> headers =
+							db.getMessageHeaders(groupId);
+					if(LOG.isLoggable(INFO))
+						LOG.info("Loaded " + headers.size() + " headers");
+					// Update the conversation
+					updateConversation(headers);
+				} 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 updateConversation(
+			final Collection<GroupMessageHeader> headers) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				List<GroupMessageHeader> sort =
+						new ArrayList<GroupMessageHeader>(headers);
+				Collections.sort(sort, AscendingHeaderComparator.INSTANCE);
+				int firstUnread = -1;
+				adapter.clear();
+				for(GroupMessageHeader h : sort) {
+					if(firstUnread == -1 && !h.isRead())
+						firstUnread = adapter.getCount();
+					adapter.add(h);
+				}
+				if(firstUnread == -1) list.setSelection(adapter.getCount() - 1);
+				else list.setSelection(firstUnread);
+			}
+		});
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		db.removeListener(this);
+		unbindService(serviceConnection);
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof MessageAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+			reloadMessageHeaders();
+		} else if(e instanceof MessageExpiredEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
+			reloadMessageHeaders();
+		} else if(e instanceof SubscriptionAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
+			reloadMessageHeaders();
+		} else if(e instanceof SubscriptionRemovedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
+			reloadMessageHeaders();
+		}
+	}
+
+	public void onClick(View view) {
+		Intent i = new Intent(this, WriteGroupMessageActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+		i.putExtra("net.sf.briar.GROUP_NAME", groupName);
+		startActivity(i);
+	}
+
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		showMessage(position);
+	}
+
+	private void showMessage(int position) {
+		GroupMessageHeader item = adapter.getItem(position);
+		Intent i = new Intent(this, ReadGroupMessageActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+		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);
+			i.putExtra("net.sf.briar.AUTHOR_ID", author.getId().getBytes());
+			i.putExtra("net.sf.briar.AUTHOR_NAME", author.getName());
+		}
+		i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType());
+		i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp());
+		i.putExtra("net.sf.briar.FIRST", position == 0);
+		i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1);
+		startActivityForResult(i, position);
+	}
+
+	@Override
+	public void onActivityResult(int request, int result, Intent data) {
+		if(result == ReadGroupMessageActivity.RESULT_PREV) {
+			int position = request - 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		} else if(result == ReadGroupMessageActivity.RESULT_NEXT) {
+			int position = request + 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		}
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..17915c8d45abe00087b8e98784b6a36b26c4e0a6
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java
@@ -0,0 +1,92 @@
+package net.sf.briar.android.groups;
+
+import static android.graphics.Typeface.BOLD;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+
+import java.util.ArrayList;
+
+import net.sf.briar.R;
+import net.sf.briar.android.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.messaging.Author;
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+class GroupAdapter extends ArrayAdapter<GroupMessageHeader> {
+
+	GroupAdapter(Context ctx) {
+		super(ctx, android.R.layout.simple_expandable_list_item_1,
+				new ArrayList<GroupMessageHeader>());
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		GroupMessageHeader item = getItem(position);
+		Context ctx = getContext();
+		// FIXME: Use a RelativeLayout
+		LinearLayout layout = new LinearLayout(ctx);
+		layout.setOrientation(HORIZONTAL);
+		layout.setGravity(CENTER_VERTICAL);
+
+		LinearLayout innerLayout = new LinearLayout(ctx);
+		// Give me all the unused width
+		innerLayout.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
+		innerLayout.setOrientation(VERTICAL);
+
+		Author author = item.getAuthor();
+
+		TextView name = new TextView(ctx);
+		name.setTextSize(18);
+		name.setMaxLines(1);
+		name.setPadding(10, 10, 10, 10);
+		Resources res = ctx.getResources();
+		if(author == null) {
+			name.setTextColor(res.getColor(R.color.anonymous_author));
+			name.setText(R.string.anonymous);
+		} else {
+			name.setTextColor(res.getColor(R.color.pseudonymous_author));
+			name.setText(author.getName());
+		}
+		innerLayout.addView(name);
+
+		if(item.getContentType().equals("text/plain")) {
+			TextView subject = new TextView(ctx);
+			subject.setTextSize(14);
+			subject.setMaxLines(2);
+			subject.setPadding(10, 0, 10, 10);
+			if(!item.isRead()) subject.setTypeface(null, BOLD);
+			subject.setText(item.getSubject());
+			innerLayout.addView(subject);
+		} else {
+			LinearLayout innerInnerLayout = new LinearLayout(ctx);
+			innerInnerLayout.setOrientation(HORIZONTAL);
+			ImageView attachment = new ImageView(ctx);
+			attachment.setPadding(10, 0, 10, 10);
+			attachment.setImageResource(R.drawable.content_attachment);
+			innerInnerLayout.addView(attachment);
+			innerInnerLayout.addView(new HorizontalSpace(ctx));
+			innerLayout.addView(innerInnerLayout);
+		}
+		layout.addView(innerLayout);
+
+		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;
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..5c0c3beecf8e5b9c99e5ee571c86e6d9ea816035
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
@@ -0,0 +1,302 @@
+package net.sf.briar.android.groups;
+
+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 java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+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;
+import java.util.logging.Logger;
+
+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;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DatabaseExecutor;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.db.NoSuchSubscriptionException;
+import net.sf.briar.api.db.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.SubscriptionAddedEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.Author;
+import net.sf.briar.api.messaging.AuthorFactory;
+import net.sf.briar.api.messaging.Group;
+import net.sf.briar.api.messaging.GroupFactory;
+import net.sf.briar.api.messaging.Message;
+import net.sf.briar.api.messaging.MessageFactory;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class GroupListActivity extends BriarActivity
+implements OnClickListener, DatabaseListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(GroupListActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private CryptoComponent crypto;
+	@Inject private DatabaseComponent db;
+	@Inject @DatabaseExecutor private Executor dbExecutor;
+	@Inject private AuthorFactory authorFactory;
+	@Inject private GroupFactory groupFactory;
+	@Inject private MessageFactory messageFactory;
+
+	private GroupListAdapter adapter = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(CommonLayoutParams.MATCH_MATCH);
+		layout.setOrientation(VERTICAL);
+		layout.setGravity(CENTER_HORIZONTAL);
+
+		adapter = new GroupListAdapter(this);
+		ListView list = new ListView(this);
+		// Give me all the width and all the unused height
+		list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
+		list.setAdapter(adapter);
+		list.setOnItemClickListener(adapter);
+		layout.addView(list);
+
+		layout.addView(new HorizontalBorder(this));
+
+		ImageButton newGroupButton = new ImageButton(this);
+		newGroupButton.setBackgroundResource(0);
+		newGroupButton.setImageResource(R.drawable.social_new_chat);
+		newGroupButton.setOnClickListener(this);
+		layout.addView(newGroupButton);
+
+		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);
+
+		// Add some fake messages to the database in a background thread
+		insertFakeMessages();
+	}
+
+	// FIXME: Remove this
+	private void insertFakeMessages() {
+		final DatabaseComponent db = this.db;
+		final GroupFactory groupFactory = this.groupFactory;
+		final MessageFactory messageFactory = this.messageFactory;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// If there are no groups in the DB, create some fake ones
+					Collection<Group> groups = db.getSubscriptions();
+					if(!groups.isEmpty()) return;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Inserting fake groups and messages");
+					// We'll also need a contact to receive messages from
+					ContactId contactId = db.addContact("Dave");
+					// Finally, we'll need some authors for the messages
+					KeyPair keyPair = crypto.generateSignatureKeyPair();
+					byte[] publicKey = keyPair.getPublic().getEncoded();
+					PrivateKey privateKey = keyPair.getPrivate();
+					Author author = authorFactory.createAuthor("Batman",
+							publicKey);
+					Author author1 = authorFactory.createAuthor("Duckman",
+							publicKey);
+					// Insert some fake groups and make them visible
+					Group group = groupFactory.createGroup("DisneyLeaks");
+					db.subscribe(group);
+					db.setVisibility(group.getId(), Arrays.asList(contactId));
+					Group group1 = groupFactory.createGroup("Godwin's Lore");
+					db.subscribe(group1);
+					db.setVisibility(group1.getId(), Arrays.asList(contactId));
+					// Insert some text messages to the groups
+					for(int i = 0; i < 20; i++) {
+						String body;
+						if(i % 3 == 0) {
+							body = "Message " + i + " is short.";
+						} else { 
+							body = "Message " + i + " is long enough to wrap"
+									+ " onto a second line on some screens.";
+						}
+						Group g = i % 2 == 0 ? group : group1;
+						Message m;
+						if(i % 5 == 0) {
+							m = messageFactory.createAnonymousMessage(null, g,
+									"text/plain", body.getBytes("UTF-8"));
+						} else if(i % 5 == 2) {
+							m = messageFactory.createPseudonymousMessage(null,
+									g, author, privateKey, "text/plain",
+									body.getBytes("UTF-8"));
+						} else {
+							m = messageFactory.createPseudonymousMessage(null,
+									g, author1, privateKey, "text/plain",
+									body.getBytes("UTF-8"));
+						}
+						if(Math.random() < 0.5) db.addLocalGroupMessage(m);
+						else db.receiveMessage(contactId, m);
+						db.setReadFlag(m.getId(), i % 4 == 0);
+					}
+					// Insert a non-text message
+					Message m = messageFactory.createAnonymousMessage(null,
+							group, "image/jpeg", new byte[1000]);
+					db.receiveMessage(contactId, m);
+					// Insert a long text message
+					StringBuilder s = new StringBuilder();
+					for(int i = 0; i < 100; i++)
+						s.append("This is a very tedious message. ");
+					String body = s.toString();
+					m = messageFactory.createAnonymousMessage(m.getId(),
+							group1, "text/plain", body.getBytes("UTF-8"));
+					db.addLocalGroupMessage(m);
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(GeneralSecurityException 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();
+				} catch(IOException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		reloadGroupList();
+	}
+
+	private void reloadGroupList() {
+		final DatabaseComponent db = this.db;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// Load the groups and message headers from the DB
+					if(LOG.isLoggable(INFO)) LOG.info("Loading groups");
+					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) {
+						// Filter out restricted groups
+						if(g.getPublicKey() != null) continue;
+						Collection<GroupMessageHeader> headers;
+						try {
+							headers = db.getMessageHeaders(g.getId());
+						} catch(NoSuchSubscriptionException e) {
+							// We'll reload the list when we get the event
+							continue;
+						}
+						if(LOG.isLoggable(INFO))
+							LOG.info("Loaded " + headers.size() + " headers");
+						if(!headers.isEmpty())
+							items.add(createItem(g, headers));
+					}
+					// Update the group list
+					updateGroupList(Collections.unmodifiableList(items));
+				} 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 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 updateGroupList(final Collection<GroupListItem> items) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				adapter.clear();
+				for(GroupListItem i : items) adapter.add(i);
+				adapter.sort(GroupComparator.INSTANCE);
+			}
+		});
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		db.removeListener(this);
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		startActivity(new Intent(this, WriteGroupMessageActivity.class));
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof MessageAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+			reloadGroupList();
+		} else if(e instanceof MessageExpiredEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
+			reloadGroupList();
+		} else if(e instanceof SubscriptionAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
+			reloadGroupList();
+		} else if(e instanceof SubscriptionRemovedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
+			reloadGroupList();
+		}
+	}
+
+	private static class GroupComparator implements Comparator<GroupListItem> {
+
+		private static final GroupComparator INSTANCE = new GroupComparator();
+
+		public int compare(GroupListItem a, GroupListItem b) {
+			return String.CASE_INSENSITIVE_ORDER.compare(a.getGroupName(),
+					b.getGroupName());
+		}
+	}
+}
\ No newline at end of file
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..102a93a06ca5a65a45afa400c29f7bb0c7ae965b
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
@@ -0,0 +1,85 @@
+package net.sf.briar.android.groups;
+
+import static android.graphics.Typeface.BOLD;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.Gravity.LEFT;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+
+import java.util.ArrayList;
+
+import net.sf.briar.android.widgets.CommonLayoutParams;
+import net.sf.briar.util.StringUtils;
+import android.content.Context;
+import android.content.Intent;
+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 GroupListAdapter extends ArrayAdapter<GroupListItem>
+implements OnItemClickListener {
+
+	GroupListAdapter(Context ctx) {
+		super(ctx, android.R.layout.simple_expandable_list_item_1,
+				new ArrayList<GroupListItem>());
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		GroupListItem item = getItem(position);
+		Context ctx = getContext();
+		LinearLayout layout = new LinearLayout(ctx);
+		layout.setOrientation(HORIZONTAL);
+		layout.setGravity(CENTER_VERTICAL);
+
+		LinearLayout innerLayout = new LinearLayout(ctx);
+		// Give me all the unused width
+		innerLayout.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
+		innerLayout.setOrientation(VERTICAL);
+		innerLayout.setGravity(LEFT);
+
+		TextView name = new TextView(ctx);
+		name.setTextSize(18);
+		name.setMaxLines(1);
+		name.setPadding(10, 10, 10, 10);
+		int unread = item.getUnreadCount();
+		if(unread > 0) name.setText(item.getGroupName() + " (" + unread + ")");
+		else name.setText(item.getGroupName());
+		innerLayout.addView(name);
+
+		if(!StringUtils.isNullOrEmpty(item.getSubject())) {
+			TextView subject = new TextView(ctx);
+			subject.setTextSize(14);
+			subject.setMaxLines(2);
+			subject.setPadding(10, 0, 10, 10);
+			if(unread > 0) subject.setTypeface(null, BOLD);
+			subject.setText(item.getSubject());
+			innerLayout.addView(subject);
+		}
+		layout.addView(innerLayout);
+
+		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) {
+		GroupListItem item = getItem(position);
+		Intent i = new Intent(getContext(), GroupActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", item.getGroupId().getBytes());
+		i.putExtra("net.sf.briar.GROUP_NAME", item.getGroupName());
+		getContext().startActivity(i);
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..38a4513c21cb24679762f1373a093e72e00feab5
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
@@ -0,0 +1,57 @@
+package net.sf.briar.android.groups;
+
+import java.util.Collections;
+import java.util.List;
+
+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;
+
+class GroupListItem {
+
+	private final Group group;
+	private final String author, subject;
+	private final long timestamp;
+	private final int unread;
+
+	GroupListItem(Group group, List<GroupMessageHeader> headers) {
+		if(headers.isEmpty()) throw new IllegalArgumentException();
+		this.group = group;
+		Collections.sort(headers, DescendingHeaderComparator.INSTANCE);
+		GroupMessageHeader newest = headers.get(0);
+		Author a = newest.getAuthor();
+		if(a == null) author = null;
+		else author = a.getName();
+		subject = newest.getSubject();
+		timestamp = newest.getTimestamp();
+		int unread = 0;
+		for(GroupMessageHeader h : headers) if(!h.isRead()) unread++;
+		this.unread = unread;
+	}
+
+	GroupId getGroupId() {
+		return group.getId();
+	}
+
+	String getGroupName() {
+		return group.getName();
+	}
+
+	String getAuthorName() {
+		return author;
+	}
+
+	String getSubject() {
+		return subject;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	int getUnreadCount() {
+		return unread;
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c16ebfaf89cb84af0d66b55fcfa04211c3eb9dc
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java
@@ -0,0 +1,36 @@
+package net.sf.briar.android.groups;
+
+import java.util.ArrayList;
+
+import net.sf.briar.api.messaging.Group;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+
+class GroupNameSpinnerAdapter extends ArrayAdapter<Group>
+implements SpinnerAdapter {
+
+	GroupNameSpinnerAdapter(Context context) {
+		super(context, android.R.layout.simple_spinner_item,
+				new ArrayList<Group>());
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		TextView name = new TextView(getContext());
+		name.setTextSize(18);
+		name.setMaxLines(1);
+		name.setPadding(10, 10, 10, 10);
+		name.setText(getItem(position).getName());
+		return name;
+	}
+
+	@Override
+	public View getDropDownView(int position, View convertView,
+			ViewGroup parent) {
+		return getView(position, convertView, parent);
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..d5750019ba58a3137df5a79cc18ddbb119621a9a
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
@@ -0,0 +1,288 @@
+package net.sf.briar.android.groups;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+import java.io.UnsupportedEncodingException;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+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.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalBorder;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.android.BundleEncrypter;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DatabaseExecutor;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.messaging.AuthorId;
+import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.MessageId;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.google.inject.Inject;
+
+public class ReadGroupMessageActivity extends BriarActivity
+implements OnClickListener {
+
+	static final int RESULT_REPLY = RESULT_FIRST_USER;
+	static final int RESULT_PREV = RESULT_FIRST_USER + 1;
+	static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
+
+	private static final Logger LOG =
+			Logger.getLogger(ReadGroupMessageActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private BundleEncrypter bundleEncrypter;
+	@Inject private DatabaseComponent db;
+	@Inject @DatabaseExecutor private Executor dbExecutor;
+
+	private GroupId groupId = null;
+	private MessageId messageId = null;
+	private AuthorId authorId = null;
+	private String authorName = null;
+	private boolean read;
+	private ImageButton readButton = null, prevButton = null, nextButton = null;
+	private ImageButton replyButton = null;
+	private TextView content = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(id == null) throw new IllegalStateException();
+		groupId = new GroupId(id);
+		String groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+		if(groupName == null) throw new IllegalStateException();
+		setTitle(groupName);
+		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);
+		if(!anonymous) {
+			id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
+			if(id == null) throw new IllegalStateException();
+			authorId = new AuthorId(id);
+			authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME");
+			if(authorName == null) throw new IllegalStateException();
+		}
+		String contentType = i.getStringExtra("net.sf.briar.CONTENT_TYPE");
+		if(contentType == null) throw new IllegalStateException();
+		long timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1);
+		if(timestamp == -1) throw new IllegalStateException();
+		boolean first = i.getBooleanExtra("net.sf.briar.FIRST", false);
+		boolean last = i.getBooleanExtra("net.sf.briar.LAST", false);
+
+		if(state != null && bundleEncrypter.decrypt(state)) {
+			read = state.getBoolean("net.sf.briar.READ");
+		} else {
+			read = false;
+			setReadInDatabase(true);
+		}
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		layout.setOrientation(VERTICAL);
+
+		ScrollView scrollView = new ScrollView(this);
+		// Give me all the width and all the unused height
+		scrollView.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
+
+		LinearLayout message = new LinearLayout(this);
+		message.setOrientation(VERTICAL);
+
+		LinearLayout header = new LinearLayout(this);
+		header.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		header.setOrientation(HORIZONTAL);
+		header.setGravity(CENTER_VERTICAL);
+
+		TextView author = new TextView(this);
+		// Give me all the unused width
+		author.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
+		author.setTextSize(18);
+		author.setMaxLines(1);
+		author.setPadding(10, 10, 10, 10);
+		Resources res = getResources();
+		if(authorName == null) {
+			author.setTextColor(res.getColor(R.color.anonymous_author));
+			author.setText(R.string.anonymous);
+		} else {
+			author.setTextColor(res.getColor(R.color.pseudonymous_author));
+			author.setText(authorName);
+		}
+		header.addView(author);
+
+		TextView date = new TextView(this);
+		date.setTextSize(14);
+		date.setPadding(0, 10, 10, 10);
+		long now = System.currentTimeMillis();
+		date.setText(DateUtils.formatSameDayTime(timestamp, now, SHORT, SHORT));
+		header.addView(date);
+		message.addView(header);
+
+		if(contentType.equals("text/plain")) {
+			// Load and display the message body
+			content = new TextView(this);
+			content.setPadding(10, 0, 10, 10);
+			message.addView(content);
+			loadMessageBody();
+		}
+		scrollView.addView(message);
+		layout.addView(scrollView);
+
+		layout.addView(new HorizontalBorder(this));
+
+		LinearLayout footer = new LinearLayout(this);
+		footer.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		footer.setOrientation(HORIZONTAL);
+		footer.setGravity(CENTER);
+
+		readButton = new ImageButton(this);
+		readButton.setBackgroundResource(0);
+		if(read) readButton.setImageResource(R.drawable.content_unread);
+		else readButton.setImageResource(R.drawable.content_read);
+		readButton.setOnClickListener(this);
+		footer.addView(readButton);
+		footer.addView(new HorizontalSpace(this));
+
+		prevButton = new ImageButton(this);
+		prevButton.setBackgroundResource(0);
+		prevButton.setImageResource(R.drawable.navigation_previous_item);
+		prevButton.setOnClickListener(this);
+		prevButton.setEnabled(!first);
+		footer.addView(prevButton);
+		footer.addView(new HorizontalSpace(this));
+
+		nextButton = new ImageButton(this);
+		nextButton.setBackgroundResource(0);
+		nextButton.setImageResource(R.drawable.navigation_next_item);
+		nextButton.setOnClickListener(this);
+		nextButton.setEnabled(!last);
+		footer.addView(nextButton);
+		footer.addView(new HorizontalSpace(this));
+
+		replyButton = new ImageButton(this);
+		replyButton.setBackgroundResource(0);
+		replyButton.setImageResource(R.drawable.social_reply_all);
+		replyButton.setOnClickListener(this);
+		footer.addView(replyButton);
+		layout.addView(footer);
+
+		setContentView(layout);
+
+		// Bind to the service so we can wait for the DB to be opened
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	private void setReadInDatabase(final boolean read) {
+		final DatabaseComponent db = this.db;
+		final MessageId messageId = this.messageId;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					db.setReadFlag(messageId, read);
+					setReadInUi(read);
+				} 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 setReadInUi(final boolean read) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				ReadGroupMessageActivity.this.read = read;
+				if(read) readButton.setImageResource(R.drawable.content_unread);
+				else readButton.setImageResource(R.drawable.content_read);
+			}
+		});
+	}
+
+	private void loadMessageBody() {
+		final DatabaseComponent db = this.db;
+		final MessageId messageId = this.messageId;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					byte[] body = db.getMessageBody(messageId);
+					final String text = new String(body, "UTF-8");
+					runOnUiThread(new Runnable() {
+						public void run() {
+							content.setText(text);
+						}
+					});
+				} 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();
+				} catch(UnsupportedEncodingException e) {
+					throw new RuntimeException(e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void onSaveInstanceState(Bundle state) {
+		state.putBoolean("net.sf.briar.READ", read);
+		bundleEncrypter.encrypt(state);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		if(view == readButton) {
+			setReadInDatabase(!read);
+		} else if(view == prevButton) {
+			setResult(RESULT_PREV);
+			finish();
+		} else if(view == nextButton) {
+			setResult(RESULT_NEXT);
+			finish();
+		} else if(view == replyButton) {
+			Intent i = new Intent(this, WriteGroupMessageActivity.class);
+			i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+			i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
+			startActivity(i);
+			setResult(RESULT_REPLY);
+			finish();
+		}
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a1a93c06f3ac3cb5ad32e72d014c289a2877988
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java
@@ -0,0 +1,217 @@
+package net.sf.briar.android.groups;
+
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+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.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.android.BundleEncrypter;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DatabaseExecutor;
+import net.sf.briar.api.db.DbException;
+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.MessageFactory;
+import net.sf.briar.api.messaging.MessageId;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.google.inject.Inject;
+
+public class WriteGroupMessageActivity extends BriarActivity
+implements OnClickListener, OnItemSelectedListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(WriteGroupMessageActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private BundleEncrypter bundleEncrypter;
+	@Inject private DatabaseComponent db;
+	@Inject @DatabaseExecutor private Executor dbExecutor;
+	@Inject private MessageFactory messageFactory;
+
+	private Group group = null;
+	private GroupId groupId = null;
+	private MessageId parentId = null;
+	private GroupNameSpinnerAdapter adapter = null;
+	private Spinner spinner = null;
+	private ImageButton sendButton = null;
+	private EditText content = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(id != null) groupId = new GroupId(id);
+		id = i.getByteArrayExtra("net.sf.briar.PARENT_ID");
+		if(id != null) parentId = new MessageId(id);
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		layout.setOrientation(VERTICAL);
+
+		LinearLayout actionBar = new LinearLayout(this);
+		actionBar.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		actionBar.setOrientation(HORIZONTAL);
+		actionBar.setGravity(CENTER_VERTICAL);
+
+		TextView to = new TextView(this);
+		to.setTextSize(18);
+		to.setPadding(10, 10, 10, 10);
+		to.setText(R.string.to);
+		actionBar.addView(to);
+
+		adapter = new GroupNameSpinnerAdapter(this);
+		spinner = new Spinner(this);
+		spinner.setAdapter(adapter);
+		spinner.setOnItemSelectedListener(this);
+		loadContactNames();
+		actionBar.addView(spinner);
+
+		actionBar.addView(new HorizontalSpace(this));
+
+		sendButton = new ImageButton(this);
+		sendButton.setBackgroundResource(0);
+		sendButton.setImageResource(R.drawable.social_send_now);
+		sendButton.setEnabled(false);
+		sendButton.setOnClickListener(this);
+		actionBar.addView(sendButton);
+		layout.addView(actionBar);
+
+		content = new EditText(this);
+		content.setPadding(10, 10, 10, 10);
+		if(state != null && bundleEncrypter.decrypt(state)) {
+			Parcelable p = state.getParcelable("net.sf.briar.CONTENT");
+			if(p != null) content.onRestoreInstanceState(p);
+		}
+		layout.addView(content);
+
+		setContentView(layout);
+
+		// Bind to the service so we can wait for the DB to be opened
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	private void loadContactNames() {
+		final DatabaseComponent db = this.db;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					final Collection<Group> groups = db.getSubscriptions();
+					runOnUiThread(new Runnable() {
+						public void run() {
+							for(Group g : groups) {
+								if(g.getId().equals(groupId)) {
+									group = g;
+									spinner.setSelection(adapter.getCount());
+								}
+								adapter.add(g);
+							}
+						}
+					});
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	@Override
+	public void onSaveInstanceState(Bundle state) {
+		Parcelable p = content.onSaveInstanceState();
+		state.putParcelable("net.sf.briar.CONTENT", p);
+		bundleEncrypter.encrypt(state);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		if(group == null) throw new IllegalStateException();
+		try {
+			storeMessage(content.getText().toString().getBytes("UTF-8"));
+		} catch(UnsupportedEncodingException e) {
+			throw new RuntimeException(e);
+		}
+		finish();
+	}
+
+	private void storeMessage(final byte[] body) {
+		final DatabaseComponent db = this.db;
+		final MessageFactory messageFactory = this.messageFactory;
+		final Group group = this.group;
+		final MessageId parentId = this.parentId;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					Message m = messageFactory.createAnonymousMessage(parentId,
+							group, "text/plain", body);
+					db.addLocalGroupMessage(m);
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(GeneralSecurityException e) {
+					throw new RuntimeException(e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				} catch(IOException e) {
+					throw new RuntimeException(e);
+				}
+			}
+		});
+	}
+
+	public void onItemSelected(AdapterView<?> parent, View view, int position,
+			long id) {
+		group = adapter.getItem(position);
+		groupId = group.getId();
+		sendButton.setEnabled(true);
+	}
+
+	public void onNothingSelected(AdapterView<?> parent) {
+		group = null;
+		groupId = null;
+		sendButton.setEnabled(false);
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java b/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java
index 90be870c03b0b52cfc1695aee2659c5cfcd96ab8..9daf3287f5e1c0b85eafbee965442a2f57569c4f 100644
--- a/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java
+++ b/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java
@@ -4,7 +4,6 @@ import static android.view.Gravity.CENTER;
 import static android.view.Gravity.CENTER_HORIZONTAL;
 import net.sf.briar.R;
 import android.content.Context;
-import android.content.res.Resources;
 import android.widget.LinearLayout;
 import android.widget.ProgressBar;
 import android.widget.TextView;
@@ -45,9 +44,9 @@ public class ConnectionView extends AddContactView {
 			innerLayout.addView(progress);
 
 			TextView connecting = new TextView(ctx);
-			Resources res = getResources();
-			String connectingVia = res.getString(R.string.connecting_wifi);
-			connecting.setText(String.format(connectingVia, networkName));
+			String format = getResources().getString(
+					R.string.format_connecting_wifi);
+			connecting.setText(String.format(format, networkName));
 			innerLayout.addView(connecting);
 
 			addView(innerLayout);
diff --git a/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java b/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java
index 1305acc56607cb4db88a4d198bc179099f1ffcef..8cb63950fe9dbcc84268aabeb8768fb7512cd602 100644
--- a/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java
+++ b/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java
@@ -7,7 +7,6 @@ import net.sf.briar.R;
 import net.sf.briar.android.widgets.CommonLayoutParams;
 import android.content.Context;
 import android.content.Intent;
-import android.content.res.Resources;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 import android.view.View;
@@ -70,9 +69,9 @@ public class WifiWidget extends LinearLayout implements OnClickListener {
 				ok.setImageResource(R.drawable.navigation_accept);
 				ok.setPadding(10, 10, 10, 10);
 				addView(ok);
-				Resources res = getResources();
-				String connected = res.getString(R.string.wifi_connected);
-				status.setText(String.format(connected, networkName));
+				String format = getResources().getString(
+						R.string.format_wifi_connected);
+				status.setText(String.format(format, networkName));
 				addView(status);
 				ImageButton settings = new ImageButton(ctx);
 				settings.setImageResource(R.drawable.action_settings);
diff --git a/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java b/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java
index 168d7aa81ee78d694e55999c3cd80df44597b5a9..18c4d00e7da665af939054442d5b261cd63906b7 100644
--- a/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java
+++ b/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java
@@ -22,6 +22,7 @@ implements SpinnerAdapter {
 	public View getView(int position, View convertView, ViewGroup parent) {
 		TextView name = new TextView(getContext());
 		name.setTextSize(18);
+		name.setMaxLines(1);
 		name.setPadding(10, 10, 10, 10);
 		name.setText(getItem(position).getName());
 		return name;
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 60d3869012118d56ea94b15e01cb3791dbb598b5..b382e920ee2553ed4477be64aab9c25065a2ef3f 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
@@ -67,6 +67,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		contactId = new ContactId(id);
 		contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME");
 		if(contactName == null) throw new IllegalStateException();
+		setTitle(contactName);
 
 		LinearLayout layout = new LinearLayout(this);
 		layout.setLayoutParams(CommonLayoutParams.MATCH_MATCH);
@@ -104,23 +105,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		reloadMessageHeaders();
 	}
 
-	@Override
-	public void onDestroy() {
-		super.onDestroy();
-		db.removeListener(this);
-		unbindService(serviceConnection);
-	}
-
-	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof MessageAddedEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
-			reloadMessageHeaders();
-		} else if(e instanceof MessageExpiredEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			reloadMessageHeaders();
-		}
-	}
-
 	private void reloadMessageHeaders() {
 		final DatabaseComponent db = this.db;
 		final ContactId contactId = this.contactId;
@@ -168,8 +152,38 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		});
 	}
 
+	@Override
+	public void onActivityResult(int request, int result, Intent data) {
+		if(result == ReadPrivateMessageActivity.RESULT_PREV) {
+			int position = request - 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		} else if(result == ReadPrivateMessageActivity.RESULT_NEXT) {
+			int position = request + 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		}
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		db.removeListener(this);
+		unbindService(serviceConnection);
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof MessageAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+			reloadMessageHeaders();
+		} else if(e instanceof MessageExpiredEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
+			reloadMessageHeaders();
+		}
+	}
+
 	public void onClick(View view) {
-		Intent i = new Intent(this, WriteMessageActivity.class);
+		Intent i = new Intent(this, WritePrivateMessageActivity.class);
 		i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
 		startActivity(i);
 	}
@@ -181,28 +195,15 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 
 	private void showMessage(int position) {
 		PrivateMessageHeader item = adapter.getItem(position);
-		Intent i = new Intent(this, ReadMessageActivity.class);
+		Intent i = new Intent(this, ReadPrivateMessageActivity.class);
 		i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
 		i.putExtra("net.sf.briar.CONTACT_NAME", contactName);
 		i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
 		i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType());
 		i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp());
+		i.putExtra("net.sf.briar.INCOMING", item.isIncoming());
 		i.putExtra("net.sf.briar.FIRST", position == 0);
 		i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1);
-		i.putExtra("net.sf.briar.STARRED", item.isStarred());
 		startActivityForResult(i, position);
 	}
-
-	@Override
-	public void onActivityResult(int request, int result, Intent data) {
-		if(result == ReadMessageActivity.RESULT_PREV) {
-			int position = request - 1;
-			if(position >= 0 && position < adapter.getCount())
-				showMessage(position);
-		} else if(result == ReadMessageActivity.RESULT_NEXT) {
-			int position = request + 1;
-			if(position >= 0 && position < adapter.getCount())
-				showMessage(position);
-		}
-	}
 }
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
index 056935ad82664426016e213e9636c8ed0863245e..40e9626dbc1ad1fc3033b1877d55c894cc90b842 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
@@ -9,6 +9,7 @@ import java.util.ArrayList;
 
 import net.sf.briar.R;
 import net.sf.briar.android.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalSpace;
 import net.sf.briar.api.db.PrivateMessageHeader;
 import android.content.Context;
 import android.text.format.DateUtils;
@@ -34,23 +35,24 @@ class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> {
 		layout.setOrientation(HORIZONTAL);
 		layout.setGravity(CENTER_VERTICAL);
 
-		if(!item.getContentType().equals("text/plain")) {
+		if(item.getContentType().equals("text/plain")) {
+			TextView subject = new TextView(ctx);
+			// Give me all the unused width
+			subject.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
+			subject.setTextSize(14);
+			subject.setMaxLines(2);
+			subject.setPadding(10, 10, 10, 10);
+			if(!item.isRead()) subject.setTypeface(null, BOLD);
+			subject.setText(item.getSubject());
+			layout.addView(subject);
+		} else {
 			ImageView attachment = new ImageView(ctx);
 			attachment.setPadding(10, 10, 10, 10);
 			attachment.setImageResource(R.drawable.content_attachment);
 			layout.addView(attachment);
+			layout.addView(new HorizontalSpace(ctx));
 		}
 
-		TextView subject = new TextView(ctx);
-		// Give me all the unused width
-		subject.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
-		subject.setTextSize(14);
-		subject.setMaxLines(2);
-		subject.setPadding(10, 10, 10, 10);
-		if(!item.isRead()) subject.setTypeface(null, BOLD);
-		subject.setText(item.getSubject());
-		layout.addView(subject);
-
 		TextView date = new TextView(ctx);
 		date.setTextSize(14);
 		date.setPadding(0, 10, 10, 10);
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 561805748359773da5c4469758f759255503f290..5eafcf3367303550aabe2763b095eb8e5155d700 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
@@ -238,7 +238,7 @@ implements OnClickListener, DatabaseListener {
 	}
 
 	public void onClick(View view) {
-		startActivity(new Intent(this, WriteMessageActivity.class));
+		startActivity(new Intent(this, WritePrivateMessageActivity.class));
 	}
 
 	public void eventOccurred(DatabaseEvent e) {
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
index 542a14d52e173b8c7d4b8fe7d97916167dca44b8..19066131bc3c28a1807feaef81941492cbf6f19a 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
@@ -46,10 +46,12 @@ implements OnItemClickListener {
 
 		TextView name = new TextView(ctx);
 		name.setTextSize(18);
+		name.setMaxLines(1);
 		name.setPadding(10, 10, 10, 10);
 		int unread = item.getUnreadCount();
-		if(unread > 0) name.setText(item.getName() + " (" + unread + ")");
-		else name.setText(item.getName());
+		String contactName = item.getContactName();
+		if(unread > 0) name.setText(contactName + " (" + unread + ")");
+		else name.setText(contactName);
 		innerLayout.addView(name);
 
 		if(!StringUtils.isNullOrEmpty(item.getSubject())) {
@@ -78,7 +80,7 @@ implements OnItemClickListener {
 		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.getName());
+		i.putExtra("net.sf.briar.CONTACT_NAME", item.getContactName());
 		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
index b168f7a9bce3f5b0a494efefc04185dfdee5b473..eec30a91c80947d7cd4142db16e009f7c4c50991 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
@@ -30,7 +30,7 @@ class ConversationListItem {
 		return contact.getId();
 	}
 
-	String getName() {
+	String getContactName() {
 		return contact.getName();
 	}
 
diff --git a/briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
similarity index 90%
rename from briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java
rename to briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
index 04185e78f8bc985dd141960f6c822fb3cbc2ae36..afe0944578c549bc4b61bba270f150c8c6db5a6b 100644
--- a/briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
@@ -37,7 +37,7 @@ import android.widget.TextView;
 
 import com.google.inject.Inject;
 
-public class ReadMessageActivity extends BriarActivity
+public class ReadPrivateMessageActivity extends BriarActivity
 implements OnClickListener {
 
 	static final int RESULT_REPLY = RESULT_FIRST_USER;
@@ -45,7 +45,7 @@ implements OnClickListener {
 	static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
 
 	private static final Logger LOG =
-			Logger.getLogger(ReadMessageActivity.class.getName());
+			Logger.getLogger(ReadPrivateMessageActivity.class.getName());
 
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
@@ -55,11 +55,9 @@ implements OnClickListener {
 	@Inject @DatabaseExecutor private Executor dbExecutor;
 
 	private ContactId contactId = null;
-	private String contactName = null;
 	private MessageId messageId = null;
-	private boolean first, last, starred, read;
-	private ImageButton readButton = null;
-	private ImageButton prevButton = null, nextButton = null;
+	private boolean read;
+	private ImageButton readButton = null, prevButton = null, nextButton = null;
 	private ImageButton replyButton = null;
 	private TextView content = null;
 
@@ -71,8 +69,9 @@ implements OnClickListener {
 		int cid = i.getIntExtra("net.sf.briar.CONTACT_ID", -1);
 		if(cid == -1) throw new IllegalStateException();
 		contactId = new ContactId(cid);
-		contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME");
+		String contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME");
 		if(contactName == null) throw new IllegalStateException();
+		setTitle(contactName);
 		byte[] mid = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID");
 		if(mid == null) throw new IllegalStateException();
 		messageId = new MessageId(mid);
@@ -80,14 +79,13 @@ implements OnClickListener {
 		if(contentType == null) throw new IllegalStateException();
 		long timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1);
 		if(timestamp == -1) throw new IllegalStateException();
-		first = i.getBooleanExtra("net.sf.briar.FIRST", false);
-		last = i.getBooleanExtra("net.sf.briar.LAST", false);
+		boolean incoming = i.getBooleanExtra("net.sf.briar.INCOMING", false);
+		boolean first = i.getBooleanExtra("net.sf.briar.FIRST", false);
+		boolean last = i.getBooleanExtra("net.sf.briar.LAST", false);
 
 		if(state != null && bundleEncrypter.decrypt(state)) {
-			starred = state.getBoolean("net.sf.briar.STARRED");
 			read = state.getBoolean("net.sf.briar.READ");
 		} else {
-			starred = i.getBooleanExtra("net.sf.briar.STARRED", false);
 			read = false;
 			setReadInDatabase(true);
 		}
@@ -112,8 +110,11 @@ implements OnClickListener {
 		// Give me all the unused width
 		name.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
 		name.setTextSize(18);
+		name.setMaxLines(1);
 		name.setPadding(10, 10, 10, 10);
-		String format = getResources().getString(R.string.message_from);
+		String format;
+		if(incoming) format = getResources().getString(R.string.format_from);
+		else format = getResources().getString(R.string.format_to);
 		name.setText(String.format(format, contactName));
 		header.addView(name);
 
@@ -204,7 +205,7 @@ implements OnClickListener {
 	private void setReadInUi(final boolean read) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				ReadMessageActivity.this.read = read;
+				ReadPrivateMessageActivity.this.read = read;
 				if(read) readButton.setImageResource(R.drawable.content_unread);
 				else readButton.setImageResource(R.drawable.content_read);
 			}
@@ -241,7 +242,6 @@ implements OnClickListener {
 
 	@Override
 	public void onSaveInstanceState(Bundle state) {
-		state.putBoolean("net.sf.briar.STARRED", starred);
 		state.putBoolean("net.sf.briar.READ", read);
 		bundleEncrypter.encrypt(state);
 	}
@@ -262,7 +262,7 @@ implements OnClickListener {
 			setResult(RESULT_NEXT);
 			finish();
 		} else if(view == replyButton) {
-			Intent i = new Intent(this, WriteMessageActivity.class);
+			Intent i = new Intent(this, WritePrivateMessageActivity.class);
 			i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
 			i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
 			startActivity(i);
diff --git a/briar-android/src/net/sf/briar/android/messages/WriteMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
similarity index 89%
rename from briar-android/src/net/sf/briar/android/messages/WriteMessageActivity.java
rename to briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
index 4673d8180e8b3d7d76ab6d3fe85f684d311b5bed..ced62326ac5f737bc66264c187b63a5f85d0ed69 100644
--- a/briar-android/src/net/sf/briar/android/messages/WriteMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
@@ -7,6 +7,7 @@ import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.security.GeneralSecurityException;
 import java.util.Collection;
 import java.util.concurrent.Executor;
@@ -42,11 +43,11 @@ import android.widget.TextView;
 
 import com.google.inject.Inject;
 
-public class WriteMessageActivity extends BriarActivity
+public class WritePrivateMessageActivity extends BriarActivity
 implements OnClickListener, OnItemSelectedListener {
 
 	private static final Logger LOG =
-			Logger.getLogger(WriteMessageActivity.class.getName());
+			Logger.getLogger(WritePrivateMessageActivity.class.getName());
 
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
@@ -85,7 +86,7 @@ implements OnClickListener, OnItemSelectedListener {
 		TextView to = new TextView(this);
 		to.setTextSize(18);
 		to.setPadding(10, 10, 10, 10);
-		to.setText(R.string.message_to);
+		to.setText(R.string.to);
 		actionBar.addView(to);
 
 		adapter = new ContactNameSpinnerAdapter(this);
@@ -163,32 +164,36 @@ implements OnClickListener, OnItemSelectedListener {
 	public void onClick(View view) {
 		if(contactId == null) throw new IllegalStateException();
 		try {
-			byte[] body = content.getText().toString().getBytes("UTF-8");
-			storeMessage(messageFactory.createPrivateMessage(parentId,
-					"text/plain", body));
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		} catch(GeneralSecurityException e) {
+			storeMessage(content.getText().toString().getBytes("UTF-8"));
+		} catch(UnsupportedEncodingException e) {
 			throw new RuntimeException(e);
 		}
 		finish();
 	}
 
-	private void storeMessage(final Message m) {
+	private void storeMessage(final byte[] body) {
 		final DatabaseComponent db = this.db;
+		final MessageFactory messageFactory = this.messageFactory;
 		final ContactId contactId = this.contactId;
+		final MessageId parentId = this.parentId;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					serviceConnection.waitForStartup();
+					Message m = messageFactory.createPrivateMessage(parentId,
+							"text/plain", body);
 					db.addLocalPrivateMessage(m, contactId);
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
+				} catch(GeneralSecurityException e) {
+					throw new RuntimeException(e);
 				} catch(InterruptedException e) {
 					if(LOG.isLoggable(INFO))
 						LOG.info("Interrupted while waiting for service");
 					Thread.currentThread().interrupt();
+				} catch(IOException e) {
+					throw new RuntimeException(e);
 				}
 			}
 		});
diff --git a/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java b/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java
index 958092f40c729faf84cf64cfab28485512fd47ea..c708f335ca1753e51a29f66148d9580eecb037c2 100644
--- a/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java
+++ b/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java
@@ -13,6 +13,6 @@ public class HorizontalBorder extends View {
 	public HorizontalBorder(Context ctx) {
 		super(ctx);
 		setLayoutParams(new LayoutParams(MATCH_PARENT, LINE_WIDTH));
-		setBackgroundColor(getResources().getColor(R.color.HorizontalBorder));
+		setBackgroundColor(getResources().getColor(R.color.horizontal_border));
 	}
 }