diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index c2bedce0776881464f6b5e45fc3fb0f46683a398..2f5e0d87b53bc775d9c0cb23bd9fdc4491a0da2b 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -52,17 +52,29 @@
 			android:name=".android.blogs.BlogListActivity"
 			android:label="@string/blogs_title" >
 		</activity>
+		<activity
+			android:name=".android.blogs.ConfigureBlogActivity"
+			android:label="@string/app_name" >
+		</activity>
 		<activity
 			android:name=".android.blogs.CreateBlogActivity"
 			android:label="@string/create_blog_title" >
 		</activity>
+		<activity
+		    android:name=".android.blogs.ManageBlogsActivity"
+		    android:label="@string/manage_subscriptions_title" >
+		</activity>
 		<activity
 			android:name=".android.blogs.ReadBlogPostActivity"
 			android:label="@string/app_name" >
 		</activity>
 		<activity
 			android:name=".android.blogs.WriteBlogPostActivity"
-			android:label="@string/compose_blog_title" >
+			android:label="@string/new_post_title" >
+		</activity>
+		<activity
+			android:name=".android.groups.ConfigureGroupActivity"
+			android:label="@string/app_name" >
 		</activity>
 		<activity
 			android:name=".android.groups.CreateGroupActivity"
@@ -77,12 +89,16 @@
 			android:label="@string/groups_title" >
 		</activity>
 		<activity
+		    android:name=".android.groups.ManageGroupsActivity"
+		    android:label="@string/manage_subscriptions_title" >
+		</activity>
+				<activity
 			android:name=".android.groups.ReadGroupPostActivity"
 			android:label="@string/app_name" >
 		</activity>
 		<activity
 			android:name=".android.groups.WriteGroupPostActivity"
-			android:label="@string/compose_group_title" >
+			android:label="@string/new_post_title" >
 		</activity>
 		<activity
 			android:name=".android.identity.CreateIdentityActivity"
@@ -106,7 +122,7 @@
 		</activity>
 		<activity
 			android:name=".android.messages.WritePrivateMessageActivity"
-			android:label="@string/compose_message_title" >
+			android:label="@string/new_message_title" >
 		</activity>
 	</application>
 </manifest>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 8219599e76de09e2b5cf308bfcec0a3d327710e2..632d4b8f38f2bbb0170934df54cfd9168a1d76e9 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -9,7 +9,7 @@
 	<string name="confirm_password">Confirm your password:</string>
 	<string name="format_min_password">Password must be at least %1$d characters long.</string>
 	<string name="enter_password">Enter your password:</string>
-	<string name="try_again">Wrong password, try again</string>
+	<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>
@@ -50,28 +50,40 @@
 	<string name="messages_title">Messages</string>
 	<string name="no_messages">(No messages)</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="new_message_title">New Message</string>
 	<string name="from">From:</string>
 	<string name="to">To:</string>
 	<string name="anonymous">Anonymous</string>
 	<string name="new_contact_item">New contact\u2026</string>
 	<string name="groups_title">Groups</string>
-	<string name="no_posts">(No posts)</string>
+	<plurals name="groups_available">
+	    <item quantity="one">%1$d group available from contacts</item>
+	    <item quantity="two">$1$d groups available from contacts</item>
+	</plurals>
+	<string name="no_posts">No posts</string>
+	<string name="subscribe_to_this_group">Subscribe to this group</string>
 	<string name="create_group_title">New Group</string>
 	<string name="choose_group_name">Choose a name for your group:</string>
 	<string name="group_visible_to_all">Share this group with all contacts</string>
 	<string name="group_visible_to_some">Share this group with chosen contacts</string>
-	<string name="compose_group_title">New Post</string>
+	<string name="new_post_title">New Post</string>
 	<string name="new_group_item">New group\u2026</string>
 	<string name="blogs_title">Blogs</string>
+	<plurals name="blogs_available">
+	    <item quantity="one">%1$d blog available from contacts</item>
+	    <item quantity="two">$1$d blogs available from contacts</item>
+	</plurals>
+	<string name="manage_subscriptions_title">Manage Subscriptions</string>
+	<string name="subscribed_all">Subscribed, shared with all contacts</string>
+	<string name="subscribed_some">Subscribed, shared with chosen contacts</string>
+	<string name="not_subscribed">Not subscribed</string>
+	<string name="subscribe_to_this_blog">Subscribe to this blog</string>
 	<string name="create_blog_title">New Blog</string>
 	<string name="choose_blog_name">Choose a name for your blog:</string>
 	<string name="blog_visible_to_all">Share this blog with all contacts</string>
 	<string name="blog_visible_to_some">Share this blog with chosen contacts</string>
 	<string name="not_your_blog">Only the creator of this blog can write posts</string>
 	<string name="ok_button">OK</string>
-	<string name="compose_blog_title">New Post</string>
 	<string name="new_blog_item">New blog\u2026</string>
 	<string name="create_nickname_item">New nickname\u2026</string>
 	<string name="create_identity_title">Create an Identity</string>
diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
index 65f916118249ff68e9ea760896012af3b0e88d75..443e3b57c64c0af510a9621fb49d070de74209f2 100644
--- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
+++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
@@ -74,7 +74,7 @@ public class HomeScreenActivity extends BriarActivity {
 	@Inject @DatabaseUiExecutor private Executor dbUiExecutor = null;
 	@Inject @CryptoExecutor private Executor cryptoExecutor = null;
 	private boolean bound = false;
-	private TextView tryAgain = null;
+	private TextView enterPassword = null;
 	private Button continueButton = null;
 	private ProgressBar progress = null;
 
@@ -194,7 +194,7 @@ public class HomeScreenActivity extends BriarActivity {
 		layout.setOrientation(VERTICAL);
 		layout.setGravity(CENTER_HORIZONTAL);
 
-		TextView enterPassword = new TextView(this);
+		enterPassword = new TextView(this);
 		enterPassword.setGravity(CENTER);
 		enterPassword.setTextSize(18);
 		enterPassword.setPadding(10, 10, 10, 10);
@@ -214,14 +214,6 @@ public class HomeScreenActivity extends BriarActivity {
 		});
 		layout.addView(passwordEntry);
 
-		tryAgain = new TextView(this);
-		tryAgain.setGravity(CENTER);
-		tryAgain.setTextSize(14);
-		tryAgain.setPadding(10, 10, 10, 10);
-		tryAgain.setText(R.string.try_again);
-		tryAgain.setVisibility(GONE);
-		layout.addView(tryAgain);
-
 		continueButton = new Button(this);
 		continueButton.setLayoutParams(WRAP_WRAP);
 		continueButton.setText(R.string.continue_button);
@@ -241,7 +233,7 @@ public class HomeScreenActivity extends BriarActivity {
 	}
 
 	private void validatePassword(final byte[] encrypted, Editable e) {
-		if(tryAgain == null || continueButton == null || progress == null)
+		if(enterPassword == null || continueButton == null || progress == null)
 			return;
 		// Hide the soft keyboard
 		Object o = getSystemService(INPUT_METHOD_SERVICE);
@@ -270,7 +262,7 @@ public class HomeScreenActivity extends BriarActivity {
 	private void tryAgain() {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				tryAgain.setVisibility(VISIBLE);
+				enterPassword.setText(R.string.try_again);
 				continueButton.setVisibility(VISIBLE);
 				progress.setVisibility(GONE);
 			}
diff --git a/briar-android/src/net/sf/briar/android/ManageGroupsAdapter.java b/briar-android/src/net/sf/briar/android/ManageGroupsAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..cddca6333345a8a5c1a9fcd13dc0005893096066
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/ManageGroupsAdapter.java
@@ -0,0 +1,66 @@
+package net.sf.briar.android;
+
+import static android.view.View.INVISIBLE;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+
+import java.util.ArrayList;
+
+import net.sf.briar.R;
+import net.sf.briar.api.messaging.GroupStatus;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+public class ManageGroupsAdapter extends ArrayAdapter<GroupStatus>
+implements ListAdapter {
+
+	public ManageGroupsAdapter(Context ctx) {
+		super(ctx, android.R.layout.simple_expandable_list_item_1,
+				new ArrayList<GroupStatus>());
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		final GroupStatus item = getItem(position);
+		Context ctx = getContext();
+
+		LinearLayout layout = new LinearLayout(ctx);
+		layout.setOrientation(HORIZONTAL);
+
+		ImageView subscribed = new ImageView(ctx);
+		subscribed.setPadding(5, 5, 5, 5);
+		subscribed.setImageResource(R.drawable.navigation_accept);
+		if(!item.isSubscribed()) subscribed.setVisibility(INVISIBLE);
+		layout.addView(subscribed);
+
+		LinearLayout innerLayout = new LinearLayout(ctx);
+		innerLayout.setOrientation(VERTICAL);
+
+		TextView name = new TextView(ctx);
+		name.setTextSize(18);
+		name.setMaxLines(1);
+		name.setPadding(0, 10, 10, 10);
+		name.setText(item.getGroup().getName());
+		innerLayout.addView(name);
+
+		TextView status = new TextView(ctx);
+		status.setTextSize(14);
+		status.setPadding(0, 0, 10, 10);
+		if(item.isSubscribed()) {
+			if(item.isVisibleToAll()) status.setText(R.string.subscribed_all);
+			else status.setText(R.string.subscribed_some);
+		} else {
+			status.setText(R.string.not_subscribed);
+		}
+		innerLayout.addView(status);
+		layout.addView(innerLayout);
+
+		return layout;
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
index 2beedf041873bbaa322e65980ed95b7cb170bfc3..c7a8f1d0c6a912db32a3b6c25deaf338c22e4f81 100644
--- a/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
@@ -6,6 +6,7 @@ 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 static net.sf.briar.android.blogs.BlogListItem.MANAGE;
 import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
 import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP;
 import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
@@ -32,13 +33,18 @@ import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.GroupMessageAddedEvent;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent;
+import net.sf.briar.api.db.event.SubscriptionAddedEvent;
 import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.GroupStatus;
 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;
@@ -46,7 +52,8 @@ import android.widget.ListView;
 import com.google.inject.Inject;
 
 public class BlogListActivity extends BriarFragmentActivity
-implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
+implements DatabaseListener, OnClickListener, NoBlogsDialog.Listener,
+OnItemClickListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(BlogListActivity.class.getName());
@@ -57,6 +64,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 	private BlogListAdapter adapter = null;
 	private ListView list = null;
 	private ImageButton newBlogButton = null, composeButton = null;
+	private ImageButton manageBlogsButton = null;
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject private volatile DatabaseComponent db;
@@ -75,7 +83,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 		// Give me all the width and all the unused height
 		list.setLayoutParams(MATCH_WRAP_1);
 		list.setAdapter(adapter);
-		list.setOnItemClickListener(adapter);
+		list.setOnItemClickListener(this);
 		layout.addView(list);
 
 		layout.addView(new HorizontalBorder(this));
@@ -99,6 +107,13 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 		composeButton.setOnClickListener(this);
 		footer.addView(composeButton);
 		footer.addView(new HorizontalSpace(this));
+
+		manageBlogsButton = new ImageButton(this);
+		manageBlogsButton.setBackgroundResource(0);
+		manageBlogsButton.setImageResource(R.drawable.action_settings);
+		manageBlogsButton.setOnClickListener(this);
+		footer.addView(manageBlogsButton);
+		footer.addView(new HorizontalSpace(this));
 		layout.addView(footer);
 
 		setContentView(layout);
@@ -124,21 +139,28 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 					long now = System.currentTimeMillis();
 					Set<GroupId> local = new HashSet<GroupId>();
 					for(Group g : db.getLocalGroups()) local.add(g.getId());
-					for(Group g : db.getSubscriptions()) {
+					int available = 0;
+					for(GroupStatus s : db.getAvailableGroups()) {
+						Group g = s.getGroup();
 						if(!g.isRestricted()) continue;
-						boolean postable = local.contains(g.getId());
-						try {
-							Collection<GroupMessageHeader> headers =
-									db.getGroupMessageHeaders(g.getId());
-							displayHeaders(g, postable, headers);
-						} catch(NoSuchSubscriptionException e) {
-							if(LOG.isLoggable(INFO))
-								LOG.info("Subscription removed");
+						if(s.isSubscribed()) {
+							boolean postable = local.contains(g.getId());
+							try {
+								Collection<GroupMessageHeader> headers =
+										db.getGroupMessageHeaders(g.getId());
+								displayHeaders(g, postable, headers);
+							} catch(NoSuchSubscriptionException e) {
+								if(LOG.isLoggable(INFO))
+									LOG.info("Subscription removed");
+							}
+						} else {
+							available++;
 						}
 					}
 					long duration = System.currentTimeMillis() - now;
 					if(LOG.isLoggable(INFO))
 						LOG.info("Full load took " + duration + " ms");
+					displayAvailable(available);
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -155,6 +177,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				adapter.clear();
+				adapter.notifyDataSetChanged();
 			}
 		});
 	}
@@ -168,18 +191,28 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 				if(item != null) adapter.remove(item);
 				// Add a new item
 				adapter.add(new BlogListItem(g, postable, headers));
-				adapter.sort(GroupComparator.INSTANCE);
+				adapter.sort(ItemComparator.INSTANCE);
 				adapter.notifyDataSetChanged();
 				selectFirstUnread();
 			} 
 		});
 	}
 
