From 65b47bb5d2fe573aad727e2501ca55bc64efec20 Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Tue, 11 Oct 2016 10:17:51 -0300
Subject: [PATCH] Refactor Forum Controller, so it can be used by private
 groups

---
 .../briarproject/android/BriarActivity.java   |   1 +
 .../android/forum/CreateForumActivity.java    |   3 +-
 .../android/forum/ForumActivity.java          | 137 ++------
 .../android/forum/ForumController.java        |  45 +--
 .../android/forum/ForumControllerImpl.java    | 306 +++-------------
 .../android/forum/ForumListAdapter.java       |  16 +-
 .../android/threaded/ThreadListActivity.java  | 121 ++++++-
 .../threaded/ThreadListController.java        |  48 +++
 .../threaded/ThreadListControllerImpl.java    | 327 ++++++++++++++++++
 .../android/forum/ForumActivityTest.java      |   6 +-
 10 files changed, 569 insertions(+), 441 deletions(-)
 create mode 100644 briar-android/src/org/briarproject/android/threaded/ThreadListController.java
 create mode 100644 briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java

diff --git a/briar-android/src/org/briarproject/android/BriarActivity.java b/briar-android/src/org/briarproject/android/BriarActivity.java
index 49dc574a85..4c01808af7 100644
--- a/briar-android/src/org/briarproject/android/BriarActivity.java
+++ b/briar-android/src/org/briarproject/android/BriarActivity.java
@@ -26,6 +26,7 @@ public abstract class BriarActivity extends BaseActivity {
 			"briar.LOCAL_AUTHOR_HANDLE";
 	public static final String KEY_STARTUP_FAILED = "briar.STARTUP_FAILED";
 	public static final String GROUP_ID = "briar.GROUP_ID";
+	public static final String GROUP_NAME = "briar.GROUP_NAME";
 
 	public static final int REQUEST_PASSWORD = 1;
 
diff --git a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
index ff1644b1c9..4c90245999 100644
--- a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
@@ -31,7 +31,6 @@ import static android.view.View.VISIBLE;
 import static android.widget.Toast.LENGTH_LONG;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
-import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
 import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
 
 public class CreateForumActivity extends BriarActivity
@@ -150,7 +149,7 @@ public class CreateForumActivity extends BriarActivity
 				Intent i = new Intent(CreateForumActivity.this,
 						ForumActivity.class);
 				i.putExtra(GROUP_ID, f.getId().getBytes());
-				i.putExtra(FORUM_NAME, f.getName());
+				i.putExtra(GROUP_NAME, f.getName());
 				startActivity(i);
 				Toast.makeText(CreateForumActivity.this,
 						R.string.forum_created_toast, LENGTH_LONG).show();
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index 0d74863a87..74537eed76 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -2,9 +2,7 @@ package org.briarproject.android.forum;
 
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.os.Bundle;
 import android.support.annotation.LayoutRes;
-import android.support.annotation.Nullable;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.ActivityOptionsCompat;
 import android.support.v7.app.AlertDialog;
@@ -17,18 +15,13 @@ import android.widget.Toast;
 import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.controller.handler.UiResultExceptionHandler;
-import org.briarproject.android.controller.handler.UiResultHandler;
-import org.briarproject.android.forum.ForumController.ForumPostListener;
 import org.briarproject.android.sharing.ShareForumActivity;
 import org.briarproject.android.sharing.SharingStatusForumActivity;
 import org.briarproject.android.threaded.ThreadListActivity;
+import org.briarproject.android.threaded.ThreadListController;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumPostHeader;
-import org.briarproject.util.StringUtils;
-
-import java.util.ArrayList;
-import java.util.List;
 
 import javax.inject.Inject;
 
@@ -37,10 +30,8 @@ import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
 import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
 import static android.widget.Toast.LENGTH_SHORT;
 
-public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAdapter>
-		implements ForumPostListener {
-
-	static final String FORUM_NAME = "briar.FORUM_NAME";
+public class ForumActivity extends
+		ThreadListActivity<Forum, ForumEntry, ForumPostHeader, NestedForumAdapter> {
 
 	private static final int REQUEST_FORUM_SHARED = 3;
 
@@ -53,41 +44,8 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda
 	}
 
 	@Override
-	public void onCreate(final Bundle state) {
-		super.onCreate(state);
-
-		Intent i = getIntent();
-		String forumName = i.getStringExtra(FORUM_NAME);
-		if (forumName != null) setTitle(forumName);
-
-		forumController.loadForum(groupId,
-				new UiResultExceptionHandler<List<ForumEntry>, DbException>(
-						this) {
-					@Override
-					public void onResultUi(List<ForumEntry> result) {
-						Forum forum = forumController.getForum();
-						if (forum != null) setTitle(forum.getName());
-						List<ForumEntry> entries = new ArrayList<>(result);
-						if (entries.isEmpty()) {
-							list.showData();
-						} else {
-							adapter.setItems(entries);
-							list.showData();
-							if (state != null) {
-								byte[] replyId =
-										state.getByteArray(KEY_REPLY_ID);
-								if (replyId != null)
-									adapter.setReplyItemById(replyId);
-							}
-						}
-					}
-
-					@Override
-					public void onExceptionUi(DbException exception) {
-						// TODO Improve UX ?
-						finish();
-					}
-				});
+	protected ThreadListController<Forum, ForumEntry, ForumPostHeader> getController() {
+		return forumController;
 	}
 
 	@Override
@@ -101,12 +59,6 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda
 		return new NestedForumAdapter(this, layoutManager);
 	}
 
-	@Override
-	public void onResume() {
-		super.onResume();
-		notificationManager.clearForumPostNotification(groupId);
-	}
-
 	@Override
 	protected void onActivityResult(int request, int result, Intent data) {
 		super.onActivityResult(request, result, data);
@@ -157,55 +109,6 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda
 		}
 	}
 
-	protected void markItemRead(ForumEntry entry) {
-		forumController.entryRead(entry);
-	}
-
-	@Override
-	public void onForumPostReceived(ForumPostHeader header) {
-		forumController.loadPost(header,
-				new UiResultExceptionHandler<ForumEntry, DbException>(this) {
-					@Override
-					public void onResultUi(final ForumEntry result) {
-						addItem(result, false);
-					}
-
-					@Override
-					public void onExceptionUi(DbException exception) {
-						// TODO add proper exception handling
-					}
-				});
-	}
-
-	@Override
-	protected void sendItem(String text, @Nullable ForumEntry replyItem) {
-		UiResultExceptionHandler<ForumEntry, DbException> handler =
-				new UiResultExceptionHandler<ForumEntry, DbException>(this) {
-					@Override
-					public void onResultUi(ForumEntry result) {
-						addItem(result, true);
-					}
-
-					@Override
-					public void onExceptionUi(DbException exception) {
-						// TODO Improve UX ?
-						finish();
-					}
-				};
-		if (replyItem == null) {
-			// root post
-			forumController.createPost(StringUtils.toUtf8(text), handler);
-		} else {
-			forumController.createPost(StringUtils.toUtf8(text),
-					replyItem.getId(), handler);
-		}
-	}
-
-	@Override
-	public void onForumRemoved() {
-		supportFinishAfterTransition();
-	}
-
 	@Override
 	protected int getItemPostedString() {
 		return R.string.forum_new_entry_posted;
@@ -220,18 +123,24 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda
 		DialogInterface.OnClickListener okListener =
 				new DialogInterface.OnClickListener() {
 					@Override
-					public void onClick(DialogInterface dialog, int which) {
-						forumController.unsubscribe(
-								new UiResultHandler<Boolean>(
+					public void onClick(final DialogInterface dialog,
+							int which) {
+						forumController.deleteGroupItem(
+								new UiResultExceptionHandler<Void, DbException>(
 										ForumActivity.this) {
 									@Override
-									public void onResultUi(Boolean result) {
-										if (result) {
-											Toast.makeText(ForumActivity.this,
-													R.string.forum_left_toast,
-													LENGTH_SHORT)
-													.show();
-										}
+									public void onResultUi(Void v) {
+										Toast.makeText(ForumActivity.this,
+												R.string.forum_left_toast,
+												LENGTH_SHORT)
+												.show();
+									}
+
+									@Override
+									public void onExceptionUi(
+											DbException exception) {
+										// TODO proper error handling
+										dialog.dismiss();
 									}
 								});
 					}
@@ -241,8 +150,8 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda
 						R.style.BriarDialogTheme);
 		builder.setTitle(getString(R.string.dialog_title_leave_forum));
 		builder.setMessage(getString(R.string.dialog_message_leave_forum));
-		builder.setPositiveButton(R.string.dialog_button_leave, okListener);
-		builder.setNegativeButton(android.R.string.cancel, null);
+		builder.setNegativeButton(R.string.dialog_button_leave, okListener);
+		builder.setPositiveButton(R.string.cancel, null);
 		builder.show();
 	}
 
diff --git a/briar-android/src/org/briarproject/android/forum/ForumController.java b/briar-android/src/org/briarproject/android/forum/ForumController.java
index ad08dbc4dd..275ccc135e 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumController.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumController.java
@@ -1,51 +1,12 @@
 package org.briarproject.android.forum;
 
-import android.support.annotation.Nullable;
-import android.support.annotation.UiThread;
-
-import org.briarproject.android.DestroyableContext;
-import org.briarproject.android.controller.ActivityLifecycleController;
 import org.briarproject.android.controller.handler.ResultExceptionHandler;
-import org.briarproject.android.controller.handler.ResultHandler;
+import org.briarproject.android.threaded.ThreadListController;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumPostHeader;
-import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
-
-import java.util.Collection;
-import java.util.List;
-
-public interface ForumController extends ActivityLifecycleController {
-
-	void loadForum(GroupId groupId,
-			ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler);
-
-	@Nullable
-	Forum getForum();
-
-	void loadPost(ForumPostHeader header,
-			ResultExceptionHandler<ForumEntry, DbException> resultHandler);
-
-	void unsubscribe(ResultHandler<Boolean> resultHandler);
-
-	void entryRead(ForumEntry forumEntry);
-
-	void entriesRead(Collection<ForumEntry> messageIds);
-
-	void createPost(byte[] body,
-			ResultExceptionHandler<ForumEntry, DbException> resultHandler);
-
-	void createPost(byte[] body, MessageId parentId,
-			ResultExceptionHandler<ForumEntry, DbException> resultHandler);
-
-	interface ForumPostListener extends DestroyableContext {
-
-		@UiThread
-		void onForumPostReceived(ForumPostHeader header);
 
-		@UiThread
-		void onForumRemoved();
-	}
+public interface ForumController
+		extends ThreadListController<Forum, ForumEntry, ForumPostHeader> {
 
 }
diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
index b6a8ddce52..442ce23a50 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
@@ -1,11 +1,10 @@
 package org.briarproject.android.forum;
 
-import android.app.Activity;
 import android.support.annotation.Nullable;
 
-import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.controller.handler.ResultExceptionHandler;
-import org.briarproject.android.controller.handler.ResultHandler;
+import org.briarproject.android.threaded.ThreadListControllerImpl;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.CryptoExecutor;
@@ -15,9 +14,7 @@ import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.ForumPostReceivedEvent;
-import org.briarproject.api.event.GroupRemovedEvent;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPost;
@@ -26,19 +23,12 @@ import org.briarproject.api.forum.ForumPostHeader;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.lifecycle.LifecycleManager;
-import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.StringUtils;
 
 import java.security.GeneralSecurityException;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicLong;
 import java.util.logging.Logger;
 
 import javax.inject.Inject;
@@ -47,25 +37,15 @@ import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.identity.Author.Status.OURSELVES;
 
-public class ForumControllerImpl extends DbControllerImpl
-		implements ForumController, EventListener {
+public class ForumControllerImpl
+		extends ThreadListControllerImpl<Forum, ForumEntry, ForumPostHeader>
+		implements ForumController {
 
 	private static final Logger LOG =
 			Logger.getLogger(ForumControllerImpl.class.getName());
 
-	private final Executor cryptoExecutor;
 	private final ForumPostFactory forumPostFactory;
-	private final CryptoComponent crypto;
 	private final ForumManager forumManager;
-	private final EventBus eventBus;
-	private final IdentityManager identityManager;
-
-	private final Map<MessageId, byte[]> bodyCache = new ConcurrentHashMap<>();
-	private final AtomicLong newestTimeStamp = new AtomicLong();
-
-	private volatile LocalAuthor localAuthor = null;
-	private volatile Forum forum = null;
-	private volatile ForumPostListener listener;
 
 	@Inject
 	ForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
@@ -73,257 +53,70 @@ public class ForumControllerImpl extends DbControllerImpl
 			@CryptoExecutor Executor cryptoExecutor,
 			ForumPostFactory forumPostFactory, CryptoComponent crypto,
 			ForumManager forumManager, EventBus eventBus,
-			IdentityManager identityManager) {
-		super(dbExecutor, lifecycleManager);
-		this.cryptoExecutor = cryptoExecutor;
-		this.forumPostFactory = forumPostFactory;
-		this.crypto = crypto;
+			IdentityManager identityManager,
+			AndroidNotificationManager notificationManager) {
+		super(dbExecutor, lifecycleManager, cryptoExecutor, crypto, eventBus,
+				identityManager, notificationManager);
 		this.forumManager = forumManager;
-		this.eventBus = eventBus;
-		this.identityManager = identityManager;
-	}
-
-	@Override
-	public void onActivityCreate(Activity activity) {
-		if (activity instanceof ForumPostListener) {
-			listener = (ForumPostListener) activity;
-		} else {
-			throw new IllegalStateException(
-					"An activity that injects the ForumController must " +
-							"implement the ForumPostListener");
-		}
+		this.forumPostFactory = forumPostFactory;
 	}
 
 	@Override
 	public void onActivityResume() {
-		eventBus.addListener(this);
-	}
-
-	@Override
-	public void onActivityPause() {
-		eventBus.removeListener(this);
-	}
-
-	@Override
-	public void onActivityDestroy() {
+		super.onActivityResume();
+		notificationManager.clearForumPostNotification(groupId);
 	}
 
 	@Override
 	public void eventOccurred(Event e) {
-		if (forum == null) return;
+		super.eventOccurred(e);
+
 		if (e instanceof ForumPostReceivedEvent) {
 			final ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
-			if (pe.getGroupId().equals(forum.getId())) {
+			if (pe.getGroupId().equals(groupId)) {
 				LOG.info("Forum post received, adding...");
 				final ForumPostHeader fph = pe.getForumPostHeader();
 				updateNewestTimestamp(fph.getTimestamp());
 				listener.runOnUiThreadUnlessDestroyed(new Runnable() {
 					@Override
 					public void run() {
-						listener.onForumPostReceived(fph);
-					}
-				});
-			}
-		} else if (e instanceof GroupRemovedEvent) {
-			GroupRemovedEvent s = (GroupRemovedEvent) e;
-			if (s.getGroup().getId().equals(forum.getId())) {
-				LOG.info("Forum removed");
-				listener.runOnUiThreadUnlessDestroyed(new Runnable() {
-					@Override
-					public void run() {
-						listener.onForumRemoved();
+						listener.onHeaderReceived(fph);
 					}
 				});
 			}
 		}
 	}
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
-	private void loadForum(GroupId groupId) throws DbException {
-		// Get Forum
-		long now = System.currentTimeMillis();
-		forum = forumManager.getForum(groupId);
-		long duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Loading forum took " + duration + " ms");
-
-		// Get First Identity
-		now = System.currentTimeMillis();
-		localAuthor = identityManager.getLocalAuthor();
-		duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Loading author took " + duration + " ms");
+	@Override
+	protected Forum loadGroupItem() throws DbException {
+		return forumManager.getForum(groupId);
 	}
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
-	private Collection<ForumPostHeader> loadHeaders() throws DbException {
-		if (forum == null)
-			throw new RuntimeException("Forum has not been initialized");
-
-		// Get Headers
-		long now = System.currentTimeMillis();
-		Collection<ForumPostHeader> headers =
-				forumManager.getPostHeaders(forum.getId());
-		long duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Loading headers took " + duration + " ms");
-		return headers;
+	@Override
+	protected Collection<ForumPostHeader> loadHeaders() throws DbException {
+		return forumManager.getPostHeaders(groupId);
 	}
 
-	/**
-	 * This should only be run from the DbThread.
-	 *
-	 * @throws DbException
-	 */
-	private void loadBodies(Collection<ForumPostHeader> headers)
+	@Override
+	protected void loadBodies(Collection<ForumPostHeader> headers)
 			throws DbException {
-		// Get Bodies
-		long now = System.currentTimeMillis();
 		for (ForumPostHeader header : headers) {
 			if (!bodyCache.containsKey(header.getId())) {
-				byte[] body = forumManager.getPostBody(header.getId());
+				String body = StringUtils
+						.fromUtf8(forumManager.getPostBody(header.getId()));
 				bodyCache.put(header.getId(), body);
 			}
 		}
-		long duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Loading bodies took " + duration + " ms");
-	}
-
-	private List<ForumEntry> buildForumEntries(
-			Collection<ForumPostHeader> headers) {
-		List<ForumEntry> entries = new ArrayList<>();
-		for (ForumPostHeader h : headers) {
-			byte[] body = bodyCache.get(h.getId());
-			entries.add(new ForumEntry(h, StringUtils.fromUtf8(body)));
-		}
-		return entries;
-	}
-
-	private void updateNewestTimeStamp(Collection<ForumPostHeader> headers) {
-		for (ForumPostHeader h : headers) {
-			updateNewestTimestamp(h.getTimestamp());
-		}
-	}
-
-	@Override
-	public void loadForum(final GroupId groupId,
-			final ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler) {
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				LOG.info("Loading forum...");
-				try {
-					if (forum == null) {
-						loadForum(groupId);
-					}
-					// Get Forum Posts and Bodies
-					Collection<ForumPostHeader> headers = loadHeaders();
-					updateNewestTimeStamp(headers);
-					loadBodies(headers);
-					resultHandler.onResult(buildForumEntries(headers));
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-					resultHandler.onException(e);
-				}
-			}
-		});
-	}
-
-	@Override
-	@Nullable
-	public Forum getForum() {
-		return forum;
-	}
-
-	@Override
-	public void loadPost(final ForumPostHeader header,
-			final ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				LOG.info("Loading post...");
-				try {
-					loadBodies(Collections.singletonList(header));
-					resultHandler.onResult(new ForumEntry(header, StringUtils
-							.fromUtf8(bodyCache.get(header.getId()))));
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-					resultHandler.onException(e);
-				}
-			}
-		});
-	}
-
-	@Override
-	public void unsubscribe(final ResultHandler<Boolean> resultHandler) {
-		if (forum == null) return;
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					forumManager.removeForum(forum);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Removing forum took " + duration + " ms");
-					resultHandler.onResult(true);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-					resultHandler.onResult(false);
-				}
-			}
-		});
-	}
-
-	@Override
-	public void entryRead(ForumEntry forumEntry) {
-		entriesRead(Collections.singletonList(forumEntry));
 	}
 
 	@Override
