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..8f879c90a041b99df3882897e259ba47db12e6b0 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.ContactList"/>
 
 </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..5daea060852a74a324f23c79cd851ad1fee3ea7a
--- /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/introductionText"
+			android:layout_alignRight="@+id/introductionText"
+			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..88070ea669ea57ab6402247f6c18cebae68eecb9
--- /dev/null
+++ b/briar-android/res/layout/list_item_introduction_out.xml
@@ -0,0 +1,56 @@
+<?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: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..8f0daa0267b56790c8e1e9d4397ad01ae9819f13
--- /dev/null
+++ b/briar-android/res/layout/list_item_notice_in.xml
@@ -0,0 +1,33 @@
+<?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: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..0128ee56f42686c6db67bb3934aab796cb06ecb0 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/introduction_button"
+		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..bfd82ba9f3f31685d8a3ea480a0b6979f9c9d5d8 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -142,6 +142,27 @@
 	<string name="no_data">No data</string>
 	<string name="unknown_app">an unknown app</string>
 
+	<!-- Introduction Client -->
+	<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 asked to introduce %1$s to %2$s.</string>
+	<string name="introduction_request_received">%1$s has asked to introduce you to %2$s. Do you want to add %2$s to your contact list?</string>
+	<string name="introduction_request_exists_received">%1$s has asked to introduce 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 has asked to introduce 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 introduced to %1$s.</string>
+
 	<!-- Dialogs -->
 	<string name="dialog_title_lost_password">Lost Password</string>
 	<string name="dialog_message_lost_password">Password recovery is not possible. Do you want to delete your account?\n\nCaution: This will permanently delete your identities, contacts and messages</string>
@@ -152,6 +173,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..12c1c35971598950fed3e2c806f81038895ae49d 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.ContactList" 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..17f8fbac79365c602896712e0ceaf7060d379967 100644
--- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
+++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
@@ -14,10 +14,15 @@ 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.contact.ContactId;
 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 +62,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 +117,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 			public Void call() {
 				clearPrivateMessageNotification();
 				clearForumPostNotification();
+				clearIntroductionSuccessNotification();
 				return null;
 			}
 		});
@@ -135,6 +142,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 +161,15 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 				else if (c.equals(forumManager.getClientId()))
 					showForumPostNotification(m.getMessage().getGroupId());
 			}
+		} else if (e instanceof IntroductionRequestReceivedEvent) {
+			ContactId c = ((IntroductionRequestReceivedEvent) e).getContactId();
+			showIntroductionNotifications(c);
+		} else if (e instanceof IntroductionResponseReceivedEvent) {
+			ContactId c = ((IntroductionResponseReceivedEvent) e).getContactId();
+			showIntroductionNotifications(c);
+		} else if (e instanceof IntroductionSucceededEvent) {
+			Contact c = ((IntroductionSucceededEvent) e).getContact();
+			showIntroductionSucceededNotification(c);
 		}
 	}
 
@@ -335,4 +357,49 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 			}
 		});
 	}
+
+	private void showIntroductionNotifications(final ContactId c) {
+		androidExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					GroupId group = messagingManager.getConversationId(c);
+					showPrivateMessageNotification(group);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	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..0556310213d62d97d2a6aaad5e546dc51d250386 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
@@ -1,9 +1,11 @@
 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.widget.LinearLayoutManager;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -11,23 +13,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 +79,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 +100,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 +168,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 +204,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 +221,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 +233,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 +248,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 +283,36 @@ 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(ConversationItem.from(h));
+		}
+		long duration = System.currentTimeMillis() - now;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Loading message headers took " + duration + " ms");
+
+		now = System.currentTimeMillis();
+		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..ae09f0a416a405e78eb7225816b5b411a6450688 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,12 +75,18 @@ 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());
@@ -76,6 +96,9 @@ public class ConversationActivity extends BriarActivity
 	@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 +108,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 +126,21 @@ public class ConversationActivity extends BriarActivity
 
 		setContentView(R.layout.activity_conversation);
 
-		adapter = new ConversationAdapter(this);
+		// Custom Toolbar
+		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);
+		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 +163,7 @@ public class ConversationActivity extends BriarActivity
 		eventBus.addListener(this);
 		notificationManager.blockNotification(groupId);
 		notificationManager.clearPrivateMessageNotification(groupId);
