From b09e30a95f3dd83a76f7668bdf3b67e526ade503 Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Thu, 29 Sep 2016 18:39:37 -0300
Subject: [PATCH] Private Group List

---
 briar-android/build.gradle                    |   4 +-
 briar-android/res/drawable/ic_contacts.xml    |  10 +
 ...c_contacts_black_24dp.xml => ic_group.xml} |   0
 .../res/drawable/navigation_drawer_header.png | Bin 0 -> 10893 bytes
 .../res/layout/activity_invitations.xml       |   6 -
 .../res/layout/fragment_contact_list.xml      |   6 -
 ...roduction_contact_chooser.xml => list.xml} |   6 +-
 briar-android/res/layout/list_item_forum.xml  |   3 +-
 briar-android/res/layout/list_item_group.xml  | 107 +++++++++++
 .../res/layout/navigation_header.xml          |  23 +--
 briar-android/res/layout/text_avatar_view.xml |   8 +-
 .../res/menu/groups_list_actions.xml          |  12 ++
 briar-android/res/menu/navigation_drawer.xml  |   9 +-
 briar-android/res/values/dimens.xml           |   5 +-
 briar-android/res/values/strings.xml          |  13 ++
 briar-android/res/values/styles.xml           |   3 -
 .../android/ActivityComponent.java            |   2 +
 .../briarproject/android/ActivityModule.java  |   9 +
 .../android/AndroidComponent.java             |   3 +
 .../android/BriarFragmentActivity.java        |   3 +
 .../android/NavDrawerActivity.java            |   4 +
 .../android/contact/ContactListFragment.java  |   4 +-
 .../introduction/ContactChooserFragment.java  |   7 +-
 .../android/privategroup/list/GroupItem.java  |  78 ++++++++
 .../privategroup/list/GroupListAdapter.java   |  79 ++++++++
 .../list/GroupListController.java             |  44 +++++
 .../list/GroupListControllerImpl.java         | 156 ++++++++++++++++
 .../privategroup/list/GroupListFragment.java  | 171 ++++++++++++++++++
 .../privategroup/list/GroupViewHolder.java    | 135 ++++++++++++++
 .../sharing/ContactSelectorFragment.java      |   5 +-
 .../android/sharing/InvitationsActivity.java  |   4 +-
 .../android/view/TextAvatarView.java          |   3 +-
 .../briarproject/api/clients/BaseGroup.java   |   3 +
 .../api/event/GroupMessageAddedEvent.java     |  36 ++++
 .../api/privategroup/GroupMessageHeader.java  |  18 +-
 .../api/privategroup/PrivateGroupManager.java |   6 +
 .../org/briarproject/CoreEagerSingletons.java |   5 +
 .../src/org/briarproject/CoreModule.java      |   3 +
 .../privategroup/PrivateGroupManagerImpl.java |  13 ++
 39 files changed, 945 insertions(+), 61 deletions(-)
 create mode 100644 briar-android/res/drawable/ic_contacts.xml
 rename briar-android/res/drawable/{ic_contacts_black_24dp.xml => ic_group.xml} (100%)
 create mode 100644 briar-android/res/drawable/navigation_drawer_header.png
 delete mode 100644 briar-android/res/layout/activity_invitations.xml
 delete mode 100644 briar-android/res/layout/fragment_contact_list.xml
 rename briar-android/res/layout/{introduction_contact_chooser.xml => list.xml} (64%)
 create mode 100644 briar-android/res/layout/list_item_group.xml
 create mode 100644 briar-android/res/menu/groups_list_actions.xml
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/list/GroupItem.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/list/GroupListAdapter.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/list/GroupListController.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java
 create mode 100644 briar-android/src/org/briarproject/android/privategroup/list/GroupViewHolder.java
 create mode 100644 briar-api/src/org/briarproject/api/event/GroupMessageAddedEvent.java

diff --git a/briar-android/build.gradle b/briar-android/build.gradle
index 11ddb77b20..113bb63dfb 100644
--- a/briar-android/build.gradle
+++ b/briar-android/build.gradle
@@ -32,7 +32,7 @@ dependencies {
 	}
 	compile 'info.guardianproject.panic:panic:0.5'
 	compile 'info.guardianproject.trustedintents:trustedintents:0.2'
-	compile 'de.hdodenhof:circleimageview:2.0.0'
+	compile 'de.hdodenhof:circleimageview:2.1.0'
 	compile 'com.google.zxing:core:3.2.1'
 	apt 'com.google.dagger:dagger-compiler:2.0.2'
 	provided 'javax.annotation:jsr250-api:1.0'
