From 92f2e7b0fcd2dfe3d250602e30bc087006024bec Mon Sep 17 00:00:00 2001
From: Ernir Erlingsson <ernir@ymirmobile.com>
Date: Tue, 27 Sep 2016 23:34:00 +0200
Subject: [PATCH] merge with master and fixes after comments

---
 .../android/forum/ForumActivity.java          |  90 ++++++-----
 .../android/forum/ForumController.java        |  14 +-
 .../android/forum/ForumControllerImpl.java    |  32 ++--
 .../android/forum/ForumEntry.java             |  12 +-
 .../android/forum/NestedForumAdapter.java     | 150 +++++++++---------
 .../android/util/NestedTreeList.java          |   4 +-
 .../android/forum/ForumActivityTest.java      |   7 +-
 .../briarproject/api/clients/MessageTree.java |   2 +
 .../briarproject/clients/MessageTreeImpl.java |  11 +-
 .../briarproject/clients/MessageTreeTest.java |  10 ++
 10 files changed, 186 insertions(+), 146 deletions(-)

diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index 19ce6d499e..2ed6ccbfb8 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -19,6 +19,7 @@ import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.BriarActivity;
 import org.briarproject.android.api.AndroidNotificationManager;
+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.forum.NestedForumAdapter.OnNestedForumListener;
@@ -27,8 +28,8 @@ import org.briarproject.android.sharing.SharingStatusForumActivity;
 import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.android.view.TextInputView;
 import org.briarproject.android.view.TextInputView.TextInputListener;
+import org.briarproject.api.db.DbException;
 import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumPost;
 import org.briarproject.api.forum.ForumPostHeader;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.util.StringUtils;
@@ -64,7 +65,6 @@ public class ForumActivity extends BriarActivity implements
 
 	private BriarRecyclerView recyclerView;
 	private TextInputView textInput;
-	private LinearLayoutManager linearLayoutManager;
 
 	private volatile GroupId groupId = null;
 