+	private void displayAvailable(final int available) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				adapter.setAvailable(available);
+				adapter.notifyDataSetChanged();
+			}
+		});
+	}
+
 	private BlogListItem findGroup(GroupId g) {
 		int count = adapter.getCount();
 		for(int i = 0; i < count; i++) {
 			BlogListItem item = adapter.getItem(i);
-			if(item.getGroupId().equals(g)) return item;
+			if(item == MANAGE) continue;
+			if(item.getGroup().getId().equals(g)) return item;
 		}
 		return null; // Not found
 	}
@@ -187,7 +220,9 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 	private void selectFirstUnread() {
 		int firstUnread = -1, count = adapter.getCount();
 		for(int i = 0; i < count; i++) {
-			if(adapter.getItem(i).getUnreadCount() > 0) {
+			BlogListItem item = adapter.getItem(i);
+			if(item == MANAGE) continue;
+			if(item.getUnreadCount() > 0) {
 				firstUnread = i;
 				break;
 			}
@@ -208,27 +243,6 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 		unbindService(serviceConnection);
 	}
 
-	public void onClick(View view) {
-		if(view == newBlogButton) {
-			startActivity(new Intent(this, CreateBlogActivity.class));
-		} else if(view == composeButton) {
-			if(countPostableGroups() == 0) {
-				NoBlogsDialog dialog = new NoBlogsDialog();
-				dialog.setListener(this);
-				dialog.show(getSupportFragmentManager(), "NoBlogsDialog");
-			} else {
-				startActivity(new Intent(this, WriteBlogPostActivity.class));
-			}
-		}
-	}
-
-	private int countPostableGroups() {
-		int postable = 0, count = adapter.getCount();
-		for(int i = 0; i < count; i++)
-			if(adapter.getItem(i).isPostable()) postable++;
-		return postable;
-	}
-
 	public void eventOccurred(DatabaseEvent e) {
 		if(e instanceof GroupMessageAddedEvent) {
 			Group g = ((GroupMessageAddedEvent) e).getGroup();
@@ -239,6 +253,16 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 		} else if(e instanceof MessageExpiredEvent) {
 			if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
 			loadHeaders();
+		} else if(e instanceof RemoteSubscriptionsUpdatedEvent) {
+			if(LOG.isLoggable(INFO))
+				LOG.info("Remote subscriptions changed, reloading");
+			loadAvailable();
+		} else if(e instanceof SubscriptionAddedEvent) {
+			Group g = ((SubscriptionAddedEvent) e).getGroup();
+			if(g.isRestricted()) {
+				if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
+				loadHeaders();
+			}
 		} else if(e instanceof SubscriptionRemovedEvent) {
 			Group g = ((SubscriptionRemovedEvent) e).getGroup();
 			if(g.isRestricted()) {
@@ -290,23 +314,96 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
 		});
 	}
 
+	private void loadAvailable() {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					int available = 0;
+					long now = System.currentTimeMillis();
+					for(GroupStatus s : db.getAvailableGroups()) {
+						if(s.getGroup().isRestricted() && !s.isSubscribed())
+							available++;
+					}
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Loading available took " + duration + " ms");
+					displayAvailable(available);
+				} 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();
+				}
+			}
+		});
+	}
+
+	public void onClick(View view) {
+		if(view == newBlogButton) {
+			startActivity(new Intent(this, CreateBlogActivity.class));
+		} else if(view == composeButton) {
+			if(countPostableGroups() == 0) {
+				NoBlogsDialog dialog = new NoBlogsDialog();
+				dialog.setListener(this);
+				dialog.show(getSupportFragmentManager(), "NoBlogsDialog");
+			} else {
+				startActivity(new Intent(this, WriteBlogPostActivity.class));
+			}
+		} else if(view == manageBlogsButton) {
+			startActivity(new Intent(this, ManageBlogsActivity.class));
+		}
+	}
+
+	private int countPostableGroups() {
+		int postable = 0, count = adapter.getCount();
+		for(int i = 0; i < count; i++) {
+			BlogListItem item = adapter.getItem(i);
+			if(item == MANAGE) continue;
+			if(item.isPostable()) postable++;
+		}
+		return postable;
+	}
+
 	public void blogCreationSelected() {
 		startActivity(new Intent(this, CreateBlogActivity.class));
 	}
 
 	public void blogCreationCancelled() {}
 
-	private static class GroupComparator implements Comparator<BlogListItem> {
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		BlogListItem item = adapter.getItem(position);
+		if(item == MANAGE) {
+			startActivity(new Intent(this, ManageBlogsActivity.class));
+		} else {
+			Intent i = new Intent(this, BlogActivity.class);
+			i.putExtra("net.sf.briar.GROUP_ID",
+					item.getGroup().getId().getBytes());
+			i.putExtra("net.sf.briar.GROUP_NAME", item.getGroup().getName());
+			i.putExtra("net.sf.briar.POSTABLE", item.isPostable());
+			startActivity(i);
+		}
+	}
+
+	private static class ItemComparator implements Comparator<BlogListItem> {
 
-		private static final GroupComparator INSTANCE = new GroupComparator();
+		private static final ItemComparator INSTANCE = new ItemComparator();
 
 		public int compare(BlogListItem a, BlogListItem b) {
+			if(a == b) return 0;
+			// The manage blogs item comes last
+			if(a == MANAGE) return 1;
+			if(b == MANAGE) return -1;
 			// 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 group name
-			String aName = a.getGroupName(), bName = b.getGroupName();
+			String aName = a.getGroup().getName();
+			String bName = b.getGroup().getName();
 			return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
 		}
 	}
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java b/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java
index 2ec0fec77511101249f34d8fbbbdf8b6bc8781e5..4721572612c94d4b23174e7f2c9cda9788db4aae 100644
--- a/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java
@@ -1,42 +1,79 @@
 package net.sf.briar.android.blogs;
 
 import static android.graphics.Typeface.BOLD;