@@ -53,7 +53,7 @@ dependencyVerification {
 			'ch.acra:acra:afd5b28934d5166b55f261c85685ad59e8a4ebe9ca1960906afaa8c76d8dc9eb',
 			'info.guardianproject.panic:panic:a7ed9439826db2e9901649892cf9afbe76f00991b768d8f4c26332d7c9406cb2',
 			'info.guardianproject.trustedintents:trustedintents:6221456d8821a8d974c2acf86306900237cf6afaaa94a4c9c44e161350f80f3e',
-			'de.hdodenhof:circleimageview:c76d936395b50705a3f98c9220c22d2599aeb9e609f559f6048975cfc1f686b8',
+			'de.hdodenhof:circleimageview:bcbc588e19e6dcf8c120b1957776bfe229efba5d2fbe5da7156372eeacf65503',
 			'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
 			'com.android.support:support-v4:81ce890f26d35c75ad17d0f998a7e3230330c3b41e0b629566bc744bee89e448',
 			'com.android.support:appcompat-v7:00f9d93acacd6731f309724054bf51492814b4b2869f16d7d5c0038dcb8c9a0d',
diff --git a/briar-android/res/drawable/ic_contacts.xml b/briar-android/res/drawable/ic_contacts.xml
new file mode 100644
index 0000000000..cd3e499303
--- /dev/null
+++ b/briar-android/res/drawable/ic_contacts.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="M20,0L4,0v2h16L20,0zM4,24h16v-2L4,22v2zM20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM12,6.75c1.24,0 2.25,1.01 2.25,2.25s-1.01,2.25 -2.25,2.25S9.75,10.24 9.75,9 10.76,6.75 12,6.75zM17,17L7,17v-1.5c0,-1.67 3.33,-2.5 5,-2.5s5,0.83 5,2.5L17,17z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_contacts_black_24dp.xml b/briar-android/res/drawable/ic_group.xml
similarity index 100%
rename from briar-android/res/drawable/ic_contacts_black_24dp.xml
rename to briar-android/res/drawable/ic_group.xml
diff --git a/briar-android/res/drawable/navigation_drawer_header.png b/briar-android/res/drawable/navigation_drawer_header.png
new file mode 100644
index 0000000000000000000000000000000000000000..d829743726116470e35367ab3cc443cd855ccefa
GIT binary patch
literal 10893
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

literal 0
HcmV?d00001

diff --git a/briar-android/res/layout/activity_invitations.xml b/briar-android/res/layout/activity_invitations.xml
deleted file mode 100644
index 4d3d61cea8..0000000000
--- a/briar-android/res/layout/activity_invitations.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<org.briarproject.android.view.BriarRecyclerView
-	android:id="@+id/invitationsView"
-	xmlns:android="http://schemas.android.com/apk/res/android"
-	android:layout_width="match_parent"
-	android:layout_height="match_parent"/>
diff --git a/briar-android/res/layout/fragment_contact_list.xml b/briar-android/res/layout/fragment_contact_list.xml
deleted file mode 100644
index de3bddebe1..0000000000
--- a/briar-android/res/layout/fragment_contact_list.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<org.briarproject.android.view.BriarRecyclerView
-		xmlns:android="http://schemas.android.com/apk/res/android"
-		android:id="@+id/contactList"
-		android:layout_width="match_parent"
-		android:layout_height="match_parent"/>
diff --git a/briar-android/res/layout/introduction_contact_chooser.xml b/briar-android/res/layout/list.xml
similarity index 64%
rename from briar-android/res/layout/introduction_contact_chooser.xml
rename to briar-android/res/layout/list.xml
index 4a99ecbed9..0fbe7cede5 100644
--- a/briar-android/res/layout/introduction_contact_chooser.xml
+++ b/briar-android/res/layout/list.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <org.briarproject.android.view.BriarRecyclerView
+	android:id="@+id/list"
 	xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:tools="http://schemas.android.com/tools"
-	android:id="@+id/contactList"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent"
-	tools:listitem="@layout/list_item_contact"/>
+	app:scrollToEnd="false"/>
diff --git a/briar-android/res/layout/list_item_forum.xml b/briar-android/res/layout/list_item_forum.xml
index c143b6a22b..5c4df7bd17 100644
--- a/briar-android/res/layout/list_item_forum.xml
+++ b/briar-android/res/layout/list_item_forum.xml
@@ -15,7 +15,8 @@
 		android:layout_alignParentLeft="true"
 		android:layout_alignParentStart="true"
 		android:layout_centerVertical="true"
-		android:layout_marginRight="@dimen/listitem_horizontal_margin"/>
+		android:layout_marginRight="@dimen/listitem_horizontal_margin"
+		android:layout_marginTop="@dimen/listitem_horizontal_margin"/>
 
 	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
 		android:id="@+id/forumNameView"
diff --git a/briar-android/res/layout/list_item_group.xml b/briar-android/res/layout/list_item_group.xml
new file mode 100644
index 0000000000..afb0b642c1
--- /dev/null
+++ b/briar-android/res/layout/list_item_group.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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:layout_marginLeft="@dimen/listitem_horizontal_margin"
+	android:layout_marginStart="@dimen/listitem_horizontal_margin"
+	android:background="?attr/selectableItemBackground">
+
+	<org.briarproject.android.view.TextAvatarView
+		android:id="@+id/avatarView"
+		android:layout_width="@dimen/listitem_picture_frame_size"
+		android:layout_height="@dimen/listitem_picture_frame_size"
+		android:layout_alignParentLeft="true"
+		android:layout_alignParentStart="true"
+		android:layout_marginEnd="@dimen/listitem_horizontal_margin"
+		android:layout_marginRight="@dimen/listitem_horizontal_margin"
+		android:layout_marginTop="@dimen/listitem_horizontal_margin"/>
+
+	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
+		android:id="@+id/nameView"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_alignParentTop="true"
+		android:layout_marginTop="@dimen/listitem_horizontal_margin"
+		android:layout_toEndOf="@+id/avatarView"
+		android:layout_toRightOf="@+id/avatarView"
+		android:maxLines="2"
+		android:textColor="@color/briar_text_primary"
+		android:textSize="@dimen/text_size_medium"
+		tools:text="This is a name of a Private Group"/>
+
+	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
+		android:id="@+id/creatorView"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_below="@+id/nameView"
+		android:layout_marginBottom="@dimen/margin_small"
+		android:layout_toEndOf="@+id/avatarView"
+		android:layout_toRightOf="@+id/avatarView"
+		android:paddingTop="@dimen/margin_small"
+		android:textColor="@color/briar_text_secondary"
+		android:textSize="@dimen/text_size_small"
+		tools:text="Created by Santa Claus"/>
+
+	<TextView
+		android:id="@+id/messageCountView"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_below="@+id/creatorView"
+		android:layout_marginBottom="@dimen/margin_small"
+		android:layout_toEndOf="@+id/avatarView"
+		android:layout_toRightOf="@+id/avatarView"
+		android:paddingTop="@dimen/margin_small"
+		android:textColor="@color/briar_text_secondary"
+		android:textSize="@dimen/text_size_small"
+		tools:text="1337 messages"
+		tools:visibility="visible"/>
+
+	<TextView
+		android:id="@+id/dateView"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_alignParentEnd="true"
+		android:layout_alignParentRight="true"
+		android:layout_below="@+id/creatorView"
+		android:layout_marginEnd="@dimen/listitem_horizontal_margin"
+		android:layout_marginRight="@dimen/listitem_horizontal_margin"
+		android:paddingTop="@dimen/margin_small"
+		android:textColor="@color/briar_text_secondary"
+		android:textSize="@dimen/text_size_small"
+		tools:text="3 weeks ago, 12:00"
+		tools:visibility="visible"/>
+
+	<TextView
+		android:id="@+id/statusView"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_below="@+id/messageCountView"
+		android:layout_toEndOf="@+id/avatarView"
+		android:layout_toRightOf="@+id/avatarView"
+		android:paddingTop="@dimen/margin_small"
+		android:textColor="@color/briar_text_tertiary"
+		tools:text="@string/groups_group_is_empty"/>
+
+	<Button
+		android:id="@+id/removeButton"
+		style="@style/BriarButtonFlat.Negative"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_alignBottom="@+id/divider"
+		android:layout_alignParentRight="true"
+		android:layout_toRightOf="@+id/statusView"
+		android:text="@string/groups_remove"
+		tools:visibility="gone"/>
+
+	<View
+		android:id="@+id/divider"
+		style="@style/Divider.ForumList"
+		android:layout_alignParentLeft="true"
+		android:layout_alignParentStart="true"
+		android:layout_below="@+id/statusView"
+		android:layout_marginTop="@dimen/listitem_horizontal_margin"/>
+
+</RelativeLayout>
+
diff --git a/briar-android/res/layout/navigation_header.xml b/briar-android/res/layout/navigation_header.xml
index cc7f80ca83..ff120e9dd0 100644
--- a/briar-android/res/layout/navigation_header.xml
+++ b/briar-android/res/layout/navigation_header.xml
@@ -1,21 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
+<ImageView
 	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"
-	tools:showIn="@layout/navigation_menu">
-
-	<ImageView
-		android:layout_width="100dp"
-		android:layout_height="100dp"
-		android:layout_gravity="top|center_horizontal"
-		android:layout_margin="@dimen/margin_medium"
-		android:contentDescription="@string/app_name"
-		android:src="@drawable/briar_logo_large"/>
-
-	<View
-		style="@style/Divider.Horizontal"
-		android:layout_gravity="bottom"/>
-
-</FrameLayout>
\ No newline at end of file
+	android:layout_height="100dp"
+	android:contentDescription="@string/app_name"
+	android:scaleType="fitStart"
+	android:src="@drawable/navigation_drawer_header"
+	tools:showIn="@layout/navigation_menu"/>
diff --git a/briar-android/res/layout/text_avatar_view.xml b/briar-android/res/layout/text_avatar_view.xml
index 211d0f000e..de7f3b868f 100644
--- a/briar-android/res/layout/text_avatar_view.xml
+++ b/briar-android/res/layout/text_avatar_view.xml
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <merge
 	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"
 	tools:showIn="@layout/list_item_forum">
 
@@ -11,16 +10,15 @@
 		android:layout_width="@dimen/avatar_forum_size"
 		android:layout_height="@dimen/avatar_forum_size"
 		android:layout_gravity="bottom|left"
-		android:src="@android:color/transparent"
-		app:civ_fill_color="@color/briar_button_positive"/>
+		android:src="@color/briar_button_positive"/>
 
 	<android.support.v7.widget.AppCompatTextView
 		android:id="@+id/textAvatarView"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_gravity="center"
-		android:layout_marginRight="@dimen/listitem_picture_frame_offset"
-		android:layout_marginTop="@dimen/listitem_picture_frame_offset"
+		android:layout_marginRight="@dimen/listitem_picture_frame_offset_horizontal"
+		android:layout_marginTop="@dimen/listitem_picture_frame_offset_vertical"
 		android:maxLength="1"
 		android:shadowColor="@color/forum_avatar_shadow"
 		android:shadowDx="0"
diff --git a/briar-android/res/menu/groups_list_actions.xml b/briar-android/res/menu/groups_list_actions.xml
new file mode 100644
index 0000000000..511224ff16
--- /dev/null
+++ b/briar-android/res/menu/groups_list_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_add_group"
+		android:icon="@drawable/ic_add_white"
+		android:title="@string/groups_add_group_title"
+		app:showAsAction="ifRoom"/>
+
+</menu>
\ No newline at end of file
diff --git a/briar-android/res/menu/navigation_drawer.xml b/briar-android/res/menu/navigation_drawer.xml
index dfeb05e62f..9c98a51871 100644
--- a/briar-android/res/menu/navigation_drawer.xml
+++ b/briar-android/res/menu/navigation_drawer.xml
@@ -5,8 +5,12 @@
 	<group android:checkableBehavior="single">
 		<item
 			android:id="@+id/nav_btn_contacts"
-			android:icon="@drawable/ic_contacts_black_24dp"
+			android:icon="@drawable/ic_contacts"
 			android:title="@string/contact_list_button"/>
+		<item
+			android:id="@+id/nav_btn_groups"
+			android:icon="@drawable/ic_group"
+			android:title="@string/groups_button"/>
 		<item
 			android:id="@+id/nav_btn_forums"
 			android:icon="@drawable/ic_forums_black_24dp"
@@ -15,6 +19,9 @@
 			android:id="@+id/nav_btn_blogs"
 			android:icon="@drawable/blogs"
 			android:title="@string/blogs_button"/>
+	</group>
+
+	<group android:checkableBehavior="single">
 		<item
 			android:id="@+id/nav_btn_settings"
 			android:icon="@drawable/ic_settings_black_24dp"
diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml
index 5239b500db..cc465f1ea8 100644
--- a/briar-android/res/values/dimens.xml
+++ b/briar-android/res/values/dimens.xml
@@ -24,8 +24,9 @@
 	<dimen name="listitem_height_one_line_avatar">56dp</dimen>
 	<dimen name="listitem_picture_size">48dp</dimen>
 	<dimen name="listitem_picture_size_small">23dp</dimen>
-	<dimen name="listitem_picture_frame_size">53dp</dimen>
-	<dimen name="listitem_picture_frame_offset">2dp</dimen>
+	<dimen name="listitem_picture_frame_size">51dp</dimen>
+	<dimen name="listitem_picture_frame_offset_horizontal">1dp</dimen>
+	<dimen name="listitem_picture_frame_offset_vertical">2dp</dimen>
 	<dimen name="listitem_selectable_picture_size">40dp</dimen>
 	<dimen name="avatar_forum_size">48dp</dimen>
 	<dimen name="avatar_border_width">2dp</dimen>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 48e3e1f778..883dceee68 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -34,6 +34,7 @@
 	<string name="nav_drawer_open_description">Open the navigation drawer</string>
 	<string name="nav_drawer_close_description">Close the navigation drawer</string>
 	<string name="contact_list_button">Contacts</string>
+	<string name="groups_button">Private Groups</string>
 	<string name="forums_button">Forums</string>
 	<string name="blogs_button">Blogs</string>
 	<string name="settings_button">Settings</string>
@@ -143,6 +144,18 @@
 		<item quantity="other">%d new contacts added.</item>
 	</plurals>
 
+	<!-- Private Groups -->
+	<string name="groups_list_empty">You are not participating in any groups.\n\nTap the + icon at the top to create a group yourself or ask your contacts to get invited into one of their groups.</string>
+	<string name="groups_created_by">Created by %s</string>
+	<plurals name="messages">
+		<item quantity="one">%d message</item>
+		<item quantity="other">%d messages</item>
+	</plurals>
+	<string name="groups_group_is_empty">This group is empty</string>
+	<string name="groups_group_is_dissolved">This group is dissolved</string>
+	<string name="groups_remove">Remove</string>
+	<string name="groups_add_group_title">Add Private 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/res/values/styles.xml b/briar-android/res/values/styles.xml
index 76c9c0a1f4..2056a57c79 100644
--- a/briar-android/res/values/styles.xml
+++ b/briar-android/res/values/styles.xml
@@ -93,9 +93,6 @@
 	<style name="BriarAvatar">
 		<item name="civ_border_width">@dimen/avatar_border_width</item>
 		<item name="civ_border_color">@color/briar_primary</item>
-
-		<!-- Remove when we are using 'de.hdodenhof:circleimageview:2.1.0' -->
-		<item name="civ_border_overlay">true</item>
 	</style>
 
 	<style name="NavMenuButton" parent="Widget.AppCompat.Button.Borderless.Colored">
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index 990aea7a2e..d7cab786be 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -28,6 +28,7 @@ import org.briarproject.android.keyagreement.KeyAgreementActivity;
 import org.briarproject.android.keyagreement.ShowQrCodeFragment;
 import org.briarproject.android.panic.PanicPreferencesActivity;
 import org.briarproject.android.panic.PanicResponderActivity;
+import org.briarproject.android.privategroup.list.GroupListFragment;
 import org.briarproject.android.sharing.ContactSelectorFragment;
 import org.briarproject.android.sharing.InvitationsBlogActivity;
 import org.briarproject.android.sharing.InvitationsForumActivity;
@@ -114,6 +115,7 @@ public interface ActivityComponent {
 
 	// Fragments
 	void inject(ContactListFragment fragment);
+	void inject(GroupListFragment fragment);
 	void inject(ForumListFragment fragment);
 	void inject(FeedFragment fragment);
 	void inject(IntroFragment fragment);
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 21a7d35ef0..f415cc327f 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -22,6 +22,8 @@ import org.briarproject.android.controller.SetupController;
 import org.briarproject.android.controller.SetupControllerImpl;
 import org.briarproject.android.forum.ForumController;
 import org.briarproject.android.forum.ForumControllerImpl;
+import org.briarproject.android.privategroup.list.GroupListController;
+import org.briarproject.android.privategroup.list.GroupListControllerImpl;
 
 import dagger.Module;
 import dagger.Provides;
@@ -90,6 +92,13 @@ public class ActivityModule {
 		return dbController;
 	}
 
+	@ActivityScope
+	@Provides
+	protected GroupListController provideGroupListController(
+			GroupListControllerImpl groupListController) {
+		return groupListController;
+	}
+
 	@ActivityScope
 	@Provides
 	protected ForumController provideForumController(
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index ce051f5349..9b7e6edbaf 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -35,6 +35,7 @@ import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessageFactory;
 import org.briarproject.api.plugins.ConnectionRegistry;
 import org.briarproject.api.plugins.PluginManager;
+import org.briarproject.api.privategroup.PrivateGroupManager;
 import org.briarproject.api.settings.SettingsManager;
 import org.briarproject.plugins.AndroidPluginsModule;
 import org.briarproject.system.AndroidSystemModule;
@@ -93,6 +94,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
 
 	PrivateMessageFactory privateMessageFactory();
 
+	PrivateGroupManager privateGroupManager();
+
 	ForumManager forumManager();
 
 	ForumSharingManager forumSharingManager();
diff --git a/briar-android/src/org/briarproject/android/BriarFragmentActivity.java b/briar-android/src/org/briarproject/android/BriarFragmentActivity.java
index 946ebbd356..7044355301 100644
--- a/briar-android/src/org/briarproject/android/BriarFragmentActivity.java
+++ b/briar-android/src/org/briarproject/android/BriarFragmentActivity.java
@@ -10,6 +10,7 @@ import org.briarproject.android.blogs.FeedFragment;
 import org.briarproject.android.contact.ContactListFragment;
 import org.briarproject.android.forum.ForumListFragment;
 import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.android.privategroup.list.GroupListFragment;
 
 import static android.support.v4.app.FragmentManager.POP_BACK_STACK_INCLUSIVE;
 
@@ -26,6 +27,8 @@ public abstract class BriarFragmentActivity extends BriarActivity {
 
 		if (fragmentTag.equals(ContactListFragment.TAG)) {
 			actionBar.setTitle(R.string.contact_list_button);
+		} else if (fragmentTag.equals(GroupListFragment.TAG)) {
+			actionBar.setTitle(R.string.groups_button);
 		} else if (fragmentTag.equals(ForumListFragment.TAG)) {
 			actionBar.setTitle(R.string.forums_button);
 		} else if (fragmentTag.equals(FeedFragment.TAG)) {
diff --git a/briar-android/src/org/briarproject/android/NavDrawerActivity.java b/briar-android/src/org/briarproject/android/NavDrawerActivity.java
index 62af704f57..751700559e 100644
--- a/briar-android/src/org/briarproject/android/NavDrawerActivity.java
+++ b/briar-android/src/org/briarproject/android/NavDrawerActivity.java
@@ -28,6 +28,7 @@ import org.briarproject.android.controller.TransportStateListener;
 import org.briarproject.android.controller.handler.UiResultHandler;
 import org.briarproject.android.forum.ForumListFragment;
 import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener;
+import org.briarproject.android.privategroup.list.GroupListFragment;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.identity.LocalAuthor;
 
@@ -180,6 +181,9 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
 			case R.id.nav_btn_contacts:
 				startFragment(ContactListFragment.newInstance());
 				break;
+			case R.id.nav_btn_groups:
+				startFragment(GroupListFragment.newInstance());
+				break;
 			case R.id.nav_btn_forums:
 				startFragment(ForumListFragment.newInstance());
 				break;
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
index c4536bd9cb..891bfcfbc2 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
@@ -109,7 +109,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 		setHasOptionsMenu(true);
 
 		View contentView =
-				inflater.inflate(R.layout.fragment_contact_list, container,
+				inflater.inflate(R.layout.list, container,
 						false);
 
 		BaseContactListAdapter.OnItemClickListener onItemClickListener =
@@ -141,7 +141,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 				};
 
 		adapter = new ContactListAdapter(getContext(), onItemClickListener);
-		list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
+		list = (BriarRecyclerView) contentView.findViewById(R.id.list);
 		list.setLayoutManager(new LinearLayoutManager(getContext()));
 		list.setAdapter(adapter);
 		list.setEmptyText(getString(R.string.no_contacts));
diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
index 6a01c3020a..b26d3a8dc0 100644
--- a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
+++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
@@ -77,10 +77,7 @@ public class ContactChooserFragment extends BaseFragment {
 	public View onCreateView(LayoutInflater inflater, ViewGroup container,
 			Bundle savedInstanceState) {
 
-		View contentView =
-				inflater.inflate(R.layout.introduction_contact_chooser,
-						container, false);
-
+		View contentView = inflater.inflate(R.layout.list, container, false);
 
 		if (Build.VERSION.SDK_INT >= 21) {
 			setExitTransition(new Fade());
@@ -105,7 +102,7 @@ public class ContactChooserFragment extends BaseFragment {
 				};
 		adapter = new ContactChooserAdapter(getActivity(), onItemClickListener);
 
-		list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
+		list = (BriarRecyclerView) contentView.findViewById(R.id.list);
 		list.setLayoutManager(new LinearLayoutManager(getActivity()));
 		list.setAdapter(adapter);
 		list.setEmptyText(getString(R.string.no_contacts));
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupItem.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupItem.java
new file mode 100644
index 0000000000..89b208b00e
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupItem.java
@@ -0,0 +1,78 @@
+package org.briarproject.android.privategroup.list;
+
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.sync.GroupId;
+import org.jetbrains.annotations.NotNull;
+
+// This class is not thread-safe
+class GroupItem {
+
+	private final PrivateGroup privateGroup;
+	private int messageCount;
+	private long lastUpdate;
+	private int unreadCount;
+	private boolean dissolved;
+
+	GroupItem(@NotNull PrivateGroup privateGroup, int messageCount,
+			long lastUpdate, int unreadCount, boolean dissolved) {
+
+		this.privateGroup = privateGroup;
+		this.messageCount = messageCount;
+		this.lastUpdate = lastUpdate;
+		this.unreadCount = unreadCount;
+		this.dissolved = dissolved;
+	}
+
+	void addMessageHeader(GroupMessageHeader header) {
+		messageCount++;
+		if (header.getTimestamp() > lastUpdate) {
+			lastUpdate = header.getTimestamp();
+		}
+		if (!header.isRead()) {
+			unreadCount++;
+		}
+	}
+
+	@NotNull
+	PrivateGroup getPrivateGroup() {
+		return privateGroup;
+	}
+
+	@NotNull
+	GroupId getId() {
+		return privateGroup.getId();
+	}
+
+	@NotNull
+	Author getCreator() {
+		return privateGroup.getAuthor();
+	}
+
+	@NotNull
+	String getName() {
+		return privateGroup.getName();
+	}
+
+	boolean isEmpty() {
+		return messageCount == 0;
+	}
+
+	int getMessageCount() {
+		return messageCount;
+	}
+
+	long getLastUpdate() {
+		return lastUpdate;
+	}
+
+	int getUnreadCount() {
+		return unreadCount;
+	}
+
+	boolean isDissolved() {
+		return dissolved;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListAdapter.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListAdapter.java
new file mode 100644
index 0000000000..20210a486c
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListAdapter.java
@@ -0,0 +1,79 @@
+package org.briarproject.android.privategroup.list;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.briarproject.R;
+import org.briarproject.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
+import org.briarproject.android.util.BriarAdapter;
+import org.briarproject.api.sync.GroupId;
+import org.jetbrains.annotations.NotNull;
+
+import static android.support.v7.util.SortedList.INVALID_POSITION;
+
+class GroupListAdapter extends BriarAdapter<GroupItem, GroupViewHolder> {
+
+	private final OnGroupRemoveClickListener listener;
+
+	GroupListAdapter(Context ctx, OnGroupRemoveClickListener listener) {
+		super(ctx, GroupItem.class);
+		this.listener = listener;
+	}
+
+	@Override
+	public GroupViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+		View v = LayoutInflater.from(ctx).inflate(
+				R.layout.list_item_group, parent, false);
+		return new GroupViewHolder(v);
+	}
+
+	@Override
+	public void onBindViewHolder(GroupViewHolder ui, int position) {
+		ui.bindView(ctx, getItemAt(position), listener);
+	}
+
+	@Override
+	public int compare(GroupItem a, GroupItem b) {
+		if (a == b) return 0;
+		// The group with the latest message comes first
+		long aTime = a.getLastUpdate(), bTime = b.getLastUpdate();
+		if (aTime > bTime) return -1;
+		if (aTime < bTime) return 1;
+		// Break ties by group name
+		String aName = a.getName();
+		String bName = b.getName();
+		return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
+	}
+
+	@Override
+	public boolean areContentsTheSame(GroupItem a, GroupItem b) {
+		return a.getMessageCount() == b.getMessageCount() &&
+				a.getLastUpdate() == b.getLastUpdate() &&
+				a.getUnreadCount() == b.getUnreadCount() &&
+				a.isDissolved() == b.isDissolved();
+	}
+
+	@Override
+	public boolean areItemsTheSame(GroupItem a, GroupItem b) {
+		return a.getId().equals(b.getId());
+	}
+
+	int findItemPosition(@NotNull GroupId g) {
+		for (int i = 0; i < items.size(); i++) {
+			GroupItem item = items.get(i);
+			if (item.getId().equals(g)) {
+				return i;
+			}
+		}
+		return INVALID_POSITION;
+	}
+
+	void removeItem(GroupId groupId) {
+		int pos = findItemPosition(groupId);
+		if (pos != INVALID_POSITION) items.removeItemAt(pos);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListController.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListController.java
new file mode 100644
index 0000000000..27e8c90d4a
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListController.java
@@ -0,0 +1,44 @@
+package org.briarproject.android.privategroup.list;
+
+import android.support.annotation.UiThread;
+
+import org.briarproject.android.DestroyableContext;
+import org.briarproject.android.controller.DbController;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+public interface GroupListController extends DbController {
+
+	/**
+	 * The listener must be set right after the controller was injected
+	 */
+	void setGroupListListener(GroupListListener listener);
+
+	@UiThread
+	void onStart();
+
+	@UiThread
+	void onStop();
+
+	void loadGroups(
+			ResultExceptionHandler<Collection<GroupItem>, DbException> result);
+
+	void removeGroup(GroupId g,
+			ResultExceptionHandler<Void, DbException> result);
+
+	interface GroupListListener extends DestroyableContext {
+		@UiThread
+		void onGroupMessageAdded(GroupMessageHeader header);
+
+		@UiThread
+		void onGroupAdded(GroupId groupId);
+
+		@UiThread
+		void onGroupRemoved(GroupId groupId);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java
new file mode 100644
index 0000000000..6c30011dc6
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java
@@ -0,0 +1,156 @@
+package org.briarproject.android.privategroup.list;
+
+import android.support.annotation.CallSuper;
+
+import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
+import org.briarproject.api.db.DbException;
+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.GroupMessageAddedEvent;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.PrivateGroupManager;
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+
+public class GroupListControllerImpl extends DbControllerImpl
+		implements GroupListController, EventListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(GroupListControllerImpl.class.getName());
+
+	@Inject
+	PrivateGroupManager groupManager;
+	@Inject
+	EventBus eventBus;
+	@Inject
+	AndroidNotificationManager notificationManager;
+	@Inject
+	IdentityManager identityManager;
+
+	protected volatile GroupListListener listener;
+
+	@Inject
+	GroupListControllerImpl() {
+
+	}
+
+	@Override
+	public void setGroupListListener(GroupListListener listener) {
+		this.listener = listener;
+	}
+
+	@CallSuper
+	public void onStart() {
+		if (listener == null)
+			throw new IllegalStateException(
+					"GroupListListener needs to be attached");
+		eventBus.addListener(this);
+	}
+
+	@CallSuper
+	public void onStop() {
+		eventBus.removeListener(this);
+	}
+
+	@Override
+	@CallSuper
+	public void eventOccurred(Event e) {
+		if (e instanceof GroupMessageAddedEvent) {
+			final GroupMessageAddedEvent m = (GroupMessageAddedEvent) e;
+			LOG.info("New group message added");
+			listener.runOnUiThreadUnlessDestroyed(new Runnable() {
+				@Override
+				public void run() {
+					listener.onGroupMessageAdded(m.getHeader());
+				}
+			});
+		} else if (e instanceof GroupAddedEvent) {
+			final GroupAddedEvent gae = (GroupAddedEvent) e;
+			ClientId id = gae.getGroup().getClientId();
+			if (id.equals(groupManager.getClientId())) {
+				LOG.info("Private group added");
+				listener.runOnUiThreadUnlessDestroyed(new Runnable() {
+					@Override
+					public void run() {
+						listener.onGroupAdded(gae.getGroup().getId());
+					}
+				});
+			}
+		} else if (e instanceof GroupRemovedEvent) {
+			final GroupRemovedEvent gre = (GroupRemovedEvent) e;
+			ClientId id = gre.getGroup().getClientId();
+			if (id.equals(groupManager.getClientId())) {
+				LOG.info("Private group removed");
+				listener.runOnUiThreadUnlessDestroyed(new Runnable() {
+					@Override
+					public void run() {
+						listener.onGroupRemoved(gre.getGroup().getId());
+					}
+				});
+			}
+		}
+	}
+
+	@Override
+	public void loadGroups(
+			final ResultExceptionHandler<Collection<GroupItem>, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				LOG.info("Loading groups from database...");
+				try {
+					Collection<PrivateGroup> groups =
+							groupManager.getPrivateGroups();
+					List<GroupItem> items = new ArrayList<>(groups.size());
+					for (PrivateGroup g : groups) {
+						GroupCount c = groupManager.getGroupCount(g.getId());
+						boolean dissolved = groupManager.isDissolved(g.getId());
+						items.add(new GroupItem(g, c.getMsgCount(),
+								c.getLatestMsgTime(), c.getUnreadCount(),
+								dissolved));
+					}
+					handler.onResult(items);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void removeGroup(final GroupId g,
+			final ResultExceptionHandler<Void, DbException> handler) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				LOG.info("Removing group from database...");
+				try {
+					groupManager.removePrivateGroup(g);
+				} 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/privategroup/list/GroupListFragment.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java
new file mode 100644
index 0000000000..bf773c28c6
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListFragment.java
@@ -0,0 +1,171 @@
+package org.briarproject.android.privategroup.list;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.controller.handler.UiResultExceptionHandler;
+import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.android.invitation.AddContactActivity;
+import org.briarproject.android.privategroup.list.GroupListController.GroupListListener;
+import org.briarproject.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
+import org.briarproject.android.view.BriarRecyclerView;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+public class GroupListFragment extends BaseFragment implements
+		GroupListListener, OnGroupRemoveClickListener {
+
+	public final static String TAG = GroupListFragment.class.getName();
+
+	public static GroupListFragment newInstance() {
+		return new GroupListFragment();
+	}
+
+	@Inject
+	GroupListController controller;
+
+	private BriarRecyclerView list;
+	private GroupListAdapter adapter;
+
+	@Nullable
+	@Override
+	public View onCreateView(LayoutInflater inflater, ViewGroup container,
+			Bundle savedInstanceState) {
+
+		setHasOptionsMenu(true);
+
+		View v = inflater.inflate(R.layout.list, container, false);
+
+		adapter = new GroupListAdapter(getContext(), this);
+		list = (BriarRecyclerView) v.findViewById(R.id.list);
+		list.setEmptyText(R.string.groups_list_empty);
+		list.setLayoutManager(new LinearLayoutManager(getContext()));
+		list.setAdapter(adapter);
+
+		return v;
+	}
+
+	@Override
+	public void injectFragment(ActivityComponent component) {
+		component.inject(this);
+		controller.setGroupListListener(this);
+	}
+
+	@Override
+	public void onStart() {
+		super.onStart();
+		controller.onStart();
+		list.startPeriodicUpdate();
+		loadGroups();
+	}
+
+	@Override
+	public void onStop() {
+		super.onStop();
+		controller.onStop();
+		list.stopPeriodicUpdate();
+		adapter.clear();
+		list.showProgressBar();
+	}
+
+	@Override
+	public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+		inflater.inflate(R.menu.groups_list_actions, menu);
+		super.onCreateOptionsMenu(menu, inflater);
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(final MenuItem item) {
+		switch (item.getItemId()) {
+			case R.id.action_add_group:
+				// TODO
+				return true;
+			default:
+				return super.onOptionsItemSelected(item);
+		}
+	}
+
+	@UiThread
+	@Override
+	public void onGroupRemoveClick(GroupItem item) {
+		controller.removeGroup(item.getId(),
+				new UiResultExceptionHandler<Void, DbException>(listener) {
+					@Override
+					public void onResultUi(Void result) {
+						// handled by GroupRemovedEvent and onGroupRemoved()
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO handle error
+						finish();
+					}
+				});
+	}
+
+	@UiThread
+	@Override
+	public void onGroupMessageAdded(GroupMessageHeader header) {
+		int position = adapter.findItemPosition(header.getGroupId());
+		GroupItem item = adapter.getItemAt(position);
+		if (item != null) {
+			item.addMessageHeader(header);
+			adapter.updateItemAt(position, item);
+		}
+	}
+
+	@UiThread
+	@Override
+	public void onGroupAdded(GroupId groupId) {
+		loadGroups();
+	}
+
+	@UiThread
+	@Override
+	public void onGroupRemoved(GroupId groupId) {
+		adapter.removeItem(groupId);
+	}
+
+	@Override
+	public String getUniqueTag() {
+		return TAG;
+	}
+
+	private void loadGroups() {
+		controller.loadGroups(
+				new UiResultExceptionHandler<Collection<GroupItem>, DbException>(
+						listener) {
+					@Override
+					public void onResultUi(Collection<GroupItem> result) {
+						if (result.isEmpty()) {
+							list.showData();
+						} else {
+							adapter.addAll(result);
+						}
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO handle this error
+						finish();
+					}
+				});
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupViewHolder.java
new file mode 100644
index 0000000000..12b956df4f
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupViewHolder.java
@@ -0,0 +1,135 @@
+package org.briarproject.android.privategroup.list;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.util.AndroidUtils;
+import org.briarproject.android.view.TextAvatarView;
+import org.jetbrains.annotations.NotNull;
+
+import static android.support.v4.content.ContextCompat.getColor;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+class GroupViewHolder extends RecyclerView.ViewHolder {
+
+	private final static float ALPHA = 0.42f;
+
+	private final ViewGroup layout;
+	private final TextAvatarView avatar;
+	private final TextView name;
+	private final TextView creator;
+	private final TextView postCount;
+	private final TextView date;
+	private final TextView status;
+	private final Button remove;
+
+	GroupViewHolder(View v) {
+		super(v);
+
+		layout = (ViewGroup) v;
+		avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
+		name = (TextView) v.findViewById(R.id.nameView);
+		creator = (TextView) v.findViewById(R.id.creatorView);
+		postCount = (TextView) v.findViewById(R.id.messageCountView);
+		date = (TextView) v.findViewById(R.id.dateView);
+		status = (TextView) v.findViewById(R.id.statusView);
+		remove = (Button) v.findViewById(R.id.removeButton);
+	}
+
+	void bindView(Context ctx, @Nullable final GroupItem group,
+			@NotNull final OnGroupRemoveClickListener listener) {
+		if (group == null) return;
+
+		// Avatar
+		avatar.setText(group.getName().substring(0, 1));
+		avatar.setBackgroundBytes(group.getId().getBytes());
+		avatar.setUnreadCount(group.getUnreadCount());
+
+		// Group Name
+		name.setText(group.getName());
+
+		// Creator
+		creator.setText(ctx.getString(R.string.groups_created_by,
+				group.getCreator().getName()));
+
+		if (!group.isDissolved()) {
+			// full visibility
+			avatar.setAlpha(1);
+			name.setAlpha(1);
+			creator.setAlpha(1);
+
+			// Date and Status
+			if (group.isEmpty()) {
+				postCount.setVisibility(GONE);
+				date.setVisibility(GONE);
+				avatar.setProblem(true);
+				status
+						.setText(ctx.getString(R.string.groups_group_is_empty));
+				status.setVisibility(VISIBLE);
+			} else {
+				// Message Count
+				int messageCount = group.getMessageCount();
+				postCount.setVisibility(VISIBLE);
+				postCount.setText(ctx.getResources()
+						.getQuantityString(R.plurals.messages, messageCount,
+								messageCount));
+				postCount.setTextColor(
+						getColor(ctx, R.color.briar_text_secondary));
+
+				long lastUpdate = group.getLastUpdate();
+				date.setText(AndroidUtils.formatDate(ctx, lastUpdate));
+				date.setVisibility(VISIBLE);
+				avatar.setProblem(false);
+				status.setVisibility(GONE);
+			}
+			remove.setVisibility(GONE);
+		} else {
+			// grey out
+			avatar.setAlpha(ALPHA);
+			name.setAlpha(ALPHA);
+			creator.setAlpha(ALPHA);
+
+			postCount.setVisibility(GONE);
+			date.setVisibility(GONE);
+			status
+					.setText(ctx.getString(R.string.groups_group_is_dissolved));
+			status.setVisibility(VISIBLE);
+			remove.setOnClickListener(new OnClickListener() {
+				@Override
+				public void onClick(View v) {
+					listener.onGroupRemoveClick(group);
+				}
+			});
+			remove.setVisibility(VISIBLE);
+		}
+
+		// Open Group on Click
+		layout.setOnClickListener(new OnClickListener() {
+			@Override
+			public void onClick(View v) {
+/*
+				Intent i = new Intent(ctx, GroupActivity.class);
+				GroupId id = item.getId();
+				i.putExtra(GROUP_ID, id.getBytes());
+				ActivityOptionsCompat options = ActivityOptionsCompat
+						.makeCustomAnimation(ctx, android.R.anim.fade_in,
+								android.R.anim.fade_out);
+				ActivityCompat.startActivity(ctx, i, options.toBundle());
+*/
+			}
+		});
+	}
+
+	interface OnGroupRemoveClickListener {
+		void onGroupRemoveClick(GroupItem item);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java b/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java
index 286e0616dc..8e270d6407 100644
--- a/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java
+++ b/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java
@@ -99,8 +99,7 @@ public class ContactSelectorFragment extends BaseFragment implements
 	public View onCreateView(LayoutInflater inflater, ViewGroup container,
 			Bundle savedInstanceState) {
 
-		View contentView = inflater.inflate(
-				R.layout.introduction_contact_chooser, container, false);
+		View contentView = inflater.inflate(R.layout.list, container, false);
 
 		if (Build.VERSION.SDK_INT >= 21) {
 			setExitTransition(new Fade());
@@ -108,7 +107,7 @@ public class ContactSelectorFragment extends BaseFragment implements
 
 		adapter = new ContactSelectorAdapter(getActivity(), this);
 
-		list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
+		list = (BriarRecyclerView) contentView.findViewById(R.id.list);
 		list.setLayoutManager(new LinearLayoutManager(getActivity()));
 		list.setAdapter(adapter);
 		list.setEmptyText(getString(R.string.no_contacts_selector));
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java b/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
index 941dbb48f1..a6b33a0d8c 100644
--- a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
@@ -38,12 +38,12 @@ abstract class InvitationsActivity extends BriarActivity
 	public void onCreate(Bundle state) {
 		super.onCreate(state);
 
-		setContentView(R.layout.activity_invitations);
+		setContentView(R.layout.list);
 
 		adapter = getAdapter(this, this);
 
 
-		list = (BriarRecyclerView) findViewById(R.id.invitationsView);
+		list = (BriarRecyclerView) findViewById(R.id.list);
 		if (list != null) {
 			list.setLayoutManager(new LinearLayoutManager(this));
 			list.setAdapter(adapter);
diff --git a/briar-android/src/org/briarproject/android/view/TextAvatarView.java b/briar-android/src/org/briarproject/android/view/TextAvatarView.java
index 8b06ecb4c1..2f5a2cfb2c 100644
--- a/briar-android/src/org/briarproject/android/view/TextAvatarView.java
+++ b/briar-android/src/org/briarproject/android/view/TextAvatarView.java
@@ -2,6 +2,7 @@ package org.briarproject.android.view;
 
 import android.content.Context;
 import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
@@ -78,7 +79,7 @@ public class TextAvatarView extends FrameLayout {
 		int b = getByte(bytes, 2) * 3 / 4 + 96;
 		int color = Color.rgb(r, g, b);
 
-		background.setFillColor(color);
+		background.setImageDrawable(new ColorDrawable(color));
 	}
 
 	private byte getByte(byte[] bytes, int index) {
diff --git a/briar-api/src/org/briarproject/api/clients/BaseGroup.java b/briar-api/src/org/briarproject/api/clients/BaseGroup.java
index 7a6d1754e8..fe8a282f5b 100644
--- a/briar-api/src/org/briarproject/api/clients/BaseGroup.java
+++ b/briar-api/src/org/briarproject/api/clients/BaseGroup.java
@@ -16,14 +16,17 @@ public abstract class BaseGroup {
 		this.salt = salt;
 	}
 
+	@NotNull
 	public GroupId getId() {
 		return group.getId();
 	}
 
+	@NotNull
 	public Group getGroup() {
 		return group;
 	}
 
+	@NotNull
 	public String getName() {
 		return name;
 	}
diff --git a/briar-api/src/org/briarproject/api/event/GroupMessageAddedEvent.java b/briar-api/src/org/briarproject/api/event/GroupMessageAddedEvent.java
new file mode 100644
index 0000000000..d0c05c105a
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/GroupMessageAddedEvent.java
@@ -0,0 +1,36 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.sync.GroupId;
+
+/**
+ * An event that is broadcast when a private group message was added
+ * to the database.
+ */
+public class GroupMessageAddedEvent extends Event {
+
+	private final GroupId groupId;
+	private final GroupMessageHeader header;
+	private final boolean local;
+
+	public GroupMessageAddedEvent(GroupId groupId, GroupMessageHeader header,
+			boolean local) {
+
+		this.groupId = groupId;
+		this.header = header;
+		this.local = local;
+	}
+
+	public GroupId getGroupId() {
+		return groupId;
+	}
+
+	public GroupMessageHeader getHeader() {
+		return header;
+	}
+
+	public boolean isLocal() {
+		return local;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java
index 608e86ce0a..21043297bb 100644
--- a/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java
+++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessageHeader.java
@@ -2,13 +2,27 @@ package org.briarproject.api.privategroup;
 
 import org.briarproject.api.clients.PostHeader;
 import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.Author.Status;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 public class GroupMessageHeader extends PostHeader {
 
-	public GroupMessageHeader(MessageId id, MessageId parentId, long timestamp,
-			Author author, Author.Status authorStatus, boolean read) {
+	private final GroupId groupId;
+
+	public GroupMessageHeader(@NotNull GroupId groupId, @NotNull MessageId id,
+			@Nullable MessageId parentId, long timestamp,
+			@NotNull Author author, @NotNull Status authorStatus,
+			boolean read) {
 		super(id, parentId, timestamp, author, authorStatus, read);
+		this.groupId = groupId;
+	}
+
+	@NotNull
+	public GroupId getGroupId() {
+		return groupId;
 	}
 
 }
diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
index ab02e86bc0..a7fbd0cb12 100644
--- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
+++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
@@ -16,6 +16,9 @@ public interface PrivateGroupManager extends MessageTracker {
 	@NotNull
 	ClientId getClientId();
 
+	/** Removes a dissolved private group. */
+	void removePrivateGroup(GroupId g) throws DbException;
+
 	/** Stores (and sends) a local group message. */
 	void addLocalMessage(GroupMessage p) throws DbException;
 
@@ -33,6 +36,9 @@ public interface PrivateGroupManager extends MessageTracker {
 	@NotNull
 	Collection<PrivateGroup> getPrivateGroups() throws DbException;
 
+	/** Returns true if the private group has been dissolved. */
+	boolean isDissolved(GroupId g) throws DbException;
+
 	/** Returns the body of the group message with the given ID. */
 	@NotNull
 	String getMessageBody(MessageId m) throws DbException;
diff --git a/briar-core/src/org/briarproject/CoreEagerSingletons.java b/briar-core/src/org/briarproject/CoreEagerSingletons.java
index 4f1787c2b4..b4b7402dd9 100644
--- a/briar-core/src/org/briarproject/CoreEagerSingletons.java
+++ b/briar-core/src/org/briarproject/CoreEagerSingletons.java
@@ -1,5 +1,7 @@
 package org.briarproject;
 
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.PrivateGroupManager;
 import org.briarproject.blogs.BlogsModule;
 import org.briarproject.contact.ContactModule;
 import org.briarproject.crypto.CryptoModule;
@@ -11,6 +13,7 @@ import org.briarproject.introduction.IntroductionModule;
 import org.briarproject.lifecycle.LifecycleModule;
 import org.briarproject.messaging.MessagingModule;
 import org.briarproject.plugins.PluginsModule;
+import org.briarproject.privategroup.PrivateGroupModule;
 import org.briarproject.properties.PropertiesModule;
 import org.briarproject.sharing.SharingModule;
 import org.briarproject.sync.SyncModule;
@@ -39,6 +42,8 @@ public interface CoreEagerSingletons {
 
 	void inject(PluginsModule.EagerSingletons init);
 
+	void inject(PrivateGroupModule.EagerSingletons init);
+
 	void inject(PropertiesModule.EagerSingletons init);
 
 	void inject(SharingModule.EagerSingletons init);
diff --git a/briar-core/src/org/briarproject/CoreModule.java b/briar-core/src/org/briarproject/CoreModule.java
index dbfaeb256a..82754d6688 100644
--- a/briar-core/src/org/briarproject/CoreModule.java
+++ b/briar-core/src/org/briarproject/CoreModule.java
@@ -17,6 +17,7 @@ import org.briarproject.keyagreement.KeyAgreementModule;
 import org.briarproject.lifecycle.LifecycleModule;
 import org.briarproject.messaging.MessagingModule;
 import org.briarproject.plugins.PluginsModule;
+import org.briarproject.privategroup.PrivateGroupModule;
 import org.briarproject.properties.PropertiesModule;
 import org.briarproject.reliability.ReliabilityModule;
 import org.briarproject.reporting.ReportingModule;
@@ -46,6 +47,7 @@ import dagger.Module;
 		LifecycleModule.class,
 		MessagingModule.class,
 		PluginsModule.class,
+		PrivateGroupModule.class,
 		PropertiesModule.class,
 		ReliabilityModule.class,
 		ReportingModule.class,
@@ -69,6 +71,7 @@ public class CoreModule {
 		c.inject(new LifecycleModule.EagerSingletons());
 		c.inject(new MessagingModule.EagerSingletons());
 		c.inject(new PluginsModule.EagerSingletons());
+		c.inject(new PrivateGroupModule.EagerSingletons());
 		c.inject(new PropertiesModule.EagerSingletons());
 		c.inject(new SharingModule.EagerSingletons());
 		c.inject(new SyncModule.EagerSingletons());
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
index 93623476d9..3c9f5814bc 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
@@ -25,12 +25,15 @@ import org.jetbrains.annotations.NotNull;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
 public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		PrivateGroupManager {
 
+	private static final Logger LOG =
+			Logger.getLogger(PrivateGroupManagerImpl.class.getName());
 	static final ClientId CLIENT_ID = new ClientId(
 			StringUtils.fromHexString("5072697661746547726f75704d616e61"
 					+ "67657220627920546f727374656e2047"));
@@ -55,6 +58,11 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		return CLIENT_ID;
 	}
 
+	@Override
+	public void removePrivateGroup(GroupId g) throws DbException {
+
+	}
+
 	@Override
 	public void addLocalMessage(GroupMessage m) throws DbException {
 		Transaction txn = db.startTransaction(false);
@@ -91,6 +99,11 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		return Collections.emptyList();
 	}
 
+	@Override
+	public boolean isDissolved(GroupId g) throws DbException {
+		return false;
+	}
+
 	@NotNull
 	@Override
 	public String getMessageBody(MessageId m) throws DbException {
-- 
GitLab