diff --git a/briar-android/res/drawable-hdpi/msg_in_unread.9.png b/briar-android/res/drawable-hdpi/msg_in_unread.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..c22cc86322f710ab37c0f56c192f9721271f58d9
Binary files /dev/null and b/briar-android/res/drawable-hdpi/msg_in_unread.9.png differ
diff --git a/briar-android/res/drawable-mdpi/msg_in_unread.9.png b/briar-android/res/drawable-mdpi/msg_in_unread.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e5418856d9fb9ed59be30ccbb013e277ed6345e
Binary files /dev/null and b/briar-android/res/drawable-mdpi/msg_in_unread.9.png differ
diff --git a/briar-android/res/drawable-xhdpi/msg_in.9.png b/briar-android/res/drawable-xhdpi/msg_in.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..f5db8372dda83faf1082e8700e0c2996c6801fa6
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/msg_in.9.png differ
diff --git a/briar-android/res/drawable-xhdpi/msg_in_unread.9.png b/briar-android/res/drawable-xhdpi/msg_in_unread.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..341ec4f7226f0362bf1cd5d5f354f06f5666845a
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/msg_in_unread.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
new file mode 100644
index 0000000000000000000000000000000000000000..d7c2816f1339f91a2abe9fa4bd1b72b05a5b5f47
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/msg_out.9.png differ
diff --git a/briar-android/res/drawable-xxhdpi/msg_in_unread.9.png b/briar-android/res/drawable-xxhdpi/msg_in_unread.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a3bb3e7e65841e35b46dbd4c4fc2a7584bf2761
Binary files /dev/null and b/briar-android/res/drawable-xxhdpi/msg_in_unread.9.png differ
diff --git a/briar-android/res/layout/activity_conversation.xml b/briar-android/res/layout/activity_conversation.xml
index 8bc064d2ad71fef07556fac587025506633e6583..673dd14f9fe1585eb2862fdeea703a48fc5f628d 100644
--- a/briar-android/res/layout/activity_conversation.xml
+++ b/briar-android/res/layout/activity_conversation.xml
@@ -1,12 +1,18 @@
 <?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:id="@+id/layout"
 	android:orientation="vertical"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">
 
-	<!-- ListView will get inserted here -->
+	<android.support.v7.widget.RecyclerView
+		android:id="@+id/conversationView"
+		android:layout_width="match_parent"
+		android:layout_height="0dp"
+		android:layout_weight="1"
+		android:scrollbars="vertical"/>
 
 	<ProgressBar
 		android:id="@+id/listLoadingProgressBar"
@@ -15,7 +21,8 @@
 		android:layout_gravity="center"
 		android:gravity="center"
 		android:layout_weight="1"
-		android:indeterminate="true"/>
+		android:indeterminate="true"
+		android:visibility="gone"/>
 
 	<TextView
 		android:id="@+id/emptyView"
@@ -26,7 +33,8 @@
 		android:gravity="center"
 		android:padding="@dimen/margin_large"
 		android:textSize="@dimen/text_size_large"
-		android:text="@string/no_private_messages"/>
+		android:text="@string/no_private_messages"
+		android:visibility="gone"/>
 
 	<View
 		android:layout_width="match_parent"
diff --git a/briar-android/res/layout/list_item_msg_in.xml b/briar-android/res/layout/list_item_msg_in.xml
index 8194f5453a9efc4493af990f207dcc2c7f4681c4..d93d269e7796bbbc767bc020dd647888b9b1927c 100644
--- a/briar-android/res/layout/list_item_msg_in.xml
+++ b/briar-android/res/layout/list_item_msg_in.xml
@@ -11,6 +11,7 @@
 	android:paddingBottom="@dimen/margin_small">
 
 	<RelativeLayout
+		android:id="@+id/msgLayout"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_gravity="left|start"
diff --git a/briar-android/res/layout/list_item_msg_out.xml b/briar-android/res/layout/list_item_msg_out.xml
index baacbb39fc9a7864ff1336c1090cca744c3959a8..a82a07f1e058be89c27bbaa346025775ac888005 100644
--- a/briar-android/res/layout/list_item_msg_out.xml
+++ b/briar-android/res/layout/list_item_msg_out.xml
@@ -11,6 +11,7 @@
 	android:paddingBottom="@dimen/margin_small">
 
 	<RelativeLayout
+		android:id="@+id/msgLayout"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_gravity="right|end"
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
index 02bc2b83a123e1c278083ca61a30ff500ce20214..d62f6bf638e4156d7dfbc7cff7cb5c0cf3bb7504 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
@@ -2,30 +2,26 @@ package org.briarproject.android.contact;
 
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.content.res.Resources;
 import android.graphics.PorterDuff;
