From 59964c50872f938416bbb7279fe0155c046977c1 Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Fri, 11 Nov 2016 14:51:42 -0200
Subject: [PATCH] Add UI for revealing contacts within a private group

---
 briar-android/AndroidManifest.xml             |  11 ++
 briar-android/res/drawable/ic_visibility.xml  |  10 ++
 .../res/drawable/ic_visibility_off.xml        |  10 ++
 .../res/drawable/ic_visibility_white.xml      |   9 ++
 .../res/layout/activity_reveal_contacts.xml   |  25 ++++
 .../layout/list_item_revealable_contact.xml   |  76 +++++++++++
 briar-android/res/menu/group_actions.xml      |   6 +
 briar-android/res/values/strings.xml          |   7 +
 .../android/ActivityComponent.java            |   6 +
 .../briarproject/android/ActivityModule.java  |   9 ++
 .../conversation/GroupActivity.java           |  19 ++-
 .../memberlist/MemberListItem.java            |   2 +-
 .../reveal/RevealContactsActivity.java        | 100 ++++++++++++++
 .../reveal/RevealContactsController.java      |  19 +++
 .../reveal/RevealContactsControllerImpl.java  | 122 ++++++++++++++++++
 .../reveal/RevealContactsFragment.java        |  70 ++++++++++
 .../reveal/RevealableContactAdapter.java      |  43 ++++++
 .../reveal/RevealableContactItem.java         |  26 ++++
 .../reveal/RevealableContactViewHolder.java   |  64 +++++++++
 .../android/sharing/ShareActivity.java        |   1 -
 20 files changed, 628 insertions(+), 7 deletions(-)
 create mode 100644 briar-android/res/drawable/ic_visibility.xml
 create mode 100644 briar-android/res/drawable/ic_visibility_off.xml
 create mode 100644 briar-android/res/drawable/ic_visibility_white.xml
 create mode 100644 briar-android/res/layout/activity_reveal_contacts.xml
 create mode 100644 briar-android/res/layout/list_item_revealable_contact.xml
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsActivity.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsController.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsControllerImpl.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsFragment.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactAdapter.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactItem.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactViewHolder.java

diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index f5e81a2d45..16189e3751 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -142,6 +142,17 @@
 				/>
 		</activity>
 
+		<activity
+			android:name=".android.privategroup.reveal.RevealContactsActivity"
+			android:label="@string/groups_reveal_contacts"
+			android:parentActivityName=".android.privategroup.conversation.GroupActivity"
+			android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
+			<meta-data
+				android:name="android.support.PARENT_ACTIVITY"
+				android:value=".android.privategroup.conversation.GroupActivity"
+				/>
+		</activity>
+
 		<activity
 			android:name=".android.privategroup.creation.GroupInviteActivity"
 			android:label="@string/groups_invite_members"