-		loadContactDetails();
-		loadHeaders();
+		loadData();
 	}
 
 	@Override
@@ -141,12 +178,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 +190,19 @@ 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.startActivity(this, intent, options.toBundle());
+				return true;
 			case R.id.action_social_remove_person:
 				askToRemoveContact();
 				return true;
@@ -163,20 +211,26 @@ public class ConversationActivity extends BriarActivity
 		}
 	}
 
-	private void loadContactDetails() {
+	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 +244,44 @@ 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.setContactInformation(contactIdenticonKey, contactName);
 			}
 		});
 	}
 
-	private void loadHeaders() {
+	private void loadMessages() {
 		runOnDbThread(new Runnable() {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
+					if (contactId == null)
+						contactId = messagingManager.getContactId(groupId);
 					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 +292,38 @@ 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 =
+								(ConversationMessageItem) ConversationItem
+										.from(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,14 +355,14 @@ 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));
+						list.scrollToPosition(adapter.getItemCount() - 1);
 						return;
 					}
 				}
@@ -288,12 +370,24 @@ public class ConversationActivity extends BriarActivity
 		});
 	}
 
+	private void addIntroduction(final ConversationItem item) {
+		runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				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 +401,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 +427,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 +456,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 +482,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 +499,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 +513,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 +541,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 +583,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);
@@ -478,6 +596,10 @@ public class ConversationActivity extends BriarActivity
 		runOnDbThread(new Runnable() {
 			public void run() {
 				try {
+					// make sure contactId is initialised
+					if (contactId == null)
+						contactId = messagingManager.getContactId(groupId);
+					// remove contact with that ID
 					contactManager.removeContact(contactId);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
@@ -500,4 +622,73 @@ 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() {
+				long timestamp = System.currentTimeMillis();
+				timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
+				try {
+					if (accept) {
+						introductionManager
+								.acceptIntroduction(contactId, sessionId,
+										timestamp);
+					} else {
+						introductionManager
+								.declineIntroduction(contactId, sessionId,
+										timestamp);
+					}
+					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..e4c297788918c911d2b6c7ff9e620d795c42bd07 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,103 @@ 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;
 	}
 
-	public void setIdenticonKey(byte[] key) {
-		this.identiconKey = key;
+	public void setContactInformation(byte[] identiconKey, String contactName) {
+		this.identiconKey = identiconKey;
+		this.contactName = contactName;
 		notifyDataSetChanged();
 	}
 
 	@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(ViewHolder ui, int position) {
 		ConversationItem item = getItem(position);
+		if (item instanceof ConversationMessageItem) {
+			bindMessage((MessageHolder) ui, (ConversationMessageItem) item);
+		} 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);
+		} else if (item instanceof ConversationNoticeInItem) {
+			bindNotice((NoticeHolder) ui, (ConversationNoticeInItem) item);
+		} else {
+			throw new IllegalArgumentException("Unhandled Conversation Item");
+		}
+	}
+
+	private void bindMessage(MessageHolder ui, ConversationMessageItem item) {
+
 		PrivateMessageHeader header = item.getHeader();
 
-		if (header.isLocal()) {
-			if (item.isSeen()) {
-				ui.status.setImageResource(R.drawable.message_delivered);
-			} else if (item.isSent()) {
-				ui.status.setImageResource(R.drawable.message_sent);
+		if (item instanceof ConversationItem.OutgoingItem) {
+			if (((OutgoingItem) item).isSeen()) {
+				ui.status.setImageResource(R.drawable.message_delivered_white);
+			} else if (((OutgoingItem) item).isSent()) {
+				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 +187,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 +203,171 @@ class ConversationAdapter extends
 		ui.date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
 	}
 
+	private void bindIntroduction(IntroductionHolder ui,
+			final ConversationIntroductionItem 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().contactExists()) {
+				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(NoticeHolder ui, ConversationNoticeItem item) {
+
+		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 void add(final ConversationItem message) {
-		this.messages.add(message);
+	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 void clear() {
-		this.messages.beginBatchedUpdates();
+	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;
+	}
 
-		while(messages.size() != 0) {
-			messages.removeItemAt(0);
+	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;
+	}
 
-		this.messages.endBatchedUpdates();
+	public void add(final ConversationItem message) {
+		this.items.add(message);
+	}
+
+	public void clear() {
+		items.clear();
 	}
 
-	// TODO: Does this class need to be public?
-	public static class MessageHolder extends RecyclerView.ViewHolder {
+	private static class MessageHolder extends RecyclerView.ViewHolder {
 
 		public ViewGroup layout;
 		public TextView body;
@@ -217,4 +390,59 @@ class ConversationAdapter extends
 			}
 		}
 	}
-}
\ No newline at end of file
+
+	private static 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);
+			}
+		}
+	}
+
+	private static 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..fb891d0a1b3b5bb84bac329c397587bfa5cc44ea
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java
@@ -0,0 +1,32 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.introduction.IntroductionRequest;
+import org.briarproject.api.sync.MessageId;
+
+public class ConversationIntroductionInItem extends ConversationIntroductionItem
+		implements ConversationItem.IncomingItem {
+
+	private boolean read;
+
+	public ConversationIntroductionInItem(IntroductionRequest ir) {
+		super(ir);
+
+		this.read = ir.isRead();
+	}
+
+	@Override
+	int getType() {
+		return INTRODUCTION_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/ConversationIntroductionItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..e955ea3a477061a32928660fceb4e4023527193f
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java
@@ -0,0 +1,29 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.introduction.IntroductionRequest;
+
+abstract class ConversationIntroductionItem extends ConversationItem {
+
+	private IntroductionRequest ir;
+	private boolean answered;
+
+	public ConversationIntroductionItem(IntroductionRequest ir) {
+		super(ir.getMessageId(), ir.getTime());
+
+		this.ir = ir;
+		this.answered = ir.wasAnswered();
+	}
+
+	public IntroductionRequest getIntroductionRequest() {
+		return ir;
+	}
+
+	public boolean wasAnswered() {
+		return answered;
+	}
+
+	public void setAnswered(boolean answered) {
+		this.answered = answered;
+	}
+
+}
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..a2aba398f93dee2f6305b346b2d60ad206dff389
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java
@@ -0,0 +1,47 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.introduction.IntroductionRequest;
+
+/**
+ * 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 ConversationIntroductionItem
+		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..2c1492a8c3ee05706023f12dfa415a589272fb7b 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java
@@ -1,46 +1,114 @@
 package org.briarproject.android.contact;
 
+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.messaging.PrivateMessageHeader;
+import org.briarproject.api.sync.MessageId;
 
 // This class is not thread-safe
-class ConversationItem {
+public abstract class ConversationItem {
+
+	// this is needed for RecyclerView adapter which requires an int type
+	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(PrivateMessageHeader h) {
+		if (h.isLocal())
+			return new ConversationMessageOutItem(h);
+		else
+			return new ConversationMessageInItem(h);
 	}
 
-	boolean isSent() {
-		return sent;
+	public static ConversationItem from(IntroductionRequest ir) {
+		if (ir.isLocal()) {
+			return new ConversationIntroductionOutItem(ir);
+		} else {
+			return new ConversationIntroductionInItem(ir);
+		}
 	}
 
-	void setSent(boolean sent) {
-		this.sent = 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());
+		}
 	}
 
-	boolean isSeen() {
-		return seen;
+	/** 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());
 	}
 
-	void setSeen(boolean seen) {
-		this.seen = seen;
+	protected interface OutgoingItem {
+		MessageId getId();
+		boolean isSent();
+		void setSent(boolean sent);
+		boolean isSeen();
+		void setSeen(boolean seen);
 	}
+
+	protected interface IncomingItem {
+		MessageId getId();
+		boolean isRead();
+		void setRead(boolean read);
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..c24f86a1288376748fc0a8ba05390047d53368c2
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java
@@ -0,0 +1,32 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.messaging.PrivateMessageHeader;
+
+// This class is not thread-safe
+public class ConversationMessageInItem extends ConversationMessageItem
+		implements ConversationItem.IncomingItem {
+
+	private boolean read;
+
+	public ConversationMessageInItem(PrivateMessageHeader header) {
+		super(header);
+
+		read = header.isRead();
+	}
+
+	@Override
+	int getType() {
+		return MSG_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/ConversationMessageItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..db780efbcad65a42a7f0654d0b63666fedf8f6d5
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java
@@ -0,0 +1,30 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.messaging.PrivateMessageHeader;
+
+// This class is not thread-safe
+abstract class ConversationMessageItem extends ConversationItem {
+
+	private final PrivateMessageHeader header;
+	private byte[] body;
+
+	public ConversationMessageItem(PrivateMessageHeader header) {
+		super(header.getId(), header.getTimestamp());
+
+		this.header = header;
+		body = null;
+	}
+
+	PrivateMessageHeader getHeader() {
+		return header;
+	}
+
+	byte[] getBody() {
+		return body;
+	}
+
+	void setBody(byte[] body) {
+		this.body = body;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..cfdf871afb2421b58a2dce072bbff583b4d0083e
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java
@@ -0,0 +1,43 @@
+package org.briarproject.android.contact;
+
+import org.briarproject.api.messaging.PrivateMessageHeader;
+
+// This class is not thread-safe
+public class ConversationMessageOutItem extends ConversationMessageItem
+		implements ConversationItem.OutgoingItem {
+
+	private boolean sent, seen;
+
+	public ConversationMessageOutItem(PrivateMessageHeader header) {
+		super(header);
+
+		sent = header.isSent();
+		seen = header.isSeen();
+	}
+
+	@Override
+	int getType() {
+		return MSG_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/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..ec5c276797df3a8665246396f7b3f721d1824892
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
@@ -0,0 +1,238 @@
+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.Fade;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+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.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) {
+							throw new RuntimeException("c1 not initialized");
+						}
+						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(ConversationItem.from(h));
+		}
+		long duration = System.currentTimeMillis() - now;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Loading message headers took " + duration + " ms");
+
+		now = System.currentTimeMillis();
+		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..2faee1a8ab22b30d7757d91702bca976a796eb6a
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java
@@ -0,0 +1,109 @@
+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);
+		if (contactId == -1)
+			throw new IllegalArgumentException("Wrong ContactId");
+
+		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..ed0547da4e0b9eb8b69e7c8d6ea7433ba392e0b6
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/introduction/IntroductionMessageFragment.java
@@ -0,0 +1,229 @@
+package org.briarproject.android.introduction;
+
+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 = "IntroductionMessageFragment";
+	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
+	@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) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	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) {
+		// disable button to prevent accidental double invitations
+		ui.button.setEnabled(false);
+
+		String msg = ui.message.getText().toString();
+		makeIntroduction(c1, c2, msg);
+
+		// don't wait for the introduction to be made before finishing activity
+		introductionActivity.hideSoftKeyboard(ui.message);
+		introductionActivity.finish();
+	}
+
+	private void makeIntroduction(final Contact c1, final Contact c2,
+			final String msg) {
+		introductionActivity.runOnDbThread(new Runnable() {
+			public void run() {
+				// actually make the introduction
+				try {
+					long timestamp = System.currentTimeMillis();
+					introductionManager.makeIntroduction(c1, c2, msg, timestamp);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					introductionError();
+				} catch (FormatException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					introductionError();
+				}
+			}
+		});
+	}
+
+	private void introductionError() {
+		introductionActivity.runOnUiThread(new Runnable() {
+			public void run() {
+				Toast.makeText(introductionActivity,
+						R.string.introduction_error, Toast.LENGTH_SHORT)
+						.show();
+			}
+		});
+	}
+
+	private static 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();
 			}
 		};
 	}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java
index 882ccbf112a67be92c15f711fbc869384a3cb029..18c5da1c691ef626deb5634194cde763a1730b1f 100644
--- a/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java
@@ -8,6 +8,7 @@ import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
 import java.util.Collection;
@@ -20,19 +21,22 @@ public interface IntroductionManager {
 	/**
 	 * sends two initial introduction messages
 	 */
-	void makeIntroduction(Contact c1, Contact c2, String msg)
+	void makeIntroduction(Contact c1, Contact c2, String msg,
+			final long timestamp)
 			throws DbException, FormatException;
 
 	/**
 	 * Accept an introduction that had been made
 	 */
-	void acceptIntroduction(final SessionId sessionId)
+	void acceptIntroduction(final ContactId contactId,
+			final SessionId sessionId, final long timestamp)
 			throws DbException, FormatException;
 
 	/**
 	 * Decline an introduction that had been made
 	 */
-	void declineIntroduction(final SessionId sessionId)
+	void declineIntroduction(final ContactId contactId,
+			final SessionId sessionId, final long timestamp)
 			throws DbException, FormatException;
 
 	/**
@@ -46,8 +50,8 @@ public interface IntroductionManager {
 
 
 	/** Get the session state for the given session ID */
-	BdfDictionary getSessionState(Transaction txn, byte[] sessionId)
-			throws DbException, FormatException;
+	BdfDictionary getSessionState(Transaction txn, GroupId groupId,
+			byte[] sessionId) throws DbException, FormatException;
 
 	/** Gets the group used for introductions with Contact c */
 	Group getIntroductionGroup(Contact c);
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java
index facd7151fbf564b1afe1f1327c3b119781416dfe..227c0500ce5b2f6cc459c87a9104407a068076b0 100644
--- a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java
@@ -29,7 +29,7 @@ public class IntroductionRequest extends IntroductionResponse {
 		return answered;
 	}
 
-	public boolean doesExist() {
+	public boolean contactExists() {
 		return exists;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java b/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java
index 8b63ae612bc4705e779b44e503e57e810fed4ccf..38fedd9fb76d9c19083296134fb391ef2f90e86e 100644
--- a/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java
+++ b/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java
@@ -20,6 +20,14 @@ public interface TransportPropertyManager {
 	Map<TransportId, TransportProperties> getLocalProperties()
 			throws DbException;
 
+	/**
+	 * Returns the local transport properties for all transports.
+	 * <br/>
+	 * Read-Only
+	 * */
+	Map<TransportId, TransportProperties> getLocalProperties(Transaction txn)
+			throws DbException;
+
 	/** Returns the local transport properties for the given transport. */
 	TransportProperties getLocalProperties(TransportId t) throws DbException;
 
diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
index 6f76540d081a4ef7baf314ead7d9709eccb4c664..bc1e3d5f95c9f4b4431d0aa6fedeea318d84c7ba 100644
--- a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
+++ b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
@@ -109,6 +109,7 @@ public class IntroduceeEngine
 					msg.put(E_PUBLIC_KEY, localState.getRaw(OUR_PUBLIC_KEY));
 					msg.put(TRANSPORT, localAction.getDictionary(TRANSPORT));
 				}
+				msg.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME));
 				messages.add(msg);
 				logAction(currentState, localState, msg);
 
diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java
index be16ff1e495206ec06006de66e8e4708b808a19c..64af33b44d59cd2f8ff89d194cefb9fb7e97ae7d 100644
--- a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java
+++ b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java
@@ -28,6 +28,7 @@ import org.briarproject.api.introduction.IntroductionManager;
 import org.briarproject.api.introduction.SessionId;
 import org.briarproject.api.properties.TransportProperties;
 import org.briarproject.api.properties.TransportPropertyManager;
+import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
@@ -51,6 +52,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_K
 import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
 import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER;
 import static org.briarproject.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
 import static org.briarproject.api.introduction.IntroductionConstants.NAME;
 import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
 import static org.briarproject.api.introduction.IntroductionConstants.OUR_PRIVATE_KEY;
@@ -157,11 +159,15 @@ class IntroduceeManager {
 		processStateUpdate(txn, engine.onMessageReceived(state, message));
 	}
 
-	public void acceptIntroduction(Transaction txn,
-			final SessionId sessionId) throws DbException, FormatException {
+	public void acceptIntroduction(Transaction txn, final ContactId contactId,
+			final SessionId sessionId, final long timestamp)
+			throws DbException, FormatException {
+
+		Contact c = db.getContact(txn, contactId);
+		Group g = introductionManager.getIntroductionGroup(c);
 
-		BdfDictionary state =
-				introductionManager.getSessionState(txn, sessionId.getBytes());
+		BdfDictionary state = introductionManager
+				.getSessionState(txn, g.getId(), sessionId.getBytes());
 
 		// get data to connect and derive a shared secret later
 		long now = clock.currentTimeMillis();
@@ -169,7 +175,7 @@ class IntroduceeManager {
 		byte[] publicKey = keyPair.getPublic().getEncoded();
 		byte[] privateKey = keyPair.getPrivate().getEncoded();
 		Map<TransportId, TransportProperties> transportProperties =
-				transportPropertyManager.getLocalProperties();
+				transportPropertyManager.getLocalProperties(txn);
 
 		// update session state for later
 		state.put(ACCEPT, true);
@@ -182,17 +188,22 @@ class IntroduceeManager {
 		localAction.put(TYPE, TYPE_RESPONSE);
 		localAction.put(TRANSPORT,
 				encodeTransportProperties(transportProperties));
+		localAction.put(MESSAGE_TIME, timestamp);
 
 		// start engine and process its state update
 		IntroduceeEngine engine = new IntroduceeEngine();
 		processStateUpdate(txn, engine.onLocalAction(state, localAction));
 	}
 
-	public void declineIntroduction(Transaction txn, final SessionId sessionId)
+	public void declineIntroduction(Transaction txn, final ContactId contactId,
+			final SessionId sessionId, final long timestamp)
 			throws DbException, FormatException {
 
-		BdfDictionary state =
-				introductionManager.getSessionState(txn, sessionId.getBytes());
+		Contact c = db.getContact(txn, contactId);
+		Group g = introductionManager.getIntroductionGroup(c);
+
+		BdfDictionary state = introductionManager
+				.getSessionState(txn, g.getId(), sessionId.getBytes());
 
 		// update session state
 		state.put(ACCEPT, false);
@@ -200,6 +211,7 @@ class IntroduceeManager {
 		// define action
 		BdfDictionary localAction = new BdfDictionary();
 		localAction.put(TYPE, TYPE_RESPONSE);
+		localAction.put(MESSAGE_TIME, timestamp);
 
 		// start engine and process its state update
 		IntroduceeEngine engine = new IntroduceeEngine();
diff --git a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
index fb88c28df61de2cb1d8bb1a1eedbd0e2cf62a14e..796d528f1d09b5fb9b49963fef8a732efbcb25de 100644
--- a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
+++ b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
@@ -56,6 +56,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1
 import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2;
 import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
 import static org.briarproject.api.introduction.IntroductionConstants.STATE;
+import static org.briarproject.api.introduction.IntroductionConstants.TIME;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
@@ -104,6 +105,7 @@ public class IntroducerEngine
 				if (localAction.containsKey(MSG)) {
 					msg1.put(MSG, localAction.getString(MSG));
 				}
+				msg1.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME));
 				messages.add(msg1);
 				logLocalAction(currentState, localState, msg1);
 				BdfDictionary msg2 = new BdfDictionary();
@@ -115,6 +117,7 @@ public class IntroducerEngine
 				if (localAction.containsKey(MSG)) {
 					msg2.put(MSG, localAction.getString(MSG));
 				}
+				msg2.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME));
 				messages.add(msg2);
 				logLocalAction(currentState, localState, msg2);
 
diff --git a/briar-core/src/org/briarproject/introduction/IntroducerManager.java b/briar-core/src/org/briarproject/introduction/IntroducerManager.java
index 3cc906aedf32c085c132acd818a1152a773f19e2..93481367df01a89508d541d4ca6be4b21072288d 100644
--- a/briar-core/src/org/briarproject/introduction/IntroducerManager.java
+++ b/briar-core/src/org/briarproject/introduction/IntroducerManager.java
@@ -30,6 +30,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID
 import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
 import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1;
 import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2;
+import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
 import static org.briarproject.api.introduction.IntroductionConstants.MSG;
 import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1;
 import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2;
@@ -38,6 +39,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRO
 import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
 import static org.briarproject.api.introduction.IntroductionConstants.STATE;
 import static org.briarproject.api.introduction.IntroductionConstants.STORAGE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.TIME;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
@@ -99,7 +101,7 @@ class IntroducerManager {
 	}
 
 	public void makeIntroduction(Transaction txn, Contact c1, Contact c2,
-			String msg) throws DbException, FormatException {
+			String msg, long timestamp) throws DbException, FormatException {
 
 		// TODO check for existing session with those contacts?
 		//      deny new introduction under which conditions?
@@ -115,6 +117,7 @@ class IntroducerManager {
 		}
 		localAction.put(PUBLIC_KEY1, c1.getAuthor().getPublicKey());
 		localAction.put(PUBLIC_KEY2, c2.getAuthor().getPublicKey());
+		localAction.put(MESSAGE_TIME, timestamp);
 
 		// start engine and process its state update
 		IntroducerEngine engine = new IntroducerEngine();
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
index e28af29e4d1c0ffbc886e342739d6afc977d3b98..edec27bef53a4d1042639557c96d65f03f4bca06 100644
--- a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
@@ -44,7 +44,6 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.logging.Logger;
 
@@ -62,6 +61,8 @@ import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID
 import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
 import static org.briarproject.api.introduction.IntroductionConstants.EXISTS;
 import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2;
 import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
 import static org.briarproject.api.introduction.IntroductionConstants.MSG;
 import static org.briarproject.api.introduction.IntroductionConstants.NAME;
@@ -227,8 +228,8 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 		else if (type == TYPE_RESPONSE || type == TYPE_ACK || type == TYPE_ABORT) {
 			BdfDictionary state;
 			try {
-				state = getSessionState(txn,
-						message.getRaw(SESSION_ID, new byte[0]));
+				state = getSessionState(txn, groupId,
+						message.getRaw(SESSION_ID));
 			} catch (FormatException e) {
 				LOG.warning("Could not find state for message, deleting...");
 				deleteMessage(txn, m.getId());
@@ -266,12 +267,13 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 	}
 
 	@Override
-	public void makeIntroduction(Contact c1, Contact c2, String msg)
+	public void makeIntroduction(Contact c1, Contact c2, String msg,
+			final long timestamp)
 			throws DbException, FormatException {
 
 		Transaction txn = db.startTransaction(false);
 		try {
-			introducerManager.makeIntroduction(txn, c1, c2, msg);
+			introducerManager.makeIntroduction(txn, c1, c2, msg, timestamp);
 			txn.setComplete();
 		} finally {
 			db.endTransaction(txn);
@@ -279,12 +281,14 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 	}
 
 	@Override
-	public void acceptIntroduction(final SessionId sessionId)
+	public void acceptIntroduction(final ContactId contactId,
+			final SessionId sessionId, final long timestamp)
 			throws DbException, FormatException {
 
 		Transaction txn = db.startTransaction(false);
 		try {
-			introduceeManager.acceptIntroduction(txn, sessionId);
+			introduceeManager
+					.acceptIntroduction(txn, contactId, sessionId, timestamp);
 			txn.setComplete();
 		} finally {
 			db.endTransaction(txn);
@@ -292,12 +296,14 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 	}
 
 	@Override
-	public void declineIntroduction(final SessionId sessionId)
+	public void declineIntroduction(final ContactId contactId,
+			final SessionId sessionId, final long timestamp)
 			throws DbException, FormatException {
 
 		Transaction txn = db.startTransaction(false);
 		try {
-			introduceeManager.declineIntroduction(txn, sessionId);
+			introduceeManager
+					.declineIntroduction(txn, contactId, sessionId, timestamp);
 			txn.setComplete();
 		} finally {
 			db.endTransaction(txn);
@@ -322,8 +328,6 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 			statuses = db.getMessageStatus(txn, contactId, g);
 
 			// turn messages into classes for the UI
-			Map<SessionId, BdfDictionary> sessionStates =
-					new HashMap<SessionId, BdfDictionary>();
 			for (MessageStatus s : statuses) {
 				MessageId messageId = s.getMessageId();
 				BdfDictionary msg = metadata.get(messageId);
@@ -335,11 +339,8 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 
 					// get session state
 					SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID));
-					BdfDictionary state = sessionStates.get(sessionId);
-					if (state == null) {
-						state = getSessionState(txn, sessionId.getBytes());
-					}
-					sessionStates.put(sessionId, state);
+					BdfDictionary state =
+							getSessionState(txn, g, sessionId.getBytes());
 
 					boolean local;
 					long time = msg.getLong(MESSAGE_TIME);
@@ -453,19 +454,31 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 		}
 	}
 
-	public BdfDictionary getSessionState(Transaction txn, byte[] sessionId)
-			throws DbException, FormatException {
+	public BdfDictionary getSessionState(Transaction txn, GroupId groupId,
+			byte[] sessionId) throws DbException, FormatException {
 
 		try {
-			return clientHelper.getMessageMetadataAsDictionary(txn,
-					new MessageId(sessionId));
+			// See if we can find the state directly for the introducer
+			BdfDictionary state = clientHelper
+					.getMessageMetadataAsDictionary(txn,
+							new MessageId(sessionId));
+			GroupId g1 = new GroupId(state.getRaw(GROUP_ID_1));
+			GroupId g2 = new GroupId(state.getRaw(GROUP_ID_2));
+			if (!g1.equals(groupId) && !g2.equals(groupId)) {
+				throw new NoSuchMessageException();
+			}
+			return state;
 		} catch (NoSuchMessageException e) {
+			// State not found directly, so iterate over all states
+			// to find state for introducee
 			Map<MessageId, BdfDictionary> map = clientHelper
 					.getMessageMetadataAsDictionary(txn,
 							localGroup.getId());
 			for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
 				if (Arrays.equals(m.getValue().getRaw(SESSION_ID), sessionId)) {
-					return m.getValue();
+					BdfDictionary state = m.getValue();
+					GroupId g = new GroupId(state.getRaw(GROUP_ID));
+					if (g.equals(groupId)) return state;
 				}
 			}
 			if (LOG.isLoggable(WARNING)) {
@@ -492,9 +505,10 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
 		byte[] body = clientHelper.toByteArray(bdfList);
 		GroupId groupId = new GroupId(message.getRaw(GROUP_ID));
 		Group group = db.getGroup(txn, groupId);
-		long timestamp = System.currentTimeMillis();
-
+		long timestamp =
+				message.getLong(MESSAGE_TIME, System.currentTimeMillis());
 		message.put(MESSAGE_TIME, timestamp);
+
 		Metadata metadata = metadataEncoder.encode(message);
 
 		messageQueueManager
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
index 5663b6151a823399eaf800f8031bdf2dfd281ea8..71c93b4fd23a84588f59afc557bfec1d0915336a 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
@@ -108,6 +108,27 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		return Collections.unmodifiableMap(local);
 	}
 
+	@Override
+	public Map<TransportId, TransportProperties> getLocalProperties(
+			Transaction txn) throws DbException {
+		try {
+			Map<TransportId, TransportProperties> local =
+					new HashMap<TransportId, TransportProperties>();
+			// Find the latest local update for each transport
+			Map<TransportId, LatestUpdate> latest = findLatest(txn,
+					localGroup.getId(), true);
+			// Retrieve and parse the latest local properties
+			for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
+				BdfList message = clientHelper.getMessageAsList(txn,
+						e.getValue().messageId);
+				local.put(e.getKey(), parseProperties(message));
+			}
+			return local;
+		} catch (FormatException e) {
+			throw new DbException(e);
+		}
+	}
+
 	@Override
 	public TransportProperties getLocalProperties(TransportId t)
 			throws DbException {
@@ -212,26 +233,6 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
 	}
 
-	private Map<TransportId, TransportProperties> getLocalProperties(
-			Transaction txn) throws DbException {
-		try {
-			Map<TransportId, TransportProperties> local =
-					new HashMap<TransportId, TransportProperties>();
-			// Find the latest local update for each transport
-			Map<TransportId, LatestUpdate> latest = findLatest(txn,
-					localGroup.getId(), true);
-			// Retrieve and parse the latest local properties
-			for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
-				BdfList message = clientHelper.getMessageAsList(txn,
-						e.getValue().messageId);
-				local.put(e.getKey(), parseProperties(message));
-			}
-			return local;
-		} catch (FormatException e) {
-			throw new DbException(e);
-		}
-	}
-
 	private void storeMessage(Transaction txn, GroupId g, TransportId t,
 			TransportProperties p, long version, boolean local, boolean shared)
 			throws DbException {