diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index cb723afa40e7011d2ce44d7eb0810328c5093bc1..dc0874e3b0cd967d1022cc2af0b6e26c3cda169a 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -90,6 +90,7 @@
 		<activity
 			android:name=".android.contact.ConversationActivity"
 			android:label="@string/app_name"
+			android:theme="@style/BriarThemeNoActionBar.Default"
 			android:parentActivityName=".android.NavDrawerActivity"
 			android:windowSoftInputMode="stateHidden">
 			<meta-data
@@ -174,7 +175,16 @@
 			android:parentActivityName=".android.NavDrawerActivity">
 			<meta-data
 				android:name="android.support.PARENT_ACTIVITY"
-				android:value=".android.NavDrawerActivity"
+				android:value=".android.NavDrawerActivity"/>
+		</activity>
+		<activity
+			android:name=".android.introduction.IntroductionActivity"
+			android:label="@string/introduction_activity_title"
+			android:parentActivityName=".android.contact.ConversationActivity"
+			android:windowSoftInputMode="stateHidden|adjustResize">
+			<meta-data
+				android:name="android.support.PARENT_ACTIVITY"
+				android:value=".android.contact.ConversationActivity"
 				/>
 		</activity>
 		<activity
diff --git a/briar-android/res/drawable-hdpi/msg_in.9.png b/briar-android/res/drawable-hdpi/msg_in.9.png
index 974d60e2d7aeec5229de2b26495af671ddb579ba..20202244fa3d456a7edb956718dbbafdc1a69139 100644
Binary files a/briar-android/res/drawable-hdpi/msg_in.9.png and b/briar-android/res/drawable-hdpi/msg_in.9.png differ
diff --git a/briar-android/res/drawable-hdpi/msg_out.9.png b/briar-android/res/drawable-hdpi/msg_out.9.png
index 08fd35b4cbdba08150bb224a4981df94431749e7..f8cfdc72674df433a123725aa7cea9593a5cd172 100644
Binary files a/briar-android/res/drawable-hdpi/msg_out.9.png and b/briar-android/res/drawable-hdpi/msg_out.9.png differ
diff --git a/briar-android/res/drawable-hdpi/notice_in.9.png b/briar-android/res/drawable-hdpi/notice_in.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b8dd92ec4b00adc062222f13b8a6545683ca6da
Binary files /dev/null and b/briar-android/res/drawable-hdpi/notice_in.9.png differ
diff --git a/briar-android/res/drawable-hdpi/notice_out.9.png b/briar-android/res/drawable-hdpi/notice_out.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..504fe64d660ae044bd04e325fe8726c139123cc2
Binary files /dev/null and b/briar-android/res/drawable-hdpi/notice_out.9.png differ
diff --git a/briar-android/res/drawable-mdpi/msg_in.9.png b/briar-android/res/drawable-mdpi/msg_in.9.png
index f9a0267b6e8cc706a1350b1d0fccf6f4a0887b16..cedf69450c3d62696584cb416c5b1c8e98e8afd2 100644
Binary files a/briar-android/res/drawable-mdpi/msg_in.9.png and b/briar-android/res/drawable-mdpi/msg_in.9.png differ
diff --git a/briar-android/res/drawable-mdpi/msg_out.9.png b/briar-android/res/drawable-mdpi/msg_out.9.png
index f22c541f7f9087c833dd863c6cf07faab5c1d58e..bfca75665bd844e7c47cf8f275aa6e54e7b50ea2 100644
Binary files a/briar-android/res/drawable-mdpi/msg_out.9.png and b/briar-android/res/drawable-mdpi/msg_out.9.png differ
diff --git a/briar-android/res/drawable-mdpi/notice_in.9.png b/briar-android/res/drawable-mdpi/notice_in.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..afad3fe8655de3f8e46d8fe0ac32afbb07f111dc
Binary files /dev/null and b/briar-android/res/drawable-mdpi/notice_in.9.png differ
diff --git a/briar-android/res/drawable-mdpi/notice_out.9.png b/briar-android/res/drawable-mdpi/notice_out.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..c84866764c4ef993f6179685adcb6a58dd7d84d8
Binary files /dev/null and b/briar-android/res/drawable-mdpi/notice_out.9.png differ
diff --git a/briar-android/res/drawable-v21/round_button.xml b/briar-android/res/drawable-v21/round_button.xml
new file mode 100644
index 0000000000000000000000000000000000000000..788768ef6e0e69caad294de4161e142d9aac1677
--- /dev/null
+++ b/briar-android/res/drawable-v21/round_button.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+	A FAB does not work, because even with fabSize="mini" it will be too big due to shadow drawing
+	on lower API levels
+-->
+<ripple
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	android:color="@color/briar_primary_dark">
+
+	<item>
+		<shape android:shape="oval">
+			<solid android:color="@color/briar_primary"/>
+		</shape>
+	</item>
+
+</ripple>
\ No newline at end of file
diff --git a/briar-android/res/drawable-xhdpi/msg_in.9.png b/briar-android/res/drawable-xhdpi/msg_in.9.png
index f5db8372dda83faf1082e8700e0c2996c6801fa6..8bf845198fc2c05e65ecd032eedcbae5d706568c 100644
Binary files a/briar-android/res/drawable-xhdpi/msg_in.9.png and b/briar-android/res/drawable-xhdpi/msg_in.9.png differ
diff --git a/briar-android/res/drawable-xhdpi/msg_out.9.png b/briar-android/res/drawable-xhdpi/msg_out.9.png
index d7c2816f1339f91a2abe9fa4bd1b72b05a5b5f47..dd5521a5fcab3d213175fafc946f172ff22c412d 100644
Binary files a/briar-android/res/drawable-xhdpi/msg_out.9.png and b/briar-android/res/drawable-xhdpi/msg_out.9.png differ
diff --git a/briar-android/res/drawable-xhdpi/notice_in.9.png b/briar-android/res/drawable-xhdpi/notice_in.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..9af342757d22b3d1e0860f508895ba02d6eb4cdb
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/notice_in.9.png differ
diff --git a/briar-android/res/drawable-xhdpi/notice_out.9.png b/briar-android/res/drawable-xhdpi/notice_out.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..3cd51f5900209c456ab2d48f3505603ba716c8d4
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/notice_out.9.png differ
diff --git a/briar-android/res/drawable-xxhdpi/msg_in.9.png b/briar-android/res/drawable-xxhdpi/msg_in.9.png
index 3db9979cf1b13128da38558494e6604e730098b6..1330b80faccd4128c7efd9298323a1f0dba317e0 100644
Binary files a/briar-android/res/drawable-xxhdpi/msg_in.9.png and b/briar-android/res/drawable-xxhdpi/msg_in.9.png differ
diff --git a/briar-android/res/drawable-xxhdpi/msg_out.9.png b/briar-android/res/drawable-xxhdpi/msg_out.9.png
index b7aa02377fd49c6a462c58ed7972719f41ccd978..866bc8c29dd37708cfba9ba592f4c6728fb2d177 100644
Binary files a/briar-android/res/drawable-xxhdpi/msg_out.9.png and b/briar-android/res/drawable-xxhdpi/msg_out.9.png differ
diff --git a/briar-android/res/drawable-xxhdpi/notice_in.9.png b/briar-android/res/drawable-xxhdpi/notice_in.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..690bdd2994b21459c1d2b3b0ba2c89ddd3f7ef45
Binary files /dev/null and b/briar-android/res/drawable-xxhdpi/notice_in.9.png differ
diff --git a/briar-android/res/drawable-xxhdpi/notice_out.9.png b/briar-android/res/drawable-xxhdpi/notice_out.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..d1a632b6ed1b0c86dc03f88513465d4068e509ad
Binary files /dev/null and b/briar-android/res/drawable-xxhdpi/notice_out.9.png differ
diff --git a/briar-android/res/drawable/contact_offline.xml b/briar-android/res/drawable/contact_offline.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ac18913fcb5eb66321d44729888b43d03680cde3
--- /dev/null
+++ b/briar-android/res/drawable/contact_offline.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24"
+        android:viewportWidth="24">
+
+	<path
+		android:fillColor="#2D3E50"
+		android:pathData="M10.8972,19.9503 C6.5514,19.3493,3.43091,15.2154,4.0625,10.896
+C4.55452,7.53099,7.09451,4.8236,10.394,4.14714
+C14.2569,3.35517,18.1698,5.54347,19.5236,9.25295
+C20.0698,10.7495,20.1616,12.4612,19.777,13.9758
+C19.5457,14.8864,18.8106,16.3388,18.2072,17.0771
+C16.4904,19.1779,13.581,20.3215,10.8973,19.9503 Z"
+		android:strokeColor="#FFFFFF"
+		android:strokeLineCap="round"
+		android:strokeLineJoin="round"
+		android:strokeWidth="1"/>
+
+</vector>
\ No newline at end of file
diff --git a/briar-android/res/drawable/contact_online.xml b/briar-android/res/drawable/contact_online.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f68b831022c7acbe802b7f76664c1a4ddc13cec0
--- /dev/null
+++ b/briar-android/res/drawable/contact_online.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24"
+        android:viewportWidth="24">
+
+	<path
+		android:fillColor="#95D220"
+		android:pathData="M10.8972,19.9503 C6.5514,19.3493,3.43091,15.2154,4.0625,10.896
+C4.55452,7.53099,7.09451,4.8236,10.394,4.14714
+C14.2569,3.35517,18.1698,5.54347,19.5236,9.25295
+C20.0698,10.7495,20.1616,12.4612,19.777,13.9758
+C19.5457,14.8864,18.8106,16.3388,18.2072,17.0771
+C16.4904,19.1779,13.581,20.3215,10.8973,19.9503 Z"
+		android:strokeColor="#FFFFFF"
+		android:strokeLineCap="round"
+		android:strokeLineJoin="round"
+		android:strokeWidth="1.5"/>
+
+</vector>
\ No newline at end of file
diff --git a/briar-android/res/drawable/ic_contact_introduction.xml b/briar-android/res/drawable/ic_contact_introduction.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9395c7b93e79675f0ab31d05363b7aff5b91cfd1
--- /dev/null
+++ b/briar-android/res/drawable/ic_contact_introduction.xml
@@ -0,0 +1,5 @@
+<vector android:alpha="0.56" android:height="48dp"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M9.01,14L2,14v2h7.01v3L13,15l-3.99,-4v3zM14.99,13v-3L22,10L22,8h-7.01L14.99,5L11,9l3.99,4z"/>
+</vector>
diff --git a/briar-android/res/drawable/introduction_notification.xml b/briar-android/res/drawable/introduction_notification.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ac4328d1210e12e8446ff705716d5364a3532887
--- /dev/null
+++ b/briar-android/res/drawable/introduction_notification.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M21,8V7l-3,2 -3,-2v1l3,2 3,-2zm1,-5H2C0.9,3 0,3.9 0,5v14c0,1.1 0.9,2 2,2h20c1.1,0 1.99,-0.9 1.99,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM8,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zm6,12H2v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1zm8,-6h-8V6h8v6z"/>
+</vector>
diff --git a/briar-android/res/drawable/introduction_white.xml b/briar-android/res/drawable/introduction_white.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ac4328d1210e12e8446ff705716d5364a3532887
--- /dev/null
+++ b/briar-android/res/drawable/introduction_white.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M21,8V7l-3,2 -3,-2v1l3,2 3,-2zm1,-5H2C0.9,3 0,3.9 0,5v14c0,1.1 0.9,2 2,2h20c1.1,0 1.99,-0.9 1.99,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM8,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zm6,12H2v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1zm8,-6h-8V6h8v6z"/>
+</vector>
diff --git a/briar-android/res/drawable/message_delivered_white.xml b/briar-android/res/drawable/message_delivered_white.xml
new file mode 100644
index 0000000000000000000000000000000000000000..720dab1f7101da2f5d3d04724491c6829680eadb
--- /dev/null
+++ b/briar-android/res/drawable/message_delivered_white.xml
@@ -0,0 +1,5 @@
+<vector android:height="16dp"
+	android:viewportHeight="24.0" android:viewportWidth="24.0"
+	android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
+	<path android:fillColor="#FFFFFFFF" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zm4.24,-1.41L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/>
+</vector>
diff --git a/briar-android/res/drawable/message_sent_white.xml b/briar-android/res/drawable/message_sent_white.xml
new file mode 100644
index 0000000000000000000000000000000000000000..59e6d6d1dde7e9662152b1ae9dc6b3b7ac6bef80
--- /dev/null
+++ b/briar-android/res/drawable/message_sent_white.xml
@@ -0,0 +1,5 @@
+<vector android:height="16dp"
+	android:viewportHeight="24.0" android:viewportWidth="24.0"
+	android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
+	<path android:fillColor="#FFFFFFFF" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>
diff --git a/briar-android/res/drawable/message_stored_white.xml b/briar-android/res/drawable/message_stored_white.xml
new file mode 100644
index 0000000000000000000000000000000000000000..71ee22feaa013232c6218686372d94c418ebcace
--- /dev/null
+++ b/briar-android/res/drawable/message_stored_white.xml
@@ -0,0 +1,5 @@
+<vector android:height="16dp"
+	android:viewportHeight="24.0" android:viewportWidth="24.0"
+	android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
+	<path android:fillAlpha=".9" android:fillColor="#FFFFFF" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
+</vector>
diff --git a/briar-android/res/drawable/round_button.xml b/briar-android/res/drawable/round_button.xml
new file mode 100644
index 0000000000000000000000000000000000000000..421deb97a50c82b5f98e895718bbd82feb3afa71
--- /dev/null
+++ b/briar-android/res/drawable/round_button.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+	A FAB does not work, because even with fabSize="mini" it will be too big due to shadow drawing
+	on lower API levels
+-->
+<selector
+	xmlns:android="http://schemas.android.com/apk/res/android">
+	<item>
+		<shape android:shape="oval">
+			<solid android:color="@color/briar_primary"/>
+		</shape>
+	</item>
+</selector>
\ No newline at end of file
diff --git a/briar-android/res/drawable/social_send_now_white.xml b/briar-android/res/drawable/social_send_now_white.xml
new file mode 100644
index 0000000000000000000000000000000000000000..43662f48b70e19b8963903ddfa5566568e10a2e0
--- /dev/null
+++ b/briar-android/res/drawable/social_send_now_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="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
+</vector>
diff --git a/briar-android/res/layout/activity_contact_list.xml b/briar-android/res/layout/activity_contact_list.xml
index 333dac2035f20f4006695966a4c3890c68c6373b..5f1bb33709f91ac590c48bfdbc0a3f509675bd7a 100644
--- a/briar-android/res/layout/activity_contact_list.xml
+++ b/briar-android/res/layout/activity_contact_list.xml
@@ -7,7 +7,6 @@
 	android:layout_height="match_parent">
 
 	<org.briarproject.android.util.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/activity_conversation.xml b/briar-android/res/layout/activity_conversation.xml
index 88066e9956e58f39b0f99d1b5730e5eb36b71490..d9c86d64d6204b7dcd2a10bc4f523969d00001d2 100644
--- a/briar-android/res/layout/activity_conversation.xml
+++ b/briar-android/res/layout/activity_conversation.xml
@@ -1,46 +1,79 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
 	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
 	android:orientation="vertical"
 	android:layout_width="match_parent"
-	android:layout_height="match_parent">
+	android:layout_height="match_parent"
+	tools:context=".android.contact.ConversationActivity">
+
+	<android.support.v7.widget.Toolbar
+		android:id="@+id/toolbar"
+		style="@style/BriarToolbar"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:minHeight="?attr/actionBarSize"
+		app:layout_collapseMode="pin"
+		app:layout_scrollFlags="scroll|enterAlways"
+		app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
+
+		<LinearLayout
+			android:layout_width="wrap_content"
+			android:layout_height="match_parent">
+
+			<include layout="@layout/contact_avatar_status"/>
+
+			<TextView
+				android:id="@+id/contactName"
+				style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
+				android:layout_width="wrap_content"
+				android:layout_height="match_parent"
+				android:layout_marginLeft="@dimen/margin_medium"
+				android:layout_marginStart="@dimen/margin_medium"
+				android:gravity="center"
+				tools:text="Contact Name"/>
+
+		</LinearLayout>
+
+	</android.support.v7.widget.Toolbar>
 
 	<org.briarproject.android.util.BriarRecyclerView
 		android:id="@+id/conversationView"
 		android:layout_width="match_parent"
 		android:layout_height="0dp"