diff --git a/briar-android/res/drawable/ic_visibility.xml b/briar-android/res/drawable/ic_visibility.xml
new file mode 100644
index 0000000000..146e495349
--- /dev/null
+++ b/briar-android/res/drawable/ic_visibility.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:alpha="0.54"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF000000"
+		android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_visibility_off.xml b/briar-android/res/drawable/ic_visibility_off.xml
new file mode 100644
index 0000000000..80bea183c3
--- /dev/null
+++ b/briar-android/res/drawable/ic_visibility_off.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:alpha="0.53"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF000000"
+		android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_visibility_white.xml b/briar-android/res/drawable/ic_visibility_white.xml
new file mode 100644
index 0000000000..c64e5d7a12
--- /dev/null
+++ b/briar-android/res/drawable/ic_visibility_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="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
+</vector>
diff --git a/briar-android/res/layout/activity_reveal_contacts.xml b/briar-android/res/layout/activity_reveal_contacts.xml
new file mode 100644
index 0000000000..4faa8c869c
--- /dev/null
+++ b/briar-android/res/layout/activity_reveal_contacts.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	android:orientation="vertical"
+	tools:context=".android.privategroup.reveal.RevealContactsActivity">
+
+	<FrameLayout
+		android:id="@+id/fragmentContainer"
+		android:layout_width="match_parent"
+		android:layout_height="0dp"
+		android:layout_weight="1"/>
+
+	<Button
+		android:id="@+id/revealButton"
+		style="@style/BriarButton"
+		android:layout_marginEnd="@dimen/margin_small"
+		android:layout_marginLeft="@dimen/margin_small"
+		android:layout_marginRight="@dimen/margin_small"
+		android:layout_marginStart="@dimen/margin_small"
+		android:text="@string/groups_reveal_contacts"/>
+
+</LinearLayout>
diff --git a/briar-android/res/layout/list_item_revealable_contact.xml b/briar-android/res/layout/list_item_revealable_contact.xml
new file mode 100644
index 0000000000..c9606b5ecb
--- /dev/null
+++ b/briar-android/res/layout/list_item_revealable_contact.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	android:orientation="vertical">
+
+
+	<RelativeLayout
+		xmlns:android="http://schemas.android.com/apk/res/android"
+		xmlns:tools="http://schemas.android.com/tools"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:background="?attr/selectableItemBackground"
+		android:orientation="vertical"
+		android:padding="@dimen/listitem_horizontal_margin">
+
+		<de.hdodenhof.circleimageview.CircleImageView
+			android:id="@+id/avatarView"
+			style="@style/BriarAvatar"
+			android:layout_width="@dimen/listitem_selectable_picture_size"
+			android:layout_height="@dimen/listitem_selectable_picture_size"
+			android:layout_alignParentLeft="true"
+			android:layout_alignParentStart="true"
+			android:layout_marginEnd="@dimen/listitem_horizontal_margin"
+			android:layout_marginRight="@dimen/listitem_horizontal_margin"
+			android:transitionName="avatar"
+			tools:src="@drawable/ic_launcher"/>
+
+		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
+			android:id="@+id/nameView"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_toRightOf="@+id/avatarView"
+			android:textColor="@color/briar_text_primary"
+			android:textSize="@dimen/text_size_large"
+			tools:text="Revealable Contact"/>
+
+		<ImageView
+			android:id="@+id/visibilityView"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_below="@+id/nameView"
+			android:layout_marginRight="@dimen/margin_small"
+			android:layout_toRightOf="@+id/avatarView"
+			android:src="@drawable/ic_visibility"
+			tools:ignore="ContentDescription"/>
+
+		<TextView
+			android:id="@+id/infoView"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_below="@+id/nameView"
+			android:layout_toLeftOf="@+id/checkBox"
+			android:layout_toRightOf="@+id/visibilityView"
+			android:gravity="center_vertical"
+			android:text="@string/groups_reveal_visible"
+			android:textColor="@color/briar_text_tertiary"
+			android:textSize="@dimen/text_size_small"
+			tools:visibility="visible"/>
+
+		<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:clickable="false"/>
+
+	</RelativeLayout>
+
+	<View style="@style/Divider.ContactList"/>
+
+</LinearLayout>
diff --git a/briar-android/res/menu/group_actions.xml b/briar-android/res/menu/group_actions.xml
index 0c1fd68fa7..a67e732998 100644
--- a/briar-android/res/menu/group_actions.xml
+++ b/briar-android/res/menu/group_actions.xml
@@ -15,6 +15,12 @@
 		android:title="@string/groups_member_list"
 		app:showAsAction="ifRoom"/>
 
+	<item
+		android:id="@+id/action_group_reveal"
+		android:icon="@drawable/ic_visibility_white"
+		android:title="@string/groups_reveal_contacts"
+		app:showAsAction="ifRoom"/>
+
 	<item
 		android:id="@+id/action_group_invite"
 		android:icon="@drawable/ic_add_white"
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 368fc5664f..0c5274d171 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -194,6 +194,13 @@
 	<string name="groups_invitations_response_accepted_received">%s accepted the group invitation.</string>
 	<string name="groups_invitations_response_declined_received">%s declined the group invitation.</string>
 