-	public void entriesRead(final Collection<ForumEntry> forumEntries) {
-		if (forum == null) return;
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					long now = System.currentTimeMillis();
-					for (ForumEntry fe : forumEntries) {
-						forumManager
-								.setReadFlag(forum.getId(), fe.getId(), true);
-					}
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Marking read took " + duration + " ms");
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
+	protected void markRead(MessageId id) throws DbException {
+		forumManager.setReadFlag(groupId, id, true);
 	}
 
 	@Override
-	public void createPost(byte[] body,
-			ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
-		createPost(body, null, resultHandler);
-	}
-
-	@Override
-	public void createPost(final byte[] body, final MessageId parentId,
-			final ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
+	public void send(final String body, @Nullable final MessageId parentId,
+			final ResultExceptionHandler<ForumEntry, DbException> handler) {
 		cryptoExecutor.execute(new Runnable() {
 			@Override
 			public void run() {
@@ -332,17 +125,24 @@ public class ForumControllerImpl extends DbControllerImpl
 				timestamp = Math.max(timestamp, newestTimeStamp.get());
 				ForumPost p;
 				try {
+					LocalAuthor a = identityManager.getLocalAuthor();
 					KeyParser keyParser = crypto.getSignatureKeyParser();
-					byte[] b = localAuthor.getPrivateKey();
-					PrivateKey authorKey = keyParser.parsePrivateKey(b);
-					p = forumPostFactory.createPseudonymousPost(
-							forum.getId(), timestamp, parentId, localAuthor,
-							"text/plain", body, authorKey);
+					byte[] k = a.getPrivateKey();
+					PrivateKey authorKey = keyParser.parsePrivateKey(k);
+					byte[] b = StringUtils.toUtf8(body);
+					p = forumPostFactory
+							.createPseudonymousPost(groupId, timestamp,
+									parentId, a, "text/plain", b, authorKey);
 				} catch (GeneralSecurityException | FormatException e) {
 					throw new RuntimeException(e);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+					return;
 				}
 				bodyCache.put(p.getMessage().getId(), body);
-				storePost(p, resultHandler);
+				storePost(p, handler);
 			}
 		});
 	}
@@ -366,9 +166,8 @@ public class ForumControllerImpl extends DbControllerImpl
 									p.getMessage().getTimestamp(),
 									p.getAuthor(), OURSELVES, true);
 
-					resultHandler.onResult(new ForumEntry(h, StringUtils
-							.fromUtf8(bodyCache.get(p.getMessage().getId()))));
-
+					resultHandler.onResult(new ForumEntry(h,
+							bodyCache.get(p.getMessage().getId())));
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -378,11 +177,14 @@ public class ForumControllerImpl extends DbControllerImpl
 		});
 	}
 