-		android:layout_weight="1"/>
+		android:layout_weight="1"
+		android:background="@color/conversation_background"/>
 
 	<View style="@style/Divider.Horizontal"/>
 
 	<LinearLayout
-		android:orientation="horizontal"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
 		android:background="@color/button_bar_background"
+		android:orientation="horizontal"
 		android:paddingLeft="@dimen/margin_medium"
 		android:paddingStart="@dimen/margin_medium">
 
 		<EditText
 			android:id="@+id/contentView"
 			android:layout_width="0dp"
-			android:layout_height="wrap_content"
-			android:hint="@string/private_message_hint"
+			android:layout_height="match_parent"
 			android:layout_weight="1"
+			android:hint="@string/private_message_hint"
 			android:inputType="text|textMultiLine|textCapSentences"/>
 
 		<ImageButton
 			android:id="@+id/sendButton"
 			android:layout_width="38dp"
 			android:layout_height="38dp"
-			android:layout_gravity="bottom"
-			android:src="@drawable/social_send_now"
-			android:background="?attr/selectableItemBackground"
-			android:scaleType="fitEnd"
+			android:layout_margin="@dimen/margin_small"
+			android:background="@drawable/round_button"
+			android:src="@drawable/social_send_now_white"
 			android:contentDescription="@string/send"
-			android:paddingRight="@dimen/margin_medium"
-			android:paddingEnd="@dimen/margin_medium"
-			android:paddingBottom="@dimen/margin_medium"/>
+		    android:elevation="@dimen/margin_tiny"
+			/>
+
 	</LinearLayout>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/activity_introduction.xml b/briar-android/res/layout/activity_introduction.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f351897d0f7b281b42f63847603b676d7f5bf7d2
--- /dev/null
+++ b/briar-android/res/layout/activity_introduction.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+	android:id="@+id/introductionContainer"
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/briar-android/res/layout/contact_avatar_status.xml b/briar-android/res/layout/contact_avatar_status.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2ceb5954726ca3654f09ba326b9ac8b1978fc0f9
--- /dev/null
+++ b/briar-android/res/layout/contact_avatar_status.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="32dp"
+	android:layout_height="32dp"
+	tools:showIn="@layout/activity_conversation">
+
+	<de.hdodenhof.circleimageview.CircleImageView
+		android:id="@+id/contactAvatar"
+		android:layout_width="30dp"
+		android:layout_height="30dp"
+		android:transitionName="avatar"
+		app:civ_border_color="@color/action_bar_text"
+		app:civ_border_width="@dimen/avatar_border_width"
+		tools:src="@drawable/ic_launcher"/>
+
+	<ImageView
+		android:id="@+id/contactStatus"
+		android:layout_width="15dp"
+		android:layout_height="15dp"
+		android:layout_gravity="bottom|right"
+		android:scaleType="fitCenter"
+		tools:src="@drawable/contact_online"
+		tools:ignore="ContentDescription"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/introduction_contact_chooser.xml b/briar-android/res/layout/introduction_contact_chooser.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8363191ac0db145c3086706f2f9408fbc479a1f6
--- /dev/null
+++ b/briar-android/res/layout/introduction_contact_chooser.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.briarproject.android.util.BriarRecyclerView
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:id="@+id/contactList"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	tools:listitem="@layout/list_item_contact"/>
diff --git a/briar-android/res/layout/introduction_message.xml b/briar-android/res/layout/introduction_message.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7faf8bf555526cf4a6180935c92788d9b1e6e09b
--- /dev/null
+++ b/briar-android/res/layout/introduction_message.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.NestedScrollView
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	android:fillViewport="true">
+
+	<LinearLayout
+		xmlns:android="http://schemas.android.com/apk/res/android"
+		xmlns:app="http://schemas.android.com/apk/res-auto"
+		xmlns:tools="http://schemas.android.com/tools"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		android:padding="@dimen/margin_activity_horizontal"
+		android:orientation="vertical">
+
+		<RelativeLayout
+			android:id="@+id/introductionHeader"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:padding="@dimen/margin_medium">
+
+			<de.hdodenhof.circleimageview.CircleImageView
+				android:id="@+id/avatarContact1"
+				android:layout_width="@dimen/listitem_picture_size"
+				android:layout_height="@dimen/listitem_picture_size"
+				android:layout_centerHorizontal="true"
+				android:layout_marginEnd="@dimen/listitem_horizontal_margin"
+				android:layout_marginRight="@dimen/listitem_horizontal_margin"
+				android:layout_toLeftOf="@+id/introductionIcon"
+				android:layout_toStartOf="@+id/introductionIcon"
+				app:civ_border_color="@color/briar_text_primary"
+				app:civ_border_width="@dimen/avatar_border_width"
+				tools:src="@drawable/ic_launcher"/>
+
+			<ImageView
+				android:id="@+id/introductionIcon"
+				android:layout_width="@dimen/listitem_picture_size"
+				android:layout_height="@dimen/listitem_picture_size"
+				android:layout_centerHorizontal="true"
+				android:src="@drawable/ic_contact_introduction"
+				tools:ignore="ContentDescription"/>
+
+			<de.hdodenhof.circleimageview.CircleImageView
+				android:id="@+id/avatarContact2"
+				android:layout_width="@dimen/listitem_picture_size"
+				android:layout_height="@dimen/listitem_picture_size"
+				android:layout_centerHorizontal="true"
+				android:layout_marginLeft="@dimen/listitem_horizontal_margin"
+				android:layout_marginStart="@dimen/listitem_horizontal_margin"
+				android:layout_toEndOf="@+id/introductionIcon"
+				android:layout_toRightOf="@+id/introductionIcon"
+				android:transitionName="avatar"
+				app:civ_border_color="@color/briar_text_primary"
+				app:civ_border_width="@dimen/avatar_border_width"
+				tools:src="@drawable/ic_launcher"/>
+
+		</RelativeLayout>
+
+		<ProgressBar
+			android:id="@+id/progressBar"
+			style="?android:attr/progressBarStyleLarge"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_gravity="center"
+			tools:visibility="gone"/>
+
+		<TextView
+			android:id="@+id/introductionText"
+			android:layout_width="match_parent"
+			android:layout_height="0dp"
+			android:layout_marginTop="@dimen/margin_medium"
+			android:layout_weight="1"
+			android:gravity="top"
+			android:textSize="@dimen/text_size_medium"
+			tools:text="@string/introduction_message_text"/>
+
+		<EditText
+			android:id="@+id/introductionMessageView"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:layout_marginTop="@dimen/margin_medium"
+			android:gravity="bottom"
+			android:hint="@string/introduction_message_hint"
+			android:inputType="text|textMultiLine|textCapSentences"/>
+
+		<Button
+			android:id="@+id/makeIntroductionButton"
+			style="@style/BriarButton"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:text="@string/introduction_button"
+			/>
+
+	</LinearLayout>
+
+</android.support.v4.widget.NestedScrollView>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_contact.xml b/briar-android/res/layout/list_item_contact.xml
index b566dc171388e1eee1ef863011dd40470211cff6..78f0681bdd2397f0326262a28ab718ea322031f4 100644
--- a/briar-android/res/layout/list_item_contact.xml
+++ b/briar-android/res/layout/list_item_contact.xml
@@ -1,16 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
 	xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:tools="http://schemas.android.com/tools"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
 	android:layout_width="match_parent"
 	android:layout_height="wrap_content"
 	android:orientation="vertical">
 
 	<RelativeLayout
 		android:layout_width="match_parent"
-		android:layout_height="@dimen/listitem_height_one_line_avatar"
-		android:background="?attr/selectableItemBackground">
+		android:layout_height="wrap_content"
+		android:paddingTop="@dimen/listitem_horizontal_margin"
+		android:paddingBottom="@dimen/listitem_horizontal_margin"
+		android:background="?attr/selectableItemBackground"
+		>
 
 		<de.hdodenhof.circleimageview.CircleImageView
 			android:id="@+id/avatarView"
@@ -21,56 +24,62 @@
 			android:layout_centerVertical="true"
 			android:layout_marginLeft="@dimen/listitem_horizontal_margin"
 			android:layout_marginStart="@dimen/listitem_horizontal_margin"
+			android:transitionName="avatar"
+			app:civ_border_color="@color/briar_text_primary"
 			app:civ_border_width="@dimen/avatar_border_width"
-			app:civ_border_color="@color/briar_text_primary"/>
+			tools:src="@drawable/ic_launcher"/>
 
 		<LinearLayout
-			android:id="@+id/bulbHolder"
+			android:id="@+id/textViews"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:layout_alignParentEnd="true"
-			android:layout_alignParentRight="true"
+			android:orientation="vertical"
 			android:layout_centerVertical="true"
-			android:layout_marginEnd="@dimen/listitem_horizontal_margin"
-			android:layout_marginRight="@dimen/listitem_horizontal_margin"
-			android:gravity="right"
-			android:orientation="vertical">
+			android:layout_marginLeft="@dimen/listitem_horizontal_margin"
+			android:layout_marginStart="@dimen/listitem_horizontal_margin"
+			android:layout_toLeftOf="@+id/bulbView"
+			android:layout_toRightOf="@+id/avatarView"
+			android:layout_toEndOf="@+id/avatarView">
 
-			<ImageView
-				android:id="@+id/bulbView"
+			<TextView
+				android:id="@+id/nameView"
 				android:layout_width="wrap_content"
 				android:layout_height="wrap_content"
-				tools:src="@drawable/contact_disconnected"/>
+				android:maxLines="2"
+				android:textColor="@android:color/primary_text_light"
+				android:textSize="@dimen/text_size_medium"
+				tools:text="This is a name of a contact"/>
 
 			<TextView
 				android:id="@+id/dateView"
 				android:layout_width="wrap_content"
 				android:layout_height="wrap_content"
-				android:textColor="@color/no_private_messages"
+				android:textColor="@android:color/secondary_text_light"
+				android:textSize="@dimen/text_size_small"
 				tools:text="Dec 24"/>
 
+			<TextView
+				android:id="@+id/identityView"
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:textColor="@android:color/tertiary_text_light"
+				android:textSize="@dimen/text_size_tiny"
+				tools:text="My Identity"/>
+
 		</LinearLayout>
 
-		<TextView
-			android:id="@+id/nameView"
+		<ImageView
+			android:id="@+id/bulbView"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:layout_alignParentLeft="true"
-			android:layout_alignParentStart="true"
+			android:layout_alignParentEnd="true"
+			android:layout_alignParentRight="true"
 			android:layout_centerVertical="true"
-			android:layout_marginEnd="@dimen/margin_small"
-			android:layout_marginLeft="@dimen/listitem_text_left_margin"
-			android:layout_marginRight="@dimen/margin_small"
-			android:layout_marginStart="@dimen/listitem_text_left_margin"
-			android:layout_toLeftOf="@id/bulbHolder"
-			android:layout_toStartOf="@id/bulbHolder"
-			android:gravity="center_vertical"
-			android:maxLines="2"
-			android:textSize="@dimen/text_size_medium"
-			tools:text="This is a name of a contact. It can be quite long."/>
+			android:layout_marginRight="@dimen/listitem_horizontal_margin"
+			tools:src="@drawable/contact_connected"/>
 
 	</RelativeLayout>
 
-	<View style="@style/Divider.Horizontal"/>
+	<View style="@style/Divider.ContactListDevider"/>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_introduction_in.xml b/briar-android/res/layout/list_item_introduction_in.xml
new file mode 100644
index 0000000000000000000000000000000000000000..08da32d7784cefedac3c3d9aa00eca01fb6e47ba
--- /dev/null
+++ b/briar-android/res/layout/list_item_introduction_in.xml
@@ -0,0 +1,67 @@
+<?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">
+
+	<include
+		android:id="@+id/messageLayout"
+		layout="@layout/list_item_msg_in"/>
+
+	<RelativeLayout
+		android:id="@+id/introductionLayout"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="left|start"
+		android:background="@drawable/notice_in"
+		android:layout_marginLeft="@dimen/message_bubble_margin_tail"
+		android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
+
+		<TextView
+			android:id="@+id/introductionText"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:minWidth="80dp"
+			android:textIsSelectable="true"
+			android:textSize="@dimen/text_size_medium"
+			android:textStyle="italic"
+			tools:text="@string/introduction_request_received"/>
+
+		<TextView
+			android:id="@+id/introductionTime"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
+			android:layout_alignEnd="@+id/acceptButton"
+			android:layout_alignRight="@+id/acceptButton"
+			android:layout_below="@+id/acceptButton"
+			android:textColor="@color/private_message_date"
+			android:textSize="@dimen/text_size_tiny"
+			tools:text="Dec 24, 13:37"/>
+
+		<Button
+			android:id="@+id/acceptButton"
+			style="@style/BriarButtonFlat.Positive"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_marginBottom="-15dp"
+			android:layout_alignEnd="@+id/introductionText"
+			android:layout_alignRight="@+id/introductionText"
+			android:layout_below="@+id/introductionText"
+			android:text="@string/dialog_button_accept"/>
+
+		<Button
+			android:id="@+id/declineButton"
+			style="@style/BriarButtonFlat.Negative"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_below="@+id/introductionText"
+			android:layout_toLeftOf="@+id/acceptButton"
+			android:layout_toStartOf="@+id/acceptButton"
+			android:text="@string/dialog_button_decline"/>
+
+	</RelativeLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_introduction_out.xml b/briar-android/res/layout/list_item_introduction_out.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d3e1a85aa633167d6e235d72199b87998524d0ce
--- /dev/null
+++ b/briar-android/res/layout/list_item_introduction_out.xml
@@ -0,0 +1,57 @@
+<?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">
+
+	<include
+		android:id="@+id/messageLayout"
+		layout="@layout/list_item_msg_out"/>
+
+	<RelativeLayout
+		android:id="@+id/introductionLayout"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="right|end"
+		android:background="@drawable/notice_out"
+		android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
+		android:layout_marginRight="@dimen/message_bubble_margin_tail">
+
+		<TextView
+			android:id="@+id/introductionText"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:minWidth="175dp"
+			android:textIsSelectable="true"
+			android:textSize="@dimen/text_size_medium"
+			android:textStyle="italic"
+			tools:text="@string/introduction_request_received"/>
+
+		<TextView
+			android:id="@+id/introductionTime"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
+			android:layout_alignParentLeft="true"
+			android:layout_alignParentStart="true"
+			android:layout_below="@+id/introductionText"
+			android:textColor="@color/private_message_date"
+			android:textSize="@dimen/text_size_tiny"
+			tools:text="Dec 24, 13:37"/>
+
+		<ImageView
+			android:id="@+id/introductionStatus"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_toEndOf="@+id/introductionTime"
+			android:layout_toRightOf="@+id/introductionTime"
+			android:layout_alignBottom="@+id/introductionTime"
+			android:layout_marginLeft="@dimen/margin_medium"
+			tools:ignore="ContentDescription"
+			tools:src="@drawable/message_delivered"/>
+
+	</RelativeLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_msg_in.xml b/briar-android/res/layout/list_item_msg_in.xml
index fa1f793ab7cea5e936a14e89100be769182ff855..13ec9b28703386ba75908e1bfe1c03357941f12e 100644
--- a/briar-android/res/layout/list_item_msg_in.xml
+++ b/briar-android/res/layout/list_item_msg_in.xml
@@ -1,53 +1,51 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout
 	xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:tools="http://schemas.android.com/tools"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
 	android:layout_width="match_parent"
 	android:layout_height="wrap_content"
-	android:orientation="horizontal"
-	android:paddingRight="@dimen/margin_medium"
-	android:paddingEnd="@dimen/margin_medium"
-	android:paddingTop="@dimen/margin_small"
-	android:paddingBottom="@dimen/margin_small">
+	android:orientation="horizontal">
 
 	<de.hdodenhof.circleimageview.CircleImageView
 		android:id="@+id/msgAvatar"
 		android:layout_width="@dimen/listitem_picture_size"
 		android:layout_height="@dimen/listitem_picture_size"
-		android:layout_marginLeft="@dimen/listitem_horizontal_margin"
-		android:layout_marginStart="@dimen/listitem_horizontal_margin"
+		android:layout_marginLeft="@dimen/margin_medium"
+		android:layout_marginStart="@dimen/margin_medium"
+		android:visibility="gone"
+		app:civ_border_color="@color/briar_text_primary"
 		app:civ_border_width="@dimen/avatar_border_width"