@@ -93,29 +93,31 @@ public class ForumActivity extends BriarActivity implements
 		recyclerView.setEmptyText(R.string.no_forum_posts);
 
 		forumController.loadForum(groupId,
-				new UiResultHandler<List<ForumEntry>>(this) {
+				new UiResultExceptionHandler<List<ForumEntry>, DbException>(
+						this) {
 					@Override
 					public void onResultUi(List<ForumEntry> result) {
-						if (result != null) {
-							Forum forum = forumController.getForum();
-							if (forum != null) setTitle(forum.getName());
-							List<ForumEntry> entries = new ArrayList<>(result);
-							if (entries.isEmpty()) {
-								recyclerView.showData();
-							} else {
-								forumAdapter.setEntries(entries);
-								if (state != null) {
-									byte[] replyId =
-											state.getByteArray(KEY_REPLY_ID);
-									if (replyId != null)
-										forumAdapter.setReplyEntryById(replyId);
-								}
-							}
+						Forum forum = forumController.getForum();
+						if (forum != null) setTitle(forum.getName());
+						List<ForumEntry> entries = new ArrayList<>(result);
+						if (entries.isEmpty()) {
+							recyclerView.showData();
 						} else {
-							// TODO Improve UX ?
-							finish();
+							forumAdapter.setEntries(entries);
+							if (state != null) {
+								byte[] replyId =
+										state.getByteArray(KEY_REPLY_ID);
+								if (replyId != null)
+									forumAdapter.setReplyEntryById(replyId);
+							}
 						}
 					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO Improve UX ?
+						finish();
+					}
 				});
 	}
 
@@ -248,22 +250,21 @@ public class ForumActivity extends BriarActivity implements
 	public void onSendClick(String text) {
 		if (text.trim().length() == 0)
 			return;
-		if (forumController.getForum() == null) return;
 		ForumEntry replyEntry = forumAdapter.getReplyEntry();
-		UiResultHandler<ForumPost> resultHandler =
-				new UiResultHandler<ForumPost>(this) {
+		UiResultExceptionHandler<ForumEntry, DbException> resultHandler =
+				new UiResultExceptionHandler<ForumEntry, DbException>(this) {
 					@Override
-					public void onResultUi(ForumPost result) {
-						forumController.storePost(result,
-								new UiResultHandler<ForumEntry>(
-										ForumActivity.this) {
-									@Override
-									public void onResultUi(ForumEntry result) {
-										onForumEntryAdded(result, true);
-									}
-								});
+					public void onResultUi(ForumEntry result) {
+						onForumEntryAdded(result, true);
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO Improve UX ?
+						finish();
 					}
 				};
+
 		if (replyEntry == null) {
 			// root post
 			forumController.createPost(StringUtils.toUtf8(text), resultHandler);
@@ -322,11 +323,14 @@ public class ForumActivity extends BriarActivity implements
 
 	private void onForumEntryAdded(final ForumEntry entry, boolean isLocal) {
 		forumAdapter.addEntry(entry);
-		if (isLocal) {
+		if (isLocal && forumAdapter.isVisible(entry)) {
 			displaySnackbarShort(R.string.forum_new_entry_posted);
 		} else {
 			Snackbar snackbar = Snackbar.make(recyclerView,
-					R.string.forum_new_entry_received, Snackbar.LENGTH_LONG);
+					isLocal ? R.string.forum_new_entry_posted :
+							R.string.forum_new_entry_received,
+					Snackbar.LENGTH_LONG);
+			snackbar.getView().setBackgroundResource(R.color.briar_primary);
 			snackbar.setActionTextColor(ContextCompat
 					.getColor(ForumActivity.this,
 							R.color.briar_button_positive));
@@ -343,12 +347,18 @@ public class ForumActivity extends BriarActivity implements
 
 	@Override
 	public void onExternalEntryAdded(ForumPostHeader header) {
-		forumController.loadPost(header, new UiResultHandler<ForumEntry>(this) {
-			@Override
-			public void onResultUi(final ForumEntry result) {
-				onForumEntryAdded(result, false);
-			}
-		});
+		forumController.loadPost(header,
+				new UiResultExceptionHandler<ForumEntry, DbException>(this) {
+					@Override
+					public void onResultUi(final ForumEntry result) {
+						onForumEntryAdded(result, false);
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO add proper exception handling
+					}
+				});
 
 	}
 }
diff --git a/briar-android/src/org/briarproject/android/forum/ForumController.java b/briar-android/src/org/briarproject/android/forum/ForumController.java
index 79499d48de..459aafb2ff 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumController.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumController.java
@@ -4,9 +4,10 @@ import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 
 import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
 import org.briarproject.android.controller.handler.ResultHandler;
+import org.briarproject.api.db.DbException;
 import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumPost;
 import org.briarproject.api.forum.ForumPostHeader;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
@@ -17,13 +18,13 @@ import java.util.List;
 public interface ForumController extends ActivityLifecycleController {
 
 	void loadForum(GroupId groupId,
-			ResultHandler<List<ForumEntry>> resultHandler);
+			ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler);
 
 	@Nullable
 	Forum getForum();
 
 	void loadPost(ForumPostHeader header,
-			ResultHandler<ForumEntry> resultHandler);
+			ResultExceptionHandler<ForumEntry, DbException> resultHandler);
 
 	void unsubscribe(ResultHandler<Boolean> resultHandler);
 
@@ -31,12 +32,11 @@ public interface ForumController extends ActivityLifecycleController {
 
 	void entriesRead(Collection<ForumEntry> messageIds);
 
-	void createPost(byte[] body, ResultHandler<ForumPost> resultHandler);
+	void createPost(byte[] body,
+			ResultExceptionHandler<ForumEntry, DbException> resultHandler);
 
 	void createPost(byte[] body, MessageId parentId,
-			ResultHandler<ForumPost> resultHandler);
-
-	void storePost(ForumPost post, ResultHandler<ForumEntry> resultHandler);
+			ResultExceptionHandler<ForumEntry, DbException> resultHandler);
 
 	interface ForumPostListener {
 		@UiThread
diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
index ef66d18bc5..bf147491b1 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
@@ -4,6 +4,7 @@ import android.app.Activity;
 import android.support.annotation.Nullable;
 
 import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
 import org.briarproject.android.controller.handler.ResultHandler;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.crypto.CryptoComponent;
@@ -42,7 +43,7 @@ import javax.inject.Inject;
 
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.identity.Author.Status.VERIFIED;
+import static org.briarproject.api.identity.Author.Status.OURSELVES;
 
 public class ForumControllerImpl extends DbControllerImpl
 		implements ForumController, EventListener {
@@ -149,9 +150,7 @@ public class ForumControllerImpl extends DbControllerImpl
 
 		// Get First Identity
 		now = System.currentTimeMillis();
-		localAuthor =
-				identityManager.getLocalAuthors().iterator()
-						.next();
+		localAuthor = identityManager.getLocalAuthor();
 		duration = System.currentTimeMillis() - now;
 		if (LOG.isLoggable(INFO))
 			LOG.info("Loading author took " + duration + " ms");
@@ -214,7 +213,7 @@ public class ForumControllerImpl extends DbControllerImpl
 
 	@Override
 	public void loadForum(final GroupId groupId,
-			final ResultHandler<List<ForumEntry>> resultHandler) {
+			final ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
@@ -231,7 +230,7 @@ public class ForumControllerImpl extends DbControllerImpl
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
-					resultHandler.onResult(null);
+					resultHandler.onException(e);
 				}
 			}
 		});
@@ -245,7 +244,7 @@ public class ForumControllerImpl extends DbControllerImpl
 
 	@Override
 	public void loadPost(final ForumPostHeader header,
-			final ResultHandler<ForumEntry> resultHandler) {
+			final ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
@@ -255,7 +254,9 @@ public class ForumControllerImpl extends DbControllerImpl
 					resultHandler.onResult(new ForumEntry(header, StringUtils
 							.fromUtf8(bodyCache.get(header.getId()))));
 				} catch (DbException e) {
-					e.printStackTrace();
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					resultHandler.onException(e);
 				}
 			}
 		});
@@ -311,13 +312,13 @@ public class ForumControllerImpl extends DbControllerImpl
 
 	@Override
 	public void createPost(byte[] body,
-			ResultHandler<ForumPost> resultHandler) {
+			ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
 		createPost(body, null, resultHandler);
 	}
 
 	@Override
 	public void createPost(final byte[] body, final MessageId parentId,
-			final ResultHandler<ForumPost> resultHandler) {
+			final ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
 		cryptoExecutor.execute(new Runnable() {
 			@Override
 			public void run() {
@@ -336,14 +337,13 @@ public class ForumControllerImpl extends DbControllerImpl
 					throw new RuntimeException(e);
 				}
 				bodyCache.put(p.getMessage().getId(), body);
-				resultHandler.onResult(p);
+				storePost(p, resultHandler);
 			}
 		});
 	}
 
