diff --git a/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTest.java
index 41fe53fff3ee27be453ddcc3d4b781890df97cde..4f47c3444ad8bacf7f7dc57dfad82725ee8664f1 100644
--- a/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTest.java
@@ -9,6 +9,7 @@ import org.briarproject.api.blogs.BlogManager;
 import org.briarproject.api.blogs.BlogPostFactory;
 import org.briarproject.api.blogs.BlogSharingManager;
 import org.briarproject.api.clients.ContactGroupFactory;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
@@ -70,6 +71,7 @@ public class BlogSharingIntegrationTest extends BriarIntegrationTest {
 			lifecycleManager2;
 	private SyncSessionFactory sync0, sync1;
 	private BlogManager blogManager0, blogManager1;
+	private MessageTracker messageTracker0, messageTracker1;
 	private ContactManager contactManager0, contactManager1, contactManager2;
 	private Contact contact1, contact2, contact01, contact02;
 	private ContactId contactId1, contactId01;
@@ -142,6 +144,8 @@ public class BlogSharingIntegrationTest extends BriarIntegrationTest {
 		contactManager2 = t2.getContactManager();
 		blogManager0 = t0.getBlogManager();
 		blogManager1 = t1.getBlogManager();
+		messageTracker0 = t0.getMessageTracker();
+		messageTracker1 = t1.getMessageTracker();
 		blogSharingManager0 = t0.getBlogSharingManager();
 		blogSharingManager1 = t1.getBlogSharingManager();
 		blogSharingManager2 = t2.getBlogSharingManager();
@@ -192,19 +196,19 @@ public class BlogSharingIntegrationTest extends BriarIntegrationTest {
 		// get sharing group and assert group message count
 		GroupId g = contactGroupFactory.createContactGroup(CLIENT_ID, contact1)
 				.getId();
-		assertGroupCount(blogSharingManager0, g, 1, 0);
+		assertGroupCount(messageTracker0, g, 1, 0);
 
 		// sync first request message
 		sync0To1();
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener1.requestReceived);
-		assertGroupCount(blogSharingManager1, g, 2, 1);
+		assertGroupCount(messageTracker1, g, 2, 1);
 
 		// sync response back
 		sync1To0();
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener0.responseReceived);
-		assertGroupCount(blogSharingManager0, g, 2, 1);
+		assertGroupCount(messageTracker0, g, 2, 1);
 
 		// blog was added successfully
 		assertEquals(0, blogSharingManager0.getInvitations().size());
@@ -242,8 +246,8 @@ public class BlogSharingIntegrationTest extends BriarIntegrationTest {
 		assertFalse(blogSharingManager1.canBeShared(blog2.getId(), contact01));
 
 		// group message count is still correct
-		assertGroupCount(blogSharingManager0, g, 2, 1);
-		assertGroupCount(blogSharingManager1, g, 2, 1);
+		assertGroupCount(messageTracker0, g, 2, 1);
+		assertGroupCount(messageTracker1, g, 2, 1);
 
 		stopLifecycles();
 	}
diff --git a/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTestComponent.java
index 5515094fb97cb88a6519fbe1757d864149c347b0..d1cde03d4242cb388436625bb01112914b79661f 100644
--- a/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTestComponent.java
+++ b/briar-android-tests/src/test/java/org/briarproject/BlogSharingIntegrationTestComponent.java
@@ -2,6 +2,7 @@ package org.briarproject;
 
 import org.briarproject.api.blogs.BlogManager;
 import org.briarproject.api.blogs.BlogSharingManager;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.identity.IdentityManager;
@@ -82,6 +83,8 @@ interface BlogSharingIntegrationTestComponent {
 
 	BlogManager getBlogManager();
 
+	MessageTracker getMessageTracker();
+
 	SyncSessionFactory getSyncSessionFactory();
 
 }
diff --git a/briar-android-tests/src/test/java/org/briarproject/BriarIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/BriarIntegrationTest.java
index fd653c56d66e23eaa34f5cbe45d8d35019733ab5..3436fe962ec9c22d37033c876c83f6ae3a1c2b23 100644
--- a/briar-android-tests/src/test/java/org/briarproject/BriarIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/BriarIntegrationTest.java
@@ -9,7 +9,7 @@ import static org.junit.Assert.assertEquals;
 
 public abstract class BriarIntegrationTest extends BriarTestCase {
 
-	protected void assertGroupCount(MessageTracker tracker, GroupId g,
+	protected static void assertGroupCount(MessageTracker tracker, GroupId g,
 			long msgCount, long unreadCount, long latestMsg)
 			throws DbException {
 
@@ -19,7 +19,7 @@ public abstract class BriarIntegrationTest extends BriarTestCase {
 		assertEquals(latestMsg, groupCount.getLatestMsgTime());
 	}
 
-	protected void assertGroupCount(MessageTracker tracker, GroupId g,
+	protected static void assertGroupCount(MessageTracker tracker, GroupId g,
 			long msgCount, long unreadCount) throws	DbException {
 
 		GroupCount c1 = tracker.getGroupCount(g);
diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java
index 07e11e038f93ba4cc85908a71e33d2f384616e04..e574d224ae38278d9edd50ee43d0fbf4c50d4658 100644
--- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java
@@ -4,11 +4,11 @@ import junit.framework.Assert;
 
 import net.jodah.concurrentunit.Waiter;
 
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyPair;
 import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.event.Event;
@@ -66,6 +66,7 @@ public class ForumManagerTest extends BriarIntegrationTest {
 	private SyncSessionFactory sync0, sync1;
 	private ForumManager forumManager0, forumManager1;
 	private ContactManager contactManager0, contactManager1;
+	private MessageTracker messageTracker0, messageTracker1;
 	private ContactId contactId0,contactId1;
 	private IdentityManager identityManager0, identityManager1;
 	private LocalAuthor author0, author1;
@@ -118,6 +119,8 @@ public class ForumManagerTest extends BriarIntegrationTest {
 		identityManager1 = t1.getIdentityManager();
 		contactManager0 = t0.getContactManager();
 		contactManager1 = t1.getContactManager();
+		messageTracker0 = t0.getMessageTracker();
+		messageTracker1 = t1.getMessageTracker();
 		forumManager0 = t0.getForumManager();
 		forumManager1 = t1.getForumManager();
 		forumSharingManager0 = t0.getForumSharingManager();
@@ -156,16 +159,16 @@ public class ForumManagerTest extends BriarIntegrationTest {
 		forumManager0.addLocalPost(post1);
 		forumManager0.setReadFlag(forum.getGroup().getId(),
 				post1.getMessage().getId(), true);
-		assertGroupCount(forumManager0, forum.getGroup().getId(), 1, 0,
+		assertGroupCount(messageTracker0, forum.getGroup().getId(), 1, 0,
 				post1.getMessage().getTimestamp());
 		forumManager0.addLocalPost(post2);
 		forumManager0.setReadFlag(forum.getGroup().getId(),
 				post2.getMessage().getId(), false);
-		assertGroupCount(forumManager0, forum.getGroup().getId(), 2, 1,
+		assertGroupCount(messageTracker0, forum.getGroup().getId(), 2, 1,
 				post2.getMessage().getTimestamp());
 		forumManager0.setReadFlag(forum.getGroup().getId(),
 				post2.getMessage().getId(), false);
-		assertGroupCount(forumManager0, forum.getGroup().getId(), 2, 1,
+		assertGroupCount(messageTracker0, forum.getGroup().getId(), 2, 1,
 				post2.getMessage().getTimestamp());
 		Collection<ForumPostHeader> headers =
 				forumManager0.getPostHeaders(forum.getGroup().getId());
@@ -215,14 +218,14 @@ public class ForumManagerTest extends BriarIntegrationTest {
 		forumManager0.addLocalPost(post1);
 		assertEquals(1, forumManager0.getPostHeaders(g).size());
 		assertEquals(0, forumManager1.getPostHeaders(g).size());
-		assertGroupCount(forumManager0, g, 1, 0, time);
-		assertGroupCount(forumManager1, g, 0, 0, 0);
+		assertGroupCount(messageTracker0, g, 1, 0, time);
+		assertGroupCount(messageTracker1, g, 0, 0, 0);
 
 		// send post to 1
 		sync0To1();
 		deliveryWaiter.await(TIMEOUT, 1);
 		assertEquals(1, forumManager1.getPostHeaders(g).size());
-		assertGroupCount(forumManager1, g, 1, 1, time);
+		assertGroupCount(messageTracker1, g, 1, 1, time);
 
 		// add another forum post
 		long time2 = clock.currentTimeMillis();
@@ -230,14 +233,14 @@ public class ForumManagerTest extends BriarIntegrationTest {
 		forumManager1.addLocalPost(post2);
 		assertEquals(1, forumManager0.getPostHeaders(g).size());
 		assertEquals(2, forumManager1.getPostHeaders(g).size());
-		assertGroupCount(forumManager0, g, 1, 0, time);
-		assertGroupCount(forumManager1, g, 2, 1, time2);
+		assertGroupCount(messageTracker0, g, 1, 0, time);
+		assertGroupCount(messageTracker1, g, 2, 1, time2);
 
 		// send post to 0
 		sync1To0();
 		deliveryWaiter.await(TIMEOUT, 1);
 		assertEquals(2, forumManager1.getPostHeaders(g).size());
-		assertGroupCount(forumManager0, g, 2, 1, time2);
+		assertGroupCount(messageTracker0, g, 2, 1, time2);
 
 		stopLifecycles();
 	}
diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java
index fee35a0088438d7d5d04547f210002e58c3de28f..24278e4af5e878c5c16e9ff55730fd0fa52044cd 100644
--- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java
+++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java
@@ -1,5 +1,6 @@
 package org.briarproject;
 
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.forum.ForumManager;
@@ -78,6 +79,8 @@ interface ForumManagerTestComponent {
 
 	ContactManager getContactManager();
 
+	MessageTracker getMessageTracker();
+
 	ForumSharingManager getForumSharingManager();
 
 	ForumManager getForumManager();
diff --git a/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTest.java
index 90beb6595796b115400bcb8b1513286bd5b7e409..d3745f08b00c7ad667cb7f1ce7a1a6f6544e3a0c 100644
--- a/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTest.java
@@ -9,6 +9,7 @@ import org.briarproject.TestDatabaseModule;
 import org.briarproject.TestUtils;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -105,6 +106,7 @@ public class IntroductionIntegrationTest extends BriarIntegrationTest {
 			lifecycleManager2;
 	private SyncSessionFactory sync0, sync1, sync2;
 	private ContactManager contactManager0, contactManager1, contactManager2;
+	private MessageTracker messageTracker0, messageTracker1, messageTracker2;
 	private ContactId contactId0, contactId1, contactId2;
 	private IdentityManager identityManager0, identityManager1,
 			identityManager2;
@@ -172,6 +174,9 @@ public class IntroductionIntegrationTest extends BriarIntegrationTest {
 		contactManager0 = t0.getContactManager();
 		contactManager1 = t1.getContactManager();
 		contactManager2 = t2.getContactManager();
+		messageTracker0 = t0.getMessageTracker();
+		messageTracker1 = t1.getMessageTracker();
+		messageTracker2 = t2.getMessageTracker();
 		introductionManager0 = t0.getIntroductionManager();
 		introductionManager1 = t1.getIntroductionManager();
 		introductionManager2 = t2.getIntroductionManager();
@@ -205,38 +210,38 @@ public class IntroductionIntegrationTest extends BriarIntegrationTest {
 					.createIntroductionGroup(introducee1);
 			Group g2 = introductionGroupFactory
 					.createIntroductionGroup(introducee2);
-			assertGroupCount(introductionManager0, g1.getId(), 1, 0, time);
-			assertGroupCount(introductionManager0, g2.getId(), 1, 0, time);
+			assertGroupCount(messageTracker0, g1.getId(), 1, 0, time);
+			assertGroupCount(messageTracker0, g2.getId(), 1, 0, time);
 
 			// sync first request message
 			deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
 			eventWaiter.await(TIMEOUT, 1);
 			assertTrue(listener1.requestReceived);
-			assertGroupCount(introductionManager1, g1.getId(), 2, 1);
+			assertGroupCount(messageTracker1, g1.getId(), 2, 1);
 
 			// sync second request message
 			deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
 			eventWaiter.await(TIMEOUT, 1);
 			assertTrue(listener2.requestReceived);
-			assertGroupCount(introductionManager2, g2.getId(), 2, 1);
+			assertGroupCount(messageTracker2, g2.getId(), 2, 1);
 
 			// sync first response
 			deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
 			eventWaiter.await(TIMEOUT, 1);
 			assertTrue(listener0.response1Received);
-			assertGroupCount(introductionManager0, g1.getId(), 2, 1);
+			assertGroupCount(messageTracker0, g1.getId(), 2, 1);
 
 			// sync second response
 			deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0");
 			eventWaiter.await(TIMEOUT, 1);
 			assertTrue(listener0.response2Received);
-			assertGroupCount(introductionManager0, g2.getId(), 2, 1);
+			assertGroupCount(messageTracker0, g2.getId(), 2, 1);
 
 			// sync forwarded responses to introducees
 			deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
 			deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
-			assertGroupCount(introductionManager1, g1.getId(), 3, 2);
-			assertGroupCount(introductionManager2, g2.getId(), 3, 2);
+			assertGroupCount(messageTracker1, g1.getId(), 3, 2);
+			assertGroupCount(messageTracker2, g2.getId(), 3, 2);
 
 			// sync first ACK and its forward
 			deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
@@ -269,10 +274,10 @@ public class IntroductionIntegrationTest extends BriarIntegrationTest {
 			}
 
 			assertDefaultUiMessages();
-			assertGroupCount(introductionManager0, g1.getId(), 2, 1);
-			assertGroupCount(introductionManager0, g2.getId(), 2, 1);
-			assertGroupCount(introductionManager1, g1.getId(), 3, 2);
-			assertGroupCount(introductionManager2, g2.getId(), 3, 2);
+			assertGroupCount(messageTracker0, g1.getId(), 2, 1);
+			assertGroupCount(messageTracker0, g2.getId(), 2, 1);
+			assertGroupCount(messageTracker1, g1.getId(), 3, 2);
+			assertGroupCount(messageTracker2, g2.getId(), 3, 2);
 		} finally {
 			stopLifecycles();
 		}
diff --git a/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java
index 617fa1cee530f83f0125018fdda60e0ac80c71fe..1741364824258fd826122d5814cb27bd133144d6 100644
--- a/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java
+++ b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java
@@ -4,6 +4,7 @@ import org.briarproject.TestDatabaseModule;
 import org.briarproject.TestPluginsModule;
 import org.briarproject.TestSeedProviderModule;
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.event.EventBus;
@@ -50,7 +51,7 @@ import dagger.Component;
 		PropertiesModule.class,
 		MessagingModule.class
 })
-public interface IntroductionIntegrationTestComponent {
+interface IntroductionIntegrationTestComponent {
 
 	void inject(IntroductionIntegrationTest testCase);
 
@@ -82,6 +83,8 @@ public interface IntroductionIntegrationTestComponent {
 
 	TransportPropertyManager getTransportPropertyManager();
 
+	MessageTracker getMessageTracker();
+
 	SyncSessionFactory getSyncSessionFactory();
 
 	/* the following methods are only needed to manually construct messages */
diff --git a/briar-api/src/org/briarproject/api/clients/MessageQueueManager.java b/briar-api/src/org/briarproject/api/clients/MessageQueueManager.java
index b5b49ad16adb1a06e3525ae94cfca59928325a39..0766e5e4be1adfaed44267f08b2b3f1804aa39bf 100644
--- a/briar-api/src/org/briarproject/api/clients/MessageQueueManager.java
+++ b/briar-api/src/org/briarproject/api/clients/MessageQueueManager.java
@@ -1,14 +1,15 @@
 package org.briarproject.api.clients;
 
-import org.briarproject.api.FormatException;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.Transaction;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.MessageContext;
 
+@NotNullByDefault
 public interface MessageQueueManager {
 
 	/**
@@ -52,17 +53,17 @@ public interface MessageQueueManager {
 		 *
 		 * @throws DbException Should only be used for real database errors.
 		 * If this is thrown, delivery will be attempted again at next startup,
-		 * whereas if an FormatException is thrown,
+		 * whereas if an InvalidMessageException is thrown,
 		 * the message will be permanently invalidated.
-		 * @throws FormatException for any non-database error
+		 * @throws InvalidMessageException for any non-database error
 		 * that occurs while handling remotely created data.
 		 * This includes errors that occur while handling locally created data
 		 * in a context controlled by remotely created data
 		 * (for example, parsing the metadata of a dependency
 		 * of an incoming message).
-		 * Never rethrow DbException as FormatException!
+		 * Never rethrow DbException as InvalidMessageException!
 		 */
 		void incomingMessage(Transaction txn, QueueMessage q, Metadata meta)
-				throws DbException, FormatException;
+				throws DbException, InvalidMessageException;
 	}
 }
diff --git a/briar-api/src/org/briarproject/api/clients/MessageTracker.java b/briar-api/src/org/briarproject/api/clients/MessageTracker.java
index a73c87183fa949d61cd7858c2a6a443e99236f46..6351491c74742bc1e0f50eab04ce368bc02089b1 100644
--- a/briar-api/src/org/briarproject/api/clients/MessageTracker.java
+++ b/briar-api/src/org/briarproject/api/clients/MessageTracker.java
@@ -1,9 +1,13 @@
 package org.briarproject.api.clients;
 
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 
+@NotNullByDefault
 public interface MessageTracker {
 
 	/**
@@ -13,11 +17,34 @@ public interface MessageTracker {
 	GroupCount getGroupCount(GroupId g) throws DbException;
 
 	/**
-	 * Marks a message as read or unread and updates the group counts in g.
+	 * Gets the number of visible and unread messages in the group
+	 * as well as the timestamp of the latest message
 	 **/
+	GroupCount getGroupCount(Transaction txn, GroupId g) throws DbException;
+
+	/**
+	 * Updates the group count for the given incoming message.
+	 */
+	void trackIncomingMessage(Transaction txn, Message m) throws DbException;
+
+	/**
+	 * Updates the group count for the given outgoing message.
+	 */
+	void trackOutgoingMessage(Transaction txn, Message m) throws DbException;
+
+	/**
+	 * Updates the group count for the given message.
+	 */
+	void trackMessage(Transaction txn, GroupId g, long timestamp, boolean read)
+		throws DbException;
+
+	/**
+	 * Marks a message as read or unread and updates the group count.
+	 */
 	void setReadFlag(GroupId g, MessageId m, boolean read) throws DbException;
 
 	class GroupCount {
+
 		private final int msgCount, unreadCount;
 		private final long latestMsgTime;
 
diff --git a/briar-api/src/org/briarproject/api/forum/ForumManager.java b/briar-api/src/org/briarproject/api/forum/ForumManager.java
index 37aadb723b7798d4429d5feee69214fc1da0728b..75c9b9eafc912f12561144834bf240e65700c256 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumManager.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumManager.java
@@ -1,10 +1,11 @@
 package org.briarproject.api.forum;
 
-import org.briarproject.api.clients.MessageTracker;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
@@ -12,43 +13,76 @@ import org.jetbrains.annotations.Nullable;
 
 import java.util.Collection;
 
-public interface ForumManager extends MessageTracker {
+@NotNullByDefault
+public interface ForumManager {
 
-	/** The unique ID of the forum client. */
+	/**
+	 * The unique ID of the forum client.
+	 */
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.forum");
 
-	/** Subscribes to a forum. */
+	/**
+	 * Subscribes to a forum.
+	 */
 	Forum addForum(String name) throws DbException;
 
-	/** Unsubscribes from a forum. */
+	/**
+	 * Unsubscribes from a forum.
+	 */
 	void removeForum(Forum f) throws DbException;
 
-	/** Creates a local forum post. */
+	/**
+	 * Creates a local forum post.
+	 */
 	@CryptoExecutor
 	ForumPost createLocalPost(GroupId groupId, String body, long timestamp,
 			@Nullable MessageId parentId, LocalAuthor author);
 
-	/** Stores a local forum post. */
+	/**
+	 * Stores a local forum post.
+	 */
 	ForumPostHeader addLocalPost(ForumPost p) throws DbException;
 
-	/** Returns the forum with the given ID. */
+	/**
+	 * Returns the forum with the given ID.
+	 */
 	Forum getForum(GroupId g) throws DbException;
 
-	/** Returns the forum with the given ID. */
+	/**
+	 * Returns the forum with the given ID.
+	 */
 	Forum getForum(Transaction txn, GroupId g) throws DbException;
 
-	/** Returns all forums to which the user subscribes. */
+	/**
+	 * Returns all forums to which the user subscribes.
+	 */
 	Collection<Forum> getForums() throws DbException;
 
-	/** Returns the body of the forum post with the given ID. */
+	/**
+	 * Returns the body of the forum post with the given ID.
+	 */
 	String getPostBody(MessageId m) throws DbException;
 
-	/** Returns the headers of all posts in the given forum. */
+	/**
+	 * Returns the headers of all posts in the given forum.
+	 */
 	Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException;
 
-	/** Registers a hook to be called whenever a forum is removed. */
+	/**
+	 * Registers a hook to be called whenever a forum is removed.
+	 */
 	void registerRemoveForumHook(RemoveForumHook hook);
 
+	/**
+	 * Returns the group count for the given forum.
+	 */
+	GroupCount getGroupCount(GroupId g) throws DbException;
+
+	/**
+	 * Marks a message as read or unread and updates the group count.
+	 */
+	void setReadFlag(GroupId g, MessageId m, boolean read) throws DbException;
+
 	interface RemoveForumHook {
 		void removingForum(Transaction txn, Forum f) throws DbException;
 	}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java
index 6914318d833b95d2b213bc2e2cb34e107cf94819..12cc01323d3cf012f617a03981525ac4a620612f 100644
--- a/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java
@@ -1,16 +1,18 @@
 package org.briarproject.api.introduction;
 
 import org.briarproject.api.FormatException;
-import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.messaging.ConversationManager.ConversationClient;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.ClientId;
 
 import java.util.Collection;
 
-public interface IntroductionManager extends MessageTracker {
+@NotNullByDefault
+public interface IntroductionManager extends ConversationClient {
 
 	/** The unique ID of the introduction client. */
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.introduction");
diff --git a/briar-api/src/org/briarproject/api/messaging/ConversationManager.java b/briar-api/src/org/briarproject/api/messaging/ConversationManager.java
index a48a8ade84aa4ae6083e46ff29aae93c5d6a81c5..69ecc3016a7bdddc60f1ff35969f9816bae72f98 100644
--- a/briar-api/src/org/briarproject/api/messaging/ConversationManager.java
+++ b/briar-api/src/org/briarproject/api/messaging/ConversationManager.java
@@ -1,10 +1,16 @@
 package org.briarproject.api.messaging;
 
 import org.briarproject.api.clients.MessageTracker.GroupCount;
+import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
 
+@NotNullByDefault
 public interface ConversationManager {
 
 	/**
@@ -13,11 +19,19 @@ public interface ConversationManager {
 	 */
 	void registerConversationClient(ConversationClient client);
 
-	/** Get the unified group count for all private conversation messages. */
-	GroupCount getGroupCount(ContactId contactId) throws DbException;
+	/**
+	 * Get the unified group count for all private conversation messages.
+	 */
+	GroupCount getGroupCount(ContactId c) throws DbException;
 
 	interface ConversationClient {
-		GroupCount getGroupCount(Transaction txn, ContactId contactId)
+
+		Group getContactGroup(Contact c);
+
+		GroupCount getGroupCount(Transaction txn, ContactId c)
+				throws DbException;
+
+		void setReadFlag(GroupId g, MessageId m, boolean read)
 				throws DbException;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/messaging/MessagingManager.java b/briar-api/src/org/briarproject/api/messaging/MessagingManager.java
index c6de1772f1dded122d3249a3ed00f802e3041552..78c7c4a7593d6e8f7135c404ae5bee961198d0b8 100644
--- a/briar-api/src/org/briarproject/api/messaging/MessagingManager.java
+++ b/briar-api/src/org/briarproject/api/messaging/MessagingManager.java
@@ -1,15 +1,17 @@
 package org.briarproject.api.messaging;
 
-import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.messaging.ConversationManager.ConversationClient;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 import java.util.Collection;
 
-public interface MessagingManager extends MessageTracker {
+@NotNullByDefault
+public interface MessagingManager extends ConversationClient {
 
 	/** The unique ID of the messaging client. */
 	ClientId CLIENT_ID = new ClientId("org.briarproject.briar.messaging");
diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
index 89802684583bcac79facec6d96e3d110ab514265..b5665dc23fcb720692a403b7fbada1f24a0b526d 100644
--- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
+++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
@@ -1,7 +1,7 @@
 package org.briarproject.api.privategroup;
 
 import org.briarproject.api.FormatException;
-import org.briarproject.api.clients.MessageTracker;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.identity.Author;
@@ -14,7 +14,7 @@ import org.briarproject.api.sync.MessageId;
 import java.util.Collection;
 
 @NotNullByDefault
-public interface PrivateGroupManager extends MessageTracker {
+public interface PrivateGroupManager {
 
 	/**
 	 * The unique ID of the private group client.
@@ -24,9 +24,9 @@ public interface PrivateGroupManager extends MessageTracker {
 	/**
 	 * Adds a new private group and joins it.
 	 *
-	 * @param group        The private group to add
-	 * @param joinMsg      The creators's join message
-	 * @param creator      True if the group is added by its creator
+	 * @param group   The private group to add
+	 * @param joinMsg The creators's join message
+	 * @param creator True if the group is added by its creator
 	 */
 	void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg,
 			boolean creator) throws DbException;
@@ -34,9 +34,9 @@ public interface PrivateGroupManager extends MessageTracker {
 	/**
 	 * Adds a new private group and joins it.
 	 *
-	 * @param group        The private group to add
-	 * @param joinMsg      The new member's join message
-	 * @param creator      True if the group is added by its creator
+	 * @param group   The private group to add
+	 * @param joinMsg The new member's join message
+	 * @param creator True if the group is added by its creator
 	 */
 	void addPrivateGroup(Transaction txn, PrivateGroup group,
 			GroupMessage joinMsg, boolean creator) throws DbException;
@@ -101,13 +101,23 @@ public interface PrivateGroupManager extends MessageTracker {
 	 */
 	boolean isMember(Transaction txn, GroupId g, Author a) throws DbException;
 
+	/**
+	 * Returns the group count for the given group.
+	 */
+	GroupCount getGroupCount(GroupId g) throws DbException;
+
+	/**
+	 * Marks a message as read or unread and updates the group count.
+	 */
+	void setReadFlag(GroupId g, MessageId m, boolean read) throws DbException;
+
 	/**
 	 * This method needs to be called when a contact relationship
 	 * has been revealed between the user and the Author with AuthorId a
 	 * in the Group identified by the GroupId g.
 	 *
 	 * @param byContact true if the remote contact has revealed
-	 *                     the relationship first. Otherwise false.
+	 *                  the relationship first. Otherwise false.
 	 */
 	void relationshipRevealed(Transaction txn, GroupId g, AuthorId a,
 			boolean byContact) throws FormatException, DbException;
diff --git a/briar-api/src/org/briarproject/api/sharing/SharingManager.java b/briar-api/src/org/briarproject/api/sharing/SharingManager.java
index 934818f955ccd9af4f370c13d5a914e245f5eef2..66b6ef82f6c275763d6f8a9e5c2720836d0c4b93 100644
--- a/briar-api/src/org/briarproject/api/sharing/SharingManager.java
+++ b/briar-api/src/org/briarproject/api/sharing/SharingManager.java
@@ -1,15 +1,18 @@
 package org.briarproject.api.sharing;
 
-import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.messaging.ConversationManager.ConversationClient;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.GroupId;
 
 import java.util.Collection;
 
-public interface SharingManager<S extends Shareable> extends MessageTracker {
+@NotNullByDefault
+public interface SharingManager<S extends Shareable>
+		extends ConversationClient {
 
 	/**
 	 * Sends an invitation to share the given group with the given contact
diff --git a/briar-api/src/org/briarproject/api/sync/MessageFactory.java b/briar-api/src/org/briarproject/api/sync/MessageFactory.java
index 6b599a8f1fb8dfafbf9d643f2318e8de560fc122..191770e2c269cc6f447f132bdd349319e3f7054e 100644
--- a/briar-api/src/org/briarproject/api/sync/MessageFactory.java
+++ b/briar-api/src/org/briarproject/api/sync/MessageFactory.java
@@ -1,5 +1,8 @@
 package org.briarproject.api.sync;
 
+import org.briarproject.api.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
 public interface MessageFactory {
 
 	Message createMessage(GroupId g, long timestamp, byte[] body);
diff --git a/briar-api/src/org/briarproject/api/sync/ValidationManager.java b/briar-api/src/org/briarproject/api/sync/ValidationManager.java
index 911ba4771831f7b718ab2329292d690fb280af90..b35a0bd8fd85ba0972f164ab8b675953370e735d 100644
--- a/briar-api/src/org/briarproject/api/sync/ValidationManager.java
+++ b/briar-api/src/org/briarproject/api/sync/ValidationManager.java
@@ -3,11 +3,13 @@ package org.briarproject.api.sync;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.Transaction;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
 /**
  * Responsible for managing message validators and passing them messages to
  * validate.
  */
+@NotNullByDefault
 public interface ValidationManager {
 
 	enum State {
diff --git a/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java
index e591fd2d2b986726f57effd25a7293209d6110eb..cfb4706894501bf75fdbd07f6791e0478660eb4a 100644
--- a/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java
+++ b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java
@@ -27,6 +27,7 @@ import org.briarproject.api.identity.Author.Status;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
@@ -69,6 +70,7 @@ import static org.briarproject.api.contact.ContactManager.AddContactHook;
 import static org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import static org.briarproject.blogs.BlogPostValidator.authorToBdfDictionary;
 
+@NotNullByDefault
 class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
 		AddContactHook, RemoveContactHook, Client {
 
@@ -127,10 +129,8 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
 
 	@Override
 	public void removingContact(Transaction txn, Contact c) throws DbException {
-		if (c != null) {
-			Blog b = blogFactory.createBlog(c.getAuthor());
-			db.removeGroup(txn, b.getGroup());
-		}
+		Blog b = blogFactory.createBlog(c.getAuthor());
+		db.removeGroup(txn, b.getGroup());
 	}
 
 	@Override
diff --git a/briar-core/src/org/briarproject/blogs/BlogPostValidator.java b/briar-core/src/org/briarproject/blogs/BlogPostValidator.java
index dfbe7709897b3ab36fd7f60146367af5bddb0370..088354fc1697caa67d714f2eb554fc4a332e42e5 100644
--- a/briar-core/src/org/briarproject/blogs/BlogPostValidator.java
+++ b/briar-core/src/org/briarproject/blogs/BlogPostValidator.java
@@ -11,6 +11,7 @@ import org.briarproject.api.data.BdfEntry;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.identity.Author;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupFactory;
 import org.briarproject.api.sync.InvalidMessageException;
@@ -42,6 +43,7 @@ import static org.briarproject.api.blogs.MessageType.COMMENT;
 import static org.briarproject.api.blogs.MessageType.POST;
 import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
 
+@NotNullByDefault
 class BlogPostValidator extends BdfMessageValidator {
 
 	private final GroupFactory groupFactory;
@@ -76,10 +78,10 @@ class BlogPostValidator extends BdfMessageValidator {
 				addMessageMetadata(c, m.getTimestamp());
 				break;
 			case WRAPPED_POST:
-				c = validateWrappedPost(m, g, body);
+				c = validateWrappedPost(body);
 				break;
 			case WRAPPED_COMMENT:
-				c = validateWrappedComment(m, g, body);
+				c = validateWrappedComment(body);
 				break;
 			default:
 				throw new InvalidMessageException("Unknown Message Type");
@@ -164,8 +166,8 @@ class BlogPostValidator extends BdfMessageValidator {
 		return new BdfMessageContext(meta, dependencies);
 	}
 
-	private BdfMessageContext validateWrappedPost(Message m, Group g,
-			BdfList body) throws InvalidMessageException, FormatException {
+	private BdfMessageContext validateWrappedPost(BdfList body)
+			throws InvalidMessageException, FormatException {
 
 		// p_group descriptor, p_timestamp, p_content, p_signature
 		checkSize(body, 4);
@@ -202,8 +204,8 @@ class BlogPostValidator extends BdfMessageValidator {
 		return new BdfMessageContext(meta);
 	}
 
-	private BdfMessageContext validateWrappedComment(Message m, Group g,
-			BdfList body) throws InvalidMessageException, FormatException {
+	private BdfMessageContext validateWrappedComment(BdfList body)
+			throws InvalidMessageException, FormatException {
 
 		// c_group descriptor, c_timestamp, c_comment, c_parent_original_id,
 		// c_parent_id, c_signature, parent_id
diff --git a/briar-core/src/org/briarproject/clients/BdfIncomingMessageHook.java b/briar-core/src/org/briarproject/clients/BdfIncomingMessageHook.java
index 43de4f400db948e4a4acba0b787bbf3620fbf4c7..41c402133433d3006acc34346d81b0fd28cbed60 100644
--- a/briar-core/src/org/briarproject/clients/BdfIncomingMessageHook.java
+++ b/briar-core/src/org/briarproject/clients/BdfIncomingMessageHook.java
@@ -3,31 +3,25 @@ package org.briarproject.clients;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.MessageQueueManager.IncomingQueueMessageHook;
-import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.QueueMessage;
 import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfEntry;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataParser;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.Transaction;
-import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
-import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.ValidationManager.IncomingMessageHook;
 
 import static org.briarproject.api.clients.QueueMessage.QUEUE_MESSAGE_HEADER_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
-import static org.briarproject.clients.BdfConstants.GROUP_KEY_LATEST_MSG;
-import static org.briarproject.clients.BdfConstants.GROUP_KEY_MSG_COUNT;
-import static org.briarproject.clients.BdfConstants.GROUP_KEY_UNREAD_COUNT;
-import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 
+@NotNullByDefault
 public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
-		IncomingQueueMessageHook, MessageTracker {
+		IncomingQueueMessageHook {
 
 	protected final DatabaseComponent db;
 	protected final ClientHelper clientHelper;
@@ -44,16 +38,16 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
 	 * Called once for each incoming message that passes validation.
 	 *
 	 * @throws DbException Should only be used for real database errors.
-	 * Do not rethrow
+	 * If this is thrown, delivery will be attempted again at next startup,
+	 * whereas if a FormatException is thrown, the message will be permanently
+	 * invalidated.
 	 * @throws FormatException Use this for any non-database error
 	 * that occurs while handling remotely created data.
 	 * This includes errors that occur while handling locally created data
 	 * in a context controlled by remotely created data
 	 * (for example, parsing the metadata of a dependency
 	 * of an incoming message).
-	 * Throwing this will delete the incoming message and its metadata
-	 * marking it as invalid in the database.
-	 * Never rethrow DbException as FormatException
+	 * Never rethrow DbException as FormatException!
 	 */
 	protected abstract boolean incomingMessage(Transaction txn, Message m,
 			BdfList body, BdfDictionary meta) throws DbException,
@@ -71,8 +65,12 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
 
 	@Override
 	public void incomingMessage(Transaction txn, QueueMessage q, Metadata meta)
-			throws DbException, FormatException {
-		incomingMessage(txn, q, meta, QUEUE_MESSAGE_HEADER_LENGTH);
+			throws DbException, InvalidMessageException {
+		try {
+			incomingMessage(txn, q, meta, QUEUE_MESSAGE_HEADER_LENGTH);
+		} catch (FormatException e) {
+			throw new InvalidMessageException(e);
+		}
 	}
 
 	private boolean incomingMessage(Transaction txn, Message m, Metadata meta,
@@ -84,102 +82,4 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
 		return incomingMessage(txn, m, body, metaDictionary);
 	}
 
-	protected void trackIncomingMessage(Transaction txn, Message m)
-			throws DbException {
-		trackMessage(txn, m.getGroupId(), m.getTimestamp(), false);
-	}
-
-	protected void trackOutgoingMessage(Transaction txn, Message m)
-			throws DbException {
-		trackMessage(txn, m.getGroupId(), m.getTimestamp(), true);
-	}
-
-	protected void trackMessage(Transaction txn, GroupId g, long time,
-			boolean read) throws DbException {
-		GroupCount c = getGroupCount(txn, g);
-		int msgCount = c.getMsgCount() + 1;
-		int unreadCount = c.getUnreadCount() + (read ? 0 : 1);
-		long latestTime =
-				time > c.getLatestMsgTime() ? time : c.getLatestMsgTime();
-		storeGroupCount(txn, g,
-				new GroupCount(msgCount, unreadCount, latestTime));
-	}
-
-	@Override
-	public GroupCount getGroupCount(GroupId g) throws DbException {
-		GroupCount count;
-		Transaction txn = db.startTransaction(true);
-		try {
-			count = getGroupCount(txn, g);
-			db.commitTransaction(txn);
-		}
-		finally {
-			db.endTransaction(txn);
-		}
-		return count;
-	}
-
-	protected GroupCount getGroupCount(Transaction txn, GroupId g)
-			throws DbException {
-		GroupCount count;
-		try {
-			BdfDictionary d = clientHelper.getGroupMetadataAsDictionary(txn, g);
-			count = new GroupCount(
-					d.getLong(GROUP_KEY_MSG_COUNT, 0L).intValue(),
-					d.getLong(GROUP_KEY_UNREAD_COUNT, 0L).intValue(),
-					d.getLong(GROUP_KEY_LATEST_MSG, 0L)
-			);
-		} catch (FormatException e) {
-			throw new DbException(e);
-		}
-		return count;
-	}
-
-	private void storeGroupCount(Transaction txn, GroupId g, GroupCount c)
-			throws DbException{
-		try {
-			BdfDictionary d = BdfDictionary.of(
-					new BdfEntry(GROUP_KEY_MSG_COUNT, c.getMsgCount()),
-					new BdfEntry(GROUP_KEY_UNREAD_COUNT, c.getUnreadCount()),
-					new BdfEntry(GROUP_KEY_LATEST_MSG, c.getLatestMsgTime())
-			);
-			clientHelper.mergeGroupMetadata(txn, g, d);
-		} catch (FormatException e) {
-			throw new DbException(e);
-		}
-	}
-
-	@Override
-	public void setReadFlag(GroupId g, MessageId m, boolean read)
-			throws DbException {
-		Transaction txn = db.startTransaction(false);
-		try {
-			// check current read status of message
-			BdfDictionary old =
-					clientHelper.getMessageMetadataAsDictionary(txn, m);
-			boolean wasRead = old.getBoolean(MSG_KEY_READ, false);
-
-			// if status changed
-			if (wasRead != read) {
-				// mark individual message as read
-				BdfDictionary meta = new BdfDictionary();
-				meta.put(MSG_KEY_READ, read);
-				clientHelper.mergeMessageMetadata(txn, m, meta);
-
-				// update unread counter in group metadata
-				GroupCount c = getGroupCount(txn, g);
-				BdfDictionary d = new BdfDictionary();
-				int count = c.getUnreadCount() + (read ? -1 : 1);
-				if (count < 0) throw new DbException();
-				d.put(GROUP_KEY_UNREAD_COUNT, count);
-				clientHelper.mergeGroupMetadata(txn, g, d);
-			}
-			db.commitTransaction(txn);
-		} catch (FormatException e) {
-			throw new DbException(e);
-		} finally {
-			db.endTransaction(txn);
-		}
-	}
-
 }
diff --git a/briar-core/src/org/briarproject/clients/BdfMessageValidator.java b/briar-core/src/org/briarproject/clients/BdfMessageValidator.java
index 2a78e794dd05b74f22a6262071e981fea975ade7..700724766586b54d99767b4bdca4eb74663e0023 100644
--- a/briar-core/src/org/briarproject/clients/BdfMessageValidator.java
+++ b/briar-core/src/org/briarproject/clients/BdfMessageValidator.java
@@ -9,6 +9,7 @@ import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.db.Metadata;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
@@ -25,6 +26,7 @@ import static org.briarproject.api.clients.QueueMessage.QUEUE_MESSAGE_HEADER_LEN
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
 
+@NotNullByDefault
 public abstract class BdfMessageValidator implements MessageValidator,
 		QueueMessageValidator {
 
@@ -108,7 +110,7 @@ public abstract class BdfMessageValidator implements MessageValidator,
 		if (b != null && b.length != length) throw new FormatException();
 	}
 
-	protected void checkSize(BdfList list, int minSize, int maxSize)
+	protected void checkSize(@Nullable BdfList list, int minSize, int maxSize)
 			throws FormatException {
 		if (list != null) {
 			if (list.size() < minSize) throw new FormatException();
diff --git a/briar-core/src/org/briarproject/clients/ClientsModule.java b/briar-core/src/org/briarproject/clients/ClientsModule.java
index 480c7f016d1439ebce37e7ab691c6ae629fafbc3..cd8e17538aece2ec4ae6185b7de24b3a5eadc596 100644
--- a/briar-core/src/org/briarproject/clients/ClientsModule.java
+++ b/briar-core/src/org/briarproject/clients/ClientsModule.java
@@ -3,6 +3,7 @@ package org.briarproject.clients;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.ContactGroupFactory;
 import org.briarproject.api.clients.MessageQueueManager;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.QueueMessageFactory;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.data.BdfReaderFactory;
@@ -52,4 +53,8 @@ public class ClientsModule {
 		return new QueueMessageFactoryImpl(crypto);
 	}
 
+	@Provides
+	MessageTracker provideMessageTracker(MessageTrackerImpl messageTracker) {
+		return messageTracker;
+	}
 }
diff --git a/briar-core/src/org/briarproject/clients/ConversationClientImpl.java b/briar-core/src/org/briarproject/clients/ConversationClientImpl.java
index 807e6d2f1c5b01c82a97365c43070ed87730ad0f..a19e939fc5fa29ab2e31ea2b6c0d3c9552076ce0 100644
--- a/briar-core/src/org/briarproject/clients/ConversationClientImpl.java
+++ b/briar-core/src/org/briarproject/clients/ConversationClientImpl.java
@@ -1,6 +1,8 @@
 package org.briarproject.clients;
 
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageTracker;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.MetadataParser;
@@ -8,25 +10,34 @@ import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.messaging.ConversationManager.ConversationClient;
-import org.briarproject.api.sync.Group;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
 
+@NotNullByDefault
 public abstract class ConversationClientImpl extends BdfIncomingMessageHook
 		implements ConversationClient {
 
+	protected final MessageTracker messageTracker;
+
 	protected ConversationClientImpl(DatabaseComponent db,
-			ClientHelper clientHelper, MetadataParser metadataParser) {
+			ClientHelper clientHelper, MetadataParser metadataParser,
+			MessageTracker messageTracker) {
 		super(db, clientHelper, metadataParser);
+		this.messageTracker = messageTracker;
 	}
 
-	protected abstract Group getContactGroup(Contact contact);
-
 	@Override
 	public GroupCount getGroupCount(Transaction txn, ContactId contactId)
 			throws DbException {
 		Contact contact = db.getContact(txn, contactId);
 		GroupId groupId = getContactGroup(contact).getId();
-		return getGroupCount(txn, groupId);
+		return messageTracker.getGroupCount(txn, groupId);
 	}
 
+	@Override
+	public void setReadFlag(GroupId g, MessageId m, boolean read)
+			throws DbException {
+		messageTracker.setReadFlag(g, m, read);
+	}
 }
diff --git a/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java b/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java
index bcb9ff72dbfa38b20df92a3c57462af16669ba2f..d666598509dd68f5ff2f43e70d26e529d68f0e00 100644
--- a/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java
+++ b/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java
@@ -11,6 +11,7 @@ import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.Transaction;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
@@ -20,6 +21,7 @@ import org.briarproject.api.sync.MessageContext;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.ValidationManager;
 import org.briarproject.api.sync.ValidationManager.IncomingMessageHook;
+import org.briarproject.api.sync.ValidationManager.MessageValidator;
 import org.briarproject.util.ByteUtils;
 
 import java.util.ArrayList;
@@ -29,12 +31,14 @@ import java.util.Map.Entry;
 import java.util.TreeMap;
 import java.util.logging.Logger;
 
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 import static java.util.logging.Level.INFO;
 import static org.briarproject.api.clients.QueueMessage.QUEUE_MESSAGE_HEADER_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 
+@NotNullByDefault
 class MessageQueueManagerImpl implements MessageQueueManager {
 
 	private static final String OUTGOING_POSITION_KEY = "nextOut";
@@ -139,6 +143,7 @@ class MessageQueueManagerImpl implements MessageQueueManager {
 			this.pending = pending;
 		}
 
+		@Nullable
 		MessageId popIncomingMessageId() {
 			Iterator<Entry<Long, MessageId>> it = pending.entrySet().iterator();
 			if (!it.hasNext()) {
@@ -161,8 +166,9 @@ class MessageQueueManagerImpl implements MessageQueueManager {
 		}
 	}
 
+	@NotNullByDefault
 	private static class DelegatingMessageValidator
-			implements ValidationManager.MessageValidator {
+			implements MessageValidator {
 
 		private final QueueMessageValidator delegate;
 
@@ -174,21 +180,24 @@ class MessageQueueManagerImpl implements MessageQueueManager {
 		public MessageContext validateMessage(Message m, Group g)
 				throws InvalidMessageException {
 			byte[] raw = m.getRaw();
-			if (raw.length < QUEUE_MESSAGE_HEADER_LENGTH) return null;
+			if (raw.length < QUEUE_MESSAGE_HEADER_LENGTH)
+				throw new InvalidMessageException();
 			long queuePosition = ByteUtils.readUint64(raw,
 					MESSAGE_HEADER_LENGTH);
-			if (queuePosition < 0) return null;
+			if (queuePosition < 0) throw new InvalidMessageException();
 			QueueMessage q = new QueueMessage(m.getId(), m.getGroupId(),
 					m.getTimestamp(), queuePosition, raw);
 			return delegate.validateMessage(q, g);
 		}
 	}
 
+	@NotNullByDefault
 	private class DelegatingIncomingMessageHook implements IncomingMessageHook {
 
 		private final IncomingQueueMessageHook delegate;
 
-		private DelegatingIncomingMessageHook(IncomingQueueMessageHook delegate) {
+		private DelegatingIncomingMessageHook(
+				IncomingQueueMessageHook delegate) {
 			this.delegate = delegate;
 		}
 
@@ -227,20 +236,16 @@ class MessageQueueManagerImpl implements MessageQueueManager {
 				// Save the queue state before passing control to the delegate
 				saveQueueState(txn, m.getGroupId(), queueState);
 				// Deliver the messages to the delegate
-				try {
-					delegate.incomingMessage(txn, q, meta);
-					for (MessageId id : consecutive) {
-						byte[] raw = db.getRawMessage(txn, id);
-						meta = db.getMessageMetadata(txn, id);
-						q = queueMessageFactory.createMessage(id, raw);
-						if (LOG.isLoggable(INFO)) {
-							LOG.info("Delivering pending message with position "
-									+ q.getQueuePosition());
-						}
-						delegate.incomingMessage(txn, q, meta);
+				delegate.incomingMessage(txn, q, meta);
+				for (MessageId id : consecutive) {
+					byte[] raw = db.getRawMessage(txn, id);
+					meta = db.getMessageMetadata(txn, id);
+					q = queueMessageFactory.createMessage(id, raw);
+					if (LOG.isLoggable(INFO)) {
+						LOG.info("Delivering pending message with position "
+								+ q.getQueuePosition());
 					}
-				} catch (FormatException e) {
-					throw new InvalidMessageException(e);
+					delegate.incomingMessage(txn, q, meta);
 				}
 			}
 			// message queues are only useful for groups with two members
diff --git a/briar-core/src/org/briarproject/clients/MessageTrackerImpl.java b/briar-core/src/org/briarproject/clients/MessageTrackerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d5328eee6f4e1db77942bcf3d206785be25804f
--- /dev/null
+++ b/briar-core/src/org/briarproject/clients/MessageTrackerImpl.java
@@ -0,0 +1,132 @@
+package org.briarproject.clients;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageTracker;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfEntry;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageId;
+
+import javax.inject.Inject;
+
+import static org.briarproject.clients.BdfConstants.GROUP_KEY_LATEST_MSG;
+import static org.briarproject.clients.BdfConstants.GROUP_KEY_MSG_COUNT;
+import static org.briarproject.clients.BdfConstants.GROUP_KEY_UNREAD_COUNT;
+import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
+
+@NotNullByDefault
+class MessageTrackerImpl implements MessageTracker {
+
+	private final DatabaseComponent db;
+	private final ClientHelper clientHelper;
+
+	@Inject
+	MessageTrackerImpl(DatabaseComponent db, ClientHelper clientHelper) {
+		this.db = db;
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public void trackIncomingMessage(Transaction txn, Message m)
+			throws DbException {
+		trackMessage(txn, m.getGroupId(), m.getTimestamp(), false);
+	}
+
+	@Override
+	public void trackOutgoingMessage(Transaction txn, Message m)
+			throws DbException {
+		trackMessage(txn, m.getGroupId(), m.getTimestamp(), true);
+	}
+
+	@Override
+	public void trackMessage(Transaction txn, GroupId g, long time,
+			boolean read) throws DbException {
+		GroupCount c = getGroupCount(txn, g);
+		int msgCount = c.getMsgCount() + 1;
+		int unreadCount = c.getUnreadCount() + (read ? 0 : 1);
+		long latestMsgTime = Math.max(c.getLatestMsgTime(), time);
+		storeGroupCount(txn, g, new GroupCount(msgCount, unreadCount,
+				latestMsgTime));
+	}
+
+	@Override
+	public GroupCount getGroupCount(GroupId g) throws DbException {
+		GroupCount count;
+		Transaction txn = db.startTransaction(true);
+		try {
+			count = getGroupCount(txn, g);
+			db.commitTransaction(txn);
+		} finally {
+			db.endTransaction(txn);
+		}
+		return count;
+	}
+
+	@Override
+	public GroupCount getGroupCount(Transaction txn, GroupId g)
+			throws DbException {
+		try {
+			BdfDictionary d = clientHelper.getGroupMetadataAsDictionary(txn, g);
+			return new GroupCount(
+					d.getLong(GROUP_KEY_MSG_COUNT, 0L).intValue(),
+					d.getLong(GROUP_KEY_UNREAD_COUNT, 0L).intValue(),
+					d.getLong(GROUP_KEY_LATEST_MSG, 0L)
+			);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	private void storeGroupCount(Transaction txn, GroupId g, GroupCount c)
+			throws DbException {
+		try {
+			BdfDictionary d = BdfDictionary.of(
+					new BdfEntry(GROUP_KEY_MSG_COUNT, c.getMsgCount()),
+					new BdfEntry(GROUP_KEY_UNREAD_COUNT, c.getUnreadCount()),
+					new BdfEntry(GROUP_KEY_LATEST_MSG, c.getLatestMsgTime())
+			);
+			clientHelper.mergeGroupMetadata(txn, g, d);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
+	@Override
+	public void setReadFlag(GroupId g, MessageId m, boolean read)
+			throws DbException {
+		Transaction txn = db.startTransaction(false);
+		try {
+			// check current read status of message
+			BdfDictionary old =
+					clientHelper.getMessageMetadataAsDictionary(txn, m);
+			boolean wasRead = old.getBoolean(MSG_KEY_READ, false);
+
+			// if status changed
+			if (wasRead != read) {
+				// mark individual message as read
+				BdfDictionary meta = new BdfDictionary();
+				meta.put(MSG_KEY_READ, read);
+				clientHelper.mergeMessageMetadata(txn, m, meta);
+
+				// update unread counter in group metadata
+				GroupCount c = getGroupCount(txn, g);
+				int unreadCount = c.getUnreadCount() + (read ? -1 : 1);
+				if (unreadCount < 0) throw new DbException();
+				storeGroupCount(txn, g, new GroupCount(c.getMsgCount(),
+						unreadCount, c.getLatestMsgTime()));
+			}
+			db.commitTransaction(txn);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
index db82d71c123545e7a3549825f7f921e5a54f87cb..ff88d9fdbeb7537d14f34756e577abb2a6ee3eb7 100644
--- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
@@ -2,6 +2,8 @@ package org.briarproject.forum;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageTracker;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataParser;
@@ -20,6 +22,7 @@ import org.briarproject.api.identity.Author.Status;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
@@ -52,22 +55,25 @@ import static org.briarproject.api.identity.Author.Status.ANONYMOUS;
 import static org.briarproject.api.identity.Author.Status.OURSELVES;
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 
+@NotNullByDefault
 class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 
 	private final IdentityManager identityManager;
 	private final ForumFactory forumFactory;
 	private final ForumPostFactory forumPostFactory;
+	private final MessageTracker messageTracker;
 	private final List<RemoveForumHook> removeHooks;
 
 	@Inject
 	ForumManagerImpl(DatabaseComponent db, IdentityManager identityManager,
 			ClientHelper clientHelper, MetadataParser metadataParser,
-			ForumFactory forumFactory, ForumPostFactory forumPostFactory) {
+			ForumFactory forumFactory, ForumPostFactory forumPostFactory,
+			MessageTracker messageTracker) {
 		super(db, clientHelper, metadataParser);
-
 		this.identityManager = identityManager;
 		this.forumFactory = forumFactory;
 		this.forumPostFactory = forumPostFactory;
+		this.messageTracker = messageTracker;
 		removeHooks = new CopyOnWriteArrayList<RemoveForumHook>();
 	}
 
@@ -75,7 +81,7 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 	protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
 			BdfDictionary meta) throws DbException, FormatException {
 
-		trackIncomingMessage(txn, m);
+		messageTracker.trackIncomingMessage(txn, m);
 
 		ForumPostHeader post = getForumPostHeader(txn, m.getId(), meta);
 		ForumPostReceivedEvent event =
@@ -146,7 +152,7 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 			meta.put(KEY_LOCAL, true);
 			meta.put(MSG_KEY_READ, true);
 			clientHelper.addLocalMessage(txn, p.getMessage(), meta, true);
-			trackOutgoingMessage(txn, p.getMessage());
+			messageTracker.trackOutgoingMessage(txn, p.getMessage());
 			db.commitTransaction(txn);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
@@ -204,6 +210,7 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 		try {
 			// Parent ID, author, forum post body, signature
 			BdfList message = clientHelper.getMessageAsList(m);
+			if (message == null) throw new DbException();
 			return message.getString(2);
 		} catch (FormatException e) {
 			throw new DbException(e);
@@ -253,6 +260,17 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
 		removeHooks.add(hook);
 	}
 
+	@Override
+	public GroupCount getGroupCount(GroupId g) throws DbException {
+		return messageTracker.getGroupCount(g);
+	}
+
+	@Override
+	public void setReadFlag(GroupId g, MessageId m, boolean read)
+			throws DbException {
+		messageTracker.setReadFlag(g, m, read);
+	}
+
 	private Forum parseForum(Group g) throws FormatException {
 		byte[] descriptor = g.getDescriptor();
 		// Name, salt
diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
index a763b821e8200d7e5b4d95b7010c7bdbfe1730be..19e62584c84227c35d35b1b54a53c740e36da380 100644
--- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java
+++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
@@ -9,6 +9,7 @@ import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
@@ -25,6 +26,7 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENG
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
 
+@NotNullByDefault
 class ForumPostValidator extends BdfMessageValidator {
 
 	private final AuthorFactory authorFactory;
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
index 30cae32f45b85f3ed3cbc83141edb65afbffd042..e59f34e8498ac4de2b74db755a520fd929e0fa42 100644
--- a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
@@ -3,6 +3,7 @@ package org.briarproject.introduction;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.Client;
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -23,6 +24,7 @@ import org.briarproject.api.introduction.IntroductionManager;
 import org.briarproject.api.introduction.IntroductionMessage;
 import org.briarproject.api.introduction.IntroductionRequest;
 import org.briarproject.api.introduction.IntroductionResponse;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
@@ -73,6 +75,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUE
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 
+@NotNullByDefault
 class IntroductionManagerImpl extends ConversationClientImpl
 		implements IntroductionManager, Client, AddContactHook,
 		RemoveContactHook {
@@ -86,11 +89,12 @@ class IntroductionManagerImpl extends ConversationClientImpl
 
 	@Inject
 	IntroductionManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
-			MetadataParser metadataParser, IntroducerManager introducerManager,
+			MetadataParser metadataParser, MessageTracker messageTracker,
+			IntroducerManager introducerManager,
 			IntroduceeManager introduceeManager,
 			IntroductionGroupFactory introductionGroupFactory) {
 
-		super(db, clientHelper, metadataParser);
+		super(db, clientHelper, metadataParser, messageTracker);
 		this.introducerManager = introducerManager;
 		this.introduceeManager = introduceeManager;
 		this.introductionGroupFactory = introductionGroupFactory;
@@ -213,7 +217,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
 					introduceeManager.initialize(txn, groupId, message);
 			try {
 				introduceeManager.incomingMessage(txn, state, message);
-				trackIncomingMessage(txn, m);
+				messageTracker.trackIncomingMessage(txn, m);
 			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				introduceeManager.abort(txn, state);
@@ -239,7 +243,8 @@ class IntroductionManagerImpl extends ConversationClientImpl
 						LOG.warning("Unknown role '" + role + "'");
 					throw new DbException();
 				}
-				if (type == TYPE_RESPONSE) trackIncomingMessage(txn, m);
+				if (type == TYPE_RESPONSE)
+					messageTracker.trackIncomingMessage(txn, m);
 			} catch (DbException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state);
@@ -260,7 +265,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
 	}
 
 	@Override
-	protected Group getContactGroup(Contact contact) {
+	public Group getContactGroup(Contact contact) {
 		return introductionGroupFactory.createIntroductionGroup(contact);
 	}
 
@@ -274,8 +279,8 @@ class IntroductionManagerImpl extends ConversationClientImpl
 			introducerManager.makeIntroduction(txn, c1, c2, msg, timestamp);
 			Group g1 = getContactGroup(c1);
 			Group g2 = getContactGroup(c2);
-			trackMessage(txn, g1.getId(), timestamp, true);
-			trackMessage(txn, g2.getId(), timestamp, true);
+			messageTracker.trackMessage(txn, g1.getId(), timestamp, true);
+			messageTracker.trackMessage(txn, g2.getId(), timestamp, true);
 			db.commitTransaction(txn);
 		} finally {
 			db.endTransaction(txn);
@@ -295,7 +300,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
 					getSessionState(txn, g.getId(), sessionId.getBytes());
 
 			introduceeManager.acceptIntroduction(txn, state, timestamp);
-			trackMessage(txn, g.getId(), timestamp, true);
+			messageTracker.trackMessage(txn, g.getId(), timestamp, true);
 			db.commitTransaction(txn);
 		} finally {
 			db.endTransaction(txn);
@@ -315,7 +320,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
 					getSessionState(txn, g.getId(), sessionId.getBytes());
 
 			introduceeManager.declineIntroduction(txn, state, timestamp);
-			trackMessage(txn, g.getId(), timestamp, true);
+			messageTracker.trackMessage(txn, g.getId(), timestamp, true);
 			db.commitTransaction(txn);
 		} finally {
 			db.endTransaction(txn);
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionValidator.java b/briar-core/src/org/briarproject/introduction/IntroductionValidator.java
index e2227e12eab424ab4fa2cead2ae29f70333e3240..d22163227f4e8a74cce990c92fdef86a29714cf1 100644
--- a/briar-core/src/org/briarproject/introduction/IntroductionValidator.java
+++ b/briar-core/src/org/briarproject/introduction/IntroductionValidator.java
@@ -7,6 +7,7 @@ import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.system.Clock;
@@ -39,6 +40,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPO
 import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
 import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
 
+@NotNullByDefault
 class IntroductionValidator extends BdfMessageValidator {
 
 	IntroductionValidator(ClientHelper clientHelper,
diff --git a/briar-core/src/org/briarproject/messaging/ConversationManagerImpl.java b/briar-core/src/org/briarproject/messaging/ConversationManagerImpl.java
index fafd48e75c3724d88c90cfd9e194c39538b52ecb..2a480850a605fb0fcf705ac98ea8d740e18f21c2 100644
--- a/briar-core/src/org/briarproject/messaging/ConversationManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/ConversationManagerImpl.java
@@ -6,12 +6,14 @@ import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.messaging.ConversationManager;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 
 import javax.inject.Inject;
 
+@NotNullByDefault
 class ConversationManagerImpl implements ConversationManager {
 
 	private final DatabaseComponent db;
@@ -32,9 +34,7 @@ class ConversationManagerImpl implements ConversationManager {
 	}
 
 	@Override
-	public GroupCount getGroupCount(ContactId contactId)
-			throws DbException {
-
+	public GroupCount getGroupCount(ContactId contactId) throws DbException {
 		int msgCount = 0, unreadCount = 0;
 		long latestTime = 0;
 		Transaction txn = db.startTransaction(true);
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index 624bf1b74486e81b08f5829fee342316a29a9bc7..233682600d938251ffc597a747f5be787fedc192 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
@@ -4,6 +4,7 @@ import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.Client;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.ContactGroupFactory;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
@@ -18,6 +19,7 @@ import org.briarproject.api.event.PrivateMessageReceivedEvent;
 import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessage;
 import org.briarproject.api.messaging.PrivateMessageHeader;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
@@ -33,6 +35,7 @@ import javax.inject.Inject;
 
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 
+@NotNullByDefault
 class MessagingManagerImpl extends ConversationClientImpl
 		implements MessagingManager, Client, AddContactHook, RemoveContactHook {
 
@@ -40,9 +43,9 @@ class MessagingManagerImpl extends ConversationClientImpl
 
 	@Inject
 	MessagingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
-			MetadataParser metadataParser,
+			MetadataParser metadataParser, MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory) {
-		super(db, clientHelper, metadataParser);
+		super(db, clientHelper, metadataParser, messageTracker);
 		this.contactGroupFactory = contactGroupFactory;
 	}
 
@@ -72,7 +75,7 @@ class MessagingManagerImpl extends ConversationClientImpl
 	}
 
 	@Override
-	protected Group getContactGroup(Contact c) {
+	public Group getContactGroup(Contact c) {
 		return contactGroupFactory.createContactGroup(CLIENT_ID, c);
 	}
 
@@ -95,7 +98,7 @@ class MessagingManagerImpl extends ConversationClientImpl
 		PrivateMessageReceivedEvent event = new PrivateMessageReceivedEvent(
 				header, contactId, groupId);
 		txn.attach(event);
-		trackIncomingMessage(txn, m);
+		messageTracker.trackIncomingMessage(txn, m);
 
 		// don't share message
 		return false;
@@ -110,7 +113,7 @@ class MessagingManagerImpl extends ConversationClientImpl
 			meta.put("local", true);
 			meta.put("read", true);
 			clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
-			trackOutgoingMessage(txn, m.getMessage());
+			messageTracker.trackOutgoingMessage(txn, m.getMessage());
 			db.commitTransaction(txn);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
@@ -195,20 +198,11 @@ class MessagingManagerImpl extends ConversationClientImpl
 		try {
 			// 0: private message body
 			BdfList message = clientHelper.getMessageAsList(m);
+			if (message == null) throw new DbException();
 			return message.getString(0);
 		} catch (FormatException e) {
 			throw new DbException(e);
 		}
 	}
 
-	@Override
-	public GroupCount getGroupCount(Transaction txn, ContactId contactId)
-			throws DbException {
-
-		Contact contact = db.getContact(txn, contactId);
-		GroupId groupId = getContactGroup(contact).getId();
-
-		return getGroupCount(txn, groupId);
-	}
-
 }
diff --git a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
index 209a4b24666f22930d51b461c047f111d09140d9..9eeae2409606d424638a75f8ed55e27e5e01f7b8 100644
--- a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
+++ b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
@@ -6,6 +6,7 @@ import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.system.Clock;
@@ -14,6 +15,7 @@ import org.briarproject.clients.BdfMessageValidator;
 import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 
+@NotNullByDefault
 class PrivateMessageValidator extends BdfMessageValidator {
 
 	PrivateMessageValidator(ClientHelper clientHelper,
diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
index 13ab704aaee6dc0e11516dcf63dc9f5edf984cd8..a80f1ce4b58db6e0cc03ff7a1f17bd3adeab9547 100644
--- a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
+++ b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
@@ -8,6 +8,7 @@ import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.privategroup.MessageType;
 import org.briarproject.api.privategroup.PrivateGroup;
 import org.briarproject.api.privategroup.PrivateGroupFactory;
@@ -39,6 +40,7 @@ import static org.briarproject.privategroup.GroupConstants.KEY_READ;
 import static org.briarproject.privategroup.GroupConstants.KEY_TIMESTAMP;
 import static org.briarproject.privategroup.GroupConstants.KEY_TYPE;
 
+@NotNullByDefault
 class GroupMessageValidator extends BdfMessageValidator {
 
 	private final PrivateGroupFactory privateGroupFactory;
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
index 462a8e11c18ffc7a923642e543a3986d1cb18705..94febea167477b1599dca8e8e3caa10663f61e04 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
@@ -2,6 +2,8 @@ package org.briarproject.privategroup;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageTracker;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
 import org.briarproject.api.clients.ProtocolStateException;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfEntry;
@@ -76,17 +78,18 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 
 	private final PrivateGroupFactory privateGroupFactory;
 	private final IdentityManager identityManager;
+	private final MessageTracker messageTracker;
 	private final List<PrivateGroupHook> hooks;
 
 	@Inject
 	PrivateGroupManagerImpl(ClientHelper clientHelper,
 			MetadataParser metadataParser, DatabaseComponent db,
 			PrivateGroupFactory privateGroupFactory,
-			IdentityManager identityManager) {
+			IdentityManager identityManager, MessageTracker messageTracker) {
 		super(db, clientHelper, metadataParser);
-
 		this.privateGroupFactory = privateGroupFactory;
 		this.identityManager = identityManager;
+		this.messageTracker = messageTracker;
 		hooks = new CopyOnWriteArrayList<PrivateGroupHook>();
 	}
 
@@ -128,7 +131,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		meta.put(KEY_INITIAL_JOIN_MSG, creator);
 		addMessageMetadata(meta, m, true);
 		clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
-		trackOutgoingMessage(txn, m.getMessage());
+		messageTracker.trackOutgoingMessage(txn, m.getMessage());
 		addMember(txn, m.getMessage().getGroupId(), m.getMember(), VISIBLE);
 		setPreviousMsgId(txn, m.getMessage().getGroupId(),
 				m.getMessage().getId());
@@ -210,7 +213,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 
 			// track message
 			setPreviousMsgId(txn, g, m.getMessage().getId());
-			trackOutgoingMessage(txn, m.getMessage());
+			messageTracker.trackOutgoingMessage(txn, m.getMessage());
 
 			// broadcast event
 			attachGroupMessageAddedEvent(txn, m.getMessage(), meta, true);
@@ -430,6 +433,17 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		return false;
 	}
 
+	@Override
+	public GroupCount getGroupCount(GroupId g) throws DbException {
+		return messageTracker.getGroupCount(g);
+	}
+
+	@Override
+	public void setReadFlag(GroupId g, MessageId m, boolean read)
+			throws DbException {
+		messageTracker.setReadFlag(g, m, read);
+	}
+
 	@Override
 	public void relationshipRevealed(Transaction txn, GroupId g, AuthorId a,
 			boolean byContact) throws FormatException, DbException {
@@ -498,7 +512,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		}
 		addMember(txn, m.getGroupId(), member, v);
 		// track message and broadcast event
-		trackIncomingMessage(txn, m);
+		messageTracker.trackIncomingMessage(txn, m);
 		attachJoinMessageAddedEvent(txn, m, meta, false, v);
 	}
 
@@ -535,7 +549,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		if (previousType != JOIN && previousType != POST)
 			throw new FormatException();
 		// track message and broadcast event
-		trackIncomingMessage(txn, m);
+		messageTracker.trackIncomingMessage(txn, m);
 		attachGroupMessageAddedEvent(txn, m, meta, false);
 	}
 
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
index d4a51798f1a77e512aee6355681d660eaf722e1c..d90668e6d39f62fb4029f7d7c7204a0c30a85492 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
@@ -4,6 +4,7 @@ import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.Client;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.ContactGroupFactory;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -74,13 +75,14 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 	@Inject
 	protected GroupInvitationManagerImpl(DatabaseComponent db,
 			ClientHelper clientHelper, MetadataParser metadataParser,
+			MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory,
 			PrivateGroupFactory privateGroupFactory,
 			PrivateGroupManager privateGroupManager,
 			MessageParser messageParser, SessionParser sessionParser,
 			SessionEncoder sessionEncoder,
 			ProtocolEngineFactory engineFactory) {
-		super(db, clientHelper, metadataParser);
+		super(db, clientHelper, metadataParser, messageTracker);
 		this.contactGroupFactory = contactGroupFactory;
 		this.privateGroupFactory = privateGroupFactory;
 		this.privateGroupManager = privateGroupManager;
@@ -131,7 +133,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 	}
 
 	@Override
-	protected Group getContactGroup(Contact c) {
+	public Group getContactGroup(Contact c) {
 		return contactGroupFactory.createContactGroup(CLIENT_ID, c);
 	}
 
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
index 940c6b0f1c955eee2152fc3b5f1e0afb133a559e..14aef6715d4ee1575a03163ff4e4b0666e5500e4 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
@@ -1,11 +1,12 @@
 package org.briarproject.properties;
 
 import org.briarproject.api.FormatException;
-import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.BdfMessageContext;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.system.Clock;
@@ -15,6 +16,7 @@ import static org.briarproject.api.TransportId.MAX_TRANSPORT_ID_LENGTH;
 import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
 import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
 
+@NotNullByDefault
 public class TransportPropertyValidator extends BdfMessageValidator {
 
 	TransportPropertyValidator(ClientHelper clientHelper,
diff --git a/briar-core/src/org/briarproject/sharing/BlogSharingManagerImpl.java b/briar-core/src/org/briarproject/sharing/BlogSharingManagerImpl.java
index 39533d7d936a12ddc57d2c4ee6a852fb1eb0a7d9..c28ad5792dceaaebddd09c9f457e428598810d12 100644
--- a/briar-core/src/org/briarproject/sharing/BlogSharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/sharing/BlogSharingManagerImpl.java
@@ -12,6 +12,7 @@ import org.briarproject.api.blogs.BlogSharingMessage.BlogInvitation;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.ContactGroupFactory;
 import org.briarproject.api.clients.MessageQueueManager;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -28,6 +29,7 @@ import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.identity.IdentityManager;
 import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sharing.InvitationMessage;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
@@ -43,6 +45,7 @@ import static org.briarproject.api.blogs.BlogConstants.BLOG_PUBLIC_KEY;
 import static org.briarproject.api.sharing.SharingConstants.INVITATION_ID;
 import static org.briarproject.api.sharing.SharingConstants.RESPONSE_ID;
 
+@NotNullByDefault
 class BlogSharingManagerImpl extends
 		SharingManagerImpl<Blog, BlogInvitation, BlogInviteeSessionState, BlogSharerSessionState, BlogInvitationReceivedEvent, BlogInvitationResponseReceivedEvent>
 		implements BlogSharingManager, RemoveBlogHook {
@@ -63,10 +66,11 @@ class BlogSharingManagerImpl extends
 			DatabaseComponent db, MessageQueueManager messageQueueManager,
 			MetadataEncoder metadataEncoder, MetadataParser metadataParser,
 			ContactGroupFactory contactGroupFactory, SecureRandom random,
-			IdentityManager identityManager) {
+			IdentityManager identityManager, MessageTracker messageTracker) {
 
 		super(db, messageQueueManager, clientHelper, metadataParser,
-				metadataEncoder, random, contactGroupFactory, clock);
+				metadataEncoder, random, contactGroupFactory, messageTracker,
+				clock);
 
 		this.blogManager = blogManager;
 		this.identityManager = identityManager;
diff --git a/briar-core/src/org/briarproject/sharing/BlogSharingValidator.java b/briar-core/src/org/briarproject/sharing/BlogSharingValidator.java
index 781555917cbe378dc4cd2aae584bb743f01f1199..3244abca1a4ca71bca0dd354b9894df2a1308483 100644
--- a/briar-core/src/org/briarproject/sharing/BlogSharingValidator.java
+++ b/briar-core/src/org/briarproject/sharing/BlogSharingValidator.java
@@ -7,6 +7,7 @@ import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.system.Clock;
@@ -30,6 +31,7 @@ import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE
 import static org.briarproject.api.sharing.SharingConstants.TIME;
 import static org.briarproject.api.sharing.SharingConstants.TYPE;
 
+@NotNullByDefault
 class BlogSharingValidator extends BdfMessageValidator {
 
 	@Inject
diff --git a/briar-core/src/org/briarproject/sharing/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/sharing/ForumSharingManagerImpl.java
index d4c04971a0e4a3961cfe2e05b2b3bc5a5fced674..29462307477a4bb5196e35832259212af1e01c5c 100644
--- a/briar-core/src/org/briarproject/sharing/ForumSharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/sharing/ForumSharingManagerImpl.java
@@ -4,6 +4,7 @@ import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.ContactGroupFactory;
 import org.briarproject.api.clients.MessageQueueManager;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.data.BdfDictionary;
@@ -20,8 +21,10 @@ import org.briarproject.api.forum.ForumFactory;
 import org.briarproject.api.forum.ForumInvitationRequest;
 import org.briarproject.api.forum.ForumInvitationResponse;
 import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumManager.RemoveForumHook;
 import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.forum.ForumSharingMessage.ForumInvitation;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sharing.InvitationMessage;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
@@ -37,9 +40,10 @@ import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
 import static org.briarproject.api.sharing.SharingConstants.INVITATION_ID;
 import static org.briarproject.api.sharing.SharingConstants.RESPONSE_ID;
 
+@NotNullByDefault
 class ForumSharingManagerImpl extends
 		SharingManagerImpl<Forum, ForumInvitation, ForumInviteeSessionState, ForumSharerSessionState, ForumInvitationReceivedEvent, ForumInvitationResponseReceivedEvent>
-		implements ForumSharingManager, ForumManager.RemoveForumHook {
+		implements ForumSharingManager, RemoveForumHook {
 
 	private final SFactory sFactory;
 	private final IFactory iFactory;
@@ -57,9 +61,10 @@ class ForumSharingManagerImpl extends
 			MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser,
 			ContactGroupFactory contactGroupFactory,
-			SecureRandom random) {
+			SecureRandom random, MessageTracker messageTracker) {
 		super(db, messageQueueManager, clientHelper, metadataParser,
-				metadataEncoder, random, contactGroupFactory, clock);
+				metadataEncoder, random, contactGroupFactory, messageTracker,
+				clock);
 
 		sFactory = new SFactory(forumFactory, forumManager);
 		iFactory = new IFactory();
diff --git a/briar-core/src/org/briarproject/sharing/ForumSharingValidator.java b/briar-core/src/org/briarproject/sharing/ForumSharingValidator.java
index 11343f53803433a40675bb5ee916c27870ee8e17..471446b6f4dd9ae1617ad58ab9f6bf9d6f9932ec 100644
--- a/briar-core/src/org/briarproject/sharing/ForumSharingValidator.java
+++ b/briar-core/src/org/briarproject/sharing/ForumSharingValidator.java
@@ -7,6 +7,7 @@ import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.system.Clock;
@@ -30,6 +31,7 @@ import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE
 import static org.briarproject.api.sharing.SharingConstants.TIME;
 import static org.briarproject.api.sharing.SharingConstants.TYPE;
 
+@NotNullByDefault
 class ForumSharingValidator extends BdfMessageValidator {
 
 	@Inject
diff --git a/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java b/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
index cd525096c8543ea428cf05b71c0163a2deabc1f1..95697ffa40a493b0cbcb678c0ff0e39f0f4edc0d 100644
--- a/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
@@ -6,6 +6,7 @@ import org.briarproject.api.clients.Client;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.ContactGroupFactory;
 import org.briarproject.api.clients.MessageQueueManager;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -25,6 +26,7 @@ import org.briarproject.api.event.Event;
 import org.briarproject.api.event.InvitationRequestReceivedEvent;
 import org.briarproject.api.event.InvitationResponseReceivedEvent;
 import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sharing.InvitationMessage;
 import org.briarproject.api.sharing.Shareable;
 import org.briarproject.api.sharing.SharingInvitationItem;
@@ -52,6 +54,8 @@ import java.util.Map;
 import java.util.Set;
 import java.util.logging.Logger;
 
+import javax.annotation.Nullable;
+
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.clients.ProtocolEngine.StateUpdate;
@@ -86,6 +90,7 @@ import static org.briarproject.api.sharing.SharingMessage.Invitation;
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 import static org.briarproject.sharing.InviteeSessionState.State.AWAIT_LOCAL_RESPONSE;
 
+@NotNullByDefault
 abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS extends InviteeSessionState, SS extends SharerSessionState, IR extends InvitationRequestReceivedEvent, IRR extends InvitationResponseReceivedEvent>
 		extends ConversationClientImpl
 		implements SharingManager<S>, Client, AddContactHook,
@@ -105,9 +110,8 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 			MessageQueueManager messageQueueManager, ClientHelper clientHelper,
 			MetadataParser metadataParser, MetadataEncoder metadataEncoder,
 			SecureRandom random, ContactGroupFactory contactGroupFactory,
-			Clock clock) {
-		super(db, clientHelper, metadataParser);
-
+			MessageTracker messageTracker, Clock clock) {
+		super(db, clientHelper, metadataParser, messageTracker);
 		this.messageQueueManager = messageQueueManager;
 		this.metadataEncoder = metadataEncoder;
 		this.random = random;
@@ -226,7 +230,7 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 					new InviteeEngine<IS, IR>(getIRFactory(), clock);
 			processInviteeStateUpdate(txn, m.getId(),
 					engine.onMessageReceived(state, msg));
-			trackIncomingMessage(txn, m);
+			messageTracker.trackIncomingMessage(txn, m);
 		} else if (msg.getType() == SHARE_MSG_TYPE_ACCEPT ||
 				msg.getType() == SHARE_MSG_TYPE_DECLINE) {
 			// we are a sharer who just received a response
@@ -237,7 +241,7 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 							getIRRFactory(), clock);
 			processSharerStateUpdate(txn, m.getId(),
 					engine.onMessageReceived(state, msg));
-			trackIncomingMessage(txn, m);
+			messageTracker.trackIncomingMessage(txn, m);
 		} else if (msg.getType() == SHARE_MSG_TYPE_LEAVE ||
 				msg.getType() == SHARE_MSG_TYPE_ABORT) {
 			// we don't know who we are, so figure it out
@@ -296,7 +300,8 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 			// track message
 			// TODO handle this properly without engine hacks (#376)
 			long time = update.toSend.get(0).getTime();
-			trackMessage(txn, localState.getGroupId(), time, true);
+			messageTracker.trackMessage(txn, localState.getGroupId(), time,
+					true);
 
 			db.commitTransaction(txn);
 		} catch (FormatException e) {
@@ -359,7 +364,7 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		// track message
 		// TODO handle this properly without engine hacks (#376)
 		long time = update.toSend.get(0).getTime();
-		trackMessage(txn, localState.getGroupId(), time, true);
+		messageTracker.trackMessage(txn, localState.getGroupId(), time, true);
 	}
 
 	@Override
@@ -581,16 +586,6 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		}
 	}
 
-	@Override
-	public GroupCount getGroupCount(Transaction txn, ContactId contactId)
-			throws DbException {
-
-		Contact contact = db.getContact(txn, contactId);
-		GroupId groupId = getContactGroup(contact).getId();
-
-		return getGroupCount(txn, groupId);
-	}
-
 	void removingShareable(Transaction txn, S f) throws DbException {
 		try {
 			for (Contact c : db.getContacts(txn)) {
@@ -827,7 +822,8 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		throw new FormatException();
 	}
 
-	private void processStateUpdate(Transaction txn, MessageId messageId,
+	private void processStateUpdate(Transaction txn,
+			@Nullable MessageId messageId,
 			StateUpdate<SharingSessionState, BaseMessage> result, S f)
 			throws DbException, FormatException {
 
@@ -859,8 +855,8 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		}
 	}
 
-	private void processSharerStateUpdate(Transaction txn, MessageId messageId,
-			StateUpdate<SS, BaseMessage> result)
+	private void processSharerStateUpdate(Transaction txn,
+			@Nullable MessageId messageId, StateUpdate<SS, BaseMessage> result)
 			throws DbException, FormatException {
 
 		StateUpdate<SharingSessionState, BaseMessage> r =
@@ -874,8 +870,8 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		processStateUpdate(txn, messageId, r, f);
 	}
 
-	private void processInviteeStateUpdate(Transaction txn, MessageId messageId,
-			StateUpdate<IS, BaseMessage> result)
+	private void processInviteeStateUpdate(Transaction txn,
+			@Nullable MessageId messageId, StateUpdate<IS, BaseMessage> result)
 			throws DbException, FormatException {
 
 		StateUpdate<SharingSessionState, BaseMessage> r =
@@ -947,7 +943,7 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 	}
 
 	@Override
-	protected Group getContactGroup(Contact c) {
+	public Group getContactGroup(Contact c) {
 		return contactGroupFactory.createContactGroup(getClientId(), c);
 	}
 
diff --git a/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java b/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java
index a92f550c648f676af05cfc09a3c4d8d32aa7b148..c245ea53b83b9a89417285b9f4cdb3f9ff4d9255 100644
--- a/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java
+++ b/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java
@@ -1,19 +1,22 @@
 package org.briarproject.sync;
 
-
 import org.briarproject.api.UniqueId;
 import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.ByteUtils;
 
+import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 
+@Immutable
+@NotNullByDefault
 class MessageFactoryImpl implements MessageFactory {
 
 	private final CryptoComponent crypto;
diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
index 289bde12288c21c8e402743a85507c8b3f210072..c4be29aa40011a39d389927f2321690c9081eaf0 100644
--- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
+++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
@@ -1,6 +1,5 @@
 package org.briarproject.sync;
 
-import org.briarproject.api.UniqueId;
 import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DatabaseExecutor;
@@ -13,15 +12,15 @@ import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.MessageAddedEvent;
 import org.briarproject.api.lifecycle.Service;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageContext;
+import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.ValidationManager;
-import org.briarproject.util.ByteUtils;
 
 import java.util.Collection;
 import java.util.LinkedList;
@@ -33,15 +32,17 @@ import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
+import javax.annotation.concurrent.ThreadSafe;
 import javax.inject.Inject;
 
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 import static org.briarproject.api.sync.ValidationManager.State.INVALID;
 import static org.briarproject.api.sync.ValidationManager.State.PENDING;
 
+@ThreadSafe
+@NotNullByDefault
 class ValidationManagerImpl implements ValidationManager, Service,
 		EventListener {
 
@@ -51,6 +52,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
 	private final DatabaseComponent db;
 	private final Executor dbExecutor;
 	private final Executor cryptoExecutor;
+	private final MessageFactory messageFactory;
 	private final Map<ClientId, MessageValidator> validators;
 	private final Map<ClientId, IncomingMessageHook> hooks;
 	private final AtomicBoolean used = new AtomicBoolean(false);
@@ -58,10 +60,12 @@ class ValidationManagerImpl implements ValidationManager, Service,
 	@Inject
 	ValidationManagerImpl(DatabaseComponent db,
 			@DatabaseExecutor Executor dbExecutor,
-			@CryptoExecutor Executor cryptoExecutor) {
+			@CryptoExecutor Executor cryptoExecutor,
+			MessageFactory messageFactory) {
 		this.db = db;
 		this.dbExecutor = dbExecutor;
 		this.cryptoExecutor = cryptoExecutor;
+		this.messageFactory = messageFactory;
 		validators = new ConcurrentHashMap<ClientId, MessageValidator>();
 		hooks = new ConcurrentHashMap<ClientId, IncomingMessageHook>();
 	}
@@ -134,7 +138,8 @@ class ValidationManagerImpl implements ValidationManager, Service,
 			try {
 				MessageId id = unvalidated.poll();
 				byte[] raw = db.getRawMessage(txn, id);
-				m = parseMessage(id, raw);
+				if (raw == null) throw new DbException();
+				m = messageFactory.createMessage(id, raw);
 				g = db.getGroup(txn, m.getGroupId());
 				db.commitTransaction(txn);
 			} finally {
@@ -210,7 +215,9 @@ class ValidationManagerImpl implements ValidationManager, Service,
 						invalidateMessage(txn, id);
 						invalidate = getDependentsToInvalidate(txn, id);
 					} else if (allDelivered) {
-						Message m = parseMessage(id, db.getRawMessage(txn, id));
+						byte[] raw = db.getRawMessage(txn, id);
+						if (raw == null) throw new DbException();
+						Message m = messageFactory.createMessage(id, raw);
 						Group g = db.getGroup(txn, m.getGroupId());
 						ClientId c = g.getClientId();
 						Metadata meta = db.getMessageMetadataForValidator(txn,
@@ -247,15 +254,6 @@ class ValidationManagerImpl implements ValidationManager, Service,
 		}
 	}
 
-	private Message parseMessage(MessageId id, byte[] raw) {
-		if (raw.length <= MESSAGE_HEADER_LENGTH)
-			throw new IllegalArgumentException();
-		byte[] groupId = new byte[UniqueId.LENGTH];
-		System.arraycopy(raw, 0, groupId, 0, UniqueId.LENGTH);
-		long timestamp = ByteUtils.readUint64(raw, UniqueId.LENGTH);
-		return new Message(id, new GroupId(groupId), timestamp, raw);
-	}
-
 	private void validateMessageAsync(final Message m, final Group g) {
 		cryptoExecutor.execute(new Runnable() {
 			@Override
@@ -412,7 +410,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
 
 	/**
 	 * Shares the next message from the toShare queue asynchronously.
-	 *
+	 * <p>
 	 * This method should only be called for messages that have all their
 	 * dependencies delivered and have been delivered themselves.
 	 */
diff --git a/briar-tests/src/org/briarproject/clients/MessageQueueManagerImplTest.java b/briar-tests/src/org/briarproject/clients/MessageQueueManagerImplTest.java
index 39a166f4e76e0626d8539c6ecc209f78a8a26f00..8e750a9c68fcfd56fee96beed40959d894087fd8 100644
--- a/briar-tests/src/org/briarproject/clients/MessageQueueManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/clients/MessageQueueManagerImplTest.java
@@ -16,6 +16,7 @@ import org.briarproject.api.db.Transaction;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageContext;
 import org.briarproject.api.sync.MessageId;
@@ -37,8 +38,8 @@ import static org.briarproject.api.clients.QueueMessage.QUEUE_MESSAGE_HEADER_LEN
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
 
 public class MessageQueueManagerImplTest extends BriarTestCase {
 
@@ -152,7 +153,12 @@ public class MessageQueueManagerImplTest extends BriarTestCase {
 		MessageValidator delegate = captured.get();
 		assertNotNull(delegate);
 		// The message should be invalid
-		assertNull(delegate.validateMessage(message, group));
+		try {
+			delegate.validateMessage(message, group);
+			fail();
+		} catch (InvalidMessageException expected) {
+			// Expected
+		}
 
 		context.assertIsSatisfied();
 	}
@@ -193,7 +199,12 @@ public class MessageQueueManagerImplTest extends BriarTestCase {
 		MessageValidator delegate = captured.get();
 		assertNotNull(delegate);
 		// The message should be invalid
-		assertNull(delegate.validateMessage(message, group));
+		try {
+			delegate.validateMessage(message, group);
+			fail();
+		} catch (InvalidMessageException expected) {
+			// Expected
+		}
 
 		context.assertIsSatisfied();
 	}
diff --git a/briar-tests/src/org/briarproject/introduction/IntroductionManagerImplTest.java b/briar-tests/src/org/briarproject/introduction/IntroductionManagerImplTest.java
index e3c7d2ea5aee81a354f7870d9bee1f7c79bd075e..463d4a8671a1a29efb2d282bc3cab5c6ecab4936 100644
--- a/briar-tests/src/org/briarproject/introduction/IntroductionManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/introduction/IntroductionManagerImplTest.java
@@ -4,6 +4,7 @@ import org.briarproject.BriarTestCase;
 import org.briarproject.TestUtils;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageTracker;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -41,9 +42,6 @@ import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
 import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
-import static org.briarproject.clients.BdfConstants.GROUP_KEY_LATEST_MSG;
-import static org.briarproject.clients.BdfConstants.GROUP_KEY_MSG_COUNT;
-import static org.briarproject.clients.BdfConstants.GROUP_KEY_UNREAD_COUNT;
 import static org.junit.Assert.assertFalse;
 
 public class IntroductionManagerImplTest extends BriarTestCase {
@@ -54,6 +52,7 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 	private final IntroduceeManager introduceeManager;
 	private final DatabaseComponent db;
 	private final ClientHelper clientHelper;
+	private final MessageTracker messageTracker;
 	private final IntroductionGroupFactory introductionGroupFactory;
 	private final SessionId sessionId = new SessionId(TestUtils.getRandomId());
 	private final long time = 42L;
@@ -63,7 +62,6 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 	private final Group introductionGroup2;
 	private final Message message1;
 	private Transaction txn;
-	private BdfDictionary metadataBefore, metadataAfter;
 
 	public IntroductionManagerImplTest() {
 		AuthorId authorId1 = new AuthorId(TestUtils.getRandomId());
@@ -94,16 +92,6 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 				time,
 				TestUtils.getRandomBytes(MESSAGE_HEADER_LENGTH + 1)
 		);
-		metadataBefore = BdfDictionary.of(
-				new BdfEntry(GROUP_KEY_MSG_COUNT, 41),
-				new BdfEntry(GROUP_KEY_UNREAD_COUNT, 0),
-				new BdfEntry(GROUP_KEY_LATEST_MSG, 0)
-		);
-		metadataAfter = BdfDictionary.of(
-				new BdfEntry(GROUP_KEY_MSG_COUNT, 42),
-				new BdfEntry(GROUP_KEY_UNREAD_COUNT, 0),
-				new BdfEntry(GROUP_KEY_LATEST_MSG, time)
-		);
 
 		// mock ALL THE THINGS!!!
 		context = new Mockery();
@@ -113,12 +101,12 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 		db = context.mock(DatabaseComponent.class);
 		clientHelper = context.mock(ClientHelper.class);
 		MetadataParser metadataParser = context.mock(MetadataParser.class);
+		messageTracker = context.mock(MessageTracker.class);
 		introductionGroupFactory = context.mock(IntroductionGroupFactory.class);
 
-		introductionManager = new IntroductionManagerImpl(
-				db, clientHelper, metadataParser, introducerManager,
-				introduceeManager, introductionGroupFactory
-		);
+		introductionManager = new IntroductionManagerImpl(db, clientHelper,
+				metadataParser, messageTracker, introducerManager,
+				introduceeManager, introductionGroupFactory);
 	}
 
 	@Test
@@ -139,19 +127,11 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 					.createIntroductionGroup(introducee2);
 			will(returnValue(introductionGroup2));
 			// track message for group 1
-			oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
-					introductionGroup1.getId());
-			will(returnValue(metadataBefore));
-			oneOf(clientHelper)
-					.mergeGroupMetadata(txn, introductionGroup1.getId(),
-							metadataAfter);
+			oneOf(messageTracker).trackMessage(txn,
+					introductionGroup1.getId(), time, true);
 			// track message for group 2
-			oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
-					introductionGroup2.getId());
-			will(returnValue(metadataBefore));
-			oneOf(clientHelper)
-					.mergeGroupMetadata(txn, introductionGroup2.getId(),
-							metadataAfter);
+			oneOf(messageTracker).trackMessage(txn,
+					introductionGroup2.getId(), time, true);
 			oneOf(db).commitTransaction(txn);
 			oneOf(db).endTransaction(txn);
 		}});
@@ -181,12 +161,8 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 			will(returnValue(state));
 			oneOf(introduceeManager).acceptIntroduction(txn, state, time);
 			// track message
-			oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
-					introductionGroup1.getId());
-			will(returnValue(metadataBefore));
-			oneOf(clientHelper)
-					.mergeGroupMetadata(txn, introductionGroup1.getId(),
-							metadataAfter);
+			oneOf(messageTracker).trackMessage(txn,
+					introductionGroup1.getId(), time, true);
 			oneOf(db).commitTransaction(txn);
 			oneOf(db).endTransaction(txn);
 		}});
@@ -216,12 +192,8 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 			will(returnValue(state));
 			oneOf(introduceeManager).declineIntroduction(txn, state, time);
 			// track message
-			oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
-					introductionGroup1.getId());
-			will(returnValue(metadataBefore));
-			oneOf(clientHelper)
-					.mergeGroupMetadata(txn, introductionGroup1.getId(),
-							metadataAfter);
+			oneOf(messageTracker).trackMessage(txn,
+					introductionGroup1.getId(), time, true);
 			oneOf(db).commitTransaction(txn);
 			oneOf(db).endTransaction(txn);
 		}});
@@ -272,9 +244,6 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 		final BdfDictionary state = new BdfDictionary();
 		txn = new Transaction(null, false);
 
-		metadataBefore.put(GROUP_KEY_UNREAD_COUNT, 1);
-		metadataAfter.put(GROUP_KEY_UNREAD_COUNT, 2);
-
 		context.checking(new Expectations() {{
 			oneOf(introduceeManager)
 					.initialize(txn, introductionGroup1.getId(), msg);
@@ -282,12 +251,7 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 			oneOf(introduceeManager)
 					.incomingMessage(txn, state, msg);
 			// track message
-			oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
-					introductionGroup1.getId());
-			will(returnValue(metadataBefore));
-			oneOf(clientHelper)
-					.mergeGroupMetadata(txn, introductionGroup1.getId(),
-							metadataAfter);
+			oneOf(messageTracker).trackIncomingMessage(txn, message1);
 		}});
 
 		introductionManager
@@ -313,20 +277,12 @@ public class IntroductionManagerImplTest extends BriarTestCase {
 
 		txn = new Transaction(null, false);
 
-		metadataBefore.put(GROUP_KEY_UNREAD_COUNT, 41);
-		metadataAfter.put(GROUP_KEY_UNREAD_COUNT, 42);
-
 		context.checking(new Expectations() {{
 			oneOf(clientHelper).getMessageMetadataAsDictionary(txn, sessionId);
 			will(returnValue(state));
 			oneOf(introducerManager).incomingMessage(txn, state, msg);
 			// track message
-			oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
-					introductionGroup1.getId());
-			will(returnValue(metadataBefore));
-			oneOf(clientHelper)
-					.mergeGroupMetadata(txn, introductionGroup1.getId(),
-							metadataAfter);
+			oneOf(messageTracker).trackIncomingMessage(txn, message1);
 		}});
 
 		introductionManager
diff --git a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
index 373f94f808637c41a719d3b534a526b79c0687ad..905ba25294b0075573157924cf587ffdacfab644 100644
--- a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
@@ -17,6 +17,7 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.InvalidMessageException;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageContext;
+import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.ValidationManager.IncomingMessageHook;
 import org.briarproject.api.sync.ValidationManager.MessageValidator;
@@ -74,6 +75,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -97,6 +100,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			will(returnValue(txn1));
 			oneOf(db).getRawMessage(txn1, messageId);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId, raw);
+			will(returnValue(message));
 			oneOf(db).getGroup(txn1, groupId);
 			will(returnValue(group));
 			oneOf(db).commitTransaction(txn1);
@@ -122,6 +127,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			will(returnValue(txn3));
 			oneOf(db).getRawMessage(txn3, messageId1);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId1, raw);
+			will(returnValue(message1));
 			oneOf(db).getGroup(txn3, groupId);
 			will(returnValue(group));
 			oneOf(db).commitTransaction(txn3);
@@ -159,7 +166,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.startService();
@@ -173,6 +180,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -207,6 +216,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Get the message and its metadata to deliver
 			oneOf(db).getRawMessage(txn2, messageId);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId, raw);
+			will(returnValue(message));
 			oneOf(db).getGroup(txn2, groupId);
 			will(returnValue(group));
 			oneOf(db).getMessageMetadataForValidator(txn2, messageId);
@@ -230,6 +241,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Get the dependent and its metadata to deliver
 			oneOf(db).getRawMessage(txn3, messageId2);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId2, raw);
+			will(returnValue(message2));
 			oneOf(db).getGroup(txn3, groupId);
 			will(returnValue(group));
 			oneOf(db).getMessageMetadataForValidator(txn3, messageId2);
@@ -254,7 +267,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.startService();
@@ -268,6 +281,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -319,7 +334,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.startService();
@@ -333,6 +348,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -380,7 +397,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));
@@ -395,6 +412,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -424,6 +443,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			will(returnValue(txn2));
 			oneOf(db).getRawMessage(txn2, messageId1);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId1, raw);
+			will(returnValue(message1));
 			oneOf(db).getGroup(txn2, groupId);
 			will(returnValue(group));
 			oneOf(db).commitTransaction(txn2);
@@ -461,7 +482,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.startService();
@@ -476,6 +497,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -498,6 +521,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			will(returnValue(txn1));
 			oneOf(db).getRawMessage(txn1, messageId);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId, raw);
+			will(returnValue(message));
 			// Load the group - *gasp* it's gone!
 			oneOf(db).getGroup(txn1, groupId);
 			will(throwException(new NoSuchGroupException()));
@@ -508,6 +533,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			will(returnValue(txn2));
 			oneOf(db).getRawMessage(txn2, messageId1);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId1, raw);
+			will(returnValue(message1));
 			oneOf(db).getGroup(txn2, groupId);
 			will(returnValue(group));
 			oneOf(db).commitTransaction(txn2);
@@ -545,7 +572,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.startService();
@@ -559,6 +586,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -591,7 +620,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));
@@ -605,12 +634,14 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, null));
@@ -626,6 +657,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -656,7 +689,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));
@@ -671,6 +704,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -707,7 +742,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));
@@ -722,6 +757,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -773,7 +810,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));
@@ -787,6 +824,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -888,7 +927,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));
@@ -902,6 +941,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -958,6 +999,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Get message 1 and its metadata
 			oneOf(db).getRawMessage(txn2, messageId1);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId1, raw);
+			will(returnValue(message1));
 			oneOf(db).getGroup(txn2, groupId);
 			will(returnValue(group));
 			oneOf(db).getMessageMetadataForValidator(txn2, messageId1);
@@ -981,6 +1024,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Get message 2 and its metadata
 			oneOf(db).getRawMessage(txn3, messageId2);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId2, raw);
+			will(returnValue(message2));
 			oneOf(db).getGroup(txn3, groupId);
 			will(returnValue(group));
 			oneOf(db).getMessageMetadataForValidator(txn3, messageId2);
@@ -1004,6 +1049,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Get message 3 and its metadata
 			oneOf(db).getRawMessage(txn4, messageId3);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId3, raw);
+			will(returnValue(message3));
 			oneOf(db).getGroup(txn4, groupId);
 			will(returnValue(group));
 			oneOf(db).getMessageMetadataForValidator(txn4, messageId3);
@@ -1033,6 +1080,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 			// Get message 4 and its metadata
 			oneOf(db).getRawMessage(txn6, messageId4);
 			will(returnValue(raw));
+			oneOf(messageFactory).createMessage(messageId4, raw);
+			will(returnValue(message4));
 			oneOf(db).getGroup(txn6, groupId);
 			will(returnValue(group));
 			oneOf(db).getMessageMetadataForValidator(txn6, messageId4);
@@ -1049,7 +1098,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));
@@ -1063,6 +1112,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -1108,7 +1159,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));
@@ -1125,6 +1176,8 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		final Executor dbExecutor = new ImmediateExecutor();
 		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageFactory messageFactory =
+				context.mock(MessageFactory.class);
 		final MessageValidator validator = context.mock(MessageValidator.class);
 		final IncomingMessageHook hook =
 				context.mock(IncomingMessageHook.class);
@@ -1178,7 +1231,7 @@ public class ValidationManagerImplTest extends BriarTestCase {
 		}});
 
 		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
-				cryptoExecutor);
+				cryptoExecutor, messageFactory);
 		vm.registerMessageValidator(clientId, validator);
 		vm.registerIncomingMessageHook(clientId, hook);
 		vm.eventOccurred(new MessageAddedEvent(message, contactId));