-		app:civ_border_color="@color/briar_text_primary"/>
+		tools:src="@drawable/ic_launcher"/>
 
-	<RelativeLayout
+	<LinearLayout
 		android:id="@+id/msgLayout"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
-		android:layout_gravity="left|start"
 		android:background="@drawable/msg_in"
-		android:paddingLeft="17dp"
-		android:paddingTop="5dp"
-		android:paddingRight="7dp"
-		android:paddingBottom="5dp">
+		android:orientation="vertical"
+		android:layout_marginLeft="@dimen/message_bubble_margin_tail"
+		android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
 
 		<TextView
 			android:id="@+id/msgBody"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:minWidth="80dp"
 			android:textIsSelectable="true"
+			android:textSize="@dimen/text_size_medium"
 			tools:text="Short message"/>
 
 		<TextView
 			android:id="@+id/msgTime"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:textSize="10sp"
+			android:layout_gravity="right|end"
+			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
+			android:maxLines="1"
 			android:textColor="@color/private_message_date"
-			android:layout_below="@+id/msgBody"
+			android:textSize="@dimen/text_size_tiny"
 			tools:text="Dec 24, 13:37"/>
 
-	</RelativeLayout>
+	</LinearLayout>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_msg_out.xml b/briar-android/res/layout/list_item_msg_out.xml
index a82a07f1e058be89c27bbaa346025775ac888005..5902b7381e122a3791fa3e9e8843920020f20ce9 100644
--- a/briar-android/res/layout/list_item_msg_out.xml
+++ b/briar-android/res/layout/list_item_msg_out.xml
@@ -4,11 +4,7 @@
 	xmlns:tools="http://schemas.android.com/tools"
 	android:layout_width="match_parent"
 	android:layout_height="wrap_content"
-	android:orientation="vertical"
-	android:paddingLeft="@dimen/margin_medium"
-	android:paddingStart="@dimen/margin_medium"
-	android:paddingTop="@dimen/margin_small"
-	android:paddingBottom="@dimen/margin_small">
+	android:orientation="vertical">
 
 	<RelativeLayout
 		android:id="@+id/msgLayout"
@@ -16,28 +12,29 @@
 		android:layout_height="wrap_content"
 		android:layout_gravity="right|end"
 		android:background="@drawable/msg_out"
-		android:paddingLeft="7dp"
-		android:paddingTop="5dp"
-		android:paddingRight="17dp"
-		android:paddingBottom="5dp">
+		android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
+		android:layout_marginRight="@dimen/message_bubble_margin_tail">
 
 		<TextView
 			android:id="@+id/msgBody"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
+			android:textColor="@color/briar_text_primary_inverse"
 			android:textIsSelectable="true"
-			android:minWidth="80dp"
+			android:textSize="@dimen/text_size_medium"
 			tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
 
 		<TextView
 			android:id="@+id/msgTime"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
+			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
+			android:layout_alignParentLeft="true"
+			android:layout_alignParentStart="true"
 			android:layout_below="@+id/msgBody"
-			android:layout_toLeftOf="@+id/msgStatus"
-			android:textSize="10sp"
-			android:textColor="@color/private_message_date"
 			android:singleLine="true"
+			android:textColor="@color/private_message_date_inverse"
+			android:textSize="@dimen/text_size_tiny"
 			tools:text="Dec 24, 13:37"/>
 
 		<ImageView
@@ -45,10 +42,11 @@
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
 			android:layout_alignBottom="@+id/msgTime"
-			android:layout_alignRight="@+id/msgBody"
-			android:layout_alignEnd="@+id/msgBody"
-			android:layout_marginLeft="3dp"
-			tools:src="@drawable/message_delivered"/>
+			android:layout_marginLeft="@dimen/margin_medium"
+			android:layout_toEndOf="@+id/msgTime"
+			android:layout_toRightOf="@+id/msgTime"
+			tools:ignore="ContentDescription"
+			tools:src="@drawable/message_delivered_white"/>
 
 	</RelativeLayout>
 
diff --git a/briar-android/res/layout/list_item_notice_in.xml b/briar-android/res/layout/list_item_notice_in.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e7c912f9151ee2b45ef0520d8ca51cd415744e08
--- /dev/null
+++ b/briar-android/res/layout/list_item_notice_in.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+	android:id="@+id/noticeLayout"
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="wrap_content"
+	android:layout_height="wrap_content"
+	android:background="@drawable/notice_in"
+	android:orientation="vertical"
+	android:layout_marginLeft="@dimen/message_bubble_margin_tail"
+	android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
+
+	<TextView
+		android:id="@+id/noticeText"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:minWidth="80dp"
+		android:textIsSelectable="true"
+		android:textSize="@dimen/text_size_medium"
+		android:textStyle="italic"
+		tools:text="@string/introduction_response_accepted_received"/>
+
+	<TextView
+		android:id="@+id/noticeTime"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="right|end"
+		android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
+		android:maxLines="1"
+		android:textColor="@color/private_message_date"
+		android:textSize="@dimen/text_size_tiny"
+		tools:text="Dec 24, 13:37"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/list_item_notice_out.xml b/briar-android/res/layout/list_item_notice_out.xml
new file mode 100644
index 0000000000000000000000000000000000000000..499e1506fd282fc39b5dcaba798c0d9149c8c644
--- /dev/null
+++ b/briar-android/res/layout/list_item_notice_out.xml
@@ -0,0 +1,52 @@
+<?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
+		android:id="@+id/noticeLayout"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="right|end"
+		android:background="@drawable/notice_out"
+		android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
+		android:layout_marginRight="@dimen/message_bubble_margin_tail">
+
+		<TextView
+			android:id="@+id/noticeText"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:textIsSelectable="true"
+			android:textSize="@dimen/text_size_medium"
+			android:textStyle="italic"
+			tools:text="@string/introduction_response_accepted_sent"/>
+
+		<TextView
+			android:id="@+id/noticeTime"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
+			android:layout_alignParentLeft="true"
+			android:layout_alignParentStart="true"
+			android:layout_below="@+id/noticeText"
+			android:textColor="@color/private_message_date"
+			android:textSize="@dimen/text_size_tiny"
+			tools:text="Dec 24, 13:37"/>
+
+		<ImageView
+			android:id="@+id/noticeStatus"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignBottom="@+id/noticeTime"
+			android:layout_marginLeft="@dimen/margin_medium"
+			android:layout_toEndOf="@+id/noticeTime"
+			android:layout_toRightOf="@+id/noticeTime"
+			tools:ignore="ContentDescription"
+			tools:src="@drawable/message_delivered"/>
+
+	</RelativeLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/menu/contact_actions.xml b/briar-android/res/menu/conversation_actions.xml
similarity index 53%
rename from briar-android/res/menu/contact_actions.xml
rename to briar-android/res/menu/conversation_actions.xml
index 3cce37ad83fcdac5083f7620d935e4184ce66038..01090aaec6156de99a48133f0bac428ed52f1e9a 100644
--- a/briar-android/res/menu/contact_actions.xml
+++ b/briar-android/res/menu/conversation_actions.xml
@@ -3,10 +3,16 @@
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto">
 
+	<item
+		android:id="@+id/action_introduction"
+		android:icon="@drawable/introduction_white"
+		android:title="@string/make_introduction"
+		app:showAsAction="never"/>
+
 	<item
 		android:id="@+id/action_social_remove_person"
 		android:icon="@drawable/social_remove_person"
-		app:showAsAction="always"
-		android:title="@string/delete_contact"/>
+		android:title="@string/delete_contact"
+		app:showAsAction="never"/>
 
 </menu>
\ No newline at end of file
diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml
index bcb8d2b0ae8896df2e09721f3a8f0dad98fea9d7..0351b52966f0e49dfea5076cc3c39fa60a37c108 100644
--- a/briar-android/res/values/color.xml
+++ b/briar-android/res/values/color.xml
@@ -8,11 +8,13 @@
 	<color name="briar_red">#C1392B</color>
 
 	<color name="window_background">#EEEEEE</color>
+	<color name="conversation_background">#efebe9</color>
 	<color name="action_bar_text">#FFFFFF</color>
 	<color name="action_bar_background">@color/briar_blue</color>
 	<color name="button_bar_background">#FFFFFF</color>
 	<color name="dashboard_background">#FFFFFF</color>
 	<color name="private_message_date">#AAAAAA</color>
+	<color name="private_message_date_inverse">#e0e0e0</color>
 	<color name="unread_background">#FFFFFF</color>
 	<color name="horizontal_border">#CCCCCC</color>
 	<color name="forums_available_background">@color/briar_gold</color>
@@ -28,6 +30,8 @@
 	<color name="briar_text_link">@color/briar_green_dark</color>
 	<color name="briar_text_primary">@color/briar_primary</color>
 	<color name="briar_text_primary_inverse">#ffffff</color>
+	<color name="briar_text_secondary">#333333</color>
+	<color name="briar_text_tertiary">#333333</color>
 
 	<!-- this is needed as preference_category_material layout uses this color as the text color -->
 	<color name="preference_fallback_accent_color">@color/briar_accent</color>
diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml
index 41af17d52a0e4d0a1bc44e1c5ff5adbe7d955904..b1985532c12ca9fcfb5e2018686ec5a1e1dcdc65 100644
--- a/briar-android/res/values/dimens.xml
+++ b/briar-android/res/values/dimens.xml
@@ -23,8 +23,12 @@
 	<dimen name="listitem_horizontal_margin">16dp</dimen>
 	<dimen name="listitem_text_left_margin">72dp</dimen>
 	<dimen name="listitem_height_one_line_avatar">56dp</dimen>
-	<dimen name="listitem_picture_size">40dp</dimen>
+	<dimen name="listitem_picture_size">48dp</dimen>
 	<dimen name="dropdown_picture_size">32dp</dimen>
 	<dimen name="avatar_border_width">1dp</dimen>
 
+	<dimen name="message_bubble_margin_tail">14dp</dimen>
+	<dimen name="message_bubble_margin_non_tail">51dp</dimen>
+	<dimen name="message_bubble_timestamp_margin">15dp</dimen>
+
 </resources>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index f75d8a90d2a549e18c3bbd8fc1bd0938867c8f70..c37fa7ea6b8fbedab751f4180dccd69b6a902fea 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -141,6 +141,26 @@
 	<string name="transport_lan">Wi-Fi</string>
 	<string name="no_data">No data</string>
 	<string name="unknown_app">an unknown app</string>
+	<string name="make_introduction">Make Introduction</string>
+	<string name="introduction_activity_title">Select contact</string>
+	<string name="introduction_message_title">Introduce Contacts</string>
+	<string name="introduction_message_text">You can compose a message that will be sent to %1$s and %2$s along with your introduction:</string>
+	<string name="introduction_message_hint">Type message (optional)</string>
+	<string name="introduction_button">Make Introduction</string>
+	<string name="introduction_error">There was an error making the introduction.</string>
+	<string name="introduction_response_error">Error when responding to introduction</string>
+	<string name="introduction_warn_different_identities_title">Warning: Different Identities</string>
+	<string name="introduction_warn_different_identities_text">You are trying to introduce two contacts that you have added with different identities. This might reveal that both identities are yours.</string>
+	<string name="introduction_request_sent">You have introduced %1$s to %2$s.</string>
+	<string name="introduction_request_received">%1$s introduced you to %2$s. Do you want to add %2$s to your contact list?</string>
+	<string name="introduction_request_exists_received">%1$s introduced you to %2$s, but %2$s is already in your contact list. Since %1$s might not know that, you can still respond:</string>
+	<string name="introduction_request_answered_received">%1$s introduced you to %2$s.</string>
+	<string name="introduction_response_accepted_sent">You accepted the introduction to %1$s.</string>
+	<string name="introduction_response_declined_sent">You declined the introduction to %1$s.</string>
+	<string name="introduction_response_accepted_received">%1$s accepted to be introduced to %2$s.</string>
+	<string name="introduction_response_declined_received">%1$s declined to be introduced to %2$s.</string>
+	<string name="introduction_success_title">Introduced contact was added</string>
+	<string name="introduction_success_text">You have been successfully introduced to %1$s who was now added to your contact list.</string>
 
 	<!-- Dialogs -->
 	<string name="dialog_title_lost_password">Lost Password</string>
@@ -152,6 +172,9 @@
 	<string name="dialog_title_welcome">Welcome to Briar</string>
 	<string name="dialog_welcome_message">Add a contact to start communicating securely or press the icon in the upper left corner of the screen for more options.</string>
 	<string name="dialog_button_ok">OK</string>
+	<string name="dialog_button_introduce">Introduce</string>
+	<string name="dialog_button_accept">Accept</string>
+	<string name="dialog_button_decline">Decline</string>
 	<!-- Toolbar headers -->
 	<string name="dashboard_toolbar_header">Briar</string>
 	<string name="settings_toolbar_header">Settings</string>
diff --git a/briar-android/res/values/styles.xml b/briar-android/res/values/styles.xml
index 4c80c10edea097d5ccb541a2a71d07e278b296d2..9b4ebbf6cae735fdd84b5c244fced4020f4ff8cc 100644
--- a/briar-android/res/values/styles.xml
+++ b/briar-android/res/values/styles.xml
@@ -51,13 +51,31 @@
 		<item name="elevation">1dp</item>
 	</style>
 
-	<style name="BriarButton">
+	<style name="BriarDialogTheme" parent="Theme.AppCompat.Light.Dialog">
+		<item name="colorPrimary">@color/briar_primary</item>
+		<item name="colorPrimaryDark">@color/briar_primary_dark</item>
+		<item name="colorAccent">@color/briar_accent</item>
+	</style>
+
+	<style name="BriarButton" parent="Widget.AppCompat.Button.Colored">
 		<item name="android:textSize">@dimen/text_size_medium</item>
 		<item name="android:padding">@dimen/margin_large</item>
 	</style>
 
 	<style name="BriarButton.Default"/>
 
+	<style name="BriarButtonFlat.Negative" parent="Widget.AppCompat.Button.Borderless">
+		<item name="android:textColor">#ff0000</item>
+		<item name="android:textSize">@dimen/text_size_medium</item>
+		<item name="android:padding">@dimen/margin_large</item>
+	</style>
+
+	<style name="BriarButtonFlat.Positive" parent="Widget.AppCompat.Button.Borderless">
+		<item name="android:textColor">#06b9ff</item>
+		<item name="android:textSize">@dimen/text_size_medium</item>
+		<item name="android:padding">@dimen/margin_large</item>
+	</style>
+
 	<style name="BriarTextTitle">
 		<item name="android:textSize">@dimen/text_size_medium</item>
 		<item name="android:textColor">@android:color/primary_text_light</item>
@@ -76,11 +94,17 @@
 		<item name="android:background">?android:attr/listDivider</item>
 	</style>
 
-	<style name="Divider.Horizontal">
+	<style name="Divider.Horizontal" parent="Divider">
 		<item name="android:layout_width">match_parent</item>
 		<item name="android:layout_height">1px</item>
 	</style>
 
+	<style name="Divider.ContactListDevider" parent="Divider">
+		<item name="android:layout_width">match_parent</item>
+		<item name="android:layout_height">2dp</item>
+		<item name="android:layout_marginLeft">@dimen/margin_large</item>
+	</style>
+
 	<style name="NavMenuButton" parent="Widget.AppCompat.Button.Borderless.Colored">
 		<item name="android:textSize">@dimen/text_size_medium</item>
 		<item name="android:textColor">@android:color/tertiary_text_light</item>
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index 4b3caa9967b667c6ea56f04058dfc86c9550c4f8..5b73d5e53bb1e57ab287a93ecebe04a9a5e3a414 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -12,6 +12,9 @@ import org.briarproject.android.forum.ReadForumPostActivity;
 import org.briarproject.android.forum.ShareForumActivity;
 import org.briarproject.android.forum.WriteForumPostActivity;
 import org.briarproject.android.identity.CreateIdentityActivity;
+import org.briarproject.android.introduction.ContactChooserFragment;
+import org.briarproject.android.introduction.IntroductionActivity;
+import org.briarproject.android.introduction.IntroductionMessageFragment;
 import org.briarproject.android.invitation.AddContactActivity;
 import org.briarproject.android.keyagreement.ChooseIdentityFragment;
 import org.briarproject.android.keyagreement.KeyAgreementActivity;