+import static android.view.Gravity.CENTER;
 import static android.widget.LinearLayout.HORIZONTAL;
 import static android.widget.LinearLayout.VERTICAL;
 import static java.text.DateFormat.SHORT;
+import static net.sf.briar.android.blogs.BlogListItem.MANAGE;
 import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 
 import net.sf.briar.R;
 import net.sf.briar.android.widgets.HorizontalSpace;
 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.BaseAdapter;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
-class BlogListAdapter extends ArrayAdapter<BlogListItem>
-implements OnItemClickListener {
+class BlogListAdapter extends BaseAdapter {
+
+	private final Context ctx;
+	private final List<BlogListItem> list = new ArrayList<BlogListItem>();
+	private int available = 0;
 
 	BlogListAdapter(Context ctx) {
-		super(ctx, android.R.layout.simple_expandable_list_item_1,
-				new ArrayList<BlogListItem>());
+		this.ctx = ctx;
+	}
+
+	public void setAvailable(int available) {
+		this.available = available;
+	}
+
+	public void add(BlogListItem item) {
+		list.add(item);
+	}
+
+	public void clear() {
+		list.clear();
+	}
+
+	public int getCount() {
+		return available == 0 ? list.size() : list.size() + 1;
+	}
+
+	public BlogListItem getItem(int position) {
+		return position == list.size() ? MANAGE : list.get(position);
+	}
+
+	public long getItemId(int position) {
+		return android.R.layout.simple_expandable_list_item_1;
 	}
 
-	@Override
 	public View getView(int position, View convertView, ViewGroup parent) {
 		BlogListItem item = getItem(position);
-		Context ctx = getContext();
 		Resources res = ctx.getResources();
 
+		if(item == MANAGE) {
+			TextView manage = new TextView(ctx);
+			manage.setGravity(CENTER);
+			manage.setTextSize(18);
+			manage.setPadding(10, 10, 10, 10);
+			String format = res.getQuantityString(R.plurals.blogs_available,
+					available);
+			manage.setText(String.format(format, available));
+			return manage;
+		}
+
 		LinearLayout layout = new LinearLayout(ctx);
 		layout.setOrientation(HORIZONTAL);
 		if(item.getUnreadCount() > 0)
@@ -52,8 +89,9 @@ implements OnItemClickListener {
 		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());
+		String groupName = item.getGroup().getName();
+		if(unread > 0) name.setText(groupName + " (" + unread + ")");
+		else name.setText(groupName);
 		innerLayout.addView(name);
 
 		if(item.isEmpty()) {
@@ -97,13 +135,16 @@ implements OnItemClickListener {
 		return layout;
 	}
 
-	public void onItemClick(AdapterView<?> parent, View view, int position,
-			long id) {
-		BlogListItem item = getItem(position);
-		Intent i = new Intent(getContext(), BlogActivity.class);
-		i.putExtra("net.sf.briar.GROUP_ID", item.getGroupId().getBytes());
-		i.putExtra("net.sf.briar.GROUP_NAME", item.getGroupName());
-		i.putExtra("net.sf.briar.POSTABLE", item.isPostable());
-		getContext().startActivity(i);
+	@Override
+	public boolean isEmpty() {
+		return false;
+	}
+
+	public void remove(BlogListItem item) {
+		list.remove(item);
+	}
+
+	public void sort(Comparator<BlogListItem> comparator) {
+		Collections.sort(list, comparator);
 	}
 }
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java b/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java
index 4d955fcc53c41e32e247d3d086242c89a97b3133..72f3b1b8d5b2c6f7390ca49183ab942913cf7df2 100644
--- a/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java
@@ -9,10 +9,12 @@ import net.sf.briar.android.DescendingHeaderComparator;
 import net.sf.briar.api.Author;
 import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.messaging.Group;
-import net.sf.briar.api.messaging.GroupId;
 
 class BlogListItem {
 
+	static final BlogListItem MANAGE = new BlogListItem(null, false,
+			Collections.<GroupMessageHeader>emptyList());
+
 	private final Group group;
 	private final boolean postable, empty;
 	private final String authorName, contentType, subject;
@@ -47,12 +49,8 @@ class BlogListItem {
 		}
 	}
 
-	GroupId getGroupId() {
-		return group.getId();
-	}
-
-	String getGroupName() {
-		return group.getName();
+	Group getGroup() {
+		return group;
 	}
 
 	boolean isPostable() {
diff --git a/briar-android/src/net/sf/briar/android/blogs/ConfigureBlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/ConfigureBlogActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..f004a77e3de4afda952ad2496cc77457f06af26e
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/ConfigureBlogActivity.java
@@ -0,0 +1,239 @@
+package net.sf.briar.android.blogs;
+
+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.widgets.CommonLayoutParams.MATCH_MATCH;
+import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.BriarFragmentActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+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;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+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.Button;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+
+import com.google.inject.Inject;
+
+public class ConfigureBlogActivity extends BriarFragmentActivity
+implements OnClickListener, NoContactsDialog.Listener,
+SelectContactsDialog.Listener {
+
+	private static final Logger LOG =
+			Logger.getLogger(ConfigureBlogActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	private boolean wasSubscribed = false;
+	private CheckBox subscribeCheckBox = null;
+	private RadioGroup radioGroup = null;
+	private RadioButton visibleToAll = null, visibleToSome = null;
+	private Button doneButton = null;
+	private ProgressBar progress = null;
+
+	// Fields that are accessed from background threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
+	private volatile GroupId groupId = null;
+	private volatile Collection<ContactId> selected = Collections.emptyList();
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(b == null) throw new IllegalStateException();
+		groupId = new GroupId(b);
+		String groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+		if(groupName == null) throw new IllegalArgumentException();
+		setTitle(groupName);
+		wasSubscribed = i.getBooleanExtra("net.sf.briar.SUBSCRIBED", false);
+		boolean all = i.getBooleanExtra("net.sf.briar.VISIBLE_TO_ALL", false);
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(MATCH_MATCH);
+		layout.setOrientation(VERTICAL);
+		layout.setGravity(CENTER_HORIZONTAL);
+
+		subscribeCheckBox = new CheckBox(this);
+		subscribeCheckBox.setText(R.string.subscribe_to_this_blog);
+		subscribeCheckBox.setChecked(wasSubscribed);
+		subscribeCheckBox.setOnClickListener(this);
+		layout.addView(subscribeCheckBox);
+
+		radioGroup = new RadioGroup(this);
+		radioGroup.setOrientation(VERTICAL);
+		radioGroup.setEnabled(wasSubscribed);
+
+		visibleToAll = new RadioButton(this);
+		visibleToAll.setId(1);
+		visibleToAll.setText(R.string.blog_visible_to_all);
+		visibleToAll.setOnClickListener(this);
+		radioGroup.addView(visibleToAll);
+
+		visibleToSome = new RadioButton(this);
+		visibleToSome.setId(2);
+		visibleToSome.setText(R.string.blog_visible_to_some);
+		visibleToSome.setOnClickListener(this);
+		radioGroup.addView(visibleToSome);
+
+		if(all) radioGroup.check(1);
+		else radioGroup.check(2);
+		layout.addView(radioGroup);
+
+		doneButton = new Button(this);
+		doneButton.setLayoutParams(WRAP_WRAP);
+		doneButton.setText(R.string.done_button);
+		doneButton.setOnClickListener(this);
+		layout.addView(doneButton);
+
+		progress = new ProgressBar(this);
+		progress.setLayoutParams(WRAP_WRAP);
+		progress.setIndeterminate(true);
+		progress.setVisibility(GONE);
+		layout.addView(progress);
+
+		setContentView(layout);
+
+		// Bind to the service so we can wait for it to start
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		if(view == subscribeCheckBox) {
+			radioGroup.setEnabled(subscribeCheckBox.isChecked());
+		} else if(view == visibleToSome) {
+			loadContacts();
+		} else if(view == doneButton) {
+			boolean subscribe = subscribeCheckBox.isChecked();
+			boolean all = visibleToAll.isChecked();
+			Collection<ContactId> visible =
+					Collections.unmodifiableCollection(selected);
+			// Replace the button with a progress bar
+			doneButton.setVisibility(GONE);
+			progress.setVisibility(VISIBLE);
+			// Update the blog in a background thread
+			if(subscribe || wasSubscribed)
+				updateGroup(subscribe, wasSubscribed, all, visible);
+		}
+	}
+
+	private void loadContacts() {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					Collection<Contact> contacts = db.getContacts();
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Load took " + duration + " ms");
+					displayContacts(contacts);
+				} 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 displayContacts(final Collection<Contact> contacts) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				if(contacts.isEmpty()) {
+					NoContactsDialog dialog = new NoContactsDialog();
+					dialog.setListener(ConfigureBlogActivity.this);
+					dialog.show(getSupportFragmentManager(),
+							"NoContactsDialog");
+				} else {
+					SelectContactsDialog dialog = new SelectContactsDialog();
+					dialog.setListener(ConfigureBlogActivity.this);
+					dialog.setContacts(contacts);
+					dialog.show(getSupportFragmentManager(),
+							"SelectContactsDialog");
+				}
+			}
+		});
+	}
+
+	private void updateGroup(final boolean subscribe,
+			final boolean wasSubscribed, final boolean all,
+			final Collection<ContactId> visible) {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					if(subscribe) {
+						if(!wasSubscribed) db.subscribe(db.getGroup(groupId));
+						db.setVisibleToAll(groupId, all);
+						if(!all) db.setVisibility(groupId, visible);
+					} else if(wasSubscribed) {
+						db.unsubscribe(db.getGroup(groupId));
+					}
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Update took " + duration + " ms");
+				} 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();
+				}
+				finishOnUiThread();
+			}
+		});
+	}
+
+	public void contactCreationSelected() {
+		startActivity(new Intent(this, AddContactActivity.class));
+	}
+
+	public void contactCreationCancelled() {}
+
+	public void contactsSelected(Collection<ContactId> selected) {
+		this.selected = selected;
+	}
+
+	public void contactSelectionCancelled() {}
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java
index c3025a62d42f07fb56217f59bf93df0520d25c2b..35cbe2f6a4841bf860f8c8db3b46ee4c84b14f21 100644
--- a/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java
@@ -255,11 +255,7 @@ SelectContactsDialog.Listener {
 						LOG.info("Interrupted while waiting for service");
 					Thread.currentThread().interrupt();
 				}