+	<!-- Private Groups Revealing Contacts -->
+	<string name="groups_reveal_contacts">Reveal Contacts</string>
+	<string name="groups_reveal_visible">Your relationship is visible to the group</string>
+	<string name="groups_reveal_visible_revealed_by_us">Your relationship is visible to the group (revealed by you)</string>
+	<string name="groups_reveal_visible_revealed_by_contact">Your relationship is visible to the group (revealed by contact)</string>
+	<string name="groups_reveal_invisible">Your relationship is not visible to the group</string>
+
 	<!-- Forums -->
 	<string name="no_forums">You don\'t have any forums yet.\n\nWhy don\'t you create a new one yourself by tapping the + icon at the top?\n\nYou can also ask your contacts to share forums with you.</string>
 	<string name="create_forum_title">New Forum</string>
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index 6db54c450a..a8b3dee8b9 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -37,6 +37,8 @@ import org.briarproject.android.privategroup.creation.GroupInviteFragment;
 import org.briarproject.android.privategroup.invitation.GroupInvitationActivity;
 import org.briarproject.android.privategroup.list.GroupListFragment;
 import org.briarproject.android.privategroup.memberlist.GroupMemberListActivity;
+import org.briarproject.android.privategroup.reveal.RevealContactsActivity;
+import org.briarproject.android.privategroup.reveal.RevealContactsFragment;
 import org.briarproject.android.sharing.BlogInvitationActivity;
 import org.briarproject.android.sharing.BlogSharingStatusActivity;
 import org.briarproject.android.sharing.ForumInvitationActivity;