-	@Override
-	public void storePost(final ForumPost p,
-			final ResultHandler<ForumEntry> resultHandler) {
+	private void storePost(final ForumPost p,
+			final ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
 		runOnDbThread(new Runnable() {
 			@Override
 			public void run() {
@@ -359,8 +359,7 @@ public class ForumControllerImpl extends DbControllerImpl
 							new ForumPostHeader(p.getMessage().getId(),
 									p.getParent(),
 									p.getMessage().getTimestamp(),
-									p.getAuthor(), VERIFIED,
-									true);
+									p.getAuthor(), OURSELVES, true);
 
 					resultHandler.onResult(new ForumEntry(h, StringUtils
 							.fromUtf8(bodyCache.get(p.getMessage().getId()))));
@@ -368,6 +367,7 @@ public class ForumControllerImpl extends DbControllerImpl
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
+					resultHandler.onException(e);
 				}
 			}
 		});
diff --git a/briar-android/src/org/briarproject/android/forum/ForumEntry.java b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
index 889ef9802e..49e1ee32f5 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumEntry.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
@@ -6,6 +6,7 @@ import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.Author.Status;
 import org.briarproject.api.sync.MessageId;
 
+/* This class is not thread safe */
 public class ForumEntry implements MessageTree.MessageNode {
 
 	public final static int LEVEL_UNDEFINED = -1;
@@ -18,6 +19,7 @@ public class ForumEntry implements MessageTree.MessageNode {
 	private Status status;
 	private int level = LEVEL_UNDEFINED;
 	private boolean isShowingDescendants = true;
+	private int descendantCount = 0;
 	private boolean isRead = true;
 
 	ForumEntry(ForumPostHeader h, String text) {
@@ -70,7 +72,7 @@ public class ForumEntry implements MessageTree.MessageNode {
 		return isShowingDescendants;
 	}
 
-	void setLevel(int level) {
+	public void setLevel(int level) {
 		this.level = level;
 	}
 
@@ -89,4 +91,12 @@ public class ForumEntry implements MessageTree.MessageNode {
 	void setRead(boolean read) {
 		isRead = read;
 	}
+
+	public boolean hasDescendants() {
+		return descendantCount > 0;
+	}
+
+	public void setDescendantCount(int descendantCount) {
+		this.descendantCount = descendantCount;
+	}
 }
diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java b/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
index 175ce5a162..c85803da6e 100644
--- a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
+++ b/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
@@ -3,10 +3,8 @@ package org.briarproject.android.forum;
 import android.animation.Animator;
 import android.animation.ArgbEvaluator;
 import android.animation.ValueAnimator;
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.graphics.drawable.ColorDrawable;
-import android.os.Build;
 import android.support.annotation.Nullable;
 import android.support.v4.content.ContextCompat;
 import android.support.v7.widget.LinearLayoutManager;
@@ -26,7 +24,6 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Stack;
 
 import static android.support.v7.widget.RecyclerView.NO_POSITION;
 import static android.view.View.GONE;
@@ -61,90 +58,80 @@ public class NestedForumAdapter
 		return replyEntry;
 	}
 
-	private void setForumEntryLevels() {
-		Stack<MessageId> idStack = new Stack<>();
-		for (ForumEntry forumEntry : forumEntries) {
-			if (forumEntry.getParentId() == null) {
-				idStack.clear();
-			} else if (idStack.isEmpty() ||
-					!idStack.contains(forumEntry.getParentId())) {
-				idStack.push(forumEntry.getParentId());
-			} else if (!forumEntry.getParentId().equals(idStack.peek())) {
-				do {
-					idStack.pop();
-				} while (!forumEntry.getParentId().equals(idStack.peek()));
-			}
-			forumEntry.setLevel(idStack.size());
-		}
-	}
-
 	void setEntries(List<ForumEntry> entries) {
 		forumEntries.clear();
 		forumEntries.addAll(entries);
-		setForumEntryLevels();
 		notifyItemRangeInserted(0, entries.size());
 	}
 
 	void addEntry(ForumEntry entry) {
-		boolean isShowingDescendants = false;
 		forumEntries.add(entry);
-		setForumEntryLevels();
-		if (entry.getLevel() > 0) {
-			// update parent and make sure descendants are visible
-			// Note that the parent's visibility is guaranteed (otherwise
-			// the reply button would not be visible)
+		addedEntry = entry;
+		if (entry.getParentId() == null) {
+			notifyItemInserted(getVisiblePos(entry));
+		} else {
+			// Try to find the entry's parent and perform the proper ui update if
+			// it's present and visible.
 			for (int i = forumEntries.indexOf(entry) - 1; i >= 0; i--) {
 				ForumEntry higherEntry = forumEntries.get(i);
 				if (higherEntry.getLevel() < entry.getLevel()) {
 					// parent found
-					if (!higherEntry.isShowingDescendants()) {
-						isShowingDescendants = true;
-						showDescendants(higherEntry);
+					if (higherEntry.isShowingDescendants()) {
+						int parentVisiblePos = getVisiblePos(higherEntry);
+						if (parentVisiblePos != NO_POSITION) {
+							// parent is visible, we need to update its ui
+							notifyItemChanged(parentVisiblePos);
+							// new entry insert ui
+							int visiblePos = getVisiblePos(entry);
+							notifyItemInserted(visiblePos);
+							break;
+						}
+					} else {
+						// do not show the new entry if its parent is not showing
+						// descendants (this can be overridden by the user by
+						// pressing the snack bar)
+						break;
 					}
-					notifyItemChanged(getVisiblePos(higherEntry));
-					break;
 				}
 			}
 		}
-		if (!isShowingDescendants) {
-			int visiblePos = getVisiblePos(entry);
-			notifyItemInserted(visiblePos);
-		}
-		addedEntry = entry;
 	}
 
 	void scrollToEntry(ForumEntry entry) {
-		layoutManager
-				.scrollToPositionWithOffset(getVisiblePos(entry), 0);
-	}
-
-	private boolean hasDescendants(ForumEntry forumEntry) {
-		int i = forumEntries.indexOf(forumEntry);
-		if (i >= 0 && i < forumEntries.size() - 1) {
-			if (forumEntries.get(i + 1).getLevel() >
-					forumEntry.getLevel()) {
-				return true;
+		int visiblePos = getVisiblePos(entry);
+		if (visiblePos == NO_POSITION && entry.getParentId() != null) {
+			// The entry is not visible due to being hidden by its parent entry.
+			// Find the parent and make it visible and traverse up the parent
+			// chain if necessary to make the entry visible
+			MessageId parentId = entry.getParentId();
+			for (int i = forumEntries.indexOf(entry) - 1; i >= 0; i--) {
+				ForumEntry higherEntry = forumEntries.get(i);
+				if (higherEntry.getId().equals(parentId)) {
+					// parent found
+					showDescendants(higherEntry);
+					int parentPos = getVisiblePos(higherEntry);
+					if (parentPos != NO_POSITION) {
+						// parent or ancestor is visible, entry's visibility
+						// is ensured
+						notifyItemChanged(parentPos);
+						visiblePos = parentPos;
+						break;
+					}
+					// parent or ancestor is hidden, we need to continue up the
+					// dependency chain
+					parentId = higherEntry.getParentId();
+				}
 			}
 		}
-		return false;
-	}
-
-	private boolean hasVisibleDescendants(ForumEntry forumEntry) {
-		int visiblePos = getVisiblePos(forumEntry);
-		int levelLimit = forumEntry.getLevel();
-		if (visiblePos + 1 < getItemCount()) {
-			ForumEntry entry = getVisibleEntry(visiblePos + 1);
-			if (entry == null || entry.getLevel() > levelLimit)
-				return true;
-		}
-		return false;
+		if (visiblePos != NO_POSITION)
+			layoutManager.scrollToPositionWithOffset(visiblePos, 0);
 	}
 
 	private int getReplyCount(ForumEntry entry) {
 		int counter = 0;
 		int pos = forumEntries.indexOf(entry);
 		if (pos >= 0) {
-			int ancestorLvl = forumEntries.get(pos).getLevel();
+			int ancestorLvl = entry.getLevel();
 			for (int i = pos + 1; i < forumEntries.size(); i++) {
 				int descendantLvl = forumEntries.get(i).getLevel();
 				if (descendantLvl <= ancestorLvl)
@@ -210,14 +197,12 @@ public class NestedForumAdapter
 		List<Integer> indexList =
 				getSubTreeIndexes(visiblePos, forumEntry.getLevel());
 		if (!indexList.isEmpty()) {
-			if (Build.VERSION.SDK_INT >= 11) {
-				// stop animating children
-				for (int index : indexList) {
-					ValueAnimator anim =
-							animatingEntries.get(forumEntries.get(index));
-					if (anim != null && anim.isRunning()) {
-						anim.cancel();
-					}
+			// stop animating children
+			for (int index : indexList) {
+				ValueAnimator anim =
+						animatingEntries.get(forumEntries.get(index));
+				if (anim != null && anim.isRunning()) {
+					anim.cancel();
 				}
 			}
 			if (indexList.size() == 1) {
@@ -231,11 +216,18 @@ public class NestedForumAdapter
 	}
 
 
+	/**
+	 *
+	 * @param position is visible entry index
+	 * @return the visible entry at index position from an ordered list of visible
+	 * entries, or null if position is larger then the number of visible entries.
+	 */
 	@Nullable
 	ForumEntry getVisibleEntry(int position) {
 		int levelLimit = UNDEFINED;
 		for (ForumEntry forumEntry : forumEntries) {
 			if (levelLimit >= 0) {
+				// skip hidden entries that their parent is hiding
 				if (forumEntry.getLevel() > levelLimit) {
 					continue;
 				}
@@ -251,7 +243,6 @@ public class NestedForumAdapter
 		return null;
 	}
 
-	@TargetApi(11)
 	private void animateFadeOut(final NestedForumHolder ui,
 			final ForumEntry addedEntry) {
 		ui.setIsRecyclable(false);
@@ -341,9 +332,9 @@ public class NestedForumAdapter
 									replies, replies));
 		}
 
-		if (hasDescendants(entry)) {
+		if (entry.hasDescendants()) {
 			ui.chevron.setVisibility(VISIBLE);
-			if (hasVisibleDescendants(entry)) {
+			if (entry.isShowingDescendants()) {
 				ui.chevron.setSelected(false);
 			} else {
 				ui.chevron.setSelected(true);
@@ -369,9 +360,7 @@ public class NestedForumAdapter
 
 			ui.cell.setBackgroundColor(ContextCompat
 					.getColor(ctx, R.color.forum_cell_highlight));
-			if (Build.VERSION.SDK_INT >= 11) {
-				animateFadeOut(ui, addedEntry);
-			}
+			animateFadeOut(ui, addedEntry);
 			addedEntry = null;
 		} else {
 			ui.cell.setBackgroundColor(ContextCompat
@@ -386,12 +375,24 @@ public class NestedForumAdapter
 		});
 	}
 
+	public boolean isVisible(ForumEntry entry) {
+		return getVisiblePos(entry) != NO_POSITION;
+	}
+
+	/**
+	 * @param sEntry the ForumEntry to find the visible positoin of, or null to
+	 *               return the total cound of visible elements
+	 * @return the visible position of sEntry, or the total number of visible
+	 * elements if sEntry is null. If sEntry is not visible a NO_POSITION is
+	 * returned.
+	 */
 	private int getVisiblePos(ForumEntry sEntry) {
 		int visibleCounter = 0;
 		int levelLimit = UNDEFINED;
 		for (ForumEntry fEntry : forumEntries) {
 			if (levelLimit >= 0) {
 				if (fEntry.getLevel() > levelLimit) {
+					// skip all the entries below a non visible branch
 					continue;
 				}
 				levelLimit = UNDEFINED;
@@ -440,6 +441,7 @@ public class NestedForumAdapter
 			cell = (ViewGroup) v.findViewById(R.id.forum_cell);
 			topDivider = v.findViewById(R.id.top_divider);
 		}
+
 	}
 
 	interface OnNestedForumListener {
diff --git a/briar-android/src/org/briarproject/android/util/NestedTreeList.java b/briar-android/src/org/briarproject/android/util/NestedTreeList.java
index 23ba02298d..d190a9f6de 100644
--- a/briar-android/src/org/briarproject/android/util/NestedTreeList.java
+++ b/briar-android/src/org/briarproject/android/util/NestedTreeList.java
@@ -1,5 +1,7 @@
 package org.briarproject.android.util;
 
+import android.support.annotation.UiThread;
+
 import org.briarproject.api.clients.MessageTree;
 import org.briarproject.clients.MessageTreeImpl;
 
@@ -8,7 +10,7 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 
-/* This class is not thread safe */
+@UiThread
 public class NestedTreeList<T extends MessageTree.MessageNode>
 		implements Iterable<T> {
 
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 92023f7f91..3e5431ac94 100644
--- a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
+++ b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
@@ -7,7 +7,8 @@ import junit.framework.Assert;
 import org.briarproject.BuildConfig;
 import org.briarproject.TestUtils;
 import org.briarproject.android.TestBriarApplication;
-import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.controller.handler.UiResultExceptionHandler;
+import org.briarproject.api.db.DbException;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.sync.GroupId;
@@ -80,7 +81,8 @@ public class ForumActivityTest {
 
 	private TestForumActivity forumActivity;
 	@Captor
-	private ArgumentCaptor<UiResultHandler<List<ForumEntry>>> rc;
+	private ArgumentCaptor<UiResultExceptionHandler<List<ForumEntry>, DbException>>
+			rc;
 
 	@Before
 	public void setUp() {
@@ -91,7 +93,6 @@ public class ForumActivityTest {
 				.withIntent(intent).create().resume().get();
 	}
 
-
 	private List<ForumEntry> getDummyData() {
 		ForumEntry[] forumEntries = new ForumEntry[6];
 		for (int i = 0; i < forumEntries.length; i++) {
diff --git a/briar-api/src/org/briarproject/api/clients/MessageTree.java b/briar-api/src/org/briarproject/api/clients/MessageTree.java
index 1c0c92e9fe..14481f2a56 100644
--- a/briar-api/src/org/briarproject/api/clients/MessageTree.java
+++ b/briar-api/src/org/briarproject/api/clients/MessageTree.java
@@ -16,6 +16,8 @@ public interface MessageTree<T extends MessageTree.MessageNode> {
 	interface MessageNode {
 		MessageId getId();
 		MessageId getParentId();
+		void setLevel(int level);
+		void setDescendantCount(int descendantCount);
 		long getTimestamp();
 	}
 
diff --git a/briar-core/src/org/briarproject/clients/MessageTreeImpl.java b/briar-core/src/org/briarproject/clients/MessageTreeImpl.java
index 2c6eb1344a..b1cd81a571 100644
--- a/briar-core/src/org/briarproject/clients/MessageTreeImpl.java
+++ b/briar-core/src/org/briarproject/clients/MessageTreeImpl.java
@@ -77,10 +77,13 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
 	}
 
 
-	private void traverse(List<T> list, T node) {
+	private void traverse(List<T> list, T node, int level) {
 		list.add(node);
-		for (T child : nodeMap.get(node.getId())) {
-			traverse(list, child);
+		List<T> children = nodeMap.get(node.getId());
+		node.setLevel(level);
+		node.setDescendantCount(children.size());
+		for (T child : children) {
+			traverse(list, child, level+1);
 		}
 	}
 
@@ -98,7 +101,7 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
 	public synchronized Collection<T> depthFirstOrder() {
 		List<T> orderedList = new ArrayList<T>();
 		for (T root : roots) {
-			traverse(orderedList, root);
+			traverse(orderedList, root, 0);
 		}
 		return Collections.unmodifiableList(orderedList);
 	}
diff --git a/briar-tests/src/org/briarproject/clients/MessageTreeTest.java b/briar-tests/src/org/briarproject/clients/MessageTreeTest.java
index 1c6e165311..e198db4719 100644
--- a/briar-tests/src/org/briarproject/clients/MessageTreeTest.java
+++ b/briar-tests/src/org/briarproject/clients/MessageTreeTest.java
@@ -86,6 +86,16 @@ public class MessageTreeTest {
 			return parentId;
 		}
 
+		@Override
+		public void setLevel(int level) {
+
+		}
+
+		@Override
+		public void setDescendantCount(int descendantCount) {
+
+		}
+
 		@Override
 		public long getTimestamp() {
 			return timestamp;
-- 
GitLab