-				runOnUiThread(new Runnable() {
-					public void run() {
-						finish();
-					}
-				});
+				finishOnUiThread();
 			}
 		});
 	}
diff --git a/briar-android/src/net/sf/briar/android/blogs/ManageBlogsActivity.java b/briar-android/src/net/sf/briar/android/blogs/ManageBlogsActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..f18ba0ee314c8d05a054f9db4914890f32f502dc
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/ManageBlogsActivity.java
@@ -0,0 +1,168 @@
+package net.sf.briar.android.blogs;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
+
+import java.util.ArrayList;
+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.android.BriarFragmentActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.ManageGroupsAdapter;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+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.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent;
+import net.sf.briar.api.db.event.SubscriptionAddedEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.Group;
+import net.sf.briar.api.messaging.GroupStatus;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class ManageBlogsActivity extends BriarFragmentActivity
+implements DatabaseListener, OnItemClickListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(ManageBlogsActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	private ManageGroupsAdapter adapter = null;
+	private ListView list = null;
+
+	// Fields that are accessed from background threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		adapter = new ManageGroupsAdapter(this);
+		list = new ListView(this);
+		list.setLayoutParams(MATCH_MATCH);
+		list.setAdapter(adapter);
+		list.setOnItemClickListener(this);
+		setContentView(list);
+
+		// Bind to the service so we can wait for it to start
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		db.addListener(this);
+		loadAvailableGroups();
+	}
+
+	private void loadAvailableGroups() {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					List<GroupStatus> available = new ArrayList<GroupStatus>();
+					for(GroupStatus s : db.getAvailableGroups())
+						if(s.getGroup().isRestricted()) available.add(s);
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Load took " + duration + " ms");
+					available = Collections.unmodifiableList(available);
+					displayAvailableGroups(available);
+				} 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 displayAvailableGroups(
+			final Collection<GroupStatus> available) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				adapter.clear();
+				for(GroupStatus g : available) adapter.add(g);
+				adapter.sort(ItemComparator.INSTANCE);
+				adapter.notifyDataSetChanged();
+			}
+		});
+	}
+
+	@Override
+	public void onPause() {
+		super.onPause();
+		db.removeListener(this);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof RemoteSubscriptionsUpdatedEvent) {
+			if(LOG.isLoggable(INFO))
+				LOG.info("Remote subscriptions changed, reloading");
+			loadAvailableGroups();
+		} else if(e instanceof SubscriptionAddedEvent) {
+			Group g = ((SubscriptionAddedEvent) e).getGroup();
+			if(g.isRestricted()) {
+				if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
+				loadAvailableGroups();
+			}
+		} else if(e instanceof SubscriptionRemovedEvent) {
+			Group g = ((SubscriptionRemovedEvent) e).getGroup();
+			if(g.isRestricted()) {
+				if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
+				loadAvailableGroups();
+			}
+		}
+	}
+
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		GroupStatus item = adapter.getItem(position);
+		Intent i = new Intent(this, ConfigureBlogActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", item.getGroup().getId().getBytes());
+		i.putExtra("net.sf.briar.GROUP_NAME", item.getGroup().getName());
+		i.putExtra("net.sf.briar.SUBSCRIBED", item.isSubscribed());
+		i.putExtra("net.sf.briar.VISIBLE_TO_ALL", item.isVisibleToAll());
+		startActivity(i);
+	}
+
+	private static class ItemComparator implements Comparator<GroupStatus> {
+
+		private static final ItemComparator INSTANCE = new ItemComparator();
+
+		public int compare(GroupStatus a, GroupStatus b) {
+			String aName = a.getGroup().getName();
+			String bName = b.getGroup().getName();
+			return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
+		}
+	}
+}
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 ef7866fb94d2652f2360b9084dde7809e57e059d..e52d0d4bf06d900c8d030c5c6acff171c34c2b76 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
@@ -143,7 +143,7 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 					if(last != null)
 						adapter.add(new ContactListItem(c, now, last));
 				}
-				adapter.sort(ContactComparator.INSTANCE);
+				adapter.sort(ItemComparator.INSTANCE);
 				adapter.notifyDataSetChanged();
 			}
 		});
@@ -198,10 +198,9 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 		});
 	}
 
-	private static class ContactComparator
-	implements Comparator<ContactListItem> {
+	private static class ItemComparator implements Comparator<ContactListItem> {
 
-		static final ContactComparator INSTANCE = new ContactComparator();
+		private static final ItemComparator INSTANCE = new ItemComparator();
 
 		public int compare(ContactListItem a, ContactListItem b) {
 			return String.CASE_INSENSITIVE_ORDER.compare(a.getContactName(),
diff --git a/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..1fca60001339f83c8e94550cc9ce358ff8e052b3
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java
@@ -0,0 +1,239 @@
+package net.sf.briar.android.groups;
+
+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.widgets.CommonLayoutParams.MATCH_MATCH;
+import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.BriarFragmentActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+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;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+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.Button;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+
+import com.google.inject.Inject;
+
+public class ConfigureGroupActivity extends BriarFragmentActivity
+implements OnClickListener, NoContactsDialog.Listener,
+SelectContactsDialog.Listener {
+
+	private static final Logger LOG =
+			Logger.getLogger(ConfigureGroupActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	private boolean wasSubscribed = false;
+	private CheckBox subscribeCheckBox = null;
+	private RadioGroup radioGroup = null;
+	private RadioButton visibleToAll = null, visibleToSome = null;
+	private Button doneButton = null;
+	private ProgressBar progress = null;
+
+	// Fields that are accessed from background threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
+	private volatile GroupId groupId = null;
+	private volatile Collection<ContactId> selected = Collections.emptyList();
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(b == null) throw new IllegalStateException();
+		groupId = new GroupId(b);
+		String groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+		if(groupName == null) throw new IllegalArgumentException();
+		setTitle(groupName);
+		wasSubscribed = i.getBooleanExtra("net.sf.briar.SUBSCRIBED", false);
+		boolean all = i.getBooleanExtra("net.sf.briar.VISIBLE_TO_ALL", false);
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(MATCH_MATCH);
+		layout.setOrientation(VERTICAL);
+		layout.setGravity(CENTER_HORIZONTAL);
+
+		subscribeCheckBox = new CheckBox(this);
+		subscribeCheckBox.setText(R.string.subscribe_to_this_group);
+		subscribeCheckBox.setChecked(wasSubscribed);
+		subscribeCheckBox.setOnClickListener(this);
+		layout.addView(subscribeCheckBox);
+
+		radioGroup = new RadioGroup(this);
+		radioGroup.setOrientation(VERTICAL);
+		radioGroup.setEnabled(wasSubscribed);
+
+		visibleToAll = new RadioButton(this);
+		visibleToAll.setId(1);
+		visibleToAll.setText(R.string.group_visible_to_all);
+		visibleToAll.setOnClickListener(this);
+		radioGroup.addView(visibleToAll);
+
+		visibleToSome = new RadioButton(this);
+		visibleToSome.setId(2);
+		visibleToSome.setText(R.string.group_visible_to_some);
+		visibleToSome.setOnClickListener(this);
+		radioGroup.addView(visibleToSome);
+
+		if(all) radioGroup.check(1);
+		else radioGroup.check(2);
+		layout.addView(radioGroup);
+
+		doneButton = new Button(this);
+		doneButton.setLayoutParams(WRAP_WRAP);
+		doneButton.setText(R.string.done_button);
+		doneButton.setOnClickListener(this);
+		layout.addView(doneButton);
+
+		progress = new ProgressBar(this);
+		progress.setLayoutParams(WRAP_WRAP);
+		progress.setIndeterminate(true);
+		progress.setVisibility(GONE);
+		layout.addView(progress);
+
+		setContentView(layout);
+
+		// Bind to the service so we can wait for it to start
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		if(view == subscribeCheckBox) {
+			radioGroup.setEnabled(subscribeCheckBox.isChecked());
+		} else if(view == visibleToSome) {
+			loadContacts();
+		} else if(view == doneButton) {
+			boolean subscribe = subscribeCheckBox.isChecked();
+			boolean all = visibleToAll.isChecked();
+			Collection<ContactId> visible =
+					Collections.unmodifiableCollection(selected);
+			// Replace the button with a progress bar
+			doneButton.setVisibility(GONE);
+			progress.setVisibility(VISIBLE);
+			// Update the blog in a background thread
+			if(subscribe || wasSubscribed)
+				updateGroup(subscribe, wasSubscribed, all, visible);
+		}
+	}
+
+	private void loadContacts() {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					Collection<Contact> contacts = db.getContacts();
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Load took " + duration + " ms");
+					displayContacts(contacts);
+				} 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 displayContacts(final Collection<Contact> contacts) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				if(contacts.isEmpty()) {
+					NoContactsDialog dialog = new NoContactsDialog();
+					dialog.setListener(ConfigureGroupActivity.this);
+					dialog.show(getSupportFragmentManager(),
+							"NoContactsDialog");
+				} else {
+					SelectContactsDialog dialog = new SelectContactsDialog();
+					dialog.setListener(ConfigureGroupActivity.this);
+					dialog.setContacts(contacts);
+					dialog.show(getSupportFragmentManager(),
+							"SelectContactsDialog");
+				}
+			}
+		});
+	}
+
+	private void updateGroup(final boolean subscribe,
+			final boolean wasSubscribed, final boolean all,
+			final Collection<ContactId> visible) {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					if(subscribe) {
+						if(!wasSubscribed) db.subscribe(db.getGroup(groupId));
+						db.setVisibleToAll(groupId, all);
+						if(!all) db.setVisibility(groupId, visible);
+					} else if(wasSubscribed) {
+						db.unsubscribe(db.getGroup(groupId));
+					}
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Update took " + duration + " ms");
+				} 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();
+				}
+				finishOnUiThread();
+			}
+		});
+	}
+
+	public void contactCreationSelected() {
+		startActivity(new Intent(this, AddContactActivity.class));
+	}
+
+	public void contactCreationCancelled() {}
+
+	public void contactsSelected(Collection<ContactId> selected) {
+		this.selected = selected;
+	}
+
+	public void contactSelectionCancelled() {}
+}
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 e6dab7ed0269620112a49091e63acde229cf8053..388fe72169bb2ce8863a2cc1f7bbc9a8fbdb24e2 100644
--- a/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java
@@ -189,11 +189,7 @@ SelectContactsDialog.Listener {
 					} catch(IOException e) {
 						throw new RuntimeException(e);
 					}
