diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 69505d32f3ea0d2f7bbef96cd5e21e97470dc57b..c5a1b275da416f4344c8867fe4f8b956f178fb0d 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -27,6 +27,10 @@ import org.briarproject.android.privategroup.creation.CreateGroupController;
 import org.briarproject.android.privategroup.creation.CreateGroupControllerImpl;
 import org.briarproject.android.privategroup.list.GroupListController;
 import org.briarproject.android.privategroup.list.GroupListControllerImpl;
+import org.briarproject.android.sharing.InvitationsBlogController;
+import org.briarproject.android.sharing.InvitationsBlogControllerImpl;
+import org.briarproject.android.sharing.InvitationsForumController;
+import org.briarproject.android.sharing.InvitationsForumControllerImpl;
 
 import dagger.Module;
 import dagger.Provides;
@@ -125,6 +129,22 @@ public class ActivityModule {
 		return forumController;
 	}
 
+	@ActivityScope
+	@Provides
+	protected InvitationsForumController provideInvitationsForumController(
+			InvitationsForumControllerImpl invitationsForumController) {
+		activity.addLifecycleController(invitationsForumController);
+		return invitationsForumController;
+	}
+
+	@ActivityScope
+	@Provides
+	protected InvitationsBlogController provideInvitationsBlogController(
+			InvitationsBlogControllerImpl invitationsBlogController) {
+		activity.addLifecycleController(invitationsBlogController);
+		return invitationsBlogController;
+	}
+
 	@ActivityScope
 	@Provides
 	BlogController provideBlogController(BlogControllerImpl blogController) {
diff --git a/briar-android/src/org/briarproject/android/sharing/BlogInvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/BlogInvitationAdapter.java
deleted file mode 100644
index 7b0e7dfa6e108067aed9470e96f88921216518de..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/sharing/BlogInvitationAdapter.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.briarproject.android.sharing;
-
-import android.content.Context;
-
-import org.briarproject.R;
-import org.briarproject.api.blogs.Blog;
-import org.briarproject.api.sharing.InvitationItem;
-
-class BlogInvitationAdapter extends InvitationAdapter {
-
-	BlogInvitationAdapter(Context ctx, AvailableForumClickListener listener) {
-		super(ctx, listener);
-	}
-
-	@Override
-	public void onBindViewHolder(InvitationsViewHolder ui, int position) {
-		super.onBindViewHolder(ui, position);
-		InvitationItem item = getItemAt(position);
-		if (item == null) return;
-
-		Blog blog = (Blog) item.getShareable();
-
-		ui.avatar.setAuthorAvatar(blog.getAuthor());
-
-		ui.name.setText(ctx.getString(R.string.blogs_personal_blog,
-				blog.getAuthor().getName()));
-
-		if (item.isSubscribed()) {
-			ui.subscribed.setText(ctx.getString(R.string.blogs_sharing_exists));
-		}
-	}
-
-	@Override
-	public int compare(InvitationItem o1, InvitationItem o2) {
-		return String.CASE_INSENSITIVE_ORDER
-				.compare(((Blog) o1.getShareable()).getAuthor().getName(),
-						((Blog) o2.getShareable()).getAuthor().getName());
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/sharing/ForumInvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/ForumInvitationAdapter.java
deleted file mode 100644
index cfe914a9c2949f7b74b42dc508232f2aae2f865a..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/sharing/ForumInvitationAdapter.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.briarproject.android.sharing;
-
-import android.content.Context;
-
-import org.briarproject.api.forum.Forum;
-import org.briarproject.api.sharing.InvitationItem;
-
-class ForumInvitationAdapter extends InvitationAdapter {
-
-	ForumInvitationAdapter(Context ctx, AvailableForumClickListener listener) {
-		super(ctx, listener);
-	}
-
-	@Override
-	public void onBindViewHolder(InvitationsViewHolder ui, int position) {
-		super.onBindViewHolder(ui, position);
-		InvitationItem item = getItemAt(position);
-		if (item == null) return;
-
-		Forum forum = (Forum) item.getShareable();
-
-		ui.avatar.setText(forum.getName().substring(0, 1));
-		ui.avatar.setBackgroundBytes(item.getShareable().getId().getBytes());
-
-		ui.name.setText(forum.getName());
-	}
-
-	@Override
-	public int compare(InvitationItem o1, InvitationItem o2) {
-		return String.CASE_INSENSITIVE_ORDER
-				.compare(((Forum) o1.getShareable()).getName(),
-						((Forum) o2.getShareable()).getName());
-	}
-
-}
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java
index db35fd55bc4dbd0ee3a416d6112167eee42514f1..5afd24c07c66b2750810dc195a6505dcc44e234e 100644
--- a/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java
@@ -1,111 +1,51 @@
 package org.briarproject.android.sharing;
 
 import android.content.Context;
-import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.TextView;
 
 import org.briarproject.R;
 import org.briarproject.android.util.BriarAdapter;
-import org.briarproject.android.view.TextAvatarView;
-import org.briarproject.api.contact.Contact;
 import org.briarproject.api.sharing.InvitationItem;
-import org.briarproject.util.StringUtils;
 
-import java.util.ArrayList;
-import java.util.Collection;
+public abstract class InvitationAdapter<I extends InvitationItem, VH extends InvitationViewHolder<I>>
+		extends BriarAdapter<I, VH> {
 
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
+	private final InvitationClickListener<I> listener;
 
-abstract class InvitationAdapter extends
-		BriarAdapter<InvitationItem, InvitationAdapter.InvitationsViewHolder> {
-
-	private final AvailableForumClickListener listener;
-
-	InvitationAdapter(Context ctx, AvailableForumClickListener listener) {
-		super(ctx, InvitationItem.class);
+	public InvitationAdapter(Context ctx, Class<I> c,
+			InvitationClickListener<I> listener) {
+		super(ctx, c);
 		this.listener = listener;
 	}
 
-	@Override
-	public InvitationsViewHolder onCreateViewHolder(ViewGroup parent,
-			int viewType) {
-
-		View v = LayoutInflater.from(ctx)
-				.inflate(R.layout.list_item_invitations, parent,  false);
-		return new InvitationsViewHolder(v);
+	protected View getView(ViewGroup parent) {
+		return LayoutInflater.from(ctx)
+				.inflate(R.layout.list_item_invitations, parent, false);
 	}
 
 	@Override
-	public void onBindViewHolder(InvitationsViewHolder ui, int position) {
-		final InvitationItem item = getItemAt(position);
+	public void onBindViewHolder(VH ui, int position) {
+		final I item = getItemAt(position);
 		if (item == null) return;
-
-		Collection<String> names = new ArrayList<>();
-		for (Contact c : item.getNewSharers())
-			names.add(c.getAuthor().getName());
-		ui.sharedBy.setText(ctx.getString(R.string.shared_by_format,
-				StringUtils.join(names, ", ")));
-
-		if (item.isSubscribed()) {
-			ui.subscribed.setVisibility(VISIBLE);
-		} else {
-			ui.subscribed.setVisibility(GONE);
-		}
-
-		ui.accept.setOnClickListener(new View.OnClickListener() {
-			@Override
-			public void onClick(View v) {
-				listener.onItemClick(item, true);
-			}
-		});
-		ui.decline.setOnClickListener(new View.OnClickListener() {
-			@Override
-			public void onClick(View v) {
-				listener.onItemClick(item, false);
-			}
-		});
+		ui.onBind(item, listener);
 	}
 
 	@Override
-	public boolean areContentsTheSame(InvitationItem oldItem,
-			InvitationItem newItem) {
-		return oldItem.isSubscribed() == newItem.isSubscribed() &&
-				oldItem.getNewSharers().equals(newItem.getNewSharers());
+	public boolean areItemsTheSame(I oldItem, I newItem) {
+		return oldItem.getShareable().equals(newItem.getShareable());
 	}
 
 	@Override
-	public boolean areItemsTheSame(InvitationItem oldItem,
-			InvitationItem newItem) {
-		return oldItem.getShareable().equals(newItem.getShareable());
+	public int compare(I o1, I o2) {
+		return String.CASE_INSENSITIVE_ORDER
+				.compare((o1.getShareable()).getName(),
+						(o2.getShareable()).getName());
 	}
 
-	static class InvitationsViewHolder extends RecyclerView.ViewHolder {
-
-		final TextAvatarView avatar;
-		final TextView name;
-		private final TextView sharedBy;
-		final TextView subscribed;
-		private final Button accept;
-		private final Button decline;
-
-		private InvitationsViewHolder(View v) {
-			super(v);
-
-			avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
-			name = (TextView) v.findViewById(R.id.forumNameView);
-			sharedBy = (TextView) v.findViewById(R.id.sharedByView);
-			subscribed = (TextView) v.findViewById(R.id.forumSubscribedView);
-			accept = (Button) v.findViewById(R.id.acceptButton);
-			decline = (Button) v.findViewById(R.id.declineButton);
-		}
+	public interface InvitationClickListener<I> {
+		void onItemClick(I item, boolean accept);
 	}
 
-	interface AvailableForumClickListener {
-		void onItemClick(InvitationItem item, boolean accept);
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationViewHolder.java b/briar-android/src/org/briarproject/android/sharing/InvitationViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c8337c13c3271ac3eb95d601f6bb45739c55e97
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationViewHolder.java
@@ -0,0 +1,69 @@
+package org.briarproject.android.sharing;
+
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
+import org.briarproject.android.view.TextAvatarView;
+import org.briarproject.api.sharing.InvitationItem;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+public class InvitationViewHolder<I extends InvitationItem>
+		extends RecyclerView.ViewHolder {
+
+	private final TextAvatarView avatar;
+	private final TextView name;
+	protected final TextView sharedBy;
+	private final TextView subscribed;
+	private final Button accept;
+	private final Button decline;
+
+	public InvitationViewHolder(View v) {
+		super(v);
+
+		avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
+		name = (TextView) v.findViewById(R.id.forumNameView);
+		sharedBy = (TextView) v.findViewById(R.id.sharedByView);
+		subscribed = (TextView) v.findViewById(R.id.forumSubscribedView);
+		accept = (Button) v.findViewById(R.id.acceptButton);
+		decline = (Button) v.findViewById(R.id.declineButton);
+	}
+
+	@CallSuper
+	public void onBind(@Nullable final I item,
+			final InvitationClickListener<I> listener) {
+		if (item == null) return;
+
+		avatar.setText(item.getShareable().getName().substring(0, 1));
+		avatar.setBackgroundBytes(item.getShareable().getId().getBytes());
+
+		name.setText(item.getShareable().getName());
+
+		if (item.isSubscribed()) {
+			subscribed.setVisibility(VISIBLE);
+		} else {
+			subscribed.setVisibility(GONE);
+		}
+
+		accept.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				listener.onItemClick(item, true);
+			}
+		});
+		decline.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				listener.onItemClick(item, false);
+			}
+		});
+	}
+
+}
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java b/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
index 772be9bf605e7dd472ffb152a2f395b360147988..a8313f56d5aaf8789535be176fe8b335c99963ce 100644
--- a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
@@ -2,39 +2,34 @@ package org.briarproject.android.sharing;
 
 import android.content.Context;
 import android.os.Bundle;
-import android.support.annotation.CallSuper;
+import android.support.annotation.StringRes;
 import android.support.v7.widget.LinearLayoutManager;
 import android.widget.Toast;
 
 import org.briarproject.R;
 import org.briarproject.android.BriarActivity;
+import org.briarproject.android.controller.handler.UiResultExceptionHandler;
+import org.briarproject.android.sharing.InvitationsController.InvitationListener;
 import org.briarproject.android.view.BriarRecyclerView;
-import org.briarproject.api.event.ContactRemovedEvent;
-import org.briarproject.api.event.Event;
-import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.EventListener;
+import org.briarproject.api.db.DbException;
 import org.briarproject.api.sharing.InvitationItem;
 
 import java.util.Collection;
 import java.util.logging.Logger;
 
-import javax.inject.Inject;
-
 import static android.widget.Toast.LENGTH_SHORT;
-import static org.briarproject.android.sharing.InvitationAdapter.AvailableForumClickListener;
+import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
 
-abstract class InvitationsActivity extends BriarActivity
-		implements EventListener, AvailableForumClickListener {
+public abstract class InvitationsActivity<I extends InvitationItem>
+		extends BriarActivity
+		implements InvitationListener, InvitationClickListener<I> {
 
 	protected static final Logger LOG =
 			Logger.getLogger(InvitationsActivity.class.getName());
 
-	protected InvitationAdapter adapter;
+	private InvitationAdapter<I, ?> adapter;
 	private BriarRecyclerView list;
 
-	@Inject
-	EventBus eventBus;
-
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(state);
@@ -42,7 +37,6 @@ abstract class InvitationsActivity extends BriarActivity
 		setContentView(R.layout.list);
 
 		adapter = getAdapter(this, this);
-
 		list = (BriarRecyclerView) findViewById(R.id.list);
 		if (list != null) {
 			list.setLayoutManager(new LinearLayoutManager(this));
@@ -50,32 +44,24 @@ abstract class InvitationsActivity extends BriarActivity
 		}
 	}
 
+	abstract protected InvitationAdapter<I, ?> getAdapter(Context ctx,
+			InvitationClickListener listener);
+
 	@Override
 	public void onStart() {
 		super.onStart();
-		eventBus.addListener(this);
 		loadInvitations(false);
 	}
 
 	@Override
 	public void onStop() {
 		super.onStop();
-		eventBus.removeListener(this);
 		adapter.clear();
 		list.showProgressBar();
 	}
 
 	@Override
-	@CallSuper
-	public void eventOccurred(Event e) {
-		if (e instanceof ContactRemovedEvent) {
-			LOG.info("Contact removed, reloading...");
-			loadInvitations(true);
-		}
-	}
-
-	@Override
-	public void onItemClick(InvitationItem item, boolean accept) {
+	public void onItemClick(I item, boolean accept) {
 		respondToInvitation(item, accept);
 
 		// show toast
@@ -91,26 +77,58 @@ abstract class InvitationsActivity extends BriarActivity
 		}
 	}
 
-	abstract protected InvitationAdapter getAdapter(Context ctx,
-			AvailableForumClickListener listener);
+	@Override
+	public void loadInvitations(final boolean clear) {
+		final int revision = adapter.getRevision();
+		getController().loadInvitations(clear,
+				new UiResultExceptionHandler<Collection<I>, DbException>(
+						this) {
+					@Override
+					public void onResultUi(Collection<I> items) {
+						displayInvitations(revision, items, clear);
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO proper error handling
+						finish();
+					}
+				});
+	}
+
+	abstract protected InvitationsController<I> getController();
+
+	protected void respondToInvitation(final I item,
+			final boolean accept) {
+		getController().respondToInvitation(item, accept,
+				new UiResultExceptionHandler<Void, DbException>(this) {
+					@Override
+					public void onResultUi(Void result) {
 
-	abstract protected void loadInvitations(boolean clear);
+					}
 
-	abstract protected void respondToInvitation(final InvitationItem item,
-			final boolean accept);
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO proper error handling
+						finish();
+					}
+				});
+	}
 
+	@StringRes
 	abstract protected int getAcceptRes();
 
+	@StringRes
 	abstract protected int getDeclineRes();
 
 	protected void displayInvitations(final int revision,
-			final Collection<InvitationItem> invitations, final boolean clear) {
+			final Collection<I> invitations, final boolean clear) {
 		runOnUiThreadUnlessDestroyed(new Runnable() {
 			@Override
 			public void run() {
 				if (invitations.isEmpty()) {
 					LOG.info("No more invitations available, finishing");
-					finish();
+					supportFinishAfterTransition();
 				} else if (revision == adapter.getRevision()) {
 					adapter.incrementRevision();
 					if (clear) adapter.setItems(invitations);
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsBlogActivity.java b/briar-android/src/org/briarproject/android/sharing/InvitationsBlogActivity.java
index 1b0e5f3bd7be5ce10c72991abb40792584d66bd0..da7d014e0811ad1d2687a0f9070c64f6b42e71ca 100644
--- a/briar-android/src/org/briarproject/android/sharing/InvitationsBlogActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsBlogActivity.java
@@ -4,34 +4,17 @@ import android.content.Context;
 
 import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
-import org.briarproject.api.blogs.Blog;
-import org.briarproject.api.blogs.BlogManager;
-import org.briarproject.api.blogs.BlogSharingManager;
-import org.briarproject.api.contact.Contact;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.event.BlogInvitationReceivedEvent;
-import org.briarproject.api.event.Event;
-import org.briarproject.api.event.GroupAddedEvent;
-import org.briarproject.api.event.GroupRemovedEvent;
-import org.briarproject.api.sharing.InvitationItem;
-import org.briarproject.api.sync.ClientId;
-
-import java.util.ArrayList;
-import java.util.Collection;
+import org.briarproject.api.sharing.SharingInvitationItem;
 
 import javax.inject.Inject;
 
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.android.sharing.InvitationAdapter.AvailableForumClickListener;
+import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
 
-public class InvitationsBlogActivity extends InvitationsActivity {
+public class InvitationsBlogActivity
+		extends InvitationsActivity<SharingInvitationItem> {
 
-	// Fields that are accessed from background threads must be volatile
-	@Inject
-	volatile BlogManager blogManager;
 	@Inject
-	volatile BlogSharingManager blogSharingManager;
+	InvitationsBlogController controller;
 
 	@Override
 	public void injectActivity(ActivityComponent component) {
@@ -39,75 +22,14 @@ public class InvitationsBlogActivity extends InvitationsActivity {
 	}
 
 	@Override
-	public void eventOccurred(Event e) {
-		super.eventOccurred(e);
-
-		if (e instanceof GroupAddedEvent) {
-			GroupAddedEvent g = (GroupAddedEvent) e;
-			ClientId cId = g.getGroup().getClientId();
-			if (cId.equals(blogManager.getClientId())) {
-				LOG.info("Blog added, reloading");
-				loadInvitations(false);
-			}
-		} else if (e instanceof GroupRemovedEvent) {
-			GroupRemovedEvent g = (GroupRemovedEvent) e;
-			ClientId cId = g.getGroup().getClientId();
-			if (cId.equals(blogManager.getClientId())) {
-				LOG.info("Blog removed, reloading");
-				loadInvitations(false);
-			}
-		} else if (e instanceof BlogInvitationReceivedEvent) {
-			LOG.info("Blog invitation received, reloading");
-			loadInvitations(false);
-		}
-	}
-
-	@Override
-	protected InvitationAdapter getAdapter(Context ctx,
-			AvailableForumClickListener listener) {
-		return new BlogInvitationAdapter(ctx, listener);
+	protected InvitationsController<SharingInvitationItem> getController() {
+		return controller;
 	}
 
 	@Override
-	protected void loadInvitations(final boolean clear) {
-		final int revision = adapter.getRevision();
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					Collection<InvitationItem> invitations = new ArrayList<>();
-					long now = System.currentTimeMillis();
-					invitations.addAll(blogSharingManager.getInvitations());
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Load took " + duration + " ms");
-					displayInvitations(revision, invitations, clear);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	@Override
-	protected void respondToInvitation(final InvitationItem item,
-			final boolean accept) {
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					Blog b = (Blog) item.getShareable();
-					for (Contact c : item.getNewSharers()) {
-						// TODO: What happens if a contact has been removed?
-						blogSharingManager.respondToInvitation(b, c, accept);
-					}
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
+	protected InvitationAdapter<SharingInvitationItem, ?> getAdapter(
+			Context ctx, InvitationClickListener listener) {
+		return new SharingInvitationAdapter(ctx, listener);
 	}
 
 	@Override
@@ -119,4 +41,5 @@ public class InvitationsBlogActivity extends InvitationsActivity {
 	protected int getDeclineRes() {
 		return R.string.blogs_sharing_declined_toast;
 	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsBlogController.java b/briar-android/src/org/briarproject/android/sharing/InvitationsBlogController.java
new file mode 100644
index 0000000000000000000000000000000000000000..4efcff5cf39b305d146e1400973fdeecd345d16b
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsBlogController.java
@@ -0,0 +1,7 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.api.sharing.SharingInvitationItem;
+
+public interface InvitationsBlogController
+		extends InvitationsController<SharingInvitationItem> {
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsBlogControllerImpl.java b/briar-android/src/org/briarproject/android/sharing/InvitationsBlogControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..024109f7edffdf7891d0f52caa913dfc06ec9d21
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsBlogControllerImpl.java
@@ -0,0 +1,82 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.blogs.Blog;
+import org.briarproject.api.blogs.BlogManager;
+import org.briarproject.api.blogs.BlogSharingManager;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.BlogInvitationReceivedEvent;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sharing.SharingInvitationItem;
+import org.briarproject.api.sync.ClientId;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+
+public class InvitationsBlogControllerImpl
+		extends InvitationsControllerImpl<SharingInvitationItem>
+		implements InvitationsBlogController {
+
+	private final BlogManager blogManager;
+	private final BlogSharingManager blogSharingManager;
+
+	@Inject
+	InvitationsBlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			LifecycleManager lifecycleManager, EventBus eventBus,
+			BlogManager blogManager, BlogSharingManager blogSharingManager) {
+		super(dbExecutor, lifecycleManager, eventBus);
+		this.blogManager = blogManager;
+		this.blogSharingManager = blogSharingManager;
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		super.eventOccurred(e);
+
+		if (e instanceof BlogInvitationReceivedEvent) {
+			LOG.info("Blog invitation received, reloading");
+			listener.loadInvitations(false);
+		}
+	}
+
+	@Override
+	protected ClientId getClientId() {
+		return blogManager.getClientId();
+	}
+
+	@Override
+	protected Collection<SharingInvitationItem> getInvitations() throws DbException {
+		return blogSharingManager.getInvitations();
+	}
+
+	@Override
+	public void respondToInvitation(final SharingInvitationItem item,
+			final boolean accept,
+			final ResultExceptionHandler<Void, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Blog f = (Blog) item.getShareable();
+					for (Contact c : item.getNewSharers()) {
+						// TODO: What happens if a contact has been removed?
+						blogSharingManager.respondToInvitation(f, c, accept);
+					}
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsController.java b/briar-android/src/org/briarproject/android/sharing/InvitationsController.java
new file mode 100644
index 0000000000000000000000000000000000000000..91576dfdf814ffa886fe575500ead2305d93413a
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsController.java
@@ -0,0 +1,25 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.sharing.InvitationItem;
+
+import java.util.Collection;
+
+public interface InvitationsController<I extends InvitationItem>
+		extends ActivityLifecycleController {
+
+	void loadInvitations(boolean clear,
+			ResultExceptionHandler<Collection<I>, DbException> handler);
+
+	void respondToInvitation(I item, boolean accept,
+			ResultExceptionHandler<Void, DbException> handler);
+
+	interface InvitationListener {
+
+		void loadInvitations(boolean clear);
+
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsControllerImpl.java b/briar-android/src/org/briarproject/android/sharing/InvitationsControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..5fcace519adf4ecaa3376f8e82eb3054ec751109
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsControllerImpl.java
@@ -0,0 +1,116 @@
+package org.briarproject.android.sharing;
+
+import android.app.Activity;
+import android.support.annotation.CallSuper;
+
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.ContactRemovedEvent;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.GroupAddedEvent;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sharing.InvitationItem;
+import org.briarproject.api.sync.ClientId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+public abstract class InvitationsControllerImpl<I extends InvitationItem>
+		extends DbControllerImpl
+		implements InvitationsController<I>, EventListener {
+
+	protected static final Logger LOG =
+			Logger.getLogger(InvitationsControllerImpl.class.getName());
+
+	private final EventBus eventBus;
+	protected InvitationListener listener;
+
+	public InvitationsControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			LifecycleManager lifecycleManager, EventBus eventBus) {
+		super(dbExecutor, lifecycleManager);
+		this.eventBus = eventBus;
+	}
+
+	@Override
+	public void onActivityCreate(Activity activity) {
+		listener = (InvitationListener) activity;
+	}
+
+	@Override
+	public void onActivityStart() {
+		eventBus.addListener(this);
+	}
+
+	@Override
+	public void onActivityStop() {
+		eventBus.removeListener(this);
+	}
+
+	@Override
+	public void onActivityDestroy() {
+
+	}
+
+	@CallSuper
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof ContactRemovedEvent) {
+			LOG.info("Contact removed, reloading...");
+			listener.loadInvitations(true);
+		} else if (e instanceof GroupAddedEvent) {
+			GroupAddedEvent g = (GroupAddedEvent) e;
+			ClientId cId = g.getGroup().getClientId();
+			if (cId.equals(getClientId())) {
+				LOG.info("Group added, reloading");
+				listener.loadInvitations(false);
+			}
+		} else if (e instanceof GroupRemovedEvent) {
+			GroupRemovedEvent g = (GroupRemovedEvent) e;
+			ClientId cId = g.getGroup().getClientId();
+			if (cId.equals(getClientId())) {
+				LOG.info("Group removed, reloading");
+				listener.loadInvitations(true);
+			}
+		}
+	}
+
+	protected abstract ClientId getClientId();
+
+	@Override
+	public void loadInvitations(final boolean clear,
+			final ResultExceptionHandler<Collection<I>, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				Collection<I> invitations = new ArrayList<>();
+				try {
+					long now = System.currentTimeMillis();
+					invitations.addAll(getInvitations());
+					long duration = System.currentTimeMillis() - now;
+					if (LOG.isLoggable(INFO))
+						LOG.info(
+								"Loading invitations took " + duration + " ms");
+					handler.onResult(invitations);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+	@DatabaseExecutor
+	protected abstract Collection<I> getInvitations() throws DbException;
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsForumActivity.java b/briar-android/src/org/briarproject/android/sharing/InvitationsForumActivity.java
index d64dab69f04af496aa5469aa79464c8eaa82d5a9..1c32ee1125e43d2fcb18001a1c755aedb74a8303 100644
--- a/briar-android/src/org/briarproject/android/sharing/InvitationsForumActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsForumActivity.java
@@ -4,34 +4,17 @@ import android.content.Context;
 
 import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
-import org.briarproject.api.contact.Contact;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.event.Event;
-import org.briarproject.api.event.ForumInvitationReceivedEvent;
-import org.briarproject.api.event.GroupAddedEvent;
-import org.briarproject.api.event.GroupRemovedEvent;
-import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.forum.ForumSharingManager;
-import org.briarproject.api.sharing.InvitationItem;
-import org.briarproject.api.sync.ClientId;
-
-import java.util.ArrayList;
-import java.util.Collection;
+import org.briarproject.api.sharing.SharingInvitationItem;
 
 import javax.inject.Inject;
 
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.android.sharing.InvitationAdapter.AvailableForumClickListener;
+import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
 
-public class InvitationsForumActivity extends InvitationsActivity {
+public class InvitationsForumActivity
+		extends InvitationsActivity<SharingInvitationItem> {
 
-	// Fields that are accessed from background threads must be volatile
-	@Inject
-	volatile ForumManager forumManager;
 	@Inject
-	volatile ForumSharingManager forumSharingManager;
+	InvitationsForumController controller;
 
 	@Override
 	public void injectActivity(ActivityComponent component) {
@@ -39,75 +22,14 @@ public class InvitationsForumActivity extends InvitationsActivity {
 	}
 
 	@Override
-	public void eventOccurred(Event e) {
-		super.eventOccurred(e);
-
-		if (e instanceof GroupAddedEvent) {
-			GroupAddedEvent g = (GroupAddedEvent) e;
-			ClientId cId = g.getGroup().getClientId();
-			if (cId.equals(forumManager.getClientId())) {
-				LOG.info("Forum added, reloading");
-				loadInvitations(false);
-			}
-		} else if (e instanceof GroupRemovedEvent) {
-			GroupRemovedEvent g = (GroupRemovedEvent) e;
-			ClientId cId = g.getGroup().getClientId();
-			if (cId.equals(forumManager.getClientId())) {
-				LOG.info("Forum removed, reloading");
-				loadInvitations(false);
-			}
-		} else if (e instanceof ForumInvitationReceivedEvent) {
-			LOG.info("Forum invitation received, reloading");
-			loadInvitations(false);
-		}
-	}
-
-	@Override
-	protected InvitationAdapter getAdapter(Context ctx,
-			AvailableForumClickListener listener) {
-		return new ForumInvitationAdapter(ctx, listener);
+	protected InvitationsController<SharingInvitationItem> getController() {
+		return controller;
 	}
 
 	@Override
-	protected void loadInvitations(final boolean clear) {
-		final int revision = adapter.getRevision();
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					Collection<InvitationItem> invitations = new ArrayList<>();
-					long now = System.currentTimeMillis();
-					invitations.addAll(forumSharingManager.getInvitations());
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Load took " + duration + " ms");
-					displayInvitations(revision, invitations, clear);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
-	}
-
-	@Override
-	protected void respondToInvitation(final InvitationItem item,
-			final boolean accept) {
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					Forum f = (Forum) item.getShareable();
-					for (Contact c : item.getNewSharers()) {
-						// TODO: What happens if a contact has been removed?
-						forumSharingManager.respondToInvitation(f, c, accept);
-					}
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
+	protected InvitationAdapter<SharingInvitationItem, ?> getAdapter(
+			Context ctx, InvitationClickListener listener) {
+		return new SharingInvitationAdapter(ctx, listener);
 	}
 
 	@Override
@@ -119,4 +41,5 @@ public class InvitationsForumActivity extends InvitationsActivity {
 	protected int getDeclineRes() {
 		return R.string.forum_declined_toast;
 	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsForumController.java b/briar-android/src/org/briarproject/android/sharing/InvitationsForumController.java
new file mode 100644
index 0000000000000000000000000000000000000000..e58c4a41c75bb38d3aa2b40013a1f65733499c53
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsForumController.java
@@ -0,0 +1,7 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.api.sharing.SharingInvitationItem;
+
+public interface InvitationsForumController
+		extends InvitationsController<SharingInvitationItem> {
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsForumControllerImpl.java b/briar-android/src/org/briarproject/android/sharing/InvitationsForumControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..25f672460a4752760ab3a97a47c60464aadfced8
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsForumControllerImpl.java
@@ -0,0 +1,83 @@
+package org.briarproject.android.sharing;
+
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.ForumInvitationReceivedEvent;
+import org.briarproject.api.forum.Forum;
+import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sharing.SharingInvitationItem;
+import org.briarproject.api.sync.ClientId;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+
+public class InvitationsForumControllerImpl
+		extends InvitationsControllerImpl<SharingInvitationItem>
+		implements InvitationsForumController {
+
+	private final ForumManager forumManager;
+	private final ForumSharingManager forumSharingManager;
+
+	@Inject
+	InvitationsForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			LifecycleManager lifecycleManager, EventBus eventBus,
+			ForumManager forumManager,
+			ForumSharingManager forumSharingManager) {
+		super(dbExecutor, lifecycleManager, eventBus);
+		this.forumManager = forumManager;
+		this.forumSharingManager = forumSharingManager;
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		super.eventOccurred(e);
+
+		if (e instanceof ForumInvitationReceivedEvent) {
+			LOG.info("Forum invitation received, reloading");
+			listener.loadInvitations(false);
+		}
+	}
+
+	@Override
+	protected ClientId getClientId() {
+		return forumManager.getClientId();
+	}
+
+	@Override
+	protected Collection<SharingInvitationItem> getInvitations() throws DbException {
+		return forumSharingManager.getInvitations();
+	}
+
+	@Override
+	public void respondToInvitation(final SharingInvitationItem item,
+			final boolean accept,
+			final ResultExceptionHandler<Void, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Forum f = (Forum) item.getShareable();
+					for (Contact c : item.getNewSharers()) {
+						// TODO: What happens if a contact has been removed?
+						forumSharingManager.respondToInvitation(f, c, accept);
+					}
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/SharingInvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/SharingInvitationAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..2964c9b930db4f376bd028d2d5d3b0db98d8796d
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/SharingInvitationAdapter.java
@@ -0,0 +1,29 @@
+package org.briarproject.android.sharing;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import org.briarproject.api.sharing.SharingInvitationItem;
+
+class SharingInvitationAdapter extends
+		InvitationAdapter<SharingInvitationItem, SharingInvitationViewHolder> {
+
+	SharingInvitationAdapter(Context ctx, InvitationClickListener listener) {
+		super(ctx, SharingInvitationItem.class, listener);
+	}
+
+	@Override
+	public SharingInvitationViewHolder onCreateViewHolder(
+			ViewGroup parent,
+			int viewType) {
+		return new SharingInvitationViewHolder(getView(parent));
+	}
+
+	@Override
+	public boolean areContentsTheSame(SharingInvitationItem oldItem,
+			SharingInvitationItem newItem) {
+		return oldItem.isSubscribed() == newItem.isSubscribed() &&
+				oldItem.getNewSharers().equals(newItem.getNewSharers());
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/SharingInvitationViewHolder.java b/briar-android/src/org/briarproject/android/sharing/SharingInvitationViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..b10672468f1ada6f65c482c3c84576f4020527af
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/sharing/SharingInvitationViewHolder.java
@@ -0,0 +1,35 @@
+package org.briarproject.android.sharing;
+
+import android.support.annotation.Nullable;
+import android.view.View;
+
+import org.briarproject.R;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.sharing.SharingInvitationItem;
+import org.briarproject.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class SharingInvitationViewHolder
+		extends InvitationViewHolder<SharingInvitationItem> {
+
+	public SharingInvitationViewHolder(View v) {
+		super(v);
+	}
+
+	@Override
+	public void onBind(@Nullable final SharingInvitationItem item,
+			final InvitationAdapter.InvitationClickListener<SharingInvitationItem> listener) {
+		super.onBind(item, listener);
+		if (item == null) return;
+
+		Collection<String> names = new ArrayList<>();
+		for (Contact c : item.getNewSharers())
+			names.add(c.getAuthor().getName());
+		sharedBy.setText(
+				sharedBy.getContext().getString(R.string.shared_by_format,
+						StringUtils.join(names, ", ")));
+	}
+
+}