@@ -91,6 +93,8 @@ public interface ActivityComponent {
 
 	void inject(GroupMemberListActivity activity);
 
+	void inject(RevealContactsActivity activity);
+
 	void inject(CreateForumActivity activity);
 
 	void inject(ShareForumActivity activity);
@@ -146,6 +150,8 @@ public interface ActivityComponent {
 
 	void inject(GroupInviteFragment fragment);
 
+	void inject(RevealContactsFragment activity);
+
 	void inject(ForumListFragment fragment);
 
 	void inject(FeedFragment fragment);
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 1c2db12176..4f17c24bc6 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -31,6 +31,8 @@ import org.briarproject.android.privategroup.list.GroupListController;
 import org.briarproject.android.privategroup.list.GroupListControllerImpl;
 import org.briarproject.android.privategroup.memberlist.GroupMemberListController;
 import org.briarproject.android.privategroup.memberlist.GroupMemberListControllerImpl;
+import org.briarproject.android.privategroup.reveal.RevealContactsController;
+import org.briarproject.android.privategroup.reveal.RevealContactsControllerImpl;
 import org.briarproject.android.sharing.BlogInvitationController;
 import org.briarproject.android.sharing.BlogInvitationControllerImpl;
 import org.briarproject.android.sharing.ForumInvitationController;
@@ -144,6 +146,13 @@ public class ActivityModule {
 		return groupMemberListController;
 	}
 
+	@ActivityScope
+	@Provides
+	protected RevealContactsController provideRevealContactsController(
+			RevealContactsControllerImpl revealContactsController) {
+		return revealContactsController;
+	}
+
 	@ActivityScope
 	@Provides
 	protected ForumController provideForumController(
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
index 24dba86141..8a24aaba65 100644
--- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
@@ -22,6 +22,7 @@ import org.briarproject.android.controller.handler.UiResultExceptionHandler;
 import org.briarproject.android.privategroup.conversation.GroupController.GroupListener;
 import org.briarproject.android.privategroup.creation.GroupInviteActivity;
 import org.briarproject.android.privategroup.memberlist.GroupMemberListActivity;
+import org.briarproject.android.privategroup.reveal.RevealContactsActivity;
 import org.briarproject.android.threaded.ThreadListActivity;
 import org.briarproject.android.threaded.ThreadListController;
 import org.briarproject.api.db.DbException;
@@ -48,8 +49,8 @@ public class GroupActivity extends
 	GroupController controller;
 
 	private boolean isCreator, isDissolved = false;
-	private MenuItem writeMenuItem, inviteMenuItem, leaveMenuItem,
-			dissolveMenuItem;
+	private MenuItem writeMenuItem, revealMenuItem, inviteMenuItem,
+			leaveMenuItem, dissolveMenuItem;
 
 	@Override
 	public void injectActivity(ActivityComponent component) {
@@ -135,6 +136,7 @@ public class GroupActivity extends
 		inflater.inflate(R.menu.group_actions, menu);
 
 		writeMenuItem = menu.findItem(R.id.action_group_compose_message);
+		revealMenuItem = menu.findItem(R.id.action_group_reveal);
 		inviteMenuItem = menu.findItem(R.id.action_group_invite);
 		leaveMenuItem = menu.findItem(R.id.action_group_leave);
 		dissolveMenuItem = menu.findItem(R.id.action_group_dissolve);
@@ -157,10 +159,15 @@ public class GroupActivity extends
 				i1.putExtra(GROUP_ID, groupId.getBytes());
 				ActivityCompat.startActivity(this, i1, options.toBundle());
 				return true;
-			case R.id.action_group_invite:
-				Intent i2 = new Intent(this, GroupInviteActivity.class);
+			case R.id.action_group_reveal:
+				Intent i2 = new Intent(this, RevealContactsActivity.class);
 				i2.putExtra(GROUP_ID, groupId.getBytes());
-				ActivityCompat.startActivityForResult(this, i2, REQUEST_INVITE,
+				ActivityCompat.startActivity(this, i2, options.toBundle());
+				return true;
+			case R.id.action_group_invite:
+				Intent i3 = new Intent(this, GroupInviteActivity.class);
+				i3.putExtra(GROUP_ID, groupId.getBytes());
+				ActivityCompat.startActivityForResult(this, i3, REQUEST_INVITE,
 						options.toBundle());
 				return true;
 			case R.id.action_group_leave:
@@ -218,10 +225,12 @@ public class GroupActivity extends
 	private void showMenuItems() {
 		if (leaveMenuItem == null || dissolveMenuItem == null) return;
 		if (isCreator) {
+			revealMenuItem.setVisible(false);
 			inviteMenuItem.setVisible(true);
 			leaveMenuItem.setVisible(false);
 			dissolveMenuItem.setVisible(true);
 		} else {
+			revealMenuItem.setVisible(true);
 			inviteMenuItem.setVisible(false);
 			leaveMenuItem.setVisible(true);
 			dissolveMenuItem.setVisible(false);
diff --git a/briar-android/src/org/briarproject/android/privategroup/memberlist/MemberListItem.java b/briar-android/src/org/briarproject/android/privategroup/memberlist/MemberListItem.java
index 9ff87a510b..469bdf6bd4 100644
--- a/briar-android/src/org/briarproject/android/privategroup/memberlist/MemberListItem.java
+++ b/briar-android/src/org/briarproject/android/privategroup/memberlist/MemberListItem.java
@@ -19,7 +19,7 @@ class MemberListItem {
 
 	public MemberListItem(GroupMember groupMember) {
 		this.member = groupMember.getAuthor();
-		this.sharing = groupMember.getVisibility() != INVISIBLE; // TODO #732
+		this.sharing = groupMember.getVisibility() != INVISIBLE;
 		this.status = groupMember.getStatus();
 	}
 
diff --git a/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsActivity.java b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsActivity.java
new file mode 100644
index 0000000000..f76d514fd7
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsActivity.java
@@ -0,0 +1,100 @@
+package org.briarproject.android.privategroup.reveal;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.contactselection.ContactSelectorActivity;
+import org.briarproject.android.controller.handler.UiExceptionHandler;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class RevealContactsActivity extends ContactSelectorActivity
+		implements OnClickListener {
+
+	private Button button;
+
+	@Inject
+	RevealContactsController controller;
+
+	@Override
+	public void injectActivity(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	@SuppressWarnings("ConstantConditions")
+	public void onCreate(@Nullable Bundle bundle) {
+		super.onCreate(bundle);
+
+		Intent i = getIntent();
+		byte[] b = i.getByteArrayExtra(GROUP_ID);
+		if (b == null) throw new IllegalStateException("No GroupId");
+		groupId = new GroupId(b);
+
+		button = (Button) findViewById(R.id.revealButton);
+		button.setOnClickListener(this);
+		button.setEnabled(false);
+
+		if (bundle == null) {
+			RevealContactsFragment fragment =
+					RevealContactsFragment.newInstance(groupId);
+			getSupportFragmentManager().beginTransaction()
+					.replace(R.id.fragmentContainer, fragment)
+					.commit();
+		}
+	}
+
+	@Override
+	@LayoutRes
+	protected int getLayout() {
+		return R.layout.activity_reveal_contacts;
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(MenuItem item) {
+		switch (item.getItemId()) {
+			case android.R.id.home:
+				onBackPressed();
+				return true;
+			default:
+				return super.onOptionsItemSelected(item);
+		}
+	}
+
+	@Override
+	public void contactsSelected(Collection<ContactId> contacts) {
+		super.contactsSelected(contacts);
+		button.setEnabled(!contacts.isEmpty());
+	}
+
+	@Override
+	public void onClick(View v) {
+		controller.reveal(groupId, contacts,
+				new UiExceptionHandler<DbException>(this) {
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO proper error handling
+						finish();
+					}
+				});
+		supportFinishAfterTransition();
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsController.java b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsController.java
new file mode 100644
index 0000000000..9c14e20a06
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsController.java
@@ -0,0 +1,19 @@
+package org.briarproject.android.privategroup.reveal;
+
+import org.briarproject.android.contactselection.ContactSelectorController;
+import org.briarproject.android.controller.handler.ExceptionHandler;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+@NotNullByDefault
+public interface RevealContactsController
+		extends ContactSelectorController<RevealableContactItem> {
+
+	void reveal(GroupId g, Collection<ContactId> contacts,
+			ExceptionHandler<DbException> handler);
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsControllerImpl.java
new file mode 100644
index 0000000000..40ddff92d0
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsControllerImpl.java
@@ -0,0 +1,122 @@
+package org.briarproject.android.privategroup.reveal;
+
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.ExceptionHandler;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.clients.ProtocolStateException;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.privategroup.GroupMember;
+import org.briarproject.api.privategroup.PrivateGroupManager;
+import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.privategroup.Visibility.INVISIBLE;
+
+@Immutable
+@NotNullByDefault
+public class RevealContactsControllerImpl extends DbControllerImpl
+		implements RevealContactsController {
+
+	private static final Logger LOG =
+			Logger.getLogger(RevealContactsControllerImpl.class.getName());
+
+	private final PrivateGroupManager groupManager;
+	private final GroupInvitationManager groupInvitationManager;
+	private final ContactManager contactManager;
+
+	@Inject
+	public RevealContactsControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			LifecycleManager lifecycleManager, PrivateGroupManager groupManager,
+			GroupInvitationManager groupInvitationManager,
+			ContactManager contactManager) {
+		super(dbExecutor, lifecycleManager);
+		this.groupManager = groupManager;
+		this.groupInvitationManager = groupInvitationManager;
+		this.contactManager = contactManager;
+	}
+
+	@Override
+	public void loadContacts(final GroupId g,
+			final Collection<ContactId> selection,
+			final ResultExceptionHandler<Collection<RevealableContactItem>, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Collection<RevealableContactItem> items =
+							getItems(g, selection);
+					handler.onResult(items);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+	@DatabaseExecutor
+	private Collection<RevealableContactItem> getItems(GroupId g,
+			Collection<ContactId> selection) throws DbException {
+		Collection<GroupMember> members =
+				groupManager.getMembers(g);
+		Collection<Contact> contacts =
+				contactManager.getActiveContacts();
+		Collection<RevealableContactItem> items =
+				new ArrayList<>(members.size());
+		for (GroupMember m : members) {
+			for (Contact c : contacts) {
+				if (m.getAuthor().equals(c.getAuthor())) {
+					boolean disabled = m.getVisibility() != INVISIBLE;
+					boolean selected =
+							disabled || selection.contains(c.getId());
+					items.add(new RevealableContactItem(c, selected, disabled,
+							m.getVisibility()));
+				}
+
+			}
+		}
+		return items;
+	}
+
+	@Override
+	public void reveal(final GroupId g, final Collection<ContactId> contacts,
+			final ExceptionHandler<DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				for (ContactId c : contacts) {
+					try {
+						groupInvitationManager.revealRelationship(c, g);
+					} catch (ProtocolStateException e) {
+						// action is outdated, move to next contact
+						if (LOG.isLoggable(INFO))
+							LOG.log(INFO, e.toString(), e);
+					} catch (DbException e) {
+						if (LOG.isLoggable(WARNING))
+							LOG.log(WARNING, e.toString(), e);
+						handler.onException(e);
+						break;
+					}
+				}
+			}
+		});
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsFragment.java b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsFragment.java
new file mode 100644
index 0000000000..df34de80ff
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealContactsFragment.java
@@ -0,0 +1,70 @@
+package org.briarproject.android.privategroup.reveal;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.contact.BaseContactListAdapter.OnContactClickListener;
+import org.briarproject.android.contactselection.BaseContactSelectorFragment;
+import org.briarproject.android.contactselection.ContactSelectorController;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+import static org.briarproject.android.BriarActivity.GROUP_ID;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class RevealContactsFragment extends
+		BaseContactSelectorFragment<RevealableContactItem, RevealableContactAdapter> {
+
+	private final static String TAG = RevealContactsFragment.class.getName();
+
+	@Inject
+	RevealContactsController controller;
+
+	public static RevealContactsFragment newInstance(GroupId groupId) {
+		Bundle args = new Bundle();
+		args.putByteArray(GROUP_ID, groupId.getBytes());
+		RevealContactsFragment fragment = new RevealContactsFragment();
+		fragment.setArguments(args);
+		return fragment;
+	}
+
+	@Override
+	public void injectFragment(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	protected ContactSelectorController<RevealableContactItem> getController() {
+		return controller;
+	}
+
+	@Override
+	protected RevealableContactAdapter getAdapter(Context context,
+			OnContactClickListener<RevealableContactItem> listener) {
+		return new RevealableContactAdapter(context, listener);
+	}
+
+	@Override
+	protected void onSelectionChanged() {
+		Collection<ContactId> selected = adapter.getSelectedContactIds();
+		Collection<ContactId> disabled = adapter.getDisabledContactIds();
+		selected.removeAll(disabled);
+
+		// tell the activity which contacts have been selected
+		listener.contactsSelected(selected);
+	}
+
+	@Override
+	public String getUniqueTag() {
+		return TAG;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactAdapter.java b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactAdapter.java
new file mode 100644
index 0000000000..c84303e788
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactAdapter.java
@@ -0,0 +1,43 @@
+package org.briarproject.android.privategroup.reveal;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.briarproject.R;
+import org.briarproject.android.contactselection.BaseContactSelectorAdapter;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+@NotNullByDefault
+class RevealableContactAdapter extends
+		BaseContactSelectorAdapter<RevealableContactItem, RevealableContactViewHolder> {
+
+	RevealableContactAdapter(Context context,
+			OnContactClickListener<RevealableContactItem> listener) {
+		super(context, RevealableContactItem.class, listener);
+	}
+
+	@Override
+	public RevealableContactViewHolder onCreateViewHolder(ViewGroup viewGroup,
+			int i) {
+		View v = LayoutInflater.from(ctx).inflate(
+				R.layout.list_item_revealable_contact, viewGroup, false);
+		return new RevealableContactViewHolder(v);
+	}
+
+	Collection<ContactId> getDisabledContactIds() {
+		Collection<ContactId> disabled = new ArrayList<>();
+
+		for (int i = 0; i < items.size(); i++) {
+			RevealableContactItem item = items.get(i);
+			if (item.isDisabled()) disabled.add(item.getContact().getId());
+		}
+		return disabled;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactItem.java b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactItem.java
new file mode 100644
index 0000000000..493fbda2e5
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactItem.java
@@ -0,0 +1,26 @@
+package org.briarproject.android.privategroup.reveal;
+
+import org.briarproject.android.contactselection.SelectableContactItem;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.privategroup.Visibility;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+@NotNullByDefault
+public class RevealableContactItem extends SelectableContactItem {
+
+	private final Visibility visibility;
+
+	public RevealableContactItem(Contact contact, boolean selected,
+			boolean disabled, Visibility visibility) {
+		super(contact, selected, disabled);
+		this.visibility = visibility;
+	}
+
+	public Visibility getVisibility() {
+		return visibility;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactViewHolder.java
new file mode 100644
index 0000000000..f2131482cb
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/reveal/RevealableContactViewHolder.java
@@ -0,0 +1,64 @@
+package org.briarproject.android.privategroup.reveal;
+
+import android.support.annotation.UiThread;
+import android.view.View;
+import android.widget.ImageView;
+
+import org.briarproject.R;
+import org.briarproject.android.contact.BaseContactListAdapter.OnContactClickListener;
+import org.briarproject.android.contactselection.BaseSelectableContactHolder;
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.jetbrains.annotations.Nullable;
+
+import static org.briarproject.android.util.AndroidUtils.GREY_OUT;
+import static org.briarproject.api.privategroup.Visibility.INVISIBLE;
+
+@UiThread
+@NotNullByDefault
+public class RevealableContactViewHolder
+		extends BaseSelectableContactHolder<RevealableContactItem> {
+
+	private final ImageView icon;
+
+	RevealableContactViewHolder(View v) {
+		super(v);
+
+		icon = (ImageView) v.findViewById(R.id.visibilityView);
+	}
+
+	@Override
+	protected void bind(RevealableContactItem item, @Nullable
+			OnContactClickListener<RevealableContactItem> listener) {
+		super.bind(item, listener);
+
+		switch (item.getVisibility()) {
+			case VISIBLE:
+				info.setText(R.string.groups_reveal_visible);
+				break;
+			case REVEALED_BY_US:
+				info.setText(R.string.groups_reveal_visible_revealed_by_us);
+				break;
+			case REVEALED_BY_CONTACT:
+				info.setText(
+						R.string.groups_reveal_visible_revealed_by_contact);
+				break;
+			case INVISIBLE:
+				info.setText(R.string.groups_reveal_invisible);
+				break;
+		}
+
+		if (item.getVisibility() == INVISIBLE) {
+			icon.setImageResource(R.drawable.ic_visibility_off);
+		} else {
+			icon.setImageResource(R.drawable.ic_visibility);
+		}
+	}
+
+	@Override
+	protected void grayOutItem(boolean gray) {
+		super.grayOutItem(gray);
+		float alpha = gray ? GREY_OUT : 1f;
+		icon.setAlpha(alpha);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/ShareActivity.java b/briar-android/src/org/briarproject/android/sharing/ShareActivity.java
index 8940714226..8924d5300a 100644
--- a/briar-android/src/org/briarproject/android/sharing/ShareActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/ShareActivity.java
@@ -7,7 +7,6 @@ import android.support.annotation.UiThread;
 import org.briarproject.R;
 import org.briarproject.android.contactselection.ContactSelectorActivity;
 import org.briarproject.android.contactselection.ContactSelectorFragment;
-import org.briarproject.android.contactselection.SelectableContactItem;
 import org.briarproject.android.sharing.BaseMessageFragment.MessageFragmentListener;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.nullsafety.MethodsNotNullByDefault;
-- 
GitLab