@@ -80,6 +83,12 @@ public interface AndroidComponent extends CoreEagerSingletons {
 
 	void inject(ShowQrCodeFragment fragment);
 
+	void inject(IntroductionActivity activity);
+
+	void inject(ContactChooserFragment fragment);
+
+	void inject(IntroductionMessageFragment fragment);
+
 	// Eager singleton load
 	void inject(AppModule.EagerSingletons init);
 }
diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
index 3cd074aea55c923ec0588a9772376e5640aa9e5f..2330aac866a4388a63e3c7d78327d846e0dfb564 100644
--- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
+++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
@@ -14,10 +14,14 @@ import org.briarproject.android.api.AndroidExecutor;
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.contact.ConversationActivity;
 import org.briarproject.android.forum.ForumActivity;
+import org.briarproject.api.contact.Contact;
 import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.IntroductionRequestReceivedEvent;
+import org.briarproject.api.event.IntroductionResponseReceivedEvent;
+import org.briarproject.api.event.IntroductionSucceededEvent;
 import org.briarproject.api.event.MessageValidatedEvent;
 import org.briarproject.api.event.SettingsUpdatedEvent;
 import org.briarproject.api.forum.ForumManager;
@@ -57,6 +61,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 
 	private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3;
 	private static final int FORUM_POST_NOTIFICATION_ID = 4;
+	private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 5;
 	private static final String CONTACT_URI =
 			"content://org.briarproject/contact";
 	private static final String FORUM_URI =
@@ -111,6 +116,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 			public Void call() {
 				clearPrivateMessageNotification();
 				clearForumPostNotification();
+				clearIntroductionSuccessNotification();
 				return null;
 			}
 		});
@@ -135,6 +141,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 		nm.cancel(FORUM_POST_NOTIFICATION_ID);
 	}
 
+	private void clearIntroductionSuccessNotification() {
+		Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
+		NotificationManager nm = (NotificationManager) o;
+		nm.cancel(INTRODUCTION_SUCCESS_NOTIFICATION_ID);
+	}
+
 	public void eventOccurred(Event e) {
 		if (e instanceof SettingsUpdatedEvent) {
 			SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
@@ -148,6 +160,25 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 				else if (c.equals(forumManager.getClientId()))
 					showForumPostNotification(m.getMessage().getGroupId());
 			}
+		} else if (e instanceof IntroductionRequestReceivedEvent) {
+			try {
+				GroupId group = messagingManager.getConversationId(
+						((IntroductionRequestReceivedEvent) e).getContactId());
+				showPrivateMessageNotification(group);
+			} catch (DbException ex) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, ex.toString(), ex);
+			}
+		} else if (e instanceof IntroductionResponseReceivedEvent) {
+			try {
+				GroupId group = messagingManager.getConversationId(
+						((IntroductionResponseReceivedEvent) e).getContactId());
+				showPrivateMessageNotification(group);
+			} catch (DbException ex) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, ex.toString(), ex);
+			}
+		} else if (e instanceof IntroductionSucceededEvent) {
+			Contact c = ((IntroductionSucceededEvent) e).getContact();
+			showIntroductionSucceededNotification(c);
 		}
 	}
 
@@ -335,4 +366,35 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 			}
 		});
 	}
+
+	private void showIntroductionSucceededNotification(final Contact c) {
+		androidExecutor.execute(new Runnable() {
+			public void run() {
+				NotificationCompat.Builder b =
+						new NotificationCompat.Builder(appContext);
+				b.setSmallIcon(R.drawable.introduction_notification);
+
+				b.setContentTitle(appContext
+						.getString(R.string.introduction_success_title));
+				b.setContentText(appContext
+						.getString(R.string.introduction_success_text,
+								c.getAuthor().getName()));
+				b.setDefaults(getDefaults());
+				b.setAutoCancel(true);
+
+				Intent i = new Intent(appContext, NavDrawerActivity.class);
+				i.putExtra(NavDrawerActivity.INTENT_CONTACTS, true);
+				i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
+				TaskStackBuilder t = TaskStackBuilder.create(appContext);
+				t.addParentStack(NavDrawerActivity.class);
+				t.addNextIntent(i);
+				b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
+
+				Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
+				NotificationManager nm = (NotificationManager) o;
+				nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build());
+			}
+		});
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/BaseActivity.java b/briar-android/src/org/briarproject/android/BaseActivity.java
index 8e92c0b56f44ed5fa793e710303f44e523b62544..1a93a006ec6b200e2522e8431c4168eefc1efdbc 100644
--- a/briar-android/src/org/briarproject/android/BaseActivity.java
+++ b/briar-android/src/org/briarproject/android/BaseActivity.java
@@ -54,7 +54,7 @@ public abstract class BaseActivity extends AppCompatActivity {
 		((InputMethodManager) o).showSoftInput(view, SHOW_IMPLICIT);
 	}
 