-	private void updateNewestTimestamp(long update) {
-		long newest = newestTimeStamp.get();
-		while (newest < update) {
-			if (newestTimeStamp.compareAndSet(newest, update)) return;
-			newest = newestTimeStamp.get();
-		}
+	@Override
+	protected void deleteGroupItem(Forum forum) throws DbException {
+		forumManager.removeForum(forum);
 	}
+
+	@Override
+	protected ForumEntry buildItem(ForumPostHeader header) {
+		return new ForumEntry(header, bodyCache.get(header.getId()));
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
index ca237eb35e..040b81662b 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
@@ -2,7 +2,6 @@ package org.briarproject.android.forum;
 
 import android.content.Context;
 import android.content.Intent;
-import android.support.annotation.Nullable;
 import android.support.v4.content.ContextCompat;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
@@ -21,7 +20,7 @@ import static android.support.v7.util.SortedList.INVALID_POSITION;
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 import static org.briarproject.android.BriarActivity.GROUP_ID;
-import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
+import static org.briarproject.android.BriarActivity.GROUP_NAME;
 
 class ForumListAdapter
 		extends BriarAdapter<ForumListItem, ForumListAdapter.ForumViewHolder> {
@@ -84,7 +83,7 @@ class ForumListAdapter
 				Intent i = new Intent(ctx, ForumActivity.class);
 				Forum f = item.getForum();
 				i.putExtra(GROUP_ID, f.getId().getBytes());
-				i.putExtra(FORUM_NAME, f.getName());
+				i.putExtra(GROUP_NAME, f.getName());
 				ctx.startActivity(i);
 			}
 		});
