From 9ea7113423e77727ee8c0e1ebd3f62737e75612a Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Fri, 22 Apr 2016 20:14:47 -0300
Subject: [PATCH] Refactor `ContactListAdapter` to be reusable for other use
 cases.

This commit introduces an abstract `BaseContactListAdapter` which provides
most of the adapter logic. The original `ContactListAdapter` extends it to
show date and online status of the contacts.

The new `ContactChooserAdapter` which is used for introductions extends
the `ContactListAdapter` and adds logic for graying out contacts from
different identities than the currently used one.

A new `ContactSelectorAdapter` extends the `BaseContactListAdapter` and
allows to select multiple contacts. It offers a method to return a
collection of all selected `ContactId`s.

This commit also sneaks in an animation when the 'Share Forum' button
is clicked.

Closes #292
---
 briar-android/res/drawable/ic_check_white.xml |   9 +
 .../res/layout/list_item_contact.xml          |   1 +
 .../layout/list_item_selectable_contact.xml   |  61 +++++
 .../res/menu/forum_share_actions.xml          |  12 +
 .../contact/BaseContactListAdapter.java       | 189 ++++++++++++++
 .../android/contact/ContactListAdapter.java   | 240 ++----------------
 .../android/contact/ContactListFragment.java  |   5 +-
 .../android/contact/SelectContactsDialog.java |  77 ------
 .../android/forum/ContactSelectorAdapter.java |  81 ++++++
 .../android/forum/ForumActivity.java          |  10 +-
 .../android/forum/NoContactsDialog.java       |  44 ----
 .../forum/SelectableContactListItem.java      |  36 +++
 .../android/forum/ShareForumActivity.java     | 199 ++++++---------
 .../introduction/ContactChooserAdapter.java   |  75 ++++++
 .../introduction/ContactChooserFragment.java  |   5 +-
 15 files changed, 582 insertions(+), 462 deletions(-)
 create mode 100644 briar-android/res/drawable/ic_check_white.xml
 create mode 100644 briar-android/res/layout/list_item_selectable_contact.xml
 create mode 100644 briar-android/res/menu/forum_share_actions.xml
 create mode 100644 briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java
 delete mode 100644 briar-android/src/org/briarproject/android/contact/SelectContactsDialog.java
 create mode 100644 briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java
 delete mode 100644 briar-android/src/org/briarproject/android/forum/NoContactsDialog.java
 create mode 100644 briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java
 create mode 100644 briar-android/src/org/briarproject/android/introduction/ContactChooserAdapter.java

diff --git a/briar-android/res/drawable/ic_check_white.xml b/briar-android/res/drawable/ic_check_white.xml
new file mode 100644
index 0000000000..59f823220d
--- /dev/null
+++ b/briar-android/res/drawable/ic_check_white.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FFFFFFFF"
+		android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>
diff --git a/briar-android/res/layout/list_item_contact.xml b/briar-android/res/layout/list_item_contact.xml
index 8f879c90a0..4f1410693e 100644
--- a/briar-android/res/layout/list_item_contact.xml
+++ b/briar-android/res/layout/list_item_contact.xml
@@ -64,6 +64,7 @@
 				android:layout_height="wrap_content"
 				android:textColor="@android:color/tertiary_text_light"
 				android:textSize="@dimen/text_size_tiny"
+				android:visibility="gone"
 				tools:text="My Identity"/>
 
 		</LinearLayout>
