From e98d4f2260cf2951a1430b46f7fadb850381b14b Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Wed, 23 Dec 2015 15:33:41 -0200
Subject: [PATCH] Use a RecyclerView for the ConversationView and

properly notify the view adapter of dataset changes
in order to avoid invalidating the entire dataset when not absolutely necessary.

This change also shows unread messages in a different color,
so users do not fail to notice delayed messages.
---
 .../res/drawable-hdpi/msg_in_unread.9.png     | Bin 0 -> 1560 bytes
 .../res/drawable-mdpi/msg_in_unread.9.png     | Bin 0 -> 1014 bytes
 briar-android/res/drawable-xhdpi/msg_in.9.png | Bin 0 -> 1453 bytes
 .../res/drawable-xhdpi/msg_in_unread.9.png    | Bin 0 -> 2374 bytes
 .../res/drawable-xhdpi/msg_out.9.png          | Bin 0 -> 2258 bytes
 .../res/drawable-xxhdpi/msg_in_unread.9.png   | Bin 0 -> 3680 bytes
 .../res/layout/activity_conversation.xml      |  14 +-
 briar-android/res/layout/list_item_msg_in.xml |   1 +
 .../res/layout/list_item_msg_out.xml          |   1 +
 .../android/contact/ConversationActivity.java |  85 +++++----
 .../android/contact/ConversationAdapter.java  | 177 +++++++++++++++---
 .../contact/ConversationItemComparator.java   |  18 --
 12 files changed, 204 insertions(+), 92 deletions(-)
 create mode 100644 briar-android/res/drawable-hdpi/msg_in_unread.9.png
 create mode 100644 briar-android/res/drawable-mdpi/msg_in_unread.9.png
 create mode 100644 briar-android/res/drawable-xhdpi/msg_in.9.png
 create mode 100644 briar-android/res/drawable-xhdpi/msg_in_unread.9.png
 create mode 100644 briar-android/res/drawable-xhdpi/msg_out.9.png
 create mode 100644 briar-android/res/drawable-xxhdpi/msg_in_unread.9.png
 delete mode 100644 briar-android/src/org/briarproject/android/contact/ConversationItemComparator.java

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
GIT binary patch
literal 1560
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
KMNUMnLSTaApWM#?