-import android.graphics.drawable.ColorDrawable;
 import android.os.Bundle;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
 import android.widget.EditText;
 import android.widget.ImageButton;
-import android.widget.ListView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.Toast;
 
 import org.briarproject.R;
 import org.briarproject.android.BriarActivity;
-import org.briarproject.android.util.LayoutUtils;
 import org.briarproject.api.android.AndroidNotificationManager;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -77,12 +73,11 @@ 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.ReadPrivateMessageActivity.RESULT_PREV_NEXT;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
 import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED;
 import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT;
 
 public class ConversationActivity extends BriarActivity
-implements EventListener, OnClickListener, OnItemClickListener {
+		implements EventListener, OnClickListener {
 
 	private static final int REQUEST_READ = 2;
 	private static final Logger LOG =
@@ -95,7 +90,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
 	private TextView empty = null;
 	private ProgressBar loading = null;
 	private ConversationAdapter adapter = null;
-	private ListView list = null;
+	private RecyclerView list = null;
 	private EditText content = null;
 	private ImageButton sendButton = null;
 
@@ -133,20 +128,26 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		loading.setVisibility(VISIBLE);
 
 		adapter = new ConversationAdapter(this);
-		list = new ListView(this) {
+		list = (RecyclerView) findViewById(R.id.conversationView);
+		list.setLayoutManager(new LinearLayoutManager(this));
+		list.setAdapter(adapter);
+		list.setVisibility(GONE);
+		// scroll down when opening keyboard
+		list.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
 			@Override
-			public void onSizeChanged(int w, int h, int oldw, int oldh) {
-				// Scroll to the bottom when the keyboard is shown
-				super.onSizeChanged(w, h, oldw, oldh);
-				setSelection(getCount() - 1);
+			public void onLayoutChange(View v,
+					int left, int top, int right, int bottom,
+					int oldLeft, int oldTop, int oldRight, int oldBottom) {
+				if (bottom < oldBottom) {
+					list.postDelayed(new Runnable() {
+						@Override
+						public void run() {
+							list.scrollToPosition(adapter.getItemCount() - 1);
+						}
+					}, 100);
+				}
 			}
-		};
-		list.setLayoutParams(MATCH_WRAP_1);
-		list.setDivider(null);
-		list.setAdapter(adapter);
-		list.setOnItemClickListener(this);
-		list.setEmptyView(loading);
-		layout.addView(list, 0);
+		});
 
 		content = (EditText) findViewById(R.id.contentView);
 		sendButton = (ImageButton) findViewById(R.id.sendButton);
@@ -260,12 +261,10 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				loading.setVisibility(GONE);
-				empty.setVisibility(VISIBLE);
-				list.setEmptyView(empty);
-				displayContactDetails();
 				sendButton.setEnabled(true);
-				adapter.clear();
 				if (!headers.isEmpty()) {
+					list.setVisibility(VISIBLE);
+					empty.setVisibility(GONE);
 					for (PrivateMessageHeader h : headers) {
 						ConversationItem item = new ConversationItem(h);
 						byte[] body = bodyCache.get(h.getId());
@@ -273,11 +272,12 @@ implements EventListener, OnClickListener, OnItemClickListener {
 						else item.setBody(body);
 						adapter.add(item);
 					}
-					adapter.sort(ConversationItemComparator.INSTANCE);
 					// Scroll to the bottom
-					list.setSelection(adapter.getCount() - 1);
+					list.scrollToPosition(adapter.getItemCount() - 1);
+				} else {
+					empty.setVisibility(VISIBLE);
+					list.setVisibility(GONE);
 				}
-				adapter.notifyDataSetChanged();
 			}
 		});
 	}
@@ -306,14 +306,18 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				bodyCache.put(m, body);
-				int count = adapter.getCount();
+				int count = adapter.getItemCount();
+
 				for (int i = 0; i < count; i++) {
 					ConversationItem item = adapter.getItem(i);
+
 					if (item.getHeader().getId().equals(m)) {
 						item.setBody(body);
-						adapter.notifyDataSetChanged();
+						adapter.notifyItemChanged(i);
+
 						// Scroll to the bottom
-						list.setSelection(count - 1);
+						list.scrollToPosition(count - 1);
+
 						return;
 					}
 				}
@@ -326,7 +330,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		super.onActivityResult(request, result, data);
 		if (request == REQUEST_READ && result == RESULT_PREV_NEXT) {
 			int position = data.getIntExtra("briar.POSITION", -1);
-			if (position >= 0 && position < adapter.getCount())
+			if (position >= 0 && position < adapter.getItemCount())
 				displayMessage(position);
 		}
 	}
@@ -341,7 +345,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
 	private void markMessagesRead() {
 		notificationManager.clearPrivateMessageNotification(contactId);
 		List<MessageId> unread = new ArrayList<MessageId>();
-		int count = adapter.getCount();
+		int count = adapter.getItemCount();
 		for (int i = 0; i < count; i++) {
 			PrivateMessageHeader h = adapter.getItem(i).getHeader();
 			if (!h.isRead()) unread.add(h.getId());
@@ -381,6 +385,8 @@ implements EventListener, OnClickListener, OnItemClickListener {
 			GroupId g = ((MessageAddedEvent) e).getGroupId();
 			if (g.equals(groupId)) {
 				LOG.info("Message added, reloading");
+				// TODO: find a way of not needing to reload the entire
+				// conversation just because one message was added
 				loadHeaders();
 			}
 		} else if (e instanceof MessagesSentEvent) {
@@ -417,16 +423,14 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		runOnUiThread(new Runnable() {
 			public void run() {
 				Set<MessageId> messages = new HashSet<MessageId>(messageIds);
-				boolean changed = false;
-				int count = adapter.getCount();
+				int count = adapter.getItemCount();
 				for (int i = 0; i < count; i++) {
 					ConversationItem item = adapter.getItem(i);
 					if (messages.contains(item.getHeader().getId())) {
 						item.setStatus(status);
-						changed = true;
+						adapter.notifyItemChanged(i);
 					}
 				}
-				if (changed) adapter.notifyDataSetChanged();
 			}
 		});
 	}
@@ -444,7 +448,7 @@ implements EventListener, OnClickListener, OnItemClickListener {
 	private long getMinTimestampForNewMessage() {
 		// Don't use an earlier timestamp than the newest message
 		long timestamp = 0;
-		int count = adapter.getCount();
+		int count = adapter.getItemCount();
 		for (int i = 0; i < count; i++) {
 			long t = adapter.getItem(i).getHeader().getTimestamp();
 			if (t > timestamp) timestamp = t;
@@ -485,11 +489,6 @@ implements EventListener, OnClickListener, OnItemClickListener {
 		});
 	}
 
-	public void onItemClick(AdapterView<?> parent, View view, int position,
-			long id) {
-		displayMessage(position);
-	}
-
 	private void displayMessage(int position) {
 		ConversationItem item = adapter.getItem(position);
 		PrivateMessageHeader header = item.getHeader();
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
index 0e2499c7eccbb23a4b5db2c24d08b0ab173df7e1..4b945f7d37cdf84598bb0d5313116b088771160c 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java
@@ -1,11 +1,12 @@
 package org.briarproject.android.contact;
 
 import android.content.Context;
+import android.support.v7.util.SortedList;
+import android.support.v7.widget.RecyclerView;
 import android.text.format.DateUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
 import android.widget.ImageView;
 import android.widget.TextView;
 
@@ -13,57 +14,177 @@ import org.briarproject.R;
 import org.briarproject.api.messaging.PrivateMessageHeader;
 import org.briarproject.util.StringUtils;
 
-import java.util.ArrayList;
-
 import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED;
 import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT;
 
-class ConversationAdapter extends ArrayAdapter<ConversationItem> {
+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 SortedList<ConversationItem> messages =
+			new SortedList<ConversationItem>(ConversationItem.class,
+					new SortedList.Callback<ConversationItem>() {
+						@Override
+						public void onInserted(int position, int count) {
+							notifyItemRangeInserted(position, count);
+						}
+
+						@Override
+						public void onChanged(int position, int count) {
+							notifyItemRangeChanged(position, count);
+						}
+
+						@Override
+						public void onMoved(int fromPosition, int toPosition) {
+							notifyItemMoved(fromPosition, toPosition);
+						}
+
+						@Override
+						public void onRemoved(int position, int count) {
+							notifyItemRangeRemoved(position, count);
+						}
+
+						@Override
+						public int compare(ConversationItem c1,
+								ConversationItem c2) {
+							long time1 = c1.getHeader().getTimestamp();
+							long time2 = c2.getHeader().getTimestamp();
+							if (time1 < time2) return -1;
+							if (time1 > time2) return 1;
+							return 0;
+						}
 
-	ConversationAdapter(Context ctx) {
-		super(ctx, android.R.layout.simple_expandable_list_item_1,
-				new ArrayList<ConversationItem>());
+						@Override
+						public boolean areItemsTheSame(ConversationItem c1,
+								ConversationItem c2) {
+							return c1.getHeader().getId()
+									.equals(c2.getHeader().getId());
+						}
+
+						@Override
+						public boolean areContentsTheSame(ConversationItem c1,
+								ConversationItem c2) {
+							return c1.equals(c2);
+						}
+					});
+	private Context ctx;
+
+	public ConversationAdapter(Context context) {
+		ctx = context;
 	}
 
 	@Override
-	public View getView(int position, View convertView, ViewGroup parent) {
+	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;
+		}
+	}
+
+	@Override
+	public MessageHolder 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);
+		}
+		// incoming message (non-local)
+		else {
+			v = LayoutInflater.from(viewGroup.getContext())
+					.inflate(R.layout.list_item_msg_in, viewGroup, false);
+		}
+
+		return new MessageHolder(v, type);
+	}
+
+	@Override
+	public void onBindViewHolder(final MessageHolder ui, final int position) {
 		ConversationItem item = getItem(position);
 		PrivateMessageHeader header = item.getHeader();
-		Context ctx = getContext();
-
-		LayoutInflater inflater = (LayoutInflater) ctx.getSystemService
-				(Context.LAYOUT_INFLATER_SERVICE);
 
-		View v;
 		if (header.isLocal()) {
-			v = inflater.inflate(R.layout.list_item_msg_out, null);
-
-			ImageView status = (ImageView) v.findViewById(R.id.msgStatus);
 			if (item.getStatus() == DELIVERED) {
-				status.setImageResource(R.drawable.message_delivered);
+				ui.status.setImageResource(R.drawable.message_delivered);
 			} else if (item.getStatus() == SENT) {
-				status.setImageResource(R.drawable.message_sent);
+				ui.status.setImageResource(R.drawable.message_sent);
 			} else {
-				status.setImageResource(R.drawable.message_stored);
+				ui.status.setImageResource(R.drawable.message_stored);
 			}
-		} else {
-			v = inflater.inflate(R.layout.list_item_msg_in, null);
+		} else if (!header.isRead()) {
+			// show unread messages in different color to not miss them
+			ui.layout.setBackgroundResource(R.drawable.msg_in_unread);
 		}
 
-		TextView body = (TextView) v.findViewById(R.id.msgBody);
-
 		if (item.getBody() == null) {
-			body.setText("\u2026");
+			ui.body.setText("\u2026");
 		} else if (header.getContentType().equals("text/plain")) {
-			body.setText(StringUtils.fromUtf8(item.getBody()));
+			ui.body.setText(StringUtils.fromUtf8(item.getBody()));
 		} else {
 			// TODO support other content types
 		}
 
-		TextView date = (TextView) v.findViewById(R.id.msgTime);
 		long timestamp = header.getTimestamp();
-		date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
+		ui.date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
+	}
+
+	@Override
+	public int getItemCount() {
+		return messages == null ? 0 : messages.size();
+	}
+
+	public boolean isEmpty() {
+		return messages == null || messages.size() == 0;
+	}
+
+	public ConversationItem getItem(int position) {
+		return messages.get(position);
+	}
+
+	public void add(final ConversationItem contact) {
+		this.messages.add(contact);
+	}
 
-		return v;
+	public void remove(final ConversationItem contact) {
+		this.messages.remove(contact);
+	}
+
+	public void clear() {
+		this.messages.beginBatchedUpdates();
+
+		while(messages.size() != 0) {
+			messages.removeItemAt(0);
+		}
+
+		this.messages.endBatchedUpdates();
+	}
+
+	public static class MessageHolder extends RecyclerView.ViewHolder {
+		public ViewGroup layout;
+		public TextView body;
+		public TextView date;
+		public ImageView status;
+
+		public MessageHolder(View v, int type) {
+			super(v);
+
+			layout = (ViewGroup) v.findViewById(R.id.msgLayout);
+			body = (TextView) v.findViewById(R.id.msgBody);
+			date = (TextView) v.findViewById(R.id.msgTime);
+
+			// outgoing message (local)
+			if (type == MSG_OUT) {
+				status = (ImageView) v.findViewById(R.id.msgStatus);
+			}
+		}
 	}
 }
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java b/briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java
deleted file mode 100644
index 76eb1d28dd31932cbfdf6e15ce9c235d20b63b43..0000000000000000000000000000000000000000
--- a/briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.briarproject.android.contact;
-
-import java.util.Comparator;
-
-class ConversationItemComparator implements Comparator<ConversationItem> {
-
-	static final ConversationItemComparator INSTANCE =
-			new ConversationItemComparator();
-
-	public int compare(ConversationItem a, ConversationItem b) {
-		// The oldest message comes first
-		long aTime = a.getHeader().getTimestamp();
-		long bTime = b.getHeader().getTimestamp();
-		if (aTime < bTime) return -1;
-		if (aTime > bTime) return 1;
-		return 0;
-	}
-}