-	protected void hideSoftKeyboard(View view) {
+	public void hideSoftKeyboard(View view) {
 		IBinder token = view.getWindowToken();
 		Object o = getSystemService(INPUT_METHOD_SERVICE);
 		((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java
index 38c64e839cdb90e56c11d1c0b936846256de1597..becdaa9220d13699c91b10f66f6f4f3fe9c3366a 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java
@@ -1,8 +1,12 @@
 package org.briarproject.android.contact;
 
 import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.os.Build;
+import android.support.v4.content.ContextCompat;
 import android.support.v7.util.SortedList;
 import android.support.v7.widget.RecyclerView;
 import android.text.format.DateUtils;
@@ -14,9 +18,8 @@ import android.widget.TextView;
 
 import org.briarproject.R;
 import org.briarproject.api.contact.ContactId;
-import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.identity.Author;
-import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.identity.AuthorId;
 
 import java.util.List;
 
@@ -53,13 +56,23 @@ public class ContactListAdapter
 						@Override
 						public int compare(ContactListItem c1,
 								ContactListItem c2) {
-							// sort items by time
-							// and do not take unread messages into account
-							long time1 = c1.getTimestamp();
-							long time2 = c2.getTimestamp();
-							if (time1 < time2) return 1;
-							if (time1 > time2) return -1;
-							return 0;
+							int authorCompare = 0;
+							if (chooser) {
+								authorCompare = c1.getLocalAuthor().getName()
+										.compareTo(
+												c2.getLocalAuthor().getName());
+							}
+							if (authorCompare == 0) {
+								// sort items by time
+								// and do not take unread messages into account
+								long time1 = c1.getTimestamp();
+								long time2 = c2.getTimestamp();
+								if (time1 < time2) return 1;
+								if (time1 > time2) return -1;
+								return 0;
+							} else {
+								return authorCompare;
+							}
 						}
 
 						@Override
@@ -86,10 +99,16 @@ public class ContactListAdapter
 							return true;
 						}
 					});
+	private final OnItemClickListener listener;
+	private final boolean chooser;
 	private Context ctx;
+	private AuthorId localAuthorId;
 
-	public ContactListAdapter(Context context) {
+	public ContactListAdapter(Context context, OnItemClickListener listener,
+			boolean chooser) {
 		ctx = context;
+		this.listener = listener;
+		this.chooser = chooser;
 	}
 
 	@Override
@@ -103,12 +122,11 @@ public class ContactListAdapter
 	@Override
 	public void onBindViewHolder(final ContactHolder ui, final int position) {
 		final ContactListItem item = getItem(position);
-		Resources res = ctx.getResources();
 
 		int unread = item.getUnreadCount();
-		if (unread > 0) {
+		if (!chooser && unread > 0) {
 			ui.layout.setBackgroundColor(
-					res.getColor(R.color.unread_background));
+					ContextCompat.getColor(ctx, R.color.unread_background));
 		}
 
 		if (item.isConnected()) {
@@ -121,27 +139,37 @@ public class ContactListAdapter
 		ui.avatar.setImageDrawable(
 				new IdenticonDrawable(author.getId().getBytes()));
 		String contactName = author.getName();
-		if (unread > 0) {
+
+		if (!chooser && unread > 0) {
+			// TODO show these in a bubble on top of the avatar
 			ui.name.setText(contactName + " (" + unread + ")");
 		} else {
 			ui.name.setText(contactName);
 		}
 
+		if (chooser) {
+			ui.identity.setText(item.getLocalAuthor().getName());
+		} else {
+			ui.identity.setVisibility(View.GONE);
+		}
+
 		if (item.isEmpty()) {
 			ui.date.setText(R.string.no_private_messages);
 		} else {
+			// TODO show this as X units ago
 			long timestamp = item.getTimestamp();
 			ui.date.setText(
 					DateUtils.getRelativeTimeSpanString(ctx, timestamp));
 		}
 
+		if (chooser && !item.getLocalAuthor().getId().equals(localAuthorId)) {
+			grayOutItem(ui);
+		}
+
 		ui.layout.setOnClickListener(new View.OnClickListener() {
 			@Override
 			public void onClick(View v) {
-				GroupId groupId = item.getGroupId();
-				Intent i = new Intent(ctx, ConversationActivity.class);
-				i.putExtra("briar.GROUP_ID", groupId.getBytes());
-				ctx.startActivity(i);
+				listener.onItemClick(ui.avatar, item);
 			}
 		});
 	}
@@ -151,6 +179,34 @@ public class ContactListAdapter
 		return contacts.size();
 	}
 
+	/**
+	 * Set the identity from whose perspective the contact shall be chosen.
+	 * This is only used if chooser is true.
+	 * @param authorId The ID of the local Author
+	 */
+	public void setLocalAuthor(AuthorId authorId) {
+		localAuthorId = authorId;
+		notifyDataSetChanged();
+	}
+
+	private void grayOutItem(final ContactHolder ui) {
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+			float alpha = 0.25f;
+			ui.bulb.setAlpha(alpha);
+			ui.avatar.setAlpha(alpha);
+			ui.name.setAlpha(alpha);
+			ui.date.setAlpha(alpha);
+			ui.identity.setAlpha(alpha);
+		} else {
+			ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY,
+					PorterDuff.Mode.MULTIPLY);
+			ui.bulb.setColorFilter(colorFilter);
+			ui.avatar.setColorFilter(colorFilter);
+			ui.name.setEnabled(false);
+			ui.date.setEnabled(false);
+		}
+	}
+
 	public ContactListItem getItem(int position) {
 		if (position == INVALID_POSITION || contacts.size() <= position) {
 			return null; // Not found
@@ -162,10 +218,6 @@ public class ContactListAdapter
 		contacts.updateItemAt(position, item);
 	}
 
-	public int findItemPosition(ContactListItem item) {
-		return contacts.indexOf(item);
-	}
-
 	public int findItemPosition(ContactId c) {
 		int count = getItemCount();
 		for (int i = 0; i < count; i++) {
@@ -202,6 +254,7 @@ public class ContactListAdapter
 		public ImageView bulb;
 		public ImageView avatar;
 		public TextView name;
+		public TextView identity;
 		public TextView date;
 
 		public ContactHolder(View v) {
@@ -211,7 +264,13 @@ public class ContactListAdapter
 			bulb = (ImageView) v.findViewById(R.id.bulbView);
 			avatar = (ImageView) v.findViewById(R.id.avatarView);
 			name = (TextView) v.findViewById(R.id.nameView);
+			identity = (TextView) v.findViewById(R.id.identityView);
 			date = (TextView) v.findViewById(R.id.dateView);
 		}
 	}
+
+	public interface OnItemClickListener {
+		void onItemClick(View view, ContactListItem item);
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
index 0d7c5d32356547ca4c661554b99c46866979422b..3cba02fadbe734e9f9ed9c98a9baeeff2ab06c50 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
@@ -1,9 +1,12 @@
 package org.briarproject.android.contact;
 
 import android.content.Intent;
+import android.os.Build;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
 import android.support.design.widget.FloatingActionButton;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.LinearLayoutManager;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -11,23 +14,26 @@ import android.view.ViewGroup;
 
 import org.briarproject.R;
 import org.briarproject.android.AndroidComponent;
-import org.briarproject.android.BriarApplication;
 import org.briarproject.android.fragment.BaseEventFragment;
 import org.briarproject.android.keyagreement.KeyAgreementActivity;
 import org.briarproject.android.util.BriarRecyclerView;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
-import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.NoSuchContactException;
 import org.briarproject.api.event.ContactAddedEvent;
 import org.briarproject.api.event.ContactConnectedEvent;
 import org.briarproject.api.event.ContactDisconnectedEvent;
 import org.briarproject.api.event.ContactRemovedEvent;
+import org.briarproject.api.event.ContactStatusChangedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.introduction.IntroductionManager;
+import org.briarproject.api.introduction.IntroductionMessage;
 import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessageHeader;
 import org.briarproject.api.plugins.ConnectionRegistry;
@@ -74,8 +80,12 @@ public class ContactListFragment extends BaseEventFragment {
 	@Inject
 	protected volatile ContactManager contactManager;
 	@Inject
+	protected volatile IdentityManager identityManager;
+	@Inject
 	protected volatile MessagingManager messagingManager;
 	@Inject
+	protected volatile IntroductionManager introductionManager;
+	@Inject
 	protected volatile EventBus eventBus;
 
 	@Override
@@ -91,7 +101,31 @@ public class ContactListFragment extends BaseEventFragment {
 				inflater.inflate(R.layout.activity_contact_list, container,
 						false);
 
-		adapter = new ContactListAdapter(getContext());
+		ContactListAdapter.OnItemClickListener onItemClickListener =
+				new ContactListAdapter.OnItemClickListener() {
+					@Override
+					public void onItemClick(View view, ContactListItem item) {
+
+						GroupId groupId = item.getGroupId();
+						Intent i = new Intent(getActivity(),
+								ConversationActivity.class);
+						i.putExtra("briar.GROUP_ID", groupId.getBytes());
+
+						if (Build.VERSION.SDK_INT >= 16) {
+							ActivityOptionsCompat options =
+									ActivityOptionsCompat.
+											makeSceneTransitionAnimation(
+													getActivity(),
+													view, "avatar");
+							getActivity().startActivity(i, options.toBundle());
+						} else {
+							startActivity(i);
+						}
+					}
+				};
+
+		adapter = new ContactListAdapter(getContext(), onItemClickListener,
+				false);
 		list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
 		list.setLayoutManager(new LinearLayoutManager(getContext()));
 		list.setAdapter(adapter);
@@ -135,12 +169,14 @@ public class ContactListFragment extends BaseEventFragment {
 							ContactId id = c.getId();
 							GroupId groupId =
 									messagingManager.getConversationId(id);
-							Collection<PrivateMessageHeader> headers =
-									messagingManager.getMessageHeaders(id);
+							Collection<ConversationItem> messages =
+									getMessages(id);
 							boolean connected =
 									connectionRegistry.isConnected(c.getId());
-							contacts.add(new ContactListItem(c, connected,
-									groupId, headers));
+							LocalAuthor localAuthor = identityManager
+									.getLocalAuthor(c.getLocalAuthorId());
+							contacts.add(new ContactListItem(c, localAuthor,
+									connected, groupId, messages));
 						} catch (NoSuchContactException e) {
 							// Continue
 						}
@@ -169,7 +205,12 @@ public class ContactListFragment extends BaseEventFragment {
 
 	public void eventOccurred(Event e) {
 		if (e instanceof ContactAddedEvent) {
-			LOG.info("Contact added, reloading");
+			if(((ContactAddedEvent) e).isActive()) {
+				LOG.info("Contact added as active, reloading");
+				loadContacts();
+			}
+		} else if (e instanceof ContactStatusChangedEvent) {
+			LOG.info("Contact Status changed, reloading");
 			loadContacts();
 		} else if (e instanceof ContactConnectedEvent) {
 			setConnected(((ContactConnectedEvent) e).getContactId(), true);
@@ -181,7 +222,8 @@ public class ContactListFragment extends BaseEventFragment {
 		} else if (e instanceof MessageValidatedEvent) {
 			MessageValidatedEvent m = (MessageValidatedEvent) e;
 			ClientId c = m.getClientId();
-			if (m.isValid() && c.equals(messagingManager.getClientId())) {
+			if (m.isValid() && (c.equals(messagingManager.getClientId()) ||
+					c.equals(introductionManager.getClientId()))) {
 				LOG.info("Message added, reloading");
 				reloadConversation(m.getMessage().getGroupId());
 			}
@@ -192,14 +234,10 @@ public class ContactListFragment extends BaseEventFragment {
 		listener.runOnDbThread(new Runnable() {
 			public void run() {
 				try {
-					long now = System.currentTimeMillis();
 					ContactId c = messagingManager.getContactId(g);
-					Collection<PrivateMessageHeader> headers =
-							messagingManager.getMessageHeaders(c);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Partial load took " + duration + " ms");
-					updateItem(c, headers);
+					Collection<ConversationItem> messages =
+							getMessages(c);
+					updateItem(c, messages);
 				} catch (NoSuchContactException e) {
 					LOG.info("Contact removed");
 				} catch (DbException e) {
@@ -211,13 +249,13 @@ public class ContactListFragment extends BaseEventFragment {
 	}
 
 	private void updateItem(final ContactId c,
-			final Collection<PrivateMessageHeader> headers) {
+			final Collection<ConversationItem> messages) {
 		listener.runOnUiThread(new Runnable() {
 			public void run() {
 				int position = adapter.findItemPosition(c);
 				ContactListItem item = adapter.getItem(position);
 				if (item != null) {
-					item.setHeaders(headers);
+					item.setMessages(messages);
 					adapter.updateItem(position, item);
 				}
 			}
@@ -246,4 +284,35 @@ public class ContactListFragment extends BaseEventFragment {
 			}
 		});
 	}
+
+	/** This needs to be called from the DbThread */
+	private Collection<ConversationItem> getMessages(ContactId id)
+			throws DbException {
+
+		long now = System.currentTimeMillis();
+
+		Collection<ConversationItem> messages =
+				new ArrayList<ConversationItem>();
+
+		Collection<PrivateMessageHeader> headers =
+				messagingManager.getMessageHeaders(id);
+		for (PrivateMessageHeader h : headers) {
+			messages.add(new ConversationMessageItem(h));
+		}
+		long duration = System.currentTimeMillis() - now;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Loading message headers took " + duration + " ms");
+
+		Collection<IntroductionMessage> introductions =
+				introductionManager
+						.getIntroductionMessages(id);
+		for (IntroductionMessage m : introductions) {
+			messages.add(ConversationItem.from(m));
+		}
+		duration = System.currentTimeMillis() - now;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Loading introduction messages took " + duration + " ms");
+
+		return messages;
+	}
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListItem.java b/briar-android/src/org/briarproject/android/contact/ContactListItem.java
index 2addb9e9d157a234a4554778e76099b7d7f3f636..c152d20c8b3ffdf18b73a39adc217b1a10bad8d9 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListItem.java
@@ -1,44 +1,55 @@
 package org.briarproject.android.contact;
 
 import org.briarproject.api.contact.Contact;
-import org.briarproject.api.messaging.PrivateMessageHeader;
+import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.GroupId;
 
 import java.util.Collection;
 
+import static org.briarproject.android.contact.ConversationItem.IncomingItem;
+
 // This class is not thread-safe
-class ContactListItem {
+public class ContactListItem {
 
 	private final Contact contact;
+	private final LocalAuthor localAuthor;
 	private final GroupId groupId;
 	private boolean connected, empty;
 	private long timestamp;
 	private int unread;
 
-	ContactListItem(Contact contact, boolean connected, GroupId groupId,
-			Collection<PrivateMessageHeader> headers) {
+	public ContactListItem(Contact contact, LocalAuthor localAuthor,
+			boolean connected,
+			GroupId groupId,
+			Collection<ConversationItem> messages) {
 		this.contact = contact;
+		this.localAuthor = localAuthor;
 		this.groupId = groupId;
 		this.connected = connected;
-		setHeaders(headers);
+		setMessages(messages);
 	}
 
-	void setHeaders(Collection<PrivateMessageHeader> headers) {
-		empty = headers.isEmpty();
+	void setMessages(Collection<ConversationItem> messages) {
+		empty = messages.isEmpty();
 		timestamp = 0;
 		unread = 0;
 		if (!empty) {
-			for (PrivateMessageHeader h : headers) {
-				if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp();
-				if (!h.isRead()) unread++;
+			for (ConversationItem i : messages) {
+				if (i.getTime() > timestamp) timestamp = i.getTime();
+				if (i instanceof IncomingItem && !((IncomingItem) i).isRead())
+					unread++;
 			}
 		}
 	}
 
-	Contact getContact() {
+	public Contact getContact() {
 		return contact;
 	}
 
+	public LocalAuthor getLocalAuthor() {
+		return localAuthor;
+	}
+
 	GroupId getGroupId() {
 		return groupId;
 	}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
index 45ebe6eab9dbbe9bf053731d975763393f8f7658..747c08caafb6398ce53dbd3a856dc29cc3ea41a6 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
@@ -2,11 +2,15 @@ package org.briarproject.android.contact;
 
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.graphics.PorterDuff;
 import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v4.content.ContextCompat;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AlertDialog;
 import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.Toolbar;
+import android.util.SparseArray;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
@@ -14,14 +18,17 @@ import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.EditText;
 import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import org.briarproject.R;
 import org.briarproject.android.AndroidComponent;
 import org.briarproject.android.BriarActivity;
+import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.introduction.IntroductionActivity;
 import org.briarproject.android.util.BriarRecyclerView;
 import org.briarproject.api.FormatException;
-import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
@@ -35,9 +42,16 @@ import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.IntroductionRequestReceivedEvent;
+import org.briarproject.api.event.IntroductionResponseReceivedEvent;
 import org.briarproject.api.event.MessageValidatedEvent;
 import org.briarproject.api.event.MessagesAckedEvent;
 import org.briarproject.api.event.MessagesSentEvent;
+import org.briarproject.api.introduction.IntroductionManager;
+import org.briarproject.api.introduction.IntroductionMessage;
+import org.briarproject.api.introduction.IntroductionRequest;
+import org.briarproject.api.introduction.IntroductionResponse;
+import org.briarproject.api.introduction.SessionId;
 import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessage;
 import org.briarproject.api.messaging.PrivateMessageFactory;
@@ -61,21 +75,31 @@ import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
+import de.hdodenhof.circleimageview.CircleImageView;
+import im.delight.android.identicons.IdenticonDrawable;
+
 import static android.widget.Toast.LENGTH_SHORT;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
+import static org.briarproject.android.contact.ConversationItem.IncomingItem;
 
 public class ConversationActivity extends BriarActivity
-		implements EventListener, OnClickListener {
+		implements EventListener, OnClickListener,
+		ConversationAdapter.IntroductionHandler {
 
 	private static final Logger LOG =
 			Logger.getLogger(ConversationActivity.class.getName());
+	private static final int INTRODUCTION_REQUEST_CODE = 0;
 
 	@Inject protected AndroidNotificationManager notificationManager;
 	@Inject protected ConnectionRegistry connectionRegistry;
 	@Inject @CryptoExecutor protected Executor cryptoExecutor;
 	private Map<MessageId, byte[]> bodyCache = new HashMap<MessageId, byte[]>();
 	private ConversationAdapter adapter = null;
+	private CircleImageView toolbarAvatar;
+	private ImageView toolbarStatus;
+	private TextView toolbarTitle;
 	private BriarRecyclerView list = null;
 	private EditText content = null;
 	private ImageButton sendButton = null;
@@ -85,6 +109,7 @@ public class ConversationActivity extends BriarActivity
 	@Inject protected volatile MessagingManager messagingManager;
 	@Inject protected volatile EventBus eventBus;
 	@Inject protected volatile PrivateMessageFactory privateMessageFactory;
+	@Inject protected volatile IntroductionManager introductionManager;
 	private volatile GroupId groupId = null;
 	private volatile ContactId contactId = null;
 	private volatile String contactName = null;
@@ -102,7 +127,21 @@ public class ConversationActivity extends BriarActivity
 
 		setContentView(R.layout.activity_conversation);
 
-		adapter = new ConversationAdapter(this);
+		// Custom Toolbar
+		final Toolbar tb = (Toolbar) findViewById(R.id.toolbar);
+		toolbarAvatar = (CircleImageView) tb.findViewById(R.id.contactAvatar);
+		toolbarStatus = (ImageView) tb.findViewById(R.id.contactStatus);
+		toolbarTitle = (TextView) tb.findViewById(R.id.contactName);
+		setSupportActionBar(tb);
+		final ActionBar ab = getSupportActionBar();
+		if (ab != null) {
+			ab.setDisplayShowHomeEnabled(true);
+			ab.setDisplayHomeAsUpEnabled(true);
+			ab.setDisplayShowCustomEnabled(true);
+			ab.setDisplayShowTitleEnabled(false);
+		}
+
+		adapter = new ConversationAdapter(this, this);
 		list = (BriarRecyclerView) findViewById(R.id.conversationView);
 		list.setLayoutManager(new LinearLayoutManager(this));
 		list.setAdapter(adapter);
@@ -125,8 +164,7 @@ public class ConversationActivity extends BriarActivity
 		eventBus.addListener(this);
 		notificationManager.blockNotification(groupId);
 		notificationManager.clearPrivateMessageNotification(groupId);
-		loadContactDetails();
-		loadHeaders();
+		loadData();
 	}
 
 	@Override
@@ -141,12 +179,10 @@ public class ConversationActivity extends BriarActivity
 	public boolean onCreateOptionsMenu(Menu menu) {
 		// Inflate the menu items for use in the action bar
 		MenuInflater inflater = getMenuInflater();
-		inflater.inflate(R.menu.contact_actions, menu);
+		inflater.inflate(R.menu.conversation_actions, menu);
 
-		// Adapt icon color to dark action bar
-		menu.findItem(R.id.action_social_remove_person).getIcon().setColorFilter(
-				getResources().getColor(R.color.action_bar_text),
-				PorterDuff.Mode.SRC_IN);
+		hideIntroductionActionWhenOneContact(
+				menu.findItem(R.id.action_introduction));
 
 		return super.onCreateOptionsMenu(menu);
 	}
@@ -155,6 +191,20 @@ public class ConversationActivity extends BriarActivity
 	public boolean onOptionsItemSelected(final MenuItem item) {
 		// Handle presses on the action bar items
 		switch (item.getItemId()) {
+			case android.R.id.home:
+				supportFinishAfterTransition();
+				return true;
+			case R.id.action_introduction:
+				if (contactId == null) return false;
+				Intent intent = new Intent(this, IntroductionActivity.class);
+				intent.putExtra(IntroductionActivity.CONTACT_ID,
+						contactId.getInt());
+				ActivityOptionsCompat options = ActivityOptionsCompat
+						.makeCustomAnimation(this, android.R.anim.slide_in_left,
+								android.R.anim.slide_out_right);
+				ActivityCompat.startActivityForResult(this, intent,
+						INTRODUCTION_REQUEST_CODE, options.toBundle());
+				return true;
 			case R.id.action_social_remove_person:
 				askToRemoveContact();
 				return true;
@@ -163,20 +213,37 @@ public class ConversationActivity extends BriarActivity
 		}
 	}
 
-	private void loadContactDetails() {
+	@Override
+	protected void onActivityResult(int requestCode, int resultCode,
+			Intent data) {
+
+		if (requestCode == INTRODUCTION_REQUEST_CODE) {
+			if (resultCode == RESULT_OK) {
+				loadData();
+			}
+		}
+	}
+
+	private void loadData() {
 		runOnDbThread(new Runnable() {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					contactId = messagingManager.getContactId(groupId);
-					Contact contact = contactManager.getContact(contactId);
-					contactName = contact.getAuthor().getName();
-					contactIdenticonKey = contact.getAuthor().getId().getBytes();
+					if (contactId == null)
+						contactId = messagingManager.getContactId(groupId);
+					if (contactName == null || contactIdenticonKey == null) {
+						Contact contact = contactManager.getContact(contactId);
+						contactName = contact.getAuthor().getName();
+						contactIdenticonKey =
+								contact.getAuthor().getId().getBytes();
+					}
 					connected = connectionRegistry.isConnected(contactId);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Loading contact took " + duration + " ms");
 					displayContactDetails();
+					// Load the messages here to make sure we have a contactId
+					loadMessages();
 				} catch (NoSuchContactException e) {
 					finishOnUiThread();
 				} catch (DbException e) {
@@ -190,31 +257,42 @@ public class ConversationActivity extends BriarActivity
 	private void displayContactDetails() {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				ActionBar actionBar = getSupportActionBar();
-				if (actionBar != null) {
-					actionBar.setTitle(contactName);
-					if (connected) {
-						actionBar.setSubtitle(getString(R.string.online));
-					} else {
-						actionBar.setSubtitle(getString(R.string.offline));
-					}
+				toolbarAvatar.setImageDrawable(
+						new IdenticonDrawable(contactIdenticonKey));
+				toolbarTitle.setText(contactName);
+
+				if (connected) {
+					toolbarStatus.setImageDrawable(ContextCompat
+							.getDrawable(ConversationActivity.this,
+									R.drawable.contact_online));
+					toolbarStatus
+							.setContentDescription(getString(R.string.online));
+				} else {
+					toolbarStatus.setImageDrawable(ContextCompat
+							.getDrawable(ConversationActivity.this,
+									R.drawable.contact_offline));
+					toolbarStatus
+							.setContentDescription(getString(R.string.offline));
 				}
-				adapter.setIdenticonKey(contactIdenticonKey);
+				adapter.setIdenticonKey(contactIdenticonKey, contactName);
 			}
 		});
 	}
 
-	private void loadHeaders() {
+	private void loadMessages() {
 		runOnDbThread(new Runnable() {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
 					Collection<PrivateMessageHeader> headers =
 							messagingManager.getMessageHeaders(contactId);
+					Collection<IntroductionMessage> introductions =
+							introductionManager
+									.getIntroductionMessages(contactId);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Loading headers took " + duration + " ms");
-					displayHeaders(headers);
+					displayMessages(headers, introductions);
 				} catch (NoSuchContactException e) {
 					finishOnUiThread();
 				} catch (DbException e) {
@@ -225,23 +303,37 @@ public class ConversationActivity extends BriarActivity
 		});
 	}
 
-	private void displayHeaders(
-			final Collection<PrivateMessageHeader> headers) {
+	private void displayMessages(final Collection<PrivateMessageHeader> headers,
+			final Collection<IntroductionMessage> introductions) {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				sendButton.setEnabled(true);
-				if (headers.isEmpty()) {
+				if (headers.isEmpty() && introductions.isEmpty()) {
 					// we have no messages,
 					// so let the list know to hide progress bar
 					list.showData();
 				} else {
 					for (PrivateMessageHeader h : headers) {
-						ConversationItem item = new ConversationItem(h);
+						ConversationMessageItem item =
+								new ConversationMessageItem(h);
 						byte[] body = bodyCache.get(h.getId());
 						if (body == null) loadMessageBody(h);
 						else item.setBody(body);
 						adapter.add(item);
 					}
+					for (IntroductionMessage m : introductions) {
+						ConversationItem item;
+						if (m instanceof IntroductionRequest) {
+							item = ConversationItem
+									.from((IntroductionRequest) m);
+						} else {
+							item = ConversationItem
+									.from(ConversationActivity.this,
+											contactName,
+											(IntroductionResponse) m);
+						}
+						adapter.add(item);
+					}
 					// Scroll to the bottom
 					list.scrollToPosition(adapter.getItemCount() - 1);
 				}
@@ -273,27 +365,42 @@ public class ConversationActivity extends BriarActivity
 		runOnUiThread(new Runnable() {
 			public void run() {
 				bodyCache.put(m, body);
-				int count = adapter.getItemCount();
-				for (int i = 0; i < count; i++) {
-					ConversationItem item = adapter.getItem(i);
-					if (item.getHeader().getId().equals(m)) {
+				SparseArray<ConversationMessageItem> messages =
+						adapter.getPrivateMessages();
+				for (int i = 0; i < messages.size(); i++) {
+					ConversationMessageItem item = messages.valueAt(i);
+					if (item.getId().equals(m)) {
 						item.setBody(body);
-						adapter.notifyItemChanged(i);
-						// Scroll to the bottom
-						list.scrollToPosition(count - 1);
+						adapter.notifyItemChanged(messages.keyAt(i));
 						return;
 					}
 				}
+				// Scroll to the bottom
+				list.scrollToPosition(adapter.getItemCount() - 1);
+			}
+		});
+	}
+
+	private void addIntroduction(final ConversationItem item) {
+		runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				if (adapter != null) {
+					adapter.add(item);
+					// Scroll to the bottom
+					list.scrollToPosition(adapter.getItemCount() - 1);
+				}
 			}
 		});
 	}
 
 	private void markMessagesRead() {
 		List<MessageId> unread = new ArrayList<MessageId>();
-		int count = adapter.getItemCount();
-		for (int i = 0; i < count; i++) {
-			PrivateMessageHeader h = adapter.getItem(i).getHeader();
-			if (!h.isRead()) unread.add(h.getId());
+		SparseArray<IncomingItem> list =
+				adapter.getIncomingMessages();
+		for (int i = 0; i < list.size(); i++) {
+			IncomingItem item = list.valueAt(i);
+			if (!item.isRead()) unread.add(item.getId());
 		}
 		if (unread.isEmpty()) return;
 		if (LOG.isLoggable(INFO))
@@ -307,6 +414,8 @@ public class ConversationActivity extends BriarActivity
 				try {
 					long now = System.currentTimeMillis();
 					for (MessageId m : unread)
+						// not really clean, but the messaging manager can
+						// handle introduction messages as well
 						messagingManager.setReadFlag(m, true);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
@@ -331,7 +440,7 @@ public class ConversationActivity extends BriarActivity
 			if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) {
 				LOG.info("Message added, reloading");
 				// Mark new incoming messages as read directly
-				if (m.isLocal()) loadHeaders();
+				if (m.isLocal()) loadMessages();
 				else markMessageReadIfNew(m.getMessage());
 			}
 		} else if (e instanceof MessagesSentEvent) {
@@ -360,6 +469,23 @@ public class ConversationActivity extends BriarActivity
 				connected = false;
 				displayContactDetails();
 			}
+		} else if (e instanceof IntroductionRequestReceivedEvent) {
+			IntroductionRequestReceivedEvent event =
+					(IntroductionRequestReceivedEvent) e;
+			if (event.getContactId().equals(contactId)) {
+				IntroductionRequest ir = event.getIntroductionRequest();
+				ConversationItem item = new ConversationIntroductionInItem(ir);
+				addIntroduction(item);
+			}
+		} else if (e instanceof IntroductionResponseReceivedEvent) {
+			IntroductionResponseReceivedEvent event =
+					(IntroductionResponseReceivedEvent) e;
+			if (event.getContactId().equals(contactId)) {
+				IntroductionResponse ir = event.getIntroductionResponse();
+				ConversationItem item =
+						ConversationItem.from(this, contactName, ir);
+				addIntroduction(item);
+			}
 		}
 	}
 
@@ -369,10 +495,13 @@ public class ConversationActivity extends BriarActivity
 				ConversationItem item = adapter.getLastItem();
 				if (item != null) {
 					// Mark the message read if it's the newest message
-					long lastMsgTime = item.getHeader().getTimestamp();
+					long lastMsgTime = item.getTime();
 					long newMsgTime = m.getTimestamp();
 					if (newMsgTime > lastMsgTime) markNewMessageRead(m);
-					else loadHeaders();
+					else loadMessages();
+				} else {
+					// mark the message as read as well if it is the first one
+					markNewMessageRead(m);
 				}
 			}
 		});
@@ -383,7 +512,7 @@ public class ConversationActivity extends BriarActivity
 			public void run() {
 				try {
 					messagingManager.setReadFlag(m.getId(), true);
-					loadHeaders();
+					loadMessages();
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -397,13 +526,14 @@ public class ConversationActivity extends BriarActivity
 		runOnUiThread(new Runnable() {
 			public void run() {
 				Set<MessageId> messages = new HashSet<MessageId>(messageIds);
-				int count = adapter.getItemCount();
-				for (int i = 0; i < count; i++) {
-					ConversationItem item = adapter.getItem(i);
-					if (messages.contains(item.getHeader().getId())) {
+				SparseArray<OutgoingItem> list =
+						adapter.getOutgoingMessages();
+				for (int i = 0; i < list.size(); i++) {
+					OutgoingItem item = list.valueAt(i);
+					if (messages.contains(item.getId())) {
 						item.setSent(sent);
 						item.setSeen(seen);
-						adapter.notifyItemChanged(i);
+						adapter.notifyItemChanged(list.keyAt(i));
 					}
 				}
 			}
@@ -424,7 +554,7 @@ public class ConversationActivity extends BriarActivity
 	private long getMinTimestampForNewMessage() {
 		// Don't use an earlier timestamp than the newest message
 		ConversationItem item = adapter.getLastItem();
-		return item == null ? 0 : item.getHeader().getTimestamp() + 1;
+		return item == null ? 0 : item.getTime() + 1;
 	}
 
 	private void createMessage(final byte[] body, final long timestamp) {
@@ -466,7 +596,8 @@ public class ConversationActivity extends BriarActivity
 					}
 				};
 		AlertDialog.Builder builder =
-				new AlertDialog.Builder(ConversationActivity.this);
+				new AlertDialog.Builder(ConversationActivity.this,
+						R.style.BriarDialogTheme);
 		builder.setTitle(getString(R.string.dialog_title_delete_contact));
 		builder.setMessage(getString(R.string.dialog_message_delete_contact));
 		builder.setPositiveButton(android.R.string.ok, okListener);
@@ -500,4 +631,66 @@ public class ConversationActivity extends BriarActivity
 			}
 		});
 	}
+
+	private void hideIntroductionActionWhenOneContact(final MenuItem item) {
+		runOnDbThread(new Runnable() {
+			public void run() {
+				try {
+					if (contactManager.getActiveContacts().size() < 2) {
+						hideIntroductionAction(item);
+					}
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	private void hideIntroductionAction(final MenuItem item) {
+		runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				item.setVisible(false);
+			}
+		});
+	}
+
+	@Override
+	public void respondToIntroduction(final SessionId sessionId, final boolean accept) {
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					if (accept) {
+						introductionManager.acceptIntroduction(sessionId);
+					} else {
+						introductionManager.declineIntroduction(sessionId);
+					}
+					loadMessages();
+				} catch (DbException e) {
+					introductionResponseError();
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch (FormatException e) {
+					introductionResponseError();
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+
+			}
+		});
+	}
+
+	private void introductionResponseError() {
+		runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				Toast.makeText(ConversationActivity.this,
+						R.string.introduction_response_error,
+						Toast.LENGTH_SHORT).show();
+			}
+		});
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
index 7cb7227325a837b24ba7b214e32c56c157b57df3..83cb88cb6446c674142af08b5d8f0f2b96ef098d 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
@@ -4,29 +4,37 @@ import android.content.Context;
 import android.support.v7.util.SortedList;
 import android.support.v7.widget.RecyclerView;
 import android.text.format.DateUtils;
+import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 import org.briarproject.R;
-import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.introduction.IntroductionRequest;
+import org.briarproject.api.introduction.SessionId;
 import org.briarproject.api.messaging.PrivateMessageHeader;
 import org.briarproject.util.StringUtils;
 
 import im.delight.android.identicons.IdenticonDrawable;
 
 import static android.support.v7.util.SortedList.INVALID_POSITION;
-
-class ConversationAdapter extends
-		RecyclerView.Adapter<ConversationAdapter.MessageHolder> {
-
-	private static final int MSG_OUT = 0;
-	private static final int MSG_IN = 1;
-	private static final int MSG_IN_UNREAD = 2;
-
-	private final SortedList<ConversationItem> messages =
+import static android.support.v7.widget.RecyclerView.ViewHolder;
+import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_IN;
+import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_OUT;
+import static org.briarproject.android.contact.ConversationItem.MSG_IN;
+import static org.briarproject.android.contact.ConversationItem.MSG_IN_UNREAD;
+import static org.briarproject.android.contact.ConversationItem.MSG_OUT;
+import static org.briarproject.android.contact.ConversationItem.NOTICE_IN;
+import static org.briarproject.android.contact.ConversationItem.NOTICE_OUT;
+import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
+import static org.briarproject.android.contact.ConversationItem.IncomingItem;
+
+class ConversationAdapter extends RecyclerView.Adapter {
+
+	private final SortedList<ConversationItem> items =
 			new SortedList<ConversationItem>(ConversationItem.class,
 					new SortedList.Callback<ConversationItem>() {
 						@Override
@@ -52,8 +60,8 @@ class ConversationAdapter extends
 						@Override
 						public int compare(ConversationItem c1,
 								ConversationItem c2) {
-							long time1 = c1.getHeader().getTimestamp();
-							long time2 = c2.getHeader().getTimestamp();
+							long time1 = c1.getTime();
+							long time2 = c2.getTime();
 							if (time1 < time2) return -1;
 							if (time1 > time2) return 1;
 							return 0;
@@ -62,8 +70,7 @@ class ConversationAdapter extends
 						@Override
 						public boolean areItemsTheSame(ConversationItem c1,
 								ConversationItem c2) {
-							return c1.getHeader().getId()
-									.equals(c2.getHeader().getId());
+							return c1.getId().equals(c2.getId());
 						}
 
 						@Override
@@ -73,67 +80,113 @@ class ConversationAdapter extends
 						}
 					});
 	private Context ctx;
+	private IntroductionHandler intro;
 	private byte[] identiconKey;
+	private String contactName;
 
-	public ConversationAdapter(Context context) {
+	public ConversationAdapter(Context context,
+			IntroductionHandler introductionHandler) {
 		ctx = context;
+		intro = introductionHandler;
+		setHasStableIds(true);
 	}
 
-	public void setIdenticonKey(byte[] key) {
+	public void setIdenticonKey(byte[] key, String contactName) {
 		this.identiconKey = key;
+		this.contactName = contactName;
+		// FIXME this breaks the progress animation because it is called early before data is loaded
 		notifyDataSetChanged();
 	}
 
+	@Override
+	public long getItemId(int position) {
+		return getItem(position).getId().hashCode();
+	}
+
 	@Override
 	public int getItemViewType(int position) {
-		// return different type for incoming and outgoing (local) messages
-		PrivateMessageHeader header = getItem(position).getHeader();
-		if (header.isLocal()) {
-			return MSG_OUT;
-		} else if (header.isRead()) {
-			return MSG_IN;
-		} else {
-			return MSG_IN_UNREAD;
-		}
+		return getItem(position).getType();
 	}
 
 	@Override
-	public MessageHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
+	public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
 		View v;
 
 		// outgoing message (local)
 		if (type == MSG_OUT) {
 			v = LayoutInflater.from(viewGroup.getContext())
 					.inflate(R.layout.list_item_msg_out, viewGroup, false);
+			return new MessageHolder(v, type);
+		}
+		else if (type == INTRODUCTION_IN) {
+			v = LayoutInflater.from(viewGroup.getContext())
+					.inflate(R.layout.list_item_introduction_in, viewGroup, false);
+			return new IntroductionHolder(v, type);
+		}
+		else if (type == INTRODUCTION_OUT) {
+			v = LayoutInflater.from(viewGroup.getContext())
+					.inflate(R.layout.list_item_introduction_out, viewGroup, false);
+			return new IntroductionHolder(v, type);
+		}
+		else if (type == NOTICE_IN) {
+			v = LayoutInflater.from(viewGroup.getContext())
+					.inflate(R.layout.list_item_notice_in, viewGroup, false);
+			return new NoticeHolder(v, type);
+		}
+		else if (type == NOTICE_OUT) {
+			v = LayoutInflater.from(viewGroup.getContext())
+					.inflate(R.layout.list_item_notice_out, viewGroup, false);
+			return new NoticeHolder(v, type);
 		}
 		// incoming message (non-local)
 		else {
 			v = LayoutInflater.from(viewGroup.getContext())
 					.inflate(R.layout.list_item_msg_in, viewGroup, false);
+			return new MessageHolder(v, type);
 		}
-
-		return new MessageHolder(v, type);
 	}
 
 	@Override
-	public void onBindViewHolder(final MessageHolder ui, final int position) {
+	public void onBindViewHolder(final ViewHolder ui, final int position) {
 		ConversationItem item = getItem(position);
+		if (item instanceof ConversationMessageItem) {
+			bindMessage((MessageHolder) ui, (ConversationMessageItem) item,
+					position);
+		} else if (item instanceof ConversationIntroductionOutItem) {
+			bindIntroduction((IntroductionHolder) ui,
+					(ConversationIntroductionOutItem) item, position);
+		} else if (item instanceof ConversationIntroductionInItem) {
+			bindIntroduction((IntroductionHolder) ui,
+					(ConversationIntroductionInItem) item, position);
+		} else if (item instanceof ConversationNoticeOutItem) {
+			bindNotice((NoticeHolder) ui, (ConversationNoticeOutItem) item,
+					position);
+		} else if (item instanceof ConversationNoticeInItem) {
+			bindNotice((NoticeHolder) ui, (ConversationNoticeInItem) item,
+					position);
+		} else {
+			throw new IllegalArgumentException("Unhandled Conversation Item");
+		}
+	}
+
+	private void bindMessage(final MessageHolder ui,
+			ConversationMessageItem item, final int position) {
 		PrivateMessageHeader header = item.getHeader();
 
-		if (header.isLocal()) {
+		if (item.getType() == MSG_OUT) {
 			if (item.isSeen()) {
-				ui.status.setImageResource(R.drawable.message_delivered);
+				ui.status.setImageResource(R.drawable.message_delivered_white);
 			} else if (item.isSent()) {
-				ui.status.setImageResource(R.drawable.message_sent);
+				ui.status.setImageResource(R.drawable.message_sent_white);
 			} else {
-				ui.status.setImageResource(R.drawable.message_stored);
+				ui.status.setImageResource(R.drawable.message_stored_white);
 			}
 		} else {
 			if (identiconKey != null)
-				ui.avatar.setImageDrawable(
-						new IdenticonDrawable(identiconKey));
-			if (!header.isRead()) {
-				int left = ui.layout.getPaddingLeft();
+				ui.avatar.setImageDrawable(new IdenticonDrawable(identiconKey));
+			if (item.getType() == MSG_IN_UNREAD) {
+				// TODO implement new unread message highlight according to #232
+/*				int left = ui.layout.getPaddingLeft();
 				int top = ui.layout.getPaddingTop();
 				int right = ui.layout.getPaddingRight();
 				int bottom = ui.layout.getPaddingBottom();
@@ -144,6 +197,7 @@ class ConversationAdapter extends
 				// re-apply the previous padding due to bug in some Android versions
 				// see: https://code.google.com/p/android/issues/detail?id=17885
 				ui.layout.setPadding(left, top, right, bottom);
+*/
 			}
 		}
 
@@ -159,42 +213,178 @@ class ConversationAdapter extends
 		ui.date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
 	}
 
+	private void bindIntroduction(final IntroductionHolder ui,
+			final ConversationIntroductionInItem item, final int position) {
+
+		final IntroductionRequest ir = item.getIntroductionRequest();
+
+		final String message = ir.getMessage();
+		if (StringUtils.isNullOrEmpty(message)) {
+			ui.messageLayout.setVisibility(View.GONE);
+		} else {
+			ui.messageLayout.setVisibility(View.VISIBLE);
+			if (item.getType() == INTRODUCTION_IN && identiconKey != null) {
+				ui.message.avatar.setImageDrawable(
+						new IdenticonDrawable(identiconKey));
+			}
+			ui.message.body.setText(message);
+			ui.message.date.setText(
+					DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
+		}
+
+		// Outgoing Introduction Request
+		if (item instanceof ConversationIntroductionOutItem) {
+			ui.text.setText(ctx.getString(R.string.introduction_request_sent,
+					contactName, ir.getName()));
+			ConversationIntroductionOutItem i =
+					(ConversationIntroductionOutItem) item;
+			if (i.isSeen()) {
+				ui.status.setImageResource(R.drawable.message_delivered);
+				ui.message.status.setImageResource(R.drawable.message_delivered_white);
+			} else if (i.isSent()) {
+				ui.status.setImageResource(R.drawable.message_sent);
+				ui.message.status.setImageResource(R.drawable.message_sent_white);
+			} else {
+				ui.status.setImageResource(R.drawable.message_stored);
+				ui.message.status.setImageResource(R.drawable.message_stored_white);
+			}
+		}
+		// Incoming Introduction Request (Answered)
+		else if (item.wasAnswered()) {
+			ui.text.setText(ctx.getString(
+					R.string.introduction_request_answered_received,
+					contactName, ir.getName()));
+			ui.acceptButton.setVisibility(View.GONE);
+			ui.declineButton.setVisibility(View.GONE);
+		}
+		// Incoming Introduction Request (Not Answered)
+		else {
+			if (item.getIntroductionRequest().doesExist()) {
+				ui.text.setText(ctx.getString(
+						R.string.introduction_request_exists_received,
+						contactName, ir.getName()));
+			} else {
+				ui.text.setText(
+						ctx.getString(R.string.introduction_request_received,
+								contactName, ir.getName()));
+			}
+
+			ui.acceptButton.setVisibility(View.VISIBLE);
+			ui.acceptButton.setOnClickListener(new View.OnClickListener() {
+				@Override
+				public void onClick(View v) {
+					intro.respondToIntroduction(ir.getSessionId(), true);
+					item.setAnswered(true);
+					notifyItemChanged(position);
+				}
+			});
+			ui.declineButton.setVisibility(View.VISIBLE);
+			ui.declineButton.setOnClickListener(new View.OnClickListener() {
+				@Override
+				public void onClick(View v) {
+					intro.respondToIntroduction(ir.getSessionId(), false);
+					item.setAnswered(true);
+					notifyItemChanged(position);
+				}
+			});
+		}
+		ui.date.setText(
+				DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
+	}
+
+	private void bindNotice(final NoticeHolder ui,
+			final ConversationNoticeItem item, final int position) {
+
+		ui.text.setText(item.getText());
+		ui.date.setText(
+				DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
+
+		if (item instanceof ConversationNoticeOutItem) {
+			ConversationNoticeOutItem n = (ConversationNoticeOutItem) item;
+			if (n.isSeen()) {
+				ui.status.setImageResource(R.drawable.message_delivered);
+			} else if (n.isSent()) {
+				ui.status.setImageResource(R.drawable.message_sent);
+			} else {
+				ui.status.setImageResource(R.drawable.message_stored);
+			}
+		}
+	}
+
 	@Override
 	public int getItemCount() {
-		return messages.size();
+		return items.size();
 	}
 
 	public ConversationItem getItem(int position) {
-		if (position == INVALID_POSITION || messages.size() <= position) {
+		if (position == INVALID_POSITION || items.size() <= position) {
 			return null; // Not found
 		}
-		return messages.get(position);
+		return items.get(position);
 	}
 
 	public ConversationItem getLastItem() {
-		if (messages.size() > 0) {
-			return messages.get(messages.size() - 1);
+		if (items.size() > 0) {
+			return items.get(items.size() - 1);
 		} else {
 			return null;
 		}
 	}
 
+	public SparseArray<IncomingItem> getIncomingMessages() {
+		SparseArray<IncomingItem> messages =
+				new SparseArray<IncomingItem>();
+
+		for (int i = 0; i < items.size(); i++) {
+			ConversationItem item = items.get(i);
+			if (item instanceof IncomingItem) {
+				messages.put(i, (IncomingItem) item);
+			}
+		}
+		return messages;
+	}
+
+	public SparseArray<OutgoingItem> getOutgoingMessages() {
+		SparseArray<OutgoingItem> messages =
+				new SparseArray<OutgoingItem>();
+
+		for (int i = 0; i < items.size(); i++) {
+			ConversationItem item = items.get(i);
+			if (item instanceof OutgoingItem) {
+				messages.put(i, (OutgoingItem) item);
+			}
+		}
+		return messages;
+	}
+
+	public SparseArray<ConversationMessageItem> getPrivateMessages() {
+		SparseArray<ConversationMessageItem> messages =
+				new SparseArray<ConversationMessageItem>();
+
+		for (int i = 0; i < items.size(); i++) {
+			ConversationItem item = items.get(i);
+			if (item instanceof ConversationMessageItem) {
+				messages.put(i, (ConversationMessageItem) item);
+			}
+		}
+		return messages;
+	}
+
 	public void add(final ConversationItem message) {
-		this.messages.add(message);
+		this.items.add(message);
 	}
 
 	public void clear() {
-		this.messages.beginBatchedUpdates();
+		this.items.beginBatchedUpdates();
 
-		while(messages.size() != 0) {
-			messages.removeItemAt(0);
+		while(items.size() != 0) {
+			items.removeItemAt(0);
 		}
 
-		this.messages.endBatchedUpdates();
+		this.items.endBatchedUpdates();
 	}
 
-	// TODO: Does this class need to be public?
-	public static class MessageHolder extends RecyclerView.ViewHolder {
+	protected class MessageHolder extends RecyclerView.ViewHolder {
 
 		public ViewGroup layout;
 		public TextView body;
@@ -217,4 +407,59 @@ class ConversationAdapter extends
 			}
 		}
 	}
-}
\ No newline at end of file
+
+	protected class IntroductionHolder extends RecyclerView.ViewHolder {
+
+		public ViewGroup layout;
+		public View messageLayout;
+		public MessageHolder message;
+		public TextView text;
+		public Button acceptButton;
+		public Button declineButton;
+		public TextView date;
+		public ImageView status;
+
+		public IntroductionHolder(View v, int type) {
+			super(v);
+
+			layout = (ViewGroup) v.findViewById(R.id.introductionLayout);
+			messageLayout = v.findViewById(R.id.messageLayout);
+			message = new MessageHolder(messageLayout,
+					type == INTRODUCTION_IN ? MSG_IN : MSG_OUT);
+			text = (TextView) v.findViewById(R.id.introductionText);
+			acceptButton = (Button) v.findViewById(R.id.acceptButton);
+			declineButton = (Button) v.findViewById(R.id.declineButton);
+			date = (TextView) v.findViewById(R.id.introductionTime);
+
+			if (type == INTRODUCTION_OUT) {
+				status = (ImageView) v.findViewById(R.id.introductionStatus);
+			}
+		}
+	}
+
+	protected class NoticeHolder extends RecyclerView.ViewHolder {
+
+		public ViewGroup layout;
+		public TextView text;
+		public TextView date;
+		public ImageView status;
+
+		public NoticeHolder(View v, int type) {
+			super(v);
+
+			layout = (ViewGroup) v.findViewById(R.id.noticeLayout);
+			text = (TextView) v.findViewById(R.id.noticeText);
+			date = (TextView) v.findViewById(R.id.noticeTime);
+
+			if (type == NOTICE_OUT) {
+				status = (ImageView) v.findViewById(R.id.noticeStatus);
+			}
+		}
+	}
+
+	public interface IntroductionHandler {
+		void respondToIntroduction(final SessionId sessionId,
+				final boolean accept);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..939f808792d5ef89862a4fb86eac4264cf2f21e5
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java
@@ -0,0 +1,47 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.introduction.IntroductionRequest;
+import org.briarproject.api.sync.MessageId;
+
+public class ConversationIntroductionInItem extends ConversationItem implements
+		ConversationItem.IncomingItem {
+
+	private IntroductionRequest ir;
+	private boolean answered, read;
+
+	public ConversationIntroductionInItem(IntroductionRequest ir) {
+		super(ir.getMessageId(), ir.getTime());
+
+		this.ir = ir;
+		this.answered = ir.wasAnswered();
+		this.read = ir.isRead();
+	}
+
+	@Override
+	int getType() {
+		return INTRODUCTION_IN;
+	}
+
+	public IntroductionRequest getIntroductionRequest() {
+		return ir;
+	}
+
+	public boolean wasAnswered() {
+		return answered;
+	}
+
+	public void setAnswered(boolean answered) {
+		this.answered = answered;
+	}
+
+	@Override
+	public boolean isRead() {
+		return read;
+	}
+
+	@Override
+	public void setRead(boolean read) {
+		this.read = read;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..37f32a83d977e35997a3ddd23e5d60c3420aa0dc
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java
@@ -0,0 +1,48 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.introduction.IntroductionRequest;
+import org.briarproject.api.sync.MessageId;
+
+/**
+ * This class is needed and can not be replaced by an ConversationNoticeOutItem,
+ * because it carries the optional introduction message
+ * to be displayed as a regular private message.
+ */
+public class ConversationIntroductionOutItem
+		extends ConversationIntroductionInItem
+		implements ConversationItem.OutgoingItem {
+
+	private boolean sent, seen;
+
+	public ConversationIntroductionOutItem(IntroductionRequest ir) {
+		super(ir);
+		this.sent = ir.isSent();
+		this.seen = ir.isSeen();
+	}
+
+	@Override
+	int getType() {
+		return INTRODUCTION_OUT;
+	}
+
+	@Override
+	public boolean isSent() {
+		return sent;
+	}
+
+	@Override
+	public void setSent(boolean sent) {
+		this.sent = sent;
+	}
+
+	@Override
+	public boolean isSeen() {
+		return seen;
+	}
+
+	@Override
+	public void setSeen(boolean seen) {
+		this.seen = seen;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
index 7603c5c8f19f0ee4dd064f616b3a737007ff3de5..ad037996c47c5a9447e15c2f78324fb669c5b52f 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
@@ -1,46 +1,105 @@
 package org.briarproject.android.contact;
 
-import org.briarproject.api.messaging.PrivateMessageHeader;
+import android.content.Context;
+
+import org.briarproject.R;
+import org.briarproject.api.introduction.IntroductionMessage;
+import org.briarproject.api.introduction.IntroductionRequest;
+import org.briarproject.api.introduction.IntroductionResponse;
+import org.briarproject.api.sync.MessageId;
 
 // This class is not thread-safe
-class ConversationItem {
+public abstract class ConversationItem {
+
+	final static int MSG_IN = 0;
+	final static int MSG_IN_UNREAD = 1;
+	final static int MSG_OUT = 2;
+	final static int INTRODUCTION_IN = 3;
+	final static int INTRODUCTION_OUT = 4;
+	final static int NOTICE_IN = 5;
+	final static int NOTICE_OUT = 6;
 
-	private final PrivateMessageHeader header;
-	private byte[] body;
-	private boolean sent, seen;
+	private MessageId id;
+	private long time;
 
-	ConversationItem(PrivateMessageHeader header) {
-		this.header = header;
-		body = null;
-		sent = header.isSent();
-		seen = header.isSeen();
+	public ConversationItem(MessageId id, long time) {
+		this.id = id;
+		this.time = time;
 	}
 
-	PrivateMessageHeader getHeader() {
-		return header;
+	abstract int getType();
+
+	public MessageId getId() {
+		return id;
 	}
 
-	byte[] getBody() {
-		return body;
+	long getTime() {
+		return time;
 	}
 
-	void setBody(byte[] body) {
-		this.body = body;
+	public static ConversationItem from(IntroductionRequest ir) {
+		if (ir.isLocal()) {
+			return new ConversationIntroductionOutItem(ir);
+		} else {
+			return new ConversationIntroductionInItem(ir);
+		}
 	}
 
-	boolean isSent() {
-		return sent;
+	public static ConversationItem from(Context ctx, String contactName,
+			IntroductionResponse ir) {
+
+		if (ir.isLocal()) {
+			String text;
+			if (ir.wasAccepted()) {
+				text = ctx.getString(
+						R.string.introduction_response_accepted_sent,
+						ir.getName());
+			} else {
+				text = ctx.getString(
+						R.string.introduction_response_declined_sent,
+						ir.getName());
+			}
+			return new ConversationNoticeOutItem(ir.getMessageId(), text,
+					ir.getTime(), ir.isSent(), ir.isSeen());
+		} else {
+			String text;
+			if (ir.wasAccepted()) {
+				text = ctx.getString(
+						R.string.introduction_response_accepted_received,
+						contactName, ir.getName());
+			} else {
+				text = ctx.getString(
+						R.string.introduction_response_declined_received,
+						contactName, ir.getName());
+			}
+			return new ConversationNoticeInItem(ir.getMessageId(), text,
+					ir.getTime(), ir.isRead());
+		}
 	}
 
-	void setSent(boolean sent) {
-		this.sent = sent;
+	/** This method should not be used to get user-facing objects,
+	 *  Its purpose is to provider data for the contact list.
+	 */
+	public static ConversationItem from(IntroductionMessage im) {
+		if (im.isLocal())
+			return new ConversationNoticeOutItem(im.getMessageId(), "",
+					im.getTime(), false, false);
+		return new ConversationNoticeInItem(im.getMessageId(), "", im.getTime(),
+				im.isRead());
 	}
 
-	boolean isSeen() {
-		return seen;
+	protected interface OutgoingItem {
+		MessageId getId();
+		boolean isSent();
+		void setSent(boolean sent);
+		boolean isSeen();
+		void setSeen(boolean seen);
 	}
 
-	void setSeen(boolean seen) {
-		this.seen = seen;
+	protected interface IncomingItem {
+		MessageId getId();
+		boolean isRead();
+		void setRead(boolean read);
 	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..38e5afab0c3cf1ef60d11d262261e1889b0a11ec
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java
@@ -0,0 +1,73 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.messaging.PrivateMessageHeader;
+import org.briarproject.api.sync.MessageId;
+
+// This class is not thread-safe
+public class ConversationMessageItem extends ConversationItem implements
+		ConversationItem.OutgoingItem, ConversationItem.IncomingItem {
+
+	private final PrivateMessageHeader header;
+	private byte[] body;
+	private boolean sent, seen, read;
+
+	public ConversationMessageItem(PrivateMessageHeader header) {
+		super(header.getId(), header.getTimestamp());
+
+		this.header = header;
+		body = null;
+		sent = header.isSent();
+		seen = header.isSeen();
+		read = header.isRead();
+	}
+
+	@Override
+	int getType() {
+		if (getHeader().isLocal()) return MSG_OUT;
+		if (getHeader().isRead()) return MSG_IN;
+		return MSG_IN_UNREAD;
+	}
+
+	PrivateMessageHeader getHeader() {
+		return header;
+	}
+
+	byte[] getBody() {
+		return body;
+	}
+
+	void setBody(byte[] body) {
+		this.body = body;
+	}
+
+	@Override
+	public boolean isSent() {
+		return sent;
+	}
+
+	@Override
+	public void setSent(boolean sent) {
+		this.sent = sent;
+	}
+
+	@Override
+	public boolean isSeen() {
+		return seen;
+	}
+
+	@Override
+	public void setSeen(boolean seen) {
+		this.seen = seen;
+	}
+
+	@Override
+	public boolean isRead() {
+		return read;
+	}
+
+	@Override
+	public void setRead(boolean read) {
+		this.read = read;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..610b703c17ada2defc6cd39233376929092d2658
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java
@@ -0,0 +1,32 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.sync.MessageId;
+
+public class ConversationNoticeInItem extends ConversationNoticeItem implements
+		ConversationItem.IncomingItem {
+
+	private boolean read;
+
+	public ConversationNoticeInItem(MessageId id, String text, long time,
+			boolean read) {
+		super(id, text, time);
+
+		this.read = read;
+	}
+
+	@Override
+	int getType() {
+		return NOTICE_IN;
+	}
+
+	@Override
+	public boolean isRead() {
+		return read;
+	}
+
+	@Override
+	public void setRead(boolean read) {
+		this.read = read;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..eabc73970aafd87593be54283b909dddce5d50c4
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java
@@ -0,0 +1,19 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.sync.MessageId;
+
+abstract class ConversationNoticeItem extends ConversationItem {
+
+	private String text;
+
+	public ConversationNoticeItem(MessageId id, String text, long time) {
+		super(id, time);
+
+		this.text = text;
+	}
+
+	public String getText() {
+		return text;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..b398897013f82366dbb252d43e7b679032b5a811
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java
@@ -0,0 +1,44 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.sync.MessageId;
+
+public class ConversationNoticeOutItem extends ConversationNoticeItem implements
+		ConversationItem.OutgoingItem {
+
+	private boolean sent, seen;
+
+	public ConversationNoticeOutItem(MessageId id, String text, long time,
+			boolean sent, boolean seen) {
+
+		super(id, text, time);
+
+		this.sent = sent;
+		this.seen = seen;
+	}
+
+	@Override
+	int getType() {
+		return NOTICE_OUT;
+	}
+
+	@Override
+	public  boolean isSent() {
+		return sent;
+	}
+
+	@Override
+	public void setSent(boolean sent) {
+		this.sent = sent;
+	}
+
+	@Override
+	public boolean isSeen() {
+		return seen;
+	}
+
+	@Override
+	public void setSeen(boolean seen) {
+		this.seen = seen;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..66e57686a44c3a4740a823f4c23f077aa560f1fe
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
@@ -0,0 +1,243 @@
+package org.briarproject.android.introduction;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.transition.ChangeBounds;
+import android.transition.Fade;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import org.briarproject.R;
+import org.briarproject.android.AndroidComponent;
+import org.briarproject.android.contact.ContactListAdapter;
+import org.briarproject.android.contact.ContactListItem;
+import org.briarproject.android.contact.ConversationItem;
+import org.briarproject.android.contact.ConversationMessageItem;
+import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.introduction.IntroductionManager;
+import org.briarproject.api.introduction.IntroductionMessage;
+import org.briarproject.api.messaging.MessagingManager;
+import org.briarproject.api.messaging.PrivateMessageHeader;
+import org.briarproject.api.plugins.ConnectionRegistry;
+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.INFO;
+import static java.util.logging.Level.WARNING;
+
+public class ContactChooserFragment extends BaseFragment {
+
+	public final static String TAG = "ContactChooserFragment";
+	private IntroductionActivity introductionActivity;
+	private BriarRecyclerView list;
+	private ContactListAdapter adapter;
+	private int contactId;
+
+	private static final Logger LOG =
+			Logger.getLogger(ContactChooserFragment.class.getName());
+
+	// Fields that are accessed from background threads must be volatile
+	protected volatile Contact c1;
+	@Inject
+	protected volatile ContactManager contactManager;
+	@Inject
+	protected volatile IdentityManager identityManager;
+	@Inject
+	protected volatile MessagingManager messagingManager;
+	@Inject
+	protected volatile IntroductionManager introductionManager;
+	@Inject
+	protected volatile ConnectionRegistry connectionRegistry;
+
+	@Override
+	public void onAttach(Context context) {
+		super.onAttach(context);
+		try {
+			introductionActivity = (IntroductionActivity) context;
+		} catch (ClassCastException e) {
+			throw new java.lang.InstantiationError(
+					"This fragment is only meant to be attached to the IntroductionActivity");
+		}
+	}
+
+	@Override
+	public void injectActivity(AndroidComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	public View onCreateView(LayoutInflater inflater, ViewGroup container,
+			Bundle savedInstanceState) {
+
+		View contentView =
+				inflater.inflate(R.layout.introduction_contact_chooser,
+						container, false);
+
+
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+			setExitTransition(new Fade());
+		}
+
+		ContactListAdapter.OnItemClickListener onItemClickListener =
+				new ContactListAdapter.OnItemClickListener() {
+					@Override
+					public void onItemClick(View view, ContactListItem item) {
+						if (c1 == null) {
+							Toast.makeText(getActivity(),
+									R.string.introduction_error,
+									Toast.LENGTH_SHORT).show();
+							return;
+						}
+						Contact c2 = item.getContact();
+						if (!c1.getLocalAuthorId()
+								.equals(c2.getLocalAuthorId())) {
+							warnAboutDifferentIdentities(view, c1, c2);
+						} else {
+							introductionActivity.showMessageScreen(view, c1, c2);
+						}
+					}
+				};
+		adapter =
+				new ContactListAdapter(getActivity(), onItemClickListener, true);
+
+		list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
+		list.setLayoutManager(new LinearLayoutManager(getActivity()));
+		list.setAdapter(adapter);
+		list.setEmptyText(getString(R.string.no_contacts));
+
+		contactId = introductionActivity.getContactId();
+
+		return contentView;
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+
+		loadContacts();
+	}
+
+	@Override
+	public String getUniqueTag() {
+		return TAG;
+	}
+
+	private void loadContacts() {
+		introductionActivity.runOnDbThread(new Runnable() {
+			public void run() {
+				try {
+					List<ContactListItem> contacts =
+							new ArrayList<ContactListItem>();
+					AuthorId localAuthorId= null;
+					for (Contact c : contactManager.getActiveContacts()) {
+						if (c.getId().getInt() == contactId) {
+							c1 = c;
+							localAuthorId = c1.getLocalAuthorId();
+						} else {
+							ContactId id = c.getId();
+							GroupId groupId =
+									messagingManager.getConversationId(id);
+							Collection<ConversationItem> messages =
+									getMessages(id);
+							boolean connected =
+									connectionRegistry.isConnected(c.getId());
+							LocalAuthor localAuthor = identityManager
+									.getLocalAuthor(c.getLocalAuthorId());
+							contacts.add(new ContactListItem(c, localAuthor,
+									connected, groupId, messages));
+						}
+					}
+					displayContacts(localAuthorId, contacts);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	private void displayContacts(final AuthorId localAuthorId,
+			final List<ContactListItem> contacts) {
+		introductionActivity.runOnUiThread(new Runnable() {
+			public void run() {
+				adapter.setLocalAuthor(localAuthorId);
+				adapter.clear();
+				if (contacts.size() == 0) list.showData();
+				else adapter.addAll(contacts);
+			}
+		});
+	}
+
+	private void warnAboutDifferentIdentities(final View view, final Contact c1,
+			final Contact c2) {
+
+		DialogInterface.OnClickListener okListener =
+				new DialogInterface.OnClickListener() {
+					@Override
+					public void onClick(DialogInterface dialog, int which) {
+						introductionActivity.showMessageScreen(view, c1, c2);
+					}
+				};
+		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
+				R.style.BriarDialogTheme);
+		builder.setTitle(getString(
+				R.string.introduction_warn_different_identities_title));
+		builder.setMessage(getString(
+				R.string.introduction_warn_different_identities_text));
+		builder.setPositiveButton(R.string.dialog_button_introduce, okListener);
+		builder.setNegativeButton(android.R.string.cancel, null);
+		builder.show();
+	}
+
+	/** This needs to be called from the DbThread */
+	private Collection<ConversationItem> getMessages(ContactId id)
+			throws DbException {
+
+		long now = System.currentTimeMillis();
+
+		Collection<ConversationItem> messages =
+				new ArrayList<ConversationItem>();
+
+		Collection<PrivateMessageHeader> headers =
+				messagingManager.getMessageHeaders(id);
+		for (PrivateMessageHeader h : headers) {
+			messages.add(new ConversationMessageItem(h));
+		}
+		long duration = System.currentTimeMillis() - now;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Loading message headers took " + duration + " ms");
+
+		Collection<IntroductionMessage> introductions =
+				introductionManager
+						.getIntroductionMessages(id);
+		for (IntroductionMessage m : introductions) {
+			messages.add(ConversationItem.from(m));
+		}
+		duration = System.currentTimeMillis() - now;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Loading introduction messages took " + duration + " ms");
+
+		return messages;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java b/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3905df14c91b6ad50680bf645b77fc530dd4a98
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java
@@ -0,0 +1,107 @@
+package org.briarproject.android.introduction;
+
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.transition.ChangeBounds;
+import android.transition.Fade;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.briarproject.R;
+import org.briarproject.android.AndroidComponent;
+import org.briarproject.android.BriarActivity;
+import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.api.contact.Contact;
+
+public class IntroductionActivity extends BriarActivity implements
+		BaseFragment.BaseFragmentListener {
+
+	public static final String CONTACT_ID = "briar.CONTACT_ID";
+	private int contactId;
+
+	@Override
+	public void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		Intent intent = getIntent();
+		contactId = intent.getIntExtra(CONTACT_ID, -1);
+
+		setContentView(R.layout.activity_introduction);
+
+		if (savedInstanceState == null) {
+			ContactChooserFragment chooserFragment =
+					new ContactChooserFragment();
+			getSupportFragmentManager().beginTransaction()
+					.add(R.id.introductionContainer, chooserFragment).commit();
+		}
+	}
+
+	@Override
+	public void injectActivity(AndroidComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	public void showLoadingScreen(boolean isBlocking, int stringId) {
+		// this is handled by the recycler view in ContactChooserFragment
+	}
+
+	@Override
+	public void hideLoadingScreen() {
+		// this is handled by the recycler view in ContactChooserFragment
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(final MenuItem item) {
+		// Handle presses on the action bar items
+		switch (item.getItemId()) {
+			case android.R.id.home:
+				onBackPressed();
+				return true;
+			default:
+				return super.onOptionsItemSelected(item);
+		}
+	}
+
+	@Override
+	public void onBackPressed() {
+		FragmentManager fm = getSupportFragmentManager();
+		if (fm.getBackStackEntryCount() == 1) {
+			fm.popBackStack();
+		} else {
+			super.onBackPressed();
+		}
+	}
+
+	public int getContactId() {
+		return contactId;
+	}
+
+	public void showMessageScreen(final View view, final Contact c1,
+			final Contact c2) {
+
+		IntroductionMessageFragment messageFragment =
+				IntroductionMessageFragment
+						.newInstance(c1.getId().getInt(), c2.getId().getInt());
+
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+			messageFragment.setSharedElementEnterTransition(new ChangeBounds());
+			messageFragment.setEnterTransition(new Fade());
+			messageFragment.setSharedElementReturnTransition(new ChangeBounds());
+		}
+
+		getSupportFragmentManager().beginTransaction()
+				.setCustomAnimations(android.R.anim.fade_in,
+						android.R.anim.fade_out,
+						android.R.anim.slide_in_left,
+						android.R.anim.slide_out_right)
+				.addSharedElement(view, "avatar")
+				.replace(R.id.introductionContainer, messageFragment,
+						ContactChooserFragment.TAG)
+				.addToBackStack(null)
+				.commit();
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/introduction/IntroductionMessageFragment.java b/briar-android/src/org/briarproject/android/introduction/IntroductionMessageFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..7b06c9bd7cb77488039849e7295c1b270bb5a0f7
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/introduction/IntroductionMessageFragment.java
@@ -0,0 +1,235 @@
+package org.briarproject.android.introduction;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.briarproject.R;
+import org.briarproject.android.AndroidComponent;
+import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.introduction.IntroductionManager;
+
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import de.hdodenhof.circleimageview.CircleImageView;
+import im.delight.android.identicons.IdenticonDrawable;
+
+import static java.util.logging.Level.WARNING;
+
+public class IntroductionMessageFragment extends BaseFragment {
+
+	private static final Logger LOG =
+			Logger.getLogger(IntroductionMessageFragment.class.getName());
+
+	public final static String TAG = "ContactChooserFragment";
+	private IntroductionActivity introductionActivity;
+	private ViewHolder ui;
+
+	private final static String CONTACT_ID_1 = "contact1";
+	private final static String CONTACT_ID_2 = "contact2";
+
+	// Fields that are accessed from background threads must be volatile
+	private volatile boolean introductionWasMade = false;
+	@Inject
+	protected volatile ContactManager contactManager;
+	@Inject
+	protected volatile IntroductionManager introductionManager;
+
+	public static IntroductionMessageFragment newInstance(int contactId1,
+			int contactId2) {
+		IntroductionMessageFragment f = new IntroductionMessageFragment();
+
+		Bundle args = new Bundle();
+		args.putInt(CONTACT_ID_1, contactId1);
+		args.putInt(CONTACT_ID_2, contactId2);
+		f.setArguments(args);
+
+		return f;
+	}
+
+	@Override
+	public void onAttach(Context context) {
+		super.onAttach(context);
+		try {
+			introductionActivity = (IntroductionActivity) context;
+		} catch (ClassCastException e) {
+			throw new java.lang.InstantiationError(
+					"This fragment is only meant to be attached to the IntroductionActivity");
+		}
+	}
+
+	@Override
+	public void injectActivity(AndroidComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	public View onCreateView(LayoutInflater inflater, ViewGroup container,
+			Bundle savedInstanceState) {
+
+		// change toolbar text
+		ActionBar actionBar = introductionActivity.getSupportActionBar();
+		if (actionBar != null) {
+			actionBar.setTitle(R.string.introduction_message_title);
+		}
+
+		// inflate view
+		View v =
+				inflater.inflate(R.layout.introduction_message, container,
+						false);
+
+		// show progress bar until contacts have been loaded
+		ui = new ViewHolder(v);
+		ui.text.setVisibility(View.GONE);
+		ui.button.setEnabled(false);
+
+		// get contact IDs from fragment arguments
+		int contactId1 = getArguments().getInt(CONTACT_ID_1, -1);
+		int contactId2 = getArguments().getInt(CONTACT_ID_2, -1);
+		if (contactId1 == -1 || contactId2 == -1) {
+			throw new java.lang.InstantiationError(
+					"You need to use newInstance() to instantiate");
+		}
+
+		// get contacts and then show view
+		prepareToSetUpViews(contactId1, contactId2);
+
+		return v;
+	}
+
+	@Override
+	public String getUniqueTag() {
+		return TAG;
+	}
+
+	private void prepareToSetUpViews(final int contactId1,
+			final int contactId2) {
+		introductionActivity.runOnDbThread(new Runnable() {
+			public void run() {
+				try {
+					Contact c1 = contactManager
+							.getContact(new ContactId(contactId1));
+					Contact c2 = contactManager
+							.getContact(new ContactId(contactId2));
+					setUpViews(c1, c2);
+				} catch (DbException e) {
+					// TODO
+					e.printStackTrace();
+				}
+			}
+		});
+	}
+
+	private void setUpViews(final Contact c1, final Contact c2) {
+		introductionActivity.runOnUiThread(new Runnable() {
+			public void run() {
+				// set avatars
+				ui.avatar1.setImageDrawable(new IdenticonDrawable(
+						c1.getAuthor().getId().getBytes()));
+				ui.avatar2.setImageDrawable(new IdenticonDrawable(
+						c2.getAuthor().getId().getBytes()));
+
+				// set introduction text
+				ui.text.setText(String.format(
+						getString(R.string.introduction_message_text),
+						c1.getAuthor().getName(), c2.getAuthor().getName()));
+
+				// set button action
+				ui.button.setOnClickListener(new View.OnClickListener() {
+					@Override
+					public void onClick(View v) {
+						onButtonClick(c1, c2);
+					}
+				});
+
+				// hide progress bar and show views
+				ui.progressBar.setVisibility(View.GONE);
+				ui.text.setVisibility(View.VISIBLE);
+				ui.button.setEnabled(true);
+			}
+		});
+	}
+
+	public void onButtonClick(final Contact c1, final Contact c2) {
+		String msg = ui.message.getText().toString();
+		makeIntroduction(c1, c2, msg);
+	}
+
+	private void makeIntroduction(final Contact c1, final Contact c2,
+			final String msg) {
+		introductionActivity.runOnDbThread(new Runnable() {
+			public void run() {
+				// prevent double introductions
+				if (introductionWasMade) return;
+
+				// actually make the introduction
+				try {
+					introductionManager.makeIntroduction(c1, c2, msg);
+					introductionWasMade = true;
+					postIntroduction(false);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					postIntroduction(true);
+				} catch (FormatException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					postIntroduction(true);
+				}
+			}
+		});
+	}
+
+	private void postIntroduction(final boolean error) {
+		introductionActivity.runOnUiThread(new Runnable() {
+			public void run() {
+				introductionActivity.hideSoftKeyboard(ui.message);
+				if (error) {
+					Toast.makeText(introductionActivity,
+							R.string.introduction_error, Toast.LENGTH_SHORT)
+							.show();
+					introductionActivity.setResult(Activity.RESULT_CANCELED);
+				} else {
+					introductionActivity.setResult(Activity.RESULT_OK);
+				}
+				introductionActivity.finish();
+			}
+		});
+	}
+
+	private class ViewHolder {
+		ProgressBar progressBar;
+		ViewGroup header;
+		CircleImageView avatar1;
+		CircleImageView avatar2;
+		TextView text;
+		EditText message;
+		Button button;
+
+		ViewHolder(View v) {
+			progressBar = (ProgressBar) v.findViewById(R.id.progressBar);
+			header = (ViewGroup) v.findViewById(R.id.introductionHeader);
+			avatar1 = (CircleImageView) v.findViewById(R.id.avatarContact1);
+			avatar2 = (CircleImageView) v.findViewById(R.id.avatarContact2);
+			text = (TextView) v.findViewById(R.id.introductionText);
+			message = (EditText) v.findViewById(R.id.introductionMessageView);
+			button = (Button) v.findViewById(R.id.makeIntroductionButton);
+		}
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
index 89c213502405727bf4d733038253fc2d309831c4..5e786322908abf39d4c479ae0fcb5bf8369ae9d7 100644
--- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
+++ b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
@@ -73,15 +73,10 @@ public class BriarRecyclerView extends FrameLayout {
 		}
 
 		emptyObserver = new RecyclerView.AdapterDataObserver() {
-			@Override
-			public void onChanged() {
-				showData();
-			}
-
 			@Override
 			public void onItemRangeInserted(int positionStart, int itemCount) {
 				super.onItemRangeInserted(positionStart, itemCount);
-				onChanged();
+				if (itemCount > 0) showData();
 			}
 		};
 	}