@@ -115,17 +114,6 @@ class ForumListAdapter
 		return a.getForum().equals(b.getForum());
 	}
 
-	@Nullable
-	public ForumListItem findItem(GroupId g) {
-		for (int i = 0; i < items.size(); i++) {
-			ForumListItem item = items.get(i);
-			if (item.getForum().getGroup().getId().equals(g)) {
-				return item;
-			}
-		}
-		return null;
-	}
-
 	int findItemPosition(GroupId g) {
 		int count = getItemCount();
 		for (int i = 0; i < count; i++) {
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
index ea18e720f5..6a74779230 100644
--- a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
@@ -14,36 +14,44 @@ import android.view.View;
 
 import org.briarproject.R;
 import org.briarproject.android.BriarActivity;
-import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.controller.handler.UiResultExceptionHandler;
 import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
+import org.briarproject.android.threaded.ThreadListController.ThreadListListener;
 import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.android.view.TextInputView;
 import org.briarproject.android.view.TextInputView.TextInputListener;
+import org.briarproject.api.clients.BaseGroup;
+import org.briarproject.api.clients.PostHeader;
+import org.briarproject.api.db.DbException;
 import org.briarproject.api.sync.GroupId;
 
-import javax.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 
 import static android.support.design.widget.Snackbar.make;
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
-public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadItemAdapter<I>>
+public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadItem, H extends PostHeader, A extends ThreadItemAdapter<I>>
 		extends BriarActivity
-		implements TextInputListener, ThreadItemListener<I> {
+		implements ThreadListListener<H>, TextInputListener,
+		ThreadItemListener<I> {
 
 	protected static final String KEY_INPUT_VISIBILITY = "inputVisibility";
 	protected static final String KEY_REPLY_ID = "replyId";
 
-	@Inject
-	protected AndroidNotificationManager notificationManager;
-
 	protected A adapter;
 	protected BriarRecyclerView list;
 	protected TextInputView textInput;
 	protected GroupId groupId;
+	private byte[] replyId;
+
+	protected abstract ThreadListController<G, I, H> getController();
 
 	@CallSuper
 	@Override
+	@SuppressWarnings("ConstantConditions")
 	public void onCreate(final Bundle state) {
 		super.onCreate(state);
 
@@ -53,6 +61,10 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
 		byte[] b = i.getByteArrayExtra(GROUP_ID);
 		if (b == null) throw new IllegalStateException("No GroupId in intent.");
 		groupId = new GroupId(b);
+		getController().setGroupId(groupId);
+		String groupName = i.getStringExtra(GROUP_NAME);
+		if (groupName != null) setTitle(groupName);
+		else loadAndSetTitle();
 
 		textInput = (TextInputView) findViewById(R.id.text_input_container);
 		textInput.setVisibility(GONE);
@@ -62,17 +74,64 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
 		list.setLayoutManager(linearLayoutManager);
 		adapter = createAdapter(linearLayoutManager);
 		list.setAdapter(adapter);
+
+		if (state != null) {
+			replyId = state.getByteArray(KEY_REPLY_ID);
+		}
+
+		loadItems();
 	}
 
 	protected abstract @LayoutRes int getLayout();
 
 	protected abstract A createAdapter(LinearLayoutManager layoutManager);
 
+	private void loadAndSetTitle() {
+		getController().loadGroupItem(
+				new UiResultExceptionHandler<G, DbException>(this) {
+					@Override
+					public void onResultUi(G forum) {
+						setTitle(forum.getName());
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO Proper error handling
+						finish();
+					}
+				});
+	}
+
+	private void loadItems() {
+		getController().loadItems(
+				new UiResultExceptionHandler<Collection<I>, DbException>(
+						this) {
+					@Override
+					public void onResultUi(Collection<I> result) {
+						// FIXME What's the benefit of copying the collection?
+						List<I> items = new ArrayList<>(result);
+						if (items.isEmpty()) {
+							list.showData();
+						} else {
+							adapter.setItems(items);
+							list.showData();
+							if (replyId != null)
+								adapter.setReplyItemById(replyId);
+						}
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO Proper error handling
+						finish();
+					}
+				});
+	}
+
 	@CallSuper
 	@Override
 	public void onResume() {
 		super.onResume();
-		notificationManager.blockNotification(groupId);
 		list.startPeriodicUpdate();
 	}
 
@@ -80,7 +139,6 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
 	@Override
 	public void onPause() {
 		super.onPause();
-		notificationManager.unblockNotification(groupId);
 		list.stopPeriodicUpdate();
 	}
 
@@ -130,12 +188,10 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
 	public void onItemVisible(I item) {
 		if (!item.isRead()) {
 			item.setRead(true);
-			markItemRead(item);
+			getController().markItemRead(item);
 		}
 	}
 
-	protected abstract void markItemRead(I item);
-
 	@Override
 	public void onReplyClick(I item) {
 		showTextInput(item);
@@ -167,13 +223,50 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
 		if (text.trim().length() == 0)
 			return;
 		I replyItem = adapter.getReplyItem();
-		sendItem(text, replyItem);
+		UiResultExceptionHandler<I, DbException> handler =
+				new UiResultExceptionHandler<I, DbException>(this) {
+					@Override
+					public void onResultUi(I result) {
+						addItem(result, true);
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO add proper exception handling
+						finish();
+					}
+				};
+		if (replyItem == null) {
+			// root post
+			getController().send(text, handler);
+		} else {
+			getController().send(text, replyItem.getId(), handler);
+		}
 		textInput.hideSoftKeyboard();
 		textInput.setVisibility(GONE);
 		adapter.setReplyItem(null);
 	}
 
-	protected abstract void sendItem(String text, I replyToItem);
+	@Override
+	public void onHeaderReceived(H header) {
+		getController().loadItem(header,
+				new UiResultExceptionHandler<I, DbException>(this) {
+					@Override
+					public void onResultUi(final I result) {
+						addItem(result, false);
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO add proper exception handling
+					}
+				});
+	}
+
+	@Override
+	public void onGroupRemoved() {
+		supportFinishAfterTransition();
+	}
 
 	protected void addItem(final I item, boolean isLocal) {
 		adapter.add(item);
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
new file mode 100644
index 0000000000..ec876f3602
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
@@ -0,0 +1,48 @@
+package org.briarproject.android.threaded;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+
+import org.briarproject.android.DestroyableContext;
+import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.clients.BaseGroup;
+import org.briarproject.api.clients.PostHeader;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.Collection;
+
+public interface ThreadListController<G extends BaseGroup, I extends ThreadItem, H extends PostHeader>
+		extends ActivityLifecycleController {
+
+	void setGroupId(GroupId groupId);
+
+	void loadGroupItem(ResultExceptionHandler<G, DbException> handler);
+
+	void loadItem(H header, ResultExceptionHandler<I, DbException> handler);
+
+	void loadItems(ResultExceptionHandler<Collection<I>, DbException> handler);
+
+	void markItemRead(I item);
+
+	void markItemsRead(Collection<I> items);
+
+	void send(String body, ResultExceptionHandler<I, DbException> handler);
+
+	void send(String body, @Nullable MessageId parentId,
+			ResultExceptionHandler<I, DbException> handler);
+
+	void deleteGroupItem(ResultExceptionHandler<Void, DbException> handler);
+
+	interface ThreadListListener<H> extends DestroyableContext {
+		@UiThread
+		void onHeaderReceived(H header);
+
+		@UiThread
+		void onGroupRemoved();
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
new file mode 100644
index 0000000000..e60f310c45
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
@@ -0,0 +1,327 @@
+package org.briarproject.android.threaded;
+
+import android.app.Activity;
+import android.support.annotation.CallSuper;
+
+import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.clients.BaseGroup;
+import org.briarproject.api.clients.PostHeader;
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.CryptoExecutor;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumPostFactory;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends ThreadItem, H extends PostHeader>
+		extends DbControllerImpl
+		implements ThreadListController<G, I, H>, EventListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(ThreadListControllerImpl.class.getName());
+
+	protected final Executor cryptoExecutor;
+	protected final CryptoComponent crypto;
+	protected final EventBus eventBus;
+	protected final IdentityManager identityManager;
+	protected final AndroidNotificationManager notificationManager;
+
+	protected final Map<MessageId, String> bodyCache =
+			new ConcurrentHashMap<>();
+	protected final AtomicLong newestTimeStamp = new AtomicLong();
+
+	protected volatile GroupId groupId;
+
+	protected ThreadListListener<H> listener;
+
+	protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			LifecycleManager lifecycleManager,
+			@CryptoExecutor Executor cryptoExecutor, CryptoComponent crypto,
+			EventBus eventBus, IdentityManager identityManager,
+			AndroidNotificationManager notificationManager) {
+		super(dbExecutor, lifecycleManager);
+		this.cryptoExecutor = cryptoExecutor;
+		this.crypto = crypto;
+		this.eventBus = eventBus;
+		this.identityManager = identityManager;
+		this.notificationManager = notificationManager;
+	}
+
+	@Override
+	public void setGroupId(GroupId groupId) {
+		this.groupId = groupId;
+	}
+
+	@CallSuper
+	@SuppressWarnings("unchecked")
+	@Override
+	public void onActivityCreate(Activity activity) {
+		listener = (ThreadListListener<H>) activity;
+	}
+
+	@CallSuper
+	@Override
+	public void onActivityResume() {
+		checkGroupId();
+		notificationManager.blockNotification(groupId);
+		eventBus.addListener(this);
+	}
+
+	@CallSuper
+	@Override
+	public void onActivityPause() {
+		notificationManager.unblockNotification(groupId);
+		eventBus.removeListener(this);
+	}
+
+	@Override
+	public void onActivityDestroy() {
+	}
+
+	@CallSuper
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof GroupRemovedEvent) {
+			GroupRemovedEvent s = (GroupRemovedEvent) e;
+			if (s.getGroup().getId().equals(groupId)) {
+				LOG.info("Group removed");
+				listener.runOnUiThreadUnlessDestroyed(new Runnable() {
+					@Override
+					public void run() {
+						listener.onGroupRemoved();
+					}
+				});
+			}
+		}
+	}
+
+	@Override
+	public void loadGroupItem(
+			final ResultExceptionHandler<G, DbException> handler) {
+		checkGroupId();
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					long now = System.currentTimeMillis();
+					G groupItem = loadGroupItem();
+					long duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info("Loading forum took " + duration + " ms");
+					handler.onResult(groupItem);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+	/**
+	 * This should only be run from the DbThread.
+	 *
+	 * @throws DbException
+	 */
+	protected abstract G loadGroupItem() throws DbException;
+
+	@Override
+	public void loadItems(
+			final ResultExceptionHandler<Collection<I>, DbException> handler) {
+		checkGroupId();
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				LOG.info("Loading items...");
+				try {
+					// Load headers
+					long now = System.currentTimeMillis();
+					Collection<H> headers = loadHeaders();
+					long duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info("Loading headers took " + duration + " ms");
+
+					// Update timestamp of newest item
+					updateNewestTimeStamp(headers);
+
+					// Load bodies
+					now = System.currentTimeMillis();
+					loadBodies(headers);
+					duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info("Loading bodies took " + duration + " ms");
+
+					// Build and hand over items
+					handler.onResult(buildItems(headers));
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+	/**
+	 * This should only be run from the DbThread.
+	 *
+	 * @throws DbException
+	 */
+	protected abstract Collection<H> loadHeaders() throws DbException;
+
+	/**
+	 * This should only be run from the DbThread.
+	 *
+	 * @throws DbException
+	 */
+	protected abstract void loadBodies(Collection<H> headers)
+			throws DbException;
+
+	@Override
+	public void loadItem(final H header,
+			final ResultExceptionHandler<I, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				LOG.info("Loading item...");
+				try {
+					loadBodies(Collections.singletonList(header));
+					I item = buildItem(header);
+					handler.onResult(item);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void markItemRead(I item) {
+		markItemsRead(Collections.singletonList(item));
+	}
+
+	@Override
+	public void markItemsRead(final Collection<I> items) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					long now = System.currentTimeMillis();
+					for (I i : items) {
+						markRead(i.getId());
+					}
+					long duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info("Marking read took " + duration + " ms");
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	/**
+	 * This should only be run from the DbThread.
+	 *
+	 * @throws DbException
+	 */
+	protected abstract void markRead(MessageId id) throws DbException;
+
+	@Override
+	public void send(String body,
+			ResultExceptionHandler<I, DbException> resultHandler) {
+		send(body, null, resultHandler);
+	}
+
+	@Override
+	public void deleteGroupItem(
+			final ResultExceptionHandler<Void, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					long now = System.currentTimeMillis();
+					G groupItem = loadGroupItem();
+					deleteGroupItem(groupItem);
+					long duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info("Removing group took " + duration + " ms");
+					//noinspection ConstantConditions
+					handler.onResult(null);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+	/**
+	 * This should only be run from the DbThread.
+	 *
+	 * @throws DbException
+	 */
+	protected abstract void deleteGroupItem(G groupItem) throws DbException;
+
+	private List<I> buildItems(Collection<H> headers) {
+		List<I> entries = new ArrayList<>();
+		for (H h : headers) {
+			entries.add(buildItem(h));
+		}
+		return entries;
+	}
+
+	/**
+	 * When building the item, the body can be assumed to be cached
+	 */
+	protected abstract I buildItem(H header);
+
+	private void updateNewestTimeStamp(Collection<H> headers) {
+		for (H h : headers) {
+			updateNewestTimestamp(h.getTimestamp());
+		}
+	}
+
+	protected void updateNewestTimestamp(long update) {
+		long newest = newestTimeStamp.get();
+		while (newest < update) {
+			if (newestTimeStamp.compareAndSet(newest, update)) return;
+			newest = newestTimeStamp.get();
+		}
+	}
+
+	private void checkGroupId() {
+		if (groupId == null) {
+			throw new IllegalStateException(
+					"You must set the GroupId before the controller is started.");
+		}
+	}
+
+}
diff --git a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
index 7842c11e2a..4a7fcf02e0 100644
--- a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
+++ b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
@@ -26,6 +26,7 @@ import org.robolectric.annotation.Config;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 
 import static junit.framework.Assert.assertEquals;
@@ -81,7 +82,7 @@ public class ForumActivityTest {
 
 	private TestForumActivity forumActivity;
 	@Captor
-	private ArgumentCaptor<UiResultExceptionHandler<List<ForumEntry>, DbException>>
+	private ArgumentCaptor<UiResultExceptionHandler<Collection<ForumEntry>, DbException>>
 			rc;
 
 	@Before
@@ -112,8 +113,7 @@ public class ForumActivityTest {
 	public void testNestedEntries() {
 		ForumController mc = forumActivity.getController();
 		List<ForumEntry> dummyData = getDummyData();
-		verify(mc, times(1))
-				.loadForum(Mockito.any(GroupId.class), rc.capture());
+		verify(mc, times(1)).loadItems(rc.capture());
 		rc.getValue().onResult(dummyData);
 		NestedForumAdapter adapter = forumActivity.getAdapter();
 		Assert.assertNotNull(adapter);
-- 
GitLab