-					runOnUiThread(new Runnable() {
-						public void run() {
-							finish();
-						}
-					});
+					finishOnUiThread();
 				}
 			});
 		}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
index 3deda412db42abff09c6296f61764dccc788d363..dcab38c8c34fcaab049bcf935030177c57baaef1 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
@@ -6,6 +6,7 @@ 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 static net.sf.briar.android.groups.GroupListItem.MANAGE;
 import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
 import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP;
 import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
@@ -30,13 +31,18 @@ import net.sf.briar.api.db.event.DatabaseEvent;
 import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.GroupMessageAddedEvent;
 import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent;
+import net.sf.briar.api.db.event.SubscriptionAddedEvent;
 import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.GroupStatus;
 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;
@@ -44,7 +50,8 @@ import android.widget.ListView;
 import com.google.inject.Inject;
 
 public class GroupListActivity extends BriarFragmentActivity
-implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
+implements DatabaseListener, OnClickListener, NoGroupsDialog.Listener,
+OnItemClickListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(GroupListActivity.class.getName());
@@ -55,6 +62,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 	private GroupListAdapter adapter = null;
 	private ListView list = null;
 	private ImageButton newGroupButton = null, composeButton = null;
+	private ImageButton manageGroupsButton = null;
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject private volatile DatabaseComponent db;
@@ -73,7 +81,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 		// Give me all the width and all the unused height
 		list.setLayoutParams(MATCH_WRAP_1);
 		list.setAdapter(adapter);
-		list.setOnItemClickListener(adapter);
+		list.setOnItemClickListener(this);
 		layout.addView(list);
 
 		layout.addView(new HorizontalBorder(this));
@@ -97,6 +105,13 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 		composeButton.setOnClickListener(this);
 		footer.addView(composeButton);
 		footer.addView(new HorizontalSpace(this));
+
+		manageGroupsButton = new ImageButton(this);
+		manageGroupsButton.setBackgroundResource(0);
+		manageGroupsButton.setImageResource(R.drawable.action_settings);
+		manageGroupsButton.setOnClickListener(this);
+		footer.addView(manageGroupsButton);
+		footer.addView(new HorizontalSpace(this));
 		layout.addView(footer);
 
 		setContentView(layout);
@@ -119,21 +134,28 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 			public void run() {
 				try {
 					serviceConnection.waitForStartup();
+					int available = 0;
 					long now = System.currentTimeMillis();
-					for(Group g : db.getSubscriptions()) {
+					for(GroupStatus s : db.getAvailableGroups()) {
+						Group g = s.getGroup();
 						if(g.isRestricted()) continue;
-						try {
-							Collection<GroupMessageHeader> headers =
-									db.getGroupMessageHeaders(g.getId());
-							displayHeaders(g, headers);
-						} catch(NoSuchSubscriptionException e) {
-							if(LOG.isLoggable(INFO))
-								LOG.info("Subscription removed");
+						if(s.isSubscribed()) {
+							try {
+								Collection<GroupMessageHeader> headers =
+										db.getGroupMessageHeaders(g.getId());
+								displayHeaders(g, headers);
+							} catch(NoSuchSubscriptionException e) {
+								if(LOG.isLoggable(INFO))
+									LOG.info("Subscription removed");
+							}
+						} else {
+							available++;
 						}
 					}
 					long duration = System.currentTimeMillis() - now;
 					if(LOG.isLoggable(INFO))
 						LOG.info("Full load took " + duration + " ms");
+					displayAvailable(available);
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -150,6 +172,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				adapter.clear();
+				adapter.notifyDataSetChanged();
 			}
 		});
 	}
@@ -163,18 +186,28 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 				if(item != null) adapter.remove(item);
 				// Add a new item
 				adapter.add(new GroupListItem(g, headers));
-				adapter.sort(GroupComparator.INSTANCE);
+				adapter.sort(ItemComparator.INSTANCE);
 				adapter.notifyDataSetChanged();
 				selectFirstUnread();
 			} 
 		});
 	}
 
+	private void displayAvailable(final int available) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				adapter.setAvailable(available);
+				adapter.notifyDataSetChanged();
+			}
+		});
+	}
+
 	private GroupListItem findGroup(GroupId g) {
 		int count = adapter.getCount();
 		for(int i = 0; i < count; i++) {
 			GroupListItem item = adapter.getItem(i);
-			if(item.getGroupId().equals(g)) return item;
+			if(item == MANAGE) continue;
+			if(item.getGroup().getId().equals(g)) return item;
 		}
 		return null; // Not found
 	}
@@ -182,7 +215,9 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 	private void selectFirstUnread() {
 		int firstUnread = -1, count = adapter.getCount();
 		for(int i = 0; i < count; i++) {
-			if(adapter.getItem(i).getUnreadCount() > 0) {
+			GroupListItem item = adapter.getItem(i);
+			if(item == MANAGE) continue;
+			if(item.getUnreadCount() > 0) {
 				firstUnread = i;
 				break;
 			}
@@ -203,20 +238,6 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 		unbindService(serviceConnection);
 	}
 
-	public void onClick(View view) {
-		if(view == newGroupButton) {
-			startActivity(new Intent(this, CreateGroupActivity.class));
-		} else if(view == composeButton) {
-			if(adapter.isEmpty()) {
-				NoGroupsDialog dialog = new NoGroupsDialog();
-				dialog.setListener(this);
-				dialog.show(getSupportFragmentManager(), "NoGroupsDialog");
-			} else {
-				startActivity(new Intent(this, WriteGroupPostActivity.class));
-			}
-		}
-	}
-
 	public void eventOccurred(DatabaseEvent e) {
 		if(e instanceof GroupMessageAddedEvent) {
 			Group g = ((GroupMessageAddedEvent) e).getGroup();
@@ -227,6 +248,16 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 		} else if(e instanceof MessageExpiredEvent) {
 			if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
 			loadHeaders();
+		} else if(e instanceof RemoteSubscriptionsUpdatedEvent) {
+			if(LOG.isLoggable(INFO))
+				LOG.info("Remote subscriptions changed, reloading");
+			loadAvailable();
+		} else if(e instanceof SubscriptionAddedEvent) {
+			Group g = ((SubscriptionAddedEvent) e).getGroup();
+			if(!g.isRestricted()) {
+				if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
+				loadHeaders();
+			}
 		} else if(e instanceof SubscriptionRemovedEvent) {
 			Group g = ((SubscriptionRemovedEvent) e).getGroup();
 			if(!g.isRestricted()) {
@@ -277,23 +308,85 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
 		});
 	}
 
+	private void loadAvailable() {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					int available = 0;
+					long now = System.currentTimeMillis();
+					for(GroupStatus s : db.getAvailableGroups()) {
+						if(!s.getGroup().isRestricted() && !s.isSubscribed())
+							available++;
+					}
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Loading available took " + duration + " ms");
+					displayAvailable(available);
+				} 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();
+				}
+			}
+		});
+	}
+
+	public void onClick(View view) {
+		if(view == newGroupButton) {
+			startActivity(new Intent(this, CreateGroupActivity.class));
+		} else if(view == composeButton) {
+			if(adapter.isEmpty()) {
+				NoGroupsDialog dialog = new NoGroupsDialog();
+				dialog.setListener(this);
+				dialog.show(getSupportFragmentManager(), "NoGroupsDialog");
+			} else {
+				startActivity(new Intent(this, WriteGroupPostActivity.class));
+			}
+		} else if(view == manageGroupsButton) {
+			startActivity(new Intent(this, ManageGroupsActivity.class));
+		}
+	}
+
 	public void groupCreationSelected() {
 		startActivity(new Intent(this, CreateGroupActivity.class));
 	}
 
 	public void groupCreationCancelled() {}
 