diff --git a/briar-android/res/layout/list_item_selectable_contact.xml b/briar-android/res/layout/list_item_selectable_contact.xml
new file mode 100644
index 0000000000..920129df01
--- /dev/null
+++ b/briar-android/res/layout/list_item_selectable_contact.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	android:orientation="vertical">
+
+	<RelativeLayout
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:paddingTop="@dimen/listitem_horizontal_margin"
+		android:paddingBottom="@dimen/listitem_horizontal_margin"
+		android:background="?attr/selectableItemBackground"
+		>
+
+		<de.hdodenhof.circleimageview.CircleImageView
+			android:id="@+id/avatarView"
+			android:layout_width="@dimen/listitem_picture_size"
+			android:layout_height="@dimen/listitem_picture_size"
+			android:layout_alignParentLeft="true"
+			android:layout_alignParentStart="true"
+			android:layout_centerVertical="true"
+			android:layout_marginLeft="@dimen/listitem_horizontal_margin"
+			android:layout_marginStart="@dimen/listitem_horizontal_margin"
+			android:transitionName="avatar"
+			app:civ_border_color="@color/briar_text_primary"
+			app:civ_border_width="@dimen/avatar_border_width"
+			tools:src="@drawable/ic_launcher"/>
+
+		<TextView
+			android:id="@+id/nameView"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_centerVertical="true"
+			android:layout_marginLeft="@dimen/listitem_horizontal_margin"
+			android:layout_marginStart="@dimen/listitem_horizontal_margin"
+			android:layout_toEndOf="@+id/avatarView"
+			android:layout_toLeftOf="@+id/checkBox"
+			android:layout_toRightOf="@+id/avatarView"
+			android:maxLines="2"
+			android:textColor="@android:color/primary_text_light"
+			android:textSize="@dimen/text_size_large"
+			tools:text="This is a name of a contact"/>
+
+		<CheckBox
+			android:id="@+id/checkBox"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignParentEnd="true"
+			android:layout_alignParentRight="true"
+			android:layout_centerVertical="true"
+			android:layout_marginRight="@dimen/listitem_horizontal_margin"
+			android:clickable="false"/>
+
+	</RelativeLayout>
+
+	<View style="@style/Divider.ContactList"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/menu/forum_share_actions.xml b/briar-android/res/menu/forum_share_actions.xml
new file mode 100644
index 0000000000..f128fd17c5
--- /dev/null
+++ b/briar-android/res/menu/forum_share_actions.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto">
+
+	<item
+		android:id="@+id/action_share_forum"
+		android:icon="@drawable/ic_check_white"
+		android:title="@string/forum_share_with_some"
+		app:showAsAction="always"/>
+
+</menu>
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java b/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java
new file mode 100644
index 0000000000..590c7a2d74
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java
@@ -0,0 +1,189 @@
+package org.briarproject.android.contact;
+
+import android.content.Context;
+import android.support.v7.util.SortedList;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.identity.Author;
+
+import java.util.List;
+
+import im.delight.android.identicons.IdenticonDrawable;
+
+import static android.support.v7.util.SortedList.INVALID_POSITION;
+
+public abstract class BaseContactListAdapter<VH extends BaseContactListAdapter.BaseContactHolder>
+		extends RecyclerView.Adapter<VH> {
+
+	protected SortedList<ContactListItem> contacts;
+	protected final OnItemClickListener listener;
+	protected Context ctx;
+
+	public BaseContactListAdapter(Context context, OnItemClickListener listener) {
+		this.ctx = context;
+		this.listener = listener;
+		this.contacts = new SortedList<ContactListItem>(ContactListItem.class,
+				new SortedListCallBacks());
+	}
+
+	@Override
+	public void onBindViewHolder(final VH ui, final int position) {
+		final ContactListItem item = getItem(position);
+
+		Author author = item.getContact().getAuthor();
+		ui.avatar.setImageDrawable(
+				new IdenticonDrawable(author.getId().getBytes()));
+		String contactName = author.getName();
+		ui.name.setText(contactName);
+
+		ui.layout.setOnClickListener(new View.OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				if (listener != null) listener.onItemClick(ui.avatar, item);
+			}
+		});
+	}
+
+	@Override
+	public int getItemCount() {
+		return contacts.size();
+	}
+
+	public ContactListItem getItem(int position) {
+		if (position == INVALID_POSITION || contacts.size() <= position) {
+			return null; // Not found
+		}
+		return contacts.get(position);
+	}
+
+	public void updateItem(int position, ContactListItem item) {
+		contacts.updateItemAt(position, item);
+	}
+
+	public int findItemPosition(ContactListItem c) {
+		return contacts.indexOf(c);
+	}
+
+	public int findItemPosition(ContactId c) {
+		int count = getItemCount();
+		for (int i = 0; i < count; i++) {
+			ContactListItem item = getItem(i);
+			if (item.getContact().getId().equals(c)) return i;
+		}
+		return INVALID_POSITION; // Not found
+	}
+
+	public void addAll(List<ContactListItem> contacts) {
+		this.contacts.addAll(contacts);
+	}
+
+	public void add(ContactListItem contact) {
+		contacts.add(contact);
+	}
+
+	public void remove(ContactListItem contact) {
+		contacts.remove(contact);
+	}
+
+	public void clear() {
+		contacts.clear();
+	}
+
+	public static class BaseContactHolder extends RecyclerView.ViewHolder {
+		public final ViewGroup layout;
+		public final ImageView avatar;
+		public final TextView name;
+
+		public BaseContactHolder(View v) {
+			super(v);
+
+			layout = (ViewGroup) v;
+			avatar = (ImageView) v.findViewById(R.id.avatarView);
+			name = (TextView) v.findViewById(R.id.nameView);
+		}
+	}
+
+	public int compareContactListItems(ContactListItem c1, ContactListItem c2) {
+		return compareByName(c1, c2);
+	}
+
+	protected int compareByName(ContactListItem c1, ContactListItem c2) {
+		int authorCompare = c1.getLocalAuthor().getName()
+				.compareTo(c2.getLocalAuthor().getName());
+		if (authorCompare == 0) {
+			// if names are equal, compare by time instead
+			return compareByTime(c1, c2);
+		} else {
+			return authorCompare;
+		}
+	}
+
+	protected int compareByTime(ContactListItem c1, ContactListItem c2) {
+		long time1 = c1.getTimestamp();
+		long time2 = c2.getTimestamp();
+		if (time1 < time2) return 1;
+		if (time1 > time2) return -1;
+		return 0;
+	}
+
+	protected class SortedListCallBacks extends SortedList.Callback<ContactListItem> {
+
+		@Override
+		public void onInserted(int position, int count) {
+			notifyItemRangeInserted(position, count);
+		}
+
+		@Override
+		public void onChanged(int position, int count) {
+			notifyItemRangeChanged(position, count);
+		}
+
+		@Override
+		public void onMoved(int fromPosition, int toPosition) {
+			notifyItemMoved(fromPosition, toPosition);
+		}
+
+		@Override
+		public void onRemoved(int position, int count) {
+			notifyItemRangeRemoved(position, count);
+		}
+
+		@Override
+		public int compare(ContactListItem c1, ContactListItem c2) {
+			return compareContactListItems(c1, c2);
+		}
+
+		@Override
+		public boolean areItemsTheSame(ContactListItem c1, ContactListItem c2) {
+			return c1.getContact().getId().equals(c2.getContact().getId());
+		}
+
+		@Override
+		public boolean areContentsTheSame(ContactListItem c1,
+				ContactListItem c2) {
+			// check for all properties that influence visual
+			// representation of contact
+			if (c1.isConnected() != c2.isConnected()) {
+				return false;
+			}
+			if (c1.getUnreadCount() != c2.getUnreadCount()) {
+				return false;
+			}
+			if (c1.getTimestamp() != c2.getTimestamp()) {
+				return false;
+			}
+			return true;
+		}
+	}
+
+	public interface OnItemClickListener {
+		void onItemClick(View view, ContactListItem item);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java
index becdaa9220..dee3031f74 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java
@@ -1,14 +1,7 @@
 package org.briarproject.android.contact;
 
 import android.content.Context;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.os.Build;
 import android.support.v4.content.ContextCompat;
-import android.support.v7.util.SortedList;
-import android.support.v7.widget.RecyclerView;
 import android.text.format.DateUtils;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -17,98 +10,12 @@ import android.widget.ImageView;
 import android.widget.TextView;
 
 import org.briarproject.R;
-import org.briarproject.api.contact.ContactId;
-import org.briarproject.api.identity.Author;
-import org.briarproject.api.identity.AuthorId;
-
-import java.util.List;
-
-import im.delight.android.identicons.IdenticonDrawable;
-
-import static android.support.v7.util.SortedList.INVALID_POSITION;
 
 public class ContactListAdapter
-		extends RecyclerView.Adapter<ContactListAdapter.ContactHolder> {
-
-	private final SortedList<ContactListItem> contacts =
-			new SortedList<ContactListItem>(ContactListItem.class,
-					new SortedList.Callback<ContactListItem>() {
-						@Override
-						public void onInserted(int position, int count) {
-							notifyItemRangeInserted(position, count);
-						}
-
-						@Override
-						public void onChanged(int position, int count) {
-							notifyItemRangeChanged(position, count);
-						}
-
-						@Override
-						public void onMoved(int fromPosition, int toPosition) {
-							notifyItemMoved(fromPosition, toPosition);
-						}
-
-						@Override
-						public void onRemoved(int position, int count) {
-							notifyItemRangeRemoved(position, count);
-						}
-
-						@Override
-						public int compare(ContactListItem c1,
-								ContactListItem c2) {
-							int authorCompare = 0;
-							if (chooser) {
-								authorCompare = c1.getLocalAuthor().getName()
-										.compareTo(
-												c2.getLocalAuthor().getName());
-							}
-							if (authorCompare == 0) {
-								// sort items by time
-								// and do not take unread messages into account
-								long time1 = c1.getTimestamp();
-								long time2 = c2.getTimestamp();
-								if (time1 < time2) return 1;
-								if (time1 > time2) return -1;
-								return 0;
-							} else {
-								return authorCompare;
-							}
-						}
+		extends BaseContactListAdapter<ContactListAdapter.ContactHolder> {
 
-						@Override
-						public boolean areItemsTheSame(ContactListItem c1,
-								ContactListItem c2) {
-							return c1.getContact().getId()
-									.equals(c2.getContact().getId());
-						}
-
-						@Override
-						public boolean areContentsTheSame(ContactListItem c1,
-								ContactListItem c2) {
-							// check for all properties that influence visual
-							// representation of contact
-							if (c1.isConnected() != c2.isConnected()) {
-								return false;
-							}
-							if (c1.getUnreadCount() != c2.getUnreadCount()) {
-								return false;
-							}
-							if (c1.getTimestamp() != c2.getTimestamp()) {
-								return false;
-							}
-							return true;
-						}
-					});
-	private final OnItemClickListener listener;
-	private final boolean chooser;
-	private Context ctx;
-	private AuthorId localAuthorId;
-
-	public ContactListAdapter(Context context, OnItemClickListener listener,
-			boolean chooser) {
-		ctx = context;
-		this.listener = listener;
-		this.chooser = chooser;
+	public ContactListAdapter(Context context, OnItemClickListener listener) {
+		super(context, listener);
 	}
 
 	@Override
@@ -121,38 +28,25 @@ public class ContactListAdapter
 
 	@Override
 	public void onBindViewHolder(final ContactHolder ui, final int position) {
+		super.onBindViewHolder(ui, position);
+
 		final ContactListItem item = getItem(position);
 
+		// name and unread count
+		String contactName = item.getContact().getAuthor().getName();
 		int unread = item.getUnreadCount();
-		if (!chooser && unread > 0) {
-			ui.layout.setBackgroundColor(
-					ContextCompat.getColor(ctx, R.color.unread_background));
-		}
-
-		if (item.isConnected()) {
-			ui.bulb.setImageResource(R.drawable.contact_connected);
-		} else {
-			ui.bulb.setImageResource(R.drawable.contact_disconnected);
-		}
-
-		Author author = item.getContact().getAuthor();
-		ui.avatar.setImageDrawable(
-				new IdenticonDrawable(author.getId().getBytes()));
-		String contactName = author.getName();
-
-		if (!chooser && unread > 0) {
+		if (unread > 0) {
 			// TODO show these in a bubble on top of the avatar
 			ui.name.setText(contactName + " (" + unread + ")");
-		} else {
-			ui.name.setText(contactName);
-		}
 
-		if (chooser) {
-			ui.identity.setText(item.getLocalAuthor().getName());
+			// different background for contacts with unread messages
+			ui.layout.setBackgroundColor(
+					ContextCompat.getColor(ctx, R.color.unread_background));
 		} else {
-			ui.identity.setVisibility(View.GONE);
+			ui.name.setText(contactName);
 		}
 
+		// date of last message
 		if (item.isEmpty()) {
 			ui.date.setText(R.string.no_private_messages);
 		} else {
@@ -162,115 +56,33 @@ public class ContactListAdapter
 					DateUtils.getRelativeTimeSpanString(ctx, timestamp));
 		}
 
-		if (chooser && !item.getLocalAuthor().getId().equals(localAuthorId)) {
-			grayOutItem(ui);
-		}
-
-		ui.layout.setOnClickListener(new View.OnClickListener() {
-			@Override
-			public void onClick(View v) {
-				listener.onItemClick(ui.avatar, item);
-			}
-		});
-	}
-
-	@Override
-	public int getItemCount() {
-		return contacts.size();
-	}
-
-	/**
-	 * Set the identity from whose perspective the contact shall be chosen.
-	 * This is only used if chooser is true.
-	 * @param authorId The ID of the local Author
-	 */
-	public void setLocalAuthor(AuthorId authorId) {
-		localAuthorId = authorId;
-		notifyDataSetChanged();
-	}
-
-	private void grayOutItem(final ContactHolder ui) {
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
-			float alpha = 0.25f;
-			ui.bulb.setAlpha(alpha);
-			ui.avatar.setAlpha(alpha);
-			ui.name.setAlpha(alpha);
-			ui.date.setAlpha(alpha);
-			ui.identity.setAlpha(alpha);
+		// online/offline
+		if (item.isConnected()) {
+			ui.bulb.setImageResource(R.drawable.contact_connected);
 		} else {
-			ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY,
-					PorterDuff.Mode.MULTIPLY);
-			ui.bulb.setColorFilter(colorFilter);
-			ui.avatar.setColorFilter(colorFilter);
-			ui.name.setEnabled(false);
-			ui.date.setEnabled(false);
-		}
-	}
-
-	public ContactListItem getItem(int position) {
-		if (position == INVALID_POSITION || contacts.size() <= position) {
-			return null; // Not found
-		}
-		return contacts.get(position);
-	}
-
-	public void updateItem(int position, ContactListItem item) {
-		contacts.updateItemAt(position, item);
-	}
-
-	public int findItemPosition(ContactId c) {
-		int count = getItemCount();
-		for (int i = 0; i < count; i++) {
-			ContactListItem item = getItem(i);
-			if (item.getContact().getId().equals(c)) return i;
+			ui.bulb.setImageResource(R.drawable.contact_disconnected);
 		}
-		return INVALID_POSITION; // Not found
-	}
-
-	public void addAll(List<ContactListItem> contacts) {
-		this.contacts.addAll(contacts);
 	}
 
-	public void add(ContactListItem contact) {
-		contacts.add(contact);
-	}
-
-	public void remove(ContactListItem contact) {
-		contacts.remove(contact);
-	}
+	protected static class ContactHolder
+			extends BaseContactListAdapter.BaseContactHolder {
 
-	public void clear() {
-		contacts.beginBatchedUpdates();
-
-		while(contacts.size() != 0) {
-			contacts.removeItemAt(0);
-		}
-
-		contacts.endBatchedUpdates();
-	}
-
-	public static class ContactHolder extends RecyclerView.ViewHolder {
-		public ViewGroup layout;
-		public ImageView bulb;
-		public ImageView avatar;
-		public TextView name;
-		public TextView identity;
-		public TextView date;
+		public final ImageView bulb;
+		public final TextView date;
+		public final TextView identity;
 
 		public ContactHolder(View v) {
 			super(v);
 
-			layout = (ViewGroup) v;
 			bulb = (ImageView) v.findViewById(R.id.bulbView);
-			avatar = (ImageView) v.findViewById(R.id.avatarView);
-			name = (TextView) v.findViewById(R.id.nameView);
-			identity = (TextView) v.findViewById(R.id.identityView);
 			date = (TextView) v.findViewById(R.id.dateView);
+			identity = (TextView) v.findViewById(R.id.identityView);
 		}
 	}
 
-	public interface OnItemClickListener {
-		void onItemClick(View view, ContactListItem item);
+	@Override
+	public int compareContactListItems(ContactListItem c1, ContactListItem c2) {
+		return compareByTime(c1, c2);
 	}
 
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
index f3737bd1d3..6e063bb36e 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
@@ -101,7 +101,7 @@ public class ContactListFragment extends BaseEventFragment {
 				inflater.inflate(R.layout.activity_contact_list, container,
 						false);
 
-		ContactListAdapter.OnItemClickListener onItemClickListener =
+		BaseContactListAdapter.OnItemClickListener onItemClickListener =
 				new ContactListAdapter.OnItemClickListener() {
 					@Override
 					public void onItemClick(View view, ContactListItem item) {
@@ -124,8 +124,7 @@ public class ContactListFragment extends BaseEventFragment {
 					}
 				};
 
-		adapter = new ContactListAdapter(getContext(), onItemClickListener,
-				false);
+		adapter = new ContactListAdapter(getContext(), onItemClickListener);
 		list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
 		list.setLayoutManager(new LinearLayoutManager(getContext()));
 		list.setAdapter(adapter);
diff --git a/briar-android/src/org/briarproject/android/contact/SelectContactsDialog.java b/briar-android/src/org/briarproject/android/contact/SelectContactsDialog.java
deleted file mode 100644
index 53b6588d7e..0000000000
--- a/briar-android/src/org/briarproject/android/contact/SelectContactsDialog.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package org.briarproject.android.contact;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnMultiChoiceClickListener;
-import android.support.v7.app.AlertDialog;
-
-import org.briarproject.R;
-import org.briarproject.api.contact.Contact;
-import org.briarproject.api.contact.ContactId;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-public class SelectContactsDialog implements OnMultiChoiceClickListener {
-
-	private Listener listener = null;
-	private List<Contact> contacts = null;
-	private Set<ContactId> selected = null;
-
-	public void setListener(Listener listener) {
-		this.listener = listener;
-	}
-
-	public void setContacts(Collection<Contact> contacts) {
-		this.contacts = new ArrayList<Contact>(contacts);
-	}
-
-	public void setSelected(Collection<ContactId> selected) {
-		this.selected = new HashSet<ContactId>(selected);
-	}
-
-	public Dialog build(Context ctx) {
-		if (listener == null || contacts == null || selected == null)
-			throw new IllegalStateException();
-		AlertDialog.Builder builder = new AlertDialog.Builder(ctx,
-				R.style.BriarDialogTheme);
-		int size = contacts.size();
-		String[] names = new String[size];
-		boolean[] checked = new boolean[size];
-		for (int i = 0; i < size; i++) {
-			Contact c = contacts.get(i);
-			names[i] = c.getAuthor().getName();
-			checked[i] = selected.contains(c.getId());
-		}
-		builder.setMultiChoiceItems(names, checked, this);
-		builder.setPositiveButton(R.string.done_button,
-				new DialogInterface.OnClickListener() {
-			public void onClick(DialogInterface dialog, int id) {
-				listener.contactsSelected(selected);
-			}
-		});
-		builder.setNegativeButton(R.string.cancel_button,
-				new DialogInterface.OnClickListener() {
-			public void onClick(DialogInterface dialog, int id) {
-				listener.contactSelectionCancelled();
-			}
-		});
-		return builder.create();
-	}
-
-	public void onClick(DialogInterface dialog, int which, boolean isChecked) {
-		if (isChecked) selected.add(contacts.get(which).getId());
-		else selected.remove(contacts.get(which).getId());
-	}
-
-	public interface Listener {
-
-		void contactsSelected(Collection<ContactId> selected);
-
-		void contactSelectionCancelled();
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java b/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java
new file mode 100644
index 0000000000..0e3e81181f
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java
@@ -0,0 +1,81 @@
+package org.briarproject.android.forum;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+
+import org.briarproject.R;
+import org.briarproject.android.contact.BaseContactListAdapter;
+import org.briarproject.android.contact.ContactListItem;
+import org.briarproject.api.contact.ContactId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class ContactSelectorAdapter
+		extends
+		BaseContactListAdapter<ContactSelectorAdapter.SelectableContactHolder> {
+
+	public ContactSelectorAdapter(Context context,
+			OnItemClickListener listener) {
+
+		super(context, listener);
+	}
+
+	@Override
+	public SelectableContactHolder onCreateViewHolder(ViewGroup viewGroup,
+			int i) {
+		View v = LayoutInflater.from(ctx)
+				.inflate(R.layout.list_item_selectable_contact, viewGroup,
+						false);
+
+		return new SelectableContactHolder(v);
+	}
+
+	@Override
+	public void onBindViewHolder(final SelectableContactHolder ui,
+			final int position) {
+		super.onBindViewHolder(ui, position);
+
+		final SelectableContactListItem item =
+				(SelectableContactListItem) getItem(position);
+
+		if (item.isSelected()) {
+			ui.checkBox.setChecked(true);
+		} else {
+			ui.checkBox.setChecked(false);
+		}
+	}
+
+	public Collection<ContactId> getSelectedContactIds() {
+		Collection<ContactId> selected = new ArrayList<ContactId>();
+
+		for (int i = 0; i < contacts.size(); i++) {
+			SelectableContactListItem item =
+					(SelectableContactListItem) contacts.get(i);
+			if (item.isSelected()) selected.add(item.getContact().getId());
+		}
+
+		return selected;
+	}
+
+	protected static class SelectableContactHolder
+			extends BaseContactListAdapter.BaseContactHolder {
+
+		private final CheckBox checkBox;
+
+		public SelectableContactHolder(View v) {
+			super(v);
+
+			checkBox = (CheckBox) v.findViewById(R.id.checkBox);
+		}
+	}
+
+	@Override
+	public int compareContactListItems(ContactListItem c1, ContactListItem c2) {
+		return compareByName(c1, c2);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index 8f2f58d345..cd44592df4 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -2,6 +2,8 @@ package org.briarproject.android.forum;
 
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
@@ -42,6 +44,8 @@ import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
+import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
 import static android.view.Gravity.CENTER;
 import static android.view.Gravity.CENTER_HORIZONTAL;
 import static android.view.View.GONE;
@@ -152,9 +156,13 @@ public class ForumActivity extends BriarActivity implements EventListener,
 				return true;
 			case R.id.action_forum_share:
 				Intent i2 = new Intent(this, ShareForumActivity.class);
+				i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
 				i2.putExtra(GROUP_ID, groupId.getBytes());
 				i2.putExtra(FORUM_NAME, forum.getName());
-				startActivity(i2);
+				ActivityOptionsCompat options = ActivityOptionsCompat
+						.makeCustomAnimation(this, android.R.anim.slide_in_left,
+								android.R.anim.slide_out_right);
+				ActivityCompat.startActivity(this, i2, options.toBundle());
 				return true;
 			default:
 				return super.onOptionsItemSelected(item);
diff --git a/briar-android/src/org/briarproject/android/forum/NoContactsDialog.java b/briar-android/src/org/briarproject/android/forum/NoContactsDialog.java
deleted file mode 100644
index 583e25d82c..0000000000
--- a/briar-android/src/org/briarproject/android/forum/NoContactsDialog.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.briarproject.android.forum;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.support.v7.app.AlertDialog;
-
-import org.briarproject.R;
-
-public class NoContactsDialog {
-
-	private Listener listener = null;
-
-	public void setListener(Listener listener) {
-		this.listener = listener;
-	}
-
-	public Dialog build(Context ctx) {
-		if (listener == null) throw new IllegalStateException();
-		AlertDialog.Builder builder = new AlertDialog.Builder(ctx,
-				R.style.BriarDialogTheme);
-		builder.setMessage(R.string.no_contacts_prompt);
-		builder.setPositiveButton(R.string.add_button,
-				new DialogInterface.OnClickListener() {
-			public void onClick(DialogInterface dialog, int id) {
-				listener.contactCreationSelected();
-			}
-		});
-		builder.setNegativeButton(R.string.cancel_button,
-				new DialogInterface.OnClickListener() {
-			public void onClick(DialogInterface dialog, int id) {
-				listener.contactCreationCancelled();
-			}
-		});
-		return builder.create();
-	}
-
-	public interface Listener {
-
-		void contactCreationSelected();
-
-		void contactCreationCancelled();
-	}
-}
diff --git a/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java b/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java
new file mode 100644
index 0000000000..93d266c6c0
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java
@@ -0,0 +1,36 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.android.contact.ContactListItem;
+import org.briarproject.android.contact.ConversationItem;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collections;
+
+// This class is not thread-safe
+public class SelectableContactListItem extends ContactListItem {
+
+	private boolean selected;
+
+	public SelectableContactListItem(Contact contact, LocalAuthor localAuthor,
+			GroupId groupId, boolean selected) {
+
+		super(contact, localAuthor, false, groupId, Collections.<ConversationItem>emptyList());
+
+		this.selected = selected;
+	}
+
+	public void setSelected(boolean selected) {
+		this.selected = selected;
+	}
+
+	public boolean isSelected() {
+		return selected;
+	}
+
+	public void toggleSelected() {
+		selected = !selected;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
index fd8e805f18..e2d4926b3b 100644
--- a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
@@ -2,67 +2,59 @@ package org.briarproject.android.forum;
 
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
 import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
 
 import org.briarproject.R;
 import org.briarproject.android.AndroidComponent;
 import org.briarproject.android.BriarActivity;
-import org.briarproject.android.contact.SelectContactsDialog;
-import org.briarproject.android.invitation.AddContactActivity;
-import org.briarproject.android.util.LayoutUtils;
+import org.briarproject.android.contact.BaseContactListAdapter;
+import org.briarproject.android.contact.ContactListItem;
+import org.briarproject.android.util.BriarRecyclerView;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.GroupId;
 
+import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
 import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
-import static android.view.Gravity.CENTER_HORIZONTAL;
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-import static android.widget.LinearLayout.VERTICAL;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
-import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
 
-public class ShareForumActivity extends BriarActivity
-implements OnClickListener, NoContactsDialog.Listener,
-SelectContactsDialog.Listener {
+public class ShareForumActivity extends BriarActivity implements
+		BaseContactListAdapter.OnItemClickListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(ShareForumActivity.class.getName());
 
-	private RadioGroup radioGroup = null;
-	private RadioButton shareWithAll = null, shareWithSome = null;
-	private Button shareButton = null;
-	private ProgressBar progress = null;
-	private boolean changed = false;
+	private ContactSelectorAdapter adapter;
 
 	// Fields that are accessed from background threads must be volatile
+	@Inject protected volatile IdentityManager identityManager;
 	@Inject protected volatile ContactManager contactManager;
 	@Inject protected volatile ForumSharingManager forumSharingManager;
-	private volatile GroupId groupId = null;
-	private volatile Collection<Contact> contacts = null;
-	private volatile Collection<ContactId> selected = null;
+	private volatile GroupId groupId;
 
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(state);
 
+		setContentView(R.layout.introduction_contact_chooser);
+
 		Intent i = getIntent();
 		byte[] b = i.getByteArrayExtra(GROUP_ID);
 		if (b == null) throw new IllegalStateException();
@@ -71,86 +63,73 @@ SelectContactsDialog.Listener {
 		if (forumName == null) throw new IllegalStateException();
 		setTitle(forumName);
 
-		LinearLayout layout = new LinearLayout(this);
-		layout.setLayoutParams(MATCH_MATCH);
-		layout.setOrientation(VERTICAL);
-		layout.setGravity(CENTER_HORIZONTAL);
-		int pad = LayoutUtils.getPadding(this);
-		layout.setPadding(pad, pad, pad, pad);
-
-		radioGroup = new RadioGroup(this);
-		radioGroup.setOrientation(VERTICAL);
-		radioGroup.setPadding(0, 0, 0, pad);
-
-		shareWithAll = new RadioButton(this);
-		shareWithAll.setId(2);
-		shareWithAll.setText(R.string.forum_share_with_all);
-		shareWithAll.setOnClickListener(this);
-		radioGroup.addView(shareWithAll);
-
-		shareWithSome = new RadioButton(this);
-		shareWithSome.setId(3);
-		shareWithSome.setText(R.string.forum_share_with_some);
-		shareWithSome.setOnClickListener(this);
-		radioGroup.addView(shareWithSome);
-
-		layout.addView(radioGroup);
-
-		shareButton = new Button(this);
-		shareButton.setLayoutParams(WRAP_WRAP);
-		shareButton.setText(R.string.forum_share_button);
-		shareButton.setOnClickListener(this);
-		layout.addView(shareButton);
-
-		progress = new ProgressBar(this);
-		progress.setLayoutParams(WRAP_WRAP);
-		progress.setIndeterminate(true);
-		progress.setVisibility(GONE);
-		layout.addView(progress);
-
-		setContentView(layout);
+		adapter = new ContactSelectorAdapter(this, this);
+		BriarRecyclerView list =
+				(BriarRecyclerView) findViewById(R.id.contactList);
+		list.setLayoutManager(new LinearLayoutManager(this));
+		list.setAdapter(adapter);
+		list.setEmptyText(getString(R.string.no_contacts));
 	}
 
 	@Override
-	public void injectActivity(AndroidComponent component) {
-		component.inject(this);
+	public void onResume() {
+		super.onResume();
+
+		loadContactsAndVisibility();
 	}
 
-	public void onClick(View view) {
-		if (view == shareWithAll) {
-			changed = true;
-		} else if (view == shareWithSome) {
-			changed = true;
-			if (contacts == null) loadVisibility();
-			else displayVisibility();
-		} else if (view == shareButton) {
-			if (changed) {
-				share();
-			} else {
-				finish();
-			}
+	@Override
+	public boolean onCreateOptionsMenu(Menu menu) {
+		// Inflate the menu items for use in the action bar
+		MenuInflater inflater = getMenuInflater();
+		inflater.inflate(R.menu.forum_share_actions, menu);
+
+		return super.onCreateOptionsMenu(menu);
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(final MenuItem item) {
+		// Handle presses on the action bar items
+		switch (item.getItemId()) {
+			case android.R.id.home:
+				onBackPressed();
+				return true;
+			case R.id.action_share_forum:
+				storeVisibility();
+				return true;
+			default:
+				return super.onOptionsItemSelected(item);
 		}
 	}
 
-	private void share() {
-		// Replace the button with a progress bar
-		shareButton.setVisibility(GONE);
-		progress.setVisibility(VISIBLE);
-		// Update the group in a background thread
-		storeVisibility(shareWithAll.isChecked());
+	@Override
+	public void injectActivity(AndroidComponent component) {
+		component.inject(this);
 	}
 
-	private void loadVisibility() {
+	private void loadContactsAndVisibility() {
 		runOnDbThread(new Runnable() {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					contacts = contactManager.getActiveContacts();
-					selected = forumSharingManager.getSharedWith(groupId);
+					List<ContactListItem> contacts =
+							new ArrayList<ContactListItem>();
+					Collection<ContactId> selectedContacts =
+							new HashSet<ContactId>(
+									forumSharingManager.getSharedWith(groupId));
+
+					for (Contact c : contactManager.getActiveContacts()) {
+						LocalAuthor localAuthor = identityManager
+								.getLocalAuthor(c.getLocalAuthorId());
+						boolean selected = selectedContacts.contains(c.getId());
+						contacts.add(
+								new SelectableContactListItem(c, localAuthor,
+										groupId, selected));
+					}
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Load took " + duration + " ms");
-					displayVisibility();
+					displayContacts(contacts);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -159,32 +138,22 @@ SelectContactsDialog.Listener {
 		});
 	}
 
-	private void displayVisibility() {
+	private void displayContacts(final List<ContactListItem> contact) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				if (contacts.isEmpty()) {
-					NoContactsDialog builder = new NoContactsDialog();
-					builder.setListener(ShareForumActivity.this);
-					builder.build(ShareForumActivity.this).show();
-				} else {
-					SelectContactsDialog builder = new SelectContactsDialog();
-					builder.setListener(ShareForumActivity.this);
-					builder.setContacts(contacts);
-					builder.setSelected(selected);
-					builder.build(ShareForumActivity.this).show();
-				}
+				adapter.addAll(contact);
 			}
 		});
 	}
 
-	private void storeVisibility(final boolean all) {
+	private void storeVisibility() {
 		runOnDbThread(new Runnable() {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					if (all) forumSharingManager.setSharedWithAll(groupId);
-					else forumSharingManager.setSharedWith(groupId,
-							selected);
+					Collection<ContactId> selected =
+							adapter.getSelectedContactIds();
+					forumSharingManager.setSharedWith(groupId, selected);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Update took " + duration + " ms");
@@ -197,20 +166,10 @@ SelectContactsDialog.Listener {
 		});
 	}
 
-	public void contactCreationSelected() {
-		startActivity(new Intent(this, AddContactActivity.class));
-	}
-
-	public void contactCreationCancelled() {
-		radioGroup.clearCheck();
-	}
-
-	public void contactsSelected(Collection<ContactId> selected) {
-		this.selected = Collections.unmodifiableCollection(selected);
-		share();
+	@Override
+	public void onItemClick(View view, ContactListItem item) {
+		((SelectableContactListItem) item).toggleSelected();
+		adapter.notifyItemChanged(adapter.findItemPosition(item), item);
 	}
 
-	public void contactSelectionCancelled() {
-		radioGroup.clearCheck();
-	}
 }
diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserAdapter.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserAdapter.java
new file mode 100644
index 0000000000..8c96163be0
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserAdapter.java
@@ -0,0 +1,75 @@
+package org.briarproject.android.introduction;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.os.Build;
+import android.view.View;
+
+import org.briarproject.android.contact.ContactListAdapter;
+import org.briarproject.android.contact.ContactListItem;
+import org.briarproject.api.identity.AuthorId;
+
+public class ContactChooserAdapter extends ContactListAdapter {
+
+	private AuthorId localAuthorId;
+
+	public ContactChooserAdapter(Context context,
+			OnItemClickListener listener) {
+
+		super(context, listener);
+	}
+
+	@Override
+	public void onBindViewHolder(final ContactHolder ui, final int position) {
+		super.onBindViewHolder(ui, position);
+
+		final ContactListItem item = getItem(position);
+
+		ui.name.setText(item.getContact().getAuthor().getName());
+
+		ui.identity.setText(item.getLocalAuthor().getName());
+		ui.identity.setVisibility(View.VISIBLE);
+
+		if (!item.getLocalAuthor().getId().equals(localAuthorId)) {
+			grayOutItem(ui);
+		}
+	}
+
+	@Override
+	public int compareContactListItems(ContactListItem c1, ContactListItem c2) {
+		return compareByName(c1, c2);
+	}
+
+	/**
+	 * Set the identity from whose perspective the contact shall be chosen.
+	 * Contacts that belong to a different author will be shown grayed out,
+	 * but are still clickable.
+	 * @param authorId The ID of the local Author
+	 */
+	public void setLocalAuthor(AuthorId authorId) {
+		localAuthorId = authorId;
+		notifyDataSetChanged();
+	}
+
+	private void grayOutItem(final ContactHolder ui) {
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+			float alpha = 0.25f;
+			ui.bulb.setAlpha(alpha);
+			ui.avatar.setAlpha(alpha);
+			ui.name.setAlpha(alpha);
+			ui.date.setAlpha(alpha);
+			ui.identity.setAlpha(alpha);
+		} else {
+			ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY,
+					PorterDuff.Mode.MULTIPLY);
+			ui.bulb.setColorFilter(colorFilter);
+			ui.avatar.setColorFilter(colorFilter);
+			ui.name.setEnabled(false);
+			ui.date.setEnabled(false);
+		}
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
index c381291f2a..11e30d6356 100644
--- a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
+++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
@@ -47,7 +47,7 @@ public class ContactChooserFragment extends BaseFragment {
 	public final static String TAG = "ContactChooserFragment";
 	private IntroductionActivity introductionActivity;
 	private BriarRecyclerView list;
-	private ContactListAdapter adapter;
+	private ContactChooserAdapter adapter;
 	private int contactId;
 
 	private static final Logger LOG =
@@ -111,8 +111,7 @@ public class ContactChooserFragment extends BaseFragment {
 						}
 					}
 				};
-		adapter =
-				new ContactListAdapter(getActivity(), onItemClickListener, true);
+		adapter = new ContactChooserAdapter(getActivity(), onItemClickListener);
 
 		list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
 		list.setLayoutManager(new LinearLayoutManager(getActivity()));
-- 
GitLab