literal 0
HcmV?d00001

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
GIT binary patch
literal 1014
zcmV<S0}1?zP)<h;3K|Lk000e1NJLTq001HY001Tk1^@s7*3o%F00009a7bBm000XU
z000XU0RWnu7ytkRs7XXYR9HvVSWRmaK^UHiUuha*#fx4<kXCTpdMF`C5k>HeErtF7
zZ^5w?3cV;r6hypuuqc$WMS3WRhTeoA(o#r@htiXW2vr3A0V-`%+*X_QeYd;o?#|3+
zHcbQvmLxOpJoC&m@5gKi5i|;YR=cLt>gb?_<LPIwX#zfJ_2S8={t4Jgh9)P+xXyV^
zuZgg|K5AKjJw0tNfX&9@aJUZ3G&kH%BuWq`-9136=Z-%h+3bD)443%dzK{X8xVvQ$
zcaI^$$~L#(KQP4DKGe_Gw?i~tDvT$Ftq$%A8^>vR`5vcM05g|+=oy@?6otn268)4W
z-WkS@bh{ZsPRQumuuGc$MQ7}Eq+lgY{}^c?8pM+htBFdYqHe2tE`U9*VE7%jjbm-_
z1VDU~f5Z2m?={_$84!>r+KU}D-p`~*96i9D6rNr<>gr(!wo|>*^@NNL1|qZ`0FDQQ
z9RfEo2|4w~FofqQ0^~HolmOoH$q5P-61I)dd+X+x5WF;A#u?N6IVDUB$L=zAMwzZ-
z=!J54;%?##d8^~G2jDa5iXWPYlT7f|Uz?Sp*kk)D#X}{-fP;!|7Z^*HWx|5<9H|F@
z<F1`hSQzw2sT9w(i_T^$bY$Cs@Ks{suE8+@QPcB=@l}xo@39B~r{WbYRUnT^cK29g
z0|a6x1;4C?g;~|r{dOJ`NaI@rb8{mA^dc@$Kv)2(0?;jMaq(3gAntS!=vtw0XHOu`
zWML7M0k_U$VMSFYww){h*GRcsh|&ebe+dD&hBnfqbQzov#t)*601T7THss<hOiF>0
z5$8mjX-g>;XGj0#z`r0?SkN%Jl+QD(v-1YYWV&f0fvH%jS+$x;*4Ha^b#?aq%*=Dh
zIQFFiCrd9DB4p6`@Z!Y82a-x1lU^?)C;{mt>FDSqxm;hlr)T`q*ceu=A5tfgVF8gg
z3M7=aU&jb>5YIJ3Z<Urt0ahbIL{aka?Dv^w>~OCbN<M;43Wlk-`TY1^7nIG9B;pb%
zjf^}Zy}hHYA|Z{Me@VdatlUQ76(m_29)8?v75obeb3BxEQ=gi;eVwtcMk;b6M<~6E
zV<Co{>u0R$Hldi6JuaEZ`in0I9-%_v=2gae<1V0TuN1z{+sa0NrTASHIIdk&ZX|IR
z71{rvM1ZwPR#xVkp#b98cuo^ujAZu_xN#-632BG9N8y1#VC>sh+y)S29iDG6c@zE5
kyP-j6oC+LwVHJD&2RE55#s2gDG5`Po07*qoM6N<$f>(dd*Z=?k

literal 0
HcmV?d00001

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
GIT binary patch
literal 1453
zcmV;e1ycHnP)<h;3K|Lk000e1NJLTq002S&002q|1^@s64+nyU00004XF*Lt006JZ
zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU%KS@MERCwC#
zoC|CeMHt6t_I1ZoibRYW6G@0c@sVms35wz?Vll>O4KW%?uxN}_3n3P(G=f-Pr=d19
zl%_y>vDH3^QUjuaC?Tndnkd!;Q6Y^QCG@WC-SuwovE%pc?VZPQC!Tfp&dy9anR_%l
z|NY(fee><k&QMbNg=M{`L=^jAOM*eNC^_*kHtA2CQhI)W%Liim!W9$h8>(cr)B#^-
z8P&)UYe;DDqxAnOPQbSS$WT?Qm2HNR0+6t3%xZH2$?F=0l;C&$dA|b-N<q+;Ra`f-
z`K6lbi!PkC!tK(^!8BD{&4IC4M2~)bdU&M&>4v7AhX=kHgO8*6U`*IG&I3Z0D0RKD
zan1Zy58nN*OH;2ZMm*4?vF~^H9C&y`W9tcnG2OC`vs}ZrQ3I_3C@71|7t8_(?-dII
z7VH}}pcd4G1^^JwG6Fk?PcR1RYS~izI0(72*rNn$KrN^#*Lm9pLXji@LA1GZ%I+(b
zArrNrCe%i%s-*ukSmrCTOl1f)+SDnoYf1$KYN3Q`k_@BP&t)VPD{OZSi4xWB(V@U3
zN<~fPc}M)t#0Vgde+ntJ0-8*1mtX7<2<k}o_;0gonk!gN{tJtL;>QAk03kr+10imG
z35yUQ1PB2_fG8peyeVTaCKLAmnJ!T5fPlvZ5bBPt>ngYek%9nMHSW%)7jM4nrW;$h
z1Ca@#cYDJvcPzPKk0jBlT*U|rf^1sCw)(}lFDdr|gc)1~%M1upyl>SlT7K)IPFa#J
z;BC}MTS8moy6Y=%tLOp<vw0gn(-2y=Jag>>%a?X5vOI^kJR%L@^}6Z>EAL%~5a#lh
zP?->LDz9<F>iMf5x~C5yT*6~!Ndbbox%RPn)hjD|HC4H6GNjlA=Mjn|$)5TrFMZ_x
zJNsOkdIh)F8aR;`3P)r@*rd9%RWJxmk;v9}I)-2Hw66(A^j~?l2nIFY(R*;{+0D&Q
zMfB)tE<r$yNWhSxLk)cR$<ZG-zSg=HU<~s(YA^^BF-l1gT7TERLxY<<?Q5gan4c>U
zzF@?BNDCn70AmbVU&r2q-@e@Jt&PRvqg-W{nJ^?u0tjPg=f1BzZSSv-#S;Opt~F9H
zAkB_gF#Oxvd%kGe)w@2PNQAh$`pkkciZI@6?HXwDe(+qvFv48jj~E*0$%J9Xj8~ey
zN7_33>j6fT8xUDA!YLRHZ|^$1r|08MT!F}88KcmSK#uVlcOWug#1RH#ECg>tT!P4i
zVKAbz<m0}4f=I!DlJ`k?Za}1SaRGv(%u;|5AOr{jLVyq;1PEISEy08;pC&ukYnY2r
zOX&s;t_8)$(&i8dLb1a{Fcki&R6vXc$B%<W7^sa5{mz_n4eT&};^Z%%mI?^eLMcV7
zFo?U%84$8e&AZ%BJn!iUgvL)68v<%TEvN~#k$HZ@5fF+b8Q>cFZ-1Qe;cOU~<*6dN
z#HqShchsO3)P&kdceml3Yfu`6Um(!8|MMS~R@FRu^vh%0eIsLoP60UM8$E`7V&8iY
z9663!P!mZml+PYbaz@vj2QGB~h}qNKvcZ@O4mS<n&Vb|2gd@8NvDRMNHMl0v3Qsl$
zFT-&1IOzlsNC;iS7mS><NZ#&snFe+Y!jNs8fmy<5E&!k~+(W{~CoCwg@}(pWFLemo
z;{YK-{@OO-TvxV}0!{#dRba<a97;iuZuUvj8)?{@Hjw0!=eSkPjWn!DDQ7{XV4w&K
zNW%J69N$_cAAr9Fk-ZU3s!Y4Oa>77L^;&~w(Brw#KLr>7)=CgJM_{rr00000NkvXX
Hu0mjf06vr@

literal 0
HcmV?d00001

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
GIT binary patch
literal 2374
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

literal 0
HcmV?d00001

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
GIT binary patch
literal 2258
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

literal 0
HcmV?d00001

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
GIT binary patch
literal 3680
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

literal 0
HcmV?d00001

diff --git a/briar-android/res/layout/activity_conversation.xml b/briar-android/res/layout/activity_conversation.xml
index 8bc064d2ad..673dd14f9f 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 8194f5453a..d93d269e77 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 baacbb39fc..a82a07f1e0 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 02bc2b83a1..d62f6bf638 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 0e2499c7ec..4b945f7d37 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 76eb1d28dd..0000000000
--- 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;
-	}
-}
-- 
GitLab