-	private static class GroupComparator implements Comparator<GroupListItem> {
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		GroupListItem item = adapter.getItem(position);
+		if(item == MANAGE) {
+			startActivity(new Intent(this, ManageGroupsActivity.class));
+		} else {
+			Intent i = new Intent(this, GroupActivity.class);
+			i.putExtra("net.sf.briar.GROUP_ID",
+					item.getGroup().getId().getBytes());
+			i.putExtra("net.sf.briar.GROUP_NAME", item.getGroup().getName());
+			startActivity(i);
+		}
+	}
+
+	private static class ItemComparator implements Comparator<GroupListItem> {
 
-		private static final GroupComparator INSTANCE = new GroupComparator();
+		private static final ItemComparator INSTANCE = new ItemComparator();
 
 		public int compare(GroupListItem a, GroupListItem b) {
+			if(a == b) return 0;
+			// The manage groups item comes last
+			if(a == MANAGE) return 1;
+			if(b == MANAGE) return -1;
 			// 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 group name
-			String aName = a.getGroupName(), bName = b.getGroupName();
+			String aName = a.getGroup().getName();
+			String bName = b.getGroup().getName();
 			return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
 		}
 	}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
index 455941cdbff0d5859e912b55b6e68236b9970bc0..e6bc316444bb2f861ede3cbc938ead219687fad7 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
@@ -1,42 +1,79 @@
 package net.sf.briar.android.groups;
 
 import static android.graphics.Typeface.BOLD;
+import static android.view.Gravity.CENTER;
 import static android.widget.LinearLayout.HORIZONTAL;
 import static android.widget.LinearLayout.VERTICAL;
 import static java.text.DateFormat.SHORT;
+import static net.sf.briar.android.groups.GroupListItem.MANAGE;
 import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 
 import net.sf.briar.R;
 import net.sf.briar.android.widgets.HorizontalSpace;
 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.BaseAdapter;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
-class GroupListAdapter extends ArrayAdapter<GroupListItem>
-implements OnItemClickListener {
+class GroupListAdapter extends BaseAdapter {
+
+	private final Context ctx;
+	private final List<GroupListItem> list = new ArrayList<GroupListItem>();
+	private int available = 0;
 
 	GroupListAdapter(Context ctx) {
-		super(ctx, android.R.layout.simple_expandable_list_item_1,
-				new ArrayList<GroupListItem>());
+		this.ctx = ctx;
+	}
+
+	public void setAvailable(int available) {
+		this.available = available;
+	}
+
+	public void add(GroupListItem item) {
+		list.add(item);
+	}
+
+	public void clear() {
+		list.clear();
+	}
+
+	public int getCount() {
+		return available == 0 ? list.size() : list.size() + 1;
+	}
+
+	public GroupListItem getItem(int position) {
+		return position == list.size() ? MANAGE : list.get(position);
+	}
+
+	public long getItemId(int position) {
+		return android.R.layout.simple_expandable_list_item_1;
 	}
 
-	@Override
 	public View getView(int position, View convertView, ViewGroup parent) {
 		GroupListItem item = getItem(position);
-		Context ctx = getContext();
 		Resources res = ctx.getResources();
 
+		if(item == MANAGE) {
+			TextView manage = new TextView(ctx);
+			manage.setGravity(CENTER);
+			manage.setTextSize(18);
+			manage.setPadding(10, 10, 10, 10);
+			String format = res.getQuantityString(R.plurals.groups_available,
+					available);
+			manage.setText(String.format(format, available));
+			return manage;
+		}
+
 		LinearLayout layout = new LinearLayout(ctx);
 		layout.setOrientation(HORIZONTAL);
 		if(item.getUnreadCount() > 0)
@@ -52,8 +89,9 @@ implements OnItemClickListener {
 		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());
+		String groupName = item.getGroup().getName();
+		if(unread > 0) name.setText(groupName + " (" + unread + ")");
+		else name.setText(groupName);
 		innerLayout.addView(name);
 
 		if(item.isEmpty()) {
@@ -97,12 +135,16 @@ implements OnItemClickListener {
 		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);
+	@Override
+	public boolean isEmpty() {
+		return false;
+	}
+
+	public void remove(GroupListItem item) {
+		list.remove(item);
+	}
+
+	public void sort(Comparator<GroupListItem> comparator) {
+		Collections.sort(list, comparator);
 	}
 }
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
index 0fc071ad398c4a964c71a64ce3e331c8dbba56c0..7348ed7dbfe6a2651929d739f1717517e899ee2b 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
@@ -9,10 +9,12 @@ import net.sf.briar.android.DescendingHeaderComparator;
 import net.sf.briar.api.Author;
 import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.messaging.Group;
-import net.sf.briar.api.messaging.GroupId;
 
 class GroupListItem {
 
+	static final GroupListItem MANAGE = new GroupListItem(null,
+			Collections.<GroupMessageHeader>emptyList());
+
 	private final Group group;
 	private final boolean empty;
 	private final String authorName, contentType, subject;
@@ -45,12 +47,8 @@ class GroupListItem {
 		}
 	}
 
-	GroupId getGroupId() {
-		return group.getId();
-	}
-
-	String getGroupName() {
-		return group.getName();
+	Group getGroup() {
+		return group;
 	}
 
 	boolean isEmpty() {
diff --git a/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java b/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..ced4d15da86af602cd3e326dbf1d1f4a49db906c
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java
@@ -0,0 +1,168 @@
+package net.sf.briar.android.groups;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
+
+import java.util.ArrayList;
+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.android.BriarFragmentActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.ManageGroupsAdapter;
+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.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent;
+import net.sf.briar.api.db.event.SubscriptionAddedEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.Group;
+import net.sf.briar.api.messaging.GroupStatus;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class ManageGroupsActivity extends BriarFragmentActivity
+implements DatabaseListener, OnItemClickListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(ManageGroupsActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	private ManageGroupsAdapter adapter = null;
+	private ListView list = null;
+
+	// Fields that are accessed from background threads must be volatile
+	@Inject private volatile DatabaseComponent db;
+	@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		adapter = new ManageGroupsAdapter(this);
+		list = new ListView(this);
+		list.setLayoutParams(MATCH_MATCH);
+		list.setAdapter(adapter);
+		list.setOnItemClickListener(this);
+		setContentView(list);
+
+		// Bind to the service so we can wait for it to start
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		db.addListener(this);
+		loadAvailableGroups();
+	}
+
+	private void loadAvailableGroups() {
+		dbUiExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					long now = System.currentTimeMillis();
+					List<GroupStatus> available = new ArrayList<GroupStatus>();
+					for(GroupStatus s : db.getAvailableGroups())
+						if(!s.getGroup().isRestricted()) available.add(s);
+					long duration = System.currentTimeMillis() - now;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Load took " + duration + " ms");
+					available = Collections.unmodifiableList(available);
+					displayAvailableGroups(available);
+				} 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 displayAvailableGroups(
+			final Collection<GroupStatus> available) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				adapter.clear();
+				for(GroupStatus g : available) adapter.add(g);
+				adapter.sort(ItemComparator.INSTANCE);
+				adapter.notifyDataSetChanged();
+			}
+		});
+	}
+
+	@Override
+	public void onPause() {
+		super.onPause();
+		db.removeListener(this);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof RemoteSubscriptionsUpdatedEvent) {
+			if(LOG.isLoggable(INFO))
+				LOG.info("Remote subscriptions changed, reloading");
+			loadAvailableGroups();
+		} else if(e instanceof SubscriptionAddedEvent) {
+			Group g = ((SubscriptionAddedEvent) e).getGroup();
+			if(g.isRestricted()) {
+				if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
+				loadAvailableGroups();
+			}
+		} else if(e instanceof SubscriptionRemovedEvent) {
+			Group g = ((SubscriptionRemovedEvent) e).getGroup();
+			if(g.isRestricted()) {
+				if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
+				loadAvailableGroups();
+			}
+		}
+	}
+
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		GroupStatus item = adapter.getItem(position);
+		Intent i = new Intent(this, ConfigureGroupActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", item.getGroup().getId().getBytes());
+		i.putExtra("net.sf.briar.GROUP_NAME", item.getGroup().getName());
+		i.putExtra("net.sf.briar.SUBSCRIBED", item.isSubscribed());
+		i.putExtra("net.sf.briar.VISIBLE_TO_ALL", item.isVisibleToAll());
+		startActivity(i);
+	}
+
+	private static class ItemComparator implements Comparator<GroupStatus> {
+
+		private static final ItemComparator INSTANCE = new ItemComparator();
+
+		public int compare(GroupStatus a, GroupStatus b) {
+			String aName = a.getGroup().getName();
+			String bName = b.getGroup().getName();
+			return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
+		}
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java b/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java
index e0d9493ece11783b611919b27da536db7bc3b2ff..995d7927e92e9bc874c13127a2c6966267a810c1 100644
--- a/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java
+++ b/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java
@@ -169,11 +169,7 @@ implements OnEditorActionListener, OnClickListener {
 						LOG.info("Interrupted while waiting for service");
 					Thread.currentThread().interrupt();
 				}
-				runOnUiThread(new Runnable() {
-					public void run() {
-						finish();
-					}
-				});
+				finishOnUiThread();
 			}
 		});
 	}
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 16db46e339bf51093e890eec96b3736358e5bd69..379cedee6116dc13533d75e7f35b0202e47a6abd 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
@@ -134,7 +134,7 @@ implements OnClickListener, DatabaseListener, NoContactsDialog.Listener {
 				if(item != null) adapter.remove(item);
 				// Add a new item
 				adapter.add(new ConversationListItem(c, headers));
-				adapter.sort(ConversationComparator.INSTANCE);
+				adapter.sort(ItemComparator.INSTANCE);
 				adapter.notifyDataSetChanged();
 				selectFirstUnread();
 			}
@@ -244,11 +244,10 @@ implements OnClickListener, DatabaseListener, NoContactsDialog.Listener {
 
 	public void contactCreationCancelled() {}
 
-	private static class ConversationComparator
+	private static class ItemComparator
 	implements Comparator<ConversationListItem> {
 
-		static final ConversationComparator INSTANCE =
-				new ConversationComparator();
+		private static final ItemComparator INSTANCE = new ItemComparator();
 
 		public int compare(ConversationListItem a, ConversationListItem b) {
 			// The item with the newest message comes first
diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
index 96c48cc7a9cfc8bb4ba668e8b3e7efad80643515..c0b3e43e43dd577926237cd1f35aea5c13757e6a 100644
--- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
+++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java
@@ -16,6 +16,7 @@ import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.messaging.Ack;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.GroupStatus;
 import net.sf.briar.api.messaging.LocalGroup;
 import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageId;
@@ -162,11 +163,8 @@ public interface DatabaseComponent {
 	Collection<TransportUpdate> generateTransportUpdates(ContactId c,
 			long maxLatency) throws DbException;
 
-	/**
-	 * Returns any groups that contacts have made visible but to which the user
-	 * does not subscribe.
-	 */
-	Collection<Group> getAvailableGroups() throws DbException;
+	/** Returns the status of all groups to which the user can subscribe. */
+	Collection<GroupStatus> getAvailableGroups() throws DbException;
 
 	/** Returns the configuration for the given transport. */
 	TransportConfig getConfig(TransportId t) throws DbException;
@@ -362,7 +360,7 @@ public interface DatabaseComponent {
 	 * If <tt>visible</tt> is true, the group is also made visible to all
 	 * current contacts.
 	 */
-	void setVisibleToAll(GroupId g, boolean visible) throws DbException;
+	void setVisibleToAll(GroupId g, boolean all) throws DbException;
 
 	/**
 	 * Subscribes to the given group, or returns false if the user already has
diff --git a/briar-api/src/net/sf/briar/api/messaging/GroupStatus.java b/briar-api/src/net/sf/briar/api/messaging/GroupStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..35074ed36fd90819f2f25b004ac4ce7d5b0c9298
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/messaging/GroupStatus.java
@@ -0,0 +1,25 @@
+package net.sf.briar.api.messaging;
+
+public class GroupStatus {
+
+	private final Group group;
+	private final boolean subscribed, visibleToAll;
+
+	public GroupStatus(Group group, boolean subscribed, boolean visibleToAll) {
+		this.group = group;
+		this.subscribed = subscribed;
+		this.visibleToAll = visibleToAll;
+	}
+
+	public Group getGroup() {
+		return group;
+	}
+
+	public boolean isSubscribed() {
+		return subscribed;
+	}
+
+	public boolean isVisibleToAll() {
+		return visibleToAll;
+	}
+}
diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java
index 3af45362f3549f7360fb8b6bfc93311cadffbb30..a54751b800a8b1c67291ed999736598e5db1f457 100644
--- a/briar-core/src/net/sf/briar/db/Database.java
+++ b/briar-core/src/net/sf/briar/db/Database.java
@@ -17,6 +17,7 @@ import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.db.PrivateMessageHeader;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.GroupStatus;
 import net.sf.briar.api.messaging.LocalGroup;
 import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageId;
@@ -191,6 +192,14 @@ interface Database<T> {
 	 */
 	boolean containsContact(T txn, ContactId c) throws DbException;
 
+	/**
+	 * Returns true if the database contains the given restricted group to
+	 * which the user can post messages.
+	 * <p>
+	 * Locking: identity read.
+	 */
+	boolean containsLocalGroup(T txn, GroupId g) throws DbException;
+
 	/**
 	 * Returns true if the database contains the given message.
 	 * <p>
@@ -222,10 +231,11 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Returns any groups that contacts have made visible but to which the user
-	 * does not subscribe.
+	 * Returns the status of all groups to which the user can subscribe.
+	 * <p>
+	 * Locking: subscription read.
 	 */
-	Collection<Group> getAvailableGroups(T txn) throws DbException;
+	Collection<GroupStatus> getAvailableGroups(T txn) throws DbException;
 
 	/**
 	 * Returns the configuration for the given transport.
@@ -621,6 +631,13 @@ interface Database<T> {
 	 */
 	void removeContact(T txn, ContactId c) throws DbException;
 
+	/**
+	 * Removes the given restricted group to which the user can post messages.
+	 * <p>
+	 * Locking: identity write.
+	 */
+	void removeLocalGroup(T txn, GroupId g) throws DbException;
+
 	/**
 	 * Removes a message (and all associated state) from the database.
 	 * <p>
@@ -797,7 +814,7 @@ interface Database<T> {
 	 * <p>
 	 * Locking: subscription write.
 	 */
-	void setVisibleToAll(T txn, GroupId g, boolean visible) throws DbException;
+	void setVisibleToAll(T txn, GroupId g, boolean all) throws DbException;
 
 	/**
 	 * Updates the expiry times of the given messages with respect to the given
diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
index 70aa0adc9189b369d98169ed7a1e0a993cf49ed1..b1048be58b8ccc8a05d9136406ab510b9af88911 100644
--- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java
@@ -64,6 +64,7 @@ import net.sf.briar.api.lifecycle.ShutdownManager;
 import net.sf.briar.api.messaging.Ack;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.GroupStatus;
 import net.sf.briar.api.messaging.LocalGroup;
 import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageId;
@@ -887,12 +888,12 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public Collection<Group> getAvailableGroups() throws DbException {
+	public Collection<GroupStatus> getAvailableGroups() throws DbException {
 		subscriptionLock.readLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				Collection<Group> groups = db.getAvailableGroups(txn);
+				Collection<GroupStatus> groups = db.getAvailableGroups(txn);
 				db.commitTransaction(txn);
 				return groups;
 			} catch(DbException e) {
@@ -2051,7 +2052,7 @@ DatabaseCleaner.Callback {
 			callListeners(new LocalSubscriptionsUpdatedEvent(affected));
 	}
 
-	public void setVisibleToAll(GroupId g, boolean visible) throws DbException {
+	public void setVisibleToAll(GroupId g, boolean all) throws DbException {
 		Collection<ContactId> affected = new ArrayList<ContactId>();
 		contactLock.readLock().lock();
 		try {
@@ -2062,8 +2063,8 @@ DatabaseCleaner.Callback {
 					if(!db.containsSubscription(txn, g))
 						throw new NoSuchSubscriptionException();
 					// Make the group visible or invisible to future contacts
-					db.setVisibleToAll(txn, g, visible);
-					if(visible) {
+					db.setVisibleToAll(txn, g, all);
+					if(all) {
 						// Make the group visible to all current contacts
 						Collection<ContactId> before = db.getVisibility(txn, g);
 						before = new HashSet<ContactId>(before);
@@ -2111,27 +2112,34 @@ DatabaseCleaner.Callback {
 
 	public void unsubscribe(Group g) throws DbException {
 		Collection<ContactId> affected;
-		messageLock.writeLock().lock();
+		identityLock.writeLock().lock();
 		try {
-			subscriptionLock.writeLock().lock();
+			messageLock.writeLock().lock();
 			try {
-				T txn = db.startTransaction();
+				subscriptionLock.writeLock().lock();
 				try {
-					GroupId id = g.getId();
-					if(!db.containsSubscription(txn, id))
-						throw new NoSuchSubscriptionException();
-					affected = db.getVisibility(txn, id);
-					db.removeSubscription(txn, id);
-					db.commitTransaction(txn);
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
+					T txn = db.startTransaction();
+					try {
+						GroupId id = g.getId();
+						if(!db.containsSubscription(txn, id))
+							throw new NoSuchSubscriptionException();
+						affected = db.getVisibility(txn, id);
+						db.removeSubscription(txn, id);
+						if(db.containsLocalGroup(txn, id))
+							db.removeLocalGroup(txn, id);
+						db.commitTransaction(txn);
+					} catch(DbException e) {
+						db.abortTransaction(txn);
+						throw e;
+					}
+				} finally {
+					subscriptionLock.writeLock().unlock();
 				}
 			} finally {
-				subscriptionLock.writeLock().unlock();
+				messageLock.writeLock().unlock();
 			}
 		} finally {
-			messageLock.writeLock().unlock();
+			identityLock.writeLock().unlock();
 		}
 		callListeners(new SubscriptionRemovedEvent(g));
 		callListeners(new LocalSubscriptionsUpdatedEvent(affected));
diff --git a/briar-core/src/net/sf/briar/db/H2Database.java b/briar-core/src/net/sf/briar/db/H2Database.java
index 5203f64830c1c01b05c1fc1b58f7aba9ce011d45..600b0375b859eca16123d18f2b9a9c6c8567bbed 100644
--- a/briar-core/src/net/sf/briar/db/H2Database.java
+++ b/briar-core/src/net/sf/briar/db/H2Database.java
@@ -75,7 +75,9 @@ class H2Database extends JdbcDatabase {
 
 	@Override
 	protected Connection createConnection() throws SQLException {
-		char[] password = encodePassword(config.getEncryptionKey());
+		byte[] key = config.getEncryptionKey();
+		if(key == null) throw new IllegalStateException();
+		char[] password = encodePassword(key);
 		Properties props = new Properties();
 		props.setProperty("user", "user");
 		props.put("password", password);
diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
index d33c66950ed114611268b634c25b995e186e26cb..5ab537972db591177f0bc06e8c4d3956c1a19642 100644
--- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java
+++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java
@@ -19,10 +19,12 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.Author;
@@ -40,6 +42,7 @@ import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.db.PrivateMessageHeader;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.GroupStatus;
 import net.sf.briar.api.messaging.LocalGroup;
 import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageId;
@@ -743,8 +746,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void addLocalGroup(Connection txn, LocalGroup g)
-			throws DbException {
+	public void addLocalGroup(Connection txn, LocalGroup g) throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "INSERT INTO localGroups"
@@ -1053,6 +1055,27 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public boolean containsLocalGroup(Connection txn, GroupId g)
+			throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT NULL FROM localGroups WHERE groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, g.getBytes());
+			rs = ps.executeQuery();
+			boolean found = rs.next();
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			return found;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public boolean containsMessage(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1139,25 +1162,41 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<Group> getAvailableGroups(Connection txn)
+	public Collection<GroupStatus> getAvailableGroups(Connection txn)
 			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT cg.groupId, cg.name, cg.publicKey"
-					+ " FROM contactGroups AS cg"
-					+ " LEFT OUTER JOIN groups AS g"
-					+ " ON cg.groupId = g.groupId"
-					+ " WHERE g.groupId IS NULL"
-					+ " GROUP BY cg.groupId";
+			// Add all subscribed groups to the list
+			String sql = "SELECT groupId, name, publicKey, visibleToAll"
+					+ " FROM groups";
+			ps = txn.prepareStatement(sql);
+			rs = ps.executeQuery();
+			List<GroupStatus> groups = new ArrayList<GroupStatus>();
+			Set<GroupId> subscribed = new HashSet<GroupId>();
+			while(rs.next()) {
+				GroupId id = new GroupId(rs.getBytes(1));
+				subscribed.add(id);
+				String name = rs.getString(2);
+				byte[] publicKey = rs.getBytes(3);
+				Group group = new Group(id, name, publicKey);
+				boolean visibleToAll = rs.getBoolean(4);
+				groups.add(new GroupStatus(group, true, visibleToAll));
+			}
+			rs.close();
+			ps.close();
+			// Add all contact groups to the list, unless already added
+			sql = "SELECT DISTINCT groupId, name, publicKey"
+					+ " FROM contactGroups";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
-			List<Group> groups = new ArrayList<Group>();
 			while(rs.next()) {
 				GroupId id = new GroupId(rs.getBytes(1));
+				if(subscribed.contains(id)) continue;
 				String name = rs.getString(2);
 				byte[] publicKey = rs.getBytes(3);
-				groups.add(new Group(id, name, publicKey));
+				Group group = new Group(id, name, publicKey);
+				groups.add(new GroupStatus(group, false, false));
 			}
 			rs.close();
 			ps.close();
@@ -2765,6 +2804,21 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public void removeLocalGroup(Connection txn, GroupId g) throws DbException {
+		PreparedStatement ps = null;
+		try {
+			String sql = "DELETE FROM localGroups WHERE groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, g.getBytes());
+			int affected = ps.executeUpdate();
+			if(affected != 1) throw new DbStateException();
+			ps.close();
+		} catch(SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public void removeMessage(Connection txn, MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		try {
@@ -3406,13 +3460,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void setVisibleToAll(Connection txn, GroupId g, boolean visible)
+	public void setVisibleToAll(Connection txn, GroupId g, boolean all)
 			throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "UPDATE groups SET visibleToAll = ? WHERE groupId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setBoolean(1, visible);
+			ps.setBoolean(1, all);
 			ps.setBytes(2, g.getBytes());
 			int affected = ps.executeUpdate();
 			if(affected > 1) throw new DbStateException();
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
index b1e8cfcf6b5479abb54b89b8f87f85608887b47e..02e69ad48b2fa177303099038131b728d9364398 100644
--- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java
@@ -187,6 +187,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).getVisibility(txn, groupId);
 			will(returnValue(Collections.emptyList()));
 			oneOf(database).removeSubscription(txn, groupId);
+			oneOf(database).containsLocalGroup(txn, groupId);
+			will(returnValue(false));
 			oneOf(listener).eventOccurred(with(any(
 					SubscriptionRemovedEvent.class)));
 			oneOf(listener).eventOccurred(with(any(
diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
index 65b53ff1be634d71f11e5c20fbc2ecb2c49d82b5..632f0cb72e163a80e7f3cf6ce05622a490d2f630 100644
--- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
+++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
@@ -35,6 +35,7 @@ import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.GroupMessageHeader;
 import net.sf.briar.api.messaging.Group;
 import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.GroupStatus;
 import net.sf.briar.api.messaging.Message;
 import net.sf.briar.api.messaging.MessageId;
 import net.sf.briar.api.transport.Endpoint;
@@ -1831,21 +1832,82 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testGetAvailableGroups() throws Exception {
+		ContactId contactId1 = new ContactId(2);
+		AuthorId authorId1 = new AuthorId(TestUtils.getRandomId());
+		Author author1 = new Author(authorId1, "Carol", new byte[60]);
+
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact who subscribes to a group
+		// Add two contacts who subscribe to a group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
+		assertEquals(contactId1, db.addContact(txn, author1, localAuthorId));
 		db.setSubscriptions(txn, contactId, Arrays.asList(group), 1);
+		db.setSubscriptions(txn, contactId1, Arrays.asList(group), 1);
 
-		// The group should be available
+		// The group should be available, not subscribed, not visible to all
 		assertEquals(Collections.emptyList(), db.getSubscriptions(txn));
-		assertEquals(Arrays.asList(group), db.getAvailableGroups(txn));
+		Iterator<GroupStatus> it = db.getAvailableGroups(txn).iterator();
+		assertTrue(it.hasNext());
+		GroupStatus status = it.next();
+		assertEquals(group, status.getGroup());
+		assertFalse(status.isSubscribed());
+		assertFalse(status.isVisibleToAll());
+		assertFalse(it.hasNext());
 
-		// Subscribe to the group - it should no longer be available
+		// Subscribe to the group - it should be available, subscribed,
+		// not visible to all
 		db.addSubscription(txn, group);
 		assertEquals(Arrays.asList(group), db.getSubscriptions(txn));
+		it = db.getAvailableGroups(txn).iterator();
+		assertTrue(it.hasNext());
+		status = it.next();
+		assertEquals(group, status.getGroup());
+		assertTrue(status.isSubscribed());
+		assertFalse(status.isVisibleToAll());
+		assertFalse(it.hasNext());
+
+		// The first contact unsubscribes - the group should be available,
+		// subscribed, not visible to all
+		db.setSubscriptions(txn, contactId, Collections.<Group>emptyList(), 2);
+		assertEquals(Arrays.asList(group), db.getSubscriptions(txn));
+		it = db.getAvailableGroups(txn).iterator();
+		assertTrue(it.hasNext());
+		status = it.next();
+		assertEquals(group, status.getGroup());
+		assertTrue(status.isSubscribed());
+		assertFalse(status.isVisibleToAll());
+		assertFalse(it.hasNext());
+
+		// Make the group visible to all contacts - it should be available,
+		// subscribed, visible to all
+		db.setVisibleToAll(txn, groupId, true);
+		assertEquals(Arrays.asList(group), db.getSubscriptions(txn));
+		it = db.getAvailableGroups(txn).iterator();
+		assertTrue(it.hasNext());
+		status = it.next();
+		assertEquals(group, status.getGroup());
+		assertTrue(status.isSubscribed());
+		assertTrue(status.isVisibleToAll());
+		assertFalse(it.hasNext());
+
+		// Unsubscribe from the group - it should be available, not subscribed,
+		// not visible to all
+		db.removeSubscription(txn, groupId);
+		assertEquals(Collections.emptyList(), db.getSubscriptions(txn));
+		it = db.getAvailableGroups(txn).iterator();
+		assertTrue(it.hasNext());
+		status = it.next();
+		assertEquals(group, status.getGroup());
+		assertFalse(status.isSubscribed());
+		assertFalse(status.isVisibleToAll());
+		assertFalse(it.hasNext());
+
+		// The second contact unsubscribes - the group should no longer be
+		// available
+		db.setSubscriptions(txn, contactId1, Collections.<Group>emptyList(), 2);
+		assertEquals(Collections.emptyList(), db.getSubscriptions(txn));
 		assertEquals(Collections.emptyList(), db.getAvailableGroups(txn));
 
 		db.commitTransaction(txn);