From d71ec9809dcc54d839c4a758d72e6df2dfac2962 Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Mon, 4 Mar 2013 19:45:31 +0000
Subject: [PATCH] Android UI for reading a message (text/plain only for now).

---
 briar-android/AndroidManifest.xml             |   4 +
 .../res/drawable-hdpi/social_reply.png        | Bin 0 -> 1487 bytes
 .../res/drawable-mdpi/social_reply.png        | Bin 0 -> 1340 bytes
 .../res/drawable-xhdpi/social_reply.png       | Bin 0 -> 1701 bytes
 .../android/contact/ContactListActivity.java  |   8 +-
 .../android/contact/ContactListAdapter.java   |   4 +-
 .../messages/ConversationActivity.java        |  39 +++-
 .../android/messages/ConversationAdapter.java |  20 +-
 .../android/messages/ConversationItem.java    |  67 ++++++
 .../messages/ConversationListActivity.java    |  14 +-
 .../messages/ConversationListAdapter.java     |  10 +-
 .../messages/ConversationListItem.java        |  10 +-
 .../android/messages/ReadMessageActivity.java | 201 ++++++++++++++++++
 .../net/sf/briar/api/db/MessageHeader.java    |   4 +-
 .../src/net/sf/briar/db/H2DatabaseTest.java   |  16 +-
 15 files changed, 343 insertions(+), 54 deletions(-)
 create mode 100644 briar-android/res/drawable-hdpi/social_reply.png
 create mode 100644 briar-android/res/drawable-mdpi/social_reply.png
 create mode 100644 briar-android/res/drawable-xhdpi/social_reply.png
 create mode 100644 briar-android/src/net/sf/briar/android/messages/ConversationItem.java
 create mode 100644 briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java

diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index ba01be5bfc..6b12578c8c 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -48,5 +48,9 @@
 			android:name=".android.messages.ConversationListActivity"
 			android:label="@string/messages_title" >
 		</activity>
+		<activity
+			android:name=".android.messages.ReadMessageActivity"
+			android:label="@string/messages_title" >
+		</activity>
 	</application>
 </manifest>
diff --git a/briar-android/res/drawable-hdpi/social_reply.png b/briar-android/res/drawable-hdpi/social_reply.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0fcbf356e9d6363ca546c2d803f15f01cbcb21d
GIT binary patch
literal 1487
zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC$r9IylHmNblJdl&R0hYC{G?O`
z&)mfH)S%SFl*+=BsWuD@%qp275hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y
zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP
zs8ErclUHn2VXFi-*9yo63F|8<fR&VF+bTgE72zA8;GAESs$i;Tpqp%9W~g9hqGxDg
zU}<8hqhMrUXrOOsq;FuZYiM9)YHnp<r~m~@K--E^(yW49+@N*=dA3R!B_#z``ugSN
z<$C4Ddih1^`i7R4mih)p`bI{&Koz>hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83
zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s0<RzFwUtj!6b93RUi%Wu15$?rmaB)aw
zL8^XGYH@yPQ8F;%(v(3~6<9eJr6!i-7lq{K=fFZSAS1sdzc?emK*2fKRL@YsH!(Rg
z4<rKC;p=PVnO9trn3tUD>0+w{G(#^lGsVi((%jPA(9p@$$-u?X(ACh=#L&{!#L3yw
z&D7A`#K{n**Cju>G&eP`1g19yq1PFwUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+dZns$C
zG!Lpb1-Dxqaq86vIz}H9wMbD769T3m5EGtofgE_!Pt60S_ab1zW=(s?$H2hk=;`7Z
zQgQ3e%=6kTi4w=lul-Y)A~WTYmeL_NzEiEUWn!ml{1XZnXMd{E7bEjT!6i33h|l}f
zL9L*mX{p!W{n+=_XRmMCy}5JC)5<>W`+9Pl@owAswe!DM-Yd`8wa)#J&7z4D_*ECM
zOlc6}C_C~-Z&{G0`Te_$aSgBCCZ9~HnctXbpsD;rA!vp8nx<_A>ll@Phz0P>ZV;@|
z)@WVJ_3@y%vRz-sjOOWz0vR6;*(pfWCa9ixaF?T}k@bQ^%7?978y<)|#suVksS+`0
z>JJsP5zg7P<6PR>H`^KH|MWk&m%nW)lXHE{>W1(SEbk7QR(y*(;Q8SXkBaWP&AD#;
zZ>N7!;O0BnE6(KC{9JX-r6p&UCN!mG3&=Fu&T`bOO<>ZwF0g0v!Jt>&ap#MwS6-0K
zm~nz#g3;UT<#Ue*OgWd<%YXJ<!r*pr&KgON=Cx8PCKnQvyxw<T<C{EVS*xCE>m09)
zEgqlMyuSQg;;_W&`<WAOm@|Jx`?`7s{dzmAZcW(*-eU(F76fhdy28K9!7ohxnd+Nu
zLdSSSeBN(%{l#$Zpva8n?=_>(I2oHBo^wX)h>Dqp>Z2x;P145`X1}|vlay?`=yJDF
z)lpBCn2<Tfx4cw*3?+BpsXa8=Zh~rO`E;SoC~mh0Z@y|qFk64gIXRh`SO4L&kh0^U
z+r0}I^8Q*(JUQt^u+-cM4hsINca~gMk65o-akE-6PU&62^(b-vZ55o`uWA}F#ywhl
zPk!|c-f8FiuPm^!y_5aw{}C?-P7OxY59?X$SS1){Mosd28hHkom>3v5UHx3vIVCg!
E0Fd4yZ~y=R

literal 0
HcmV?d00001

diff --git a/briar-android/res/drawable-mdpi/social_reply.png b/briar-android/res/drawable-mdpi/social_reply.png
new file mode 100644
index 0000000000000000000000000000000000000000..75723bca5710f2c2f62fb99c5413ca143a049109
GIT binary patch
literal 1340
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O`
z&)mfH)S%SFl*+=BsWuD@%qp275hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y
zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP
zs8ErclUHn2VXFi-*9yo63F|8<fR&VF+bTgE72zA8;GAESs$i;Tpqp%9W~g9hqGxDg
zU}<8hqhMrUXrOOsq;FuZYiM9)YHnp<r~m~@K--E^(yW49+@N*=dA3R!B_#z``ugSN
z<$C4Ddih1^`i7R4mih)p`bI{&Koz>hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83
zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s0<RzFwUtj!6b93RUi%Wu15$?rmaB)aw
zL8^XGYH@yPQ8F;%(v(3~6<9eJr6!i-7lq{K=fFZSAS1sdzc?emK*2fKRL@YsH!(Rg
z4<rKC;p=PVnO9trn3tUD>0+w{G(#^lGsVi((%jPA(9q4)$-u?X(ACh=#L&{!#L3yw
z&D7A`#K{n**Cju>G&eP`1g19yuGh!_r(RHE$SnZc?2=lPS(cjOR+OKs0QR(1CT_R5
z;4}}aHwC9#3|(>R)dxC89~8AnQ4JFUrXLU!o^XL2c+yYJ1E%*PV8T9?$e6*vz_`xS
z#WAGf)|=_)wOs;5jz2d{>zR_YH0A8H>6<2`7~Z^bgR^u0vG@g3gg&#MV2g~{P?Mvx
z;--fZw^_@_p4Zi{?o{1fXZ`)}hj(dywX@8(m*4-nulSs8`Y~ZAk(em=gB!Q9T8IaP
zh}YcTe)l%J(+9cZi2^J$T@EmZiak_eJ+1TLc58!b**`{?drU^9a^Vio&TU=pkUTem
z^^D7$>Pk+9=dTl9^*_G$!=F*@dDHiGZY}{K_YT;uXl^%Q%BVg2;aSt`^cc_fJ<Dv(
z6Xfq46E$FYc5sR7^lXVm=4@^bymOx0a@Xxh*ljUMxR51)Z~Fns7nu)N@a&p7=~?Bb
zV7;b)7N!o;+kZxdvrl=gw&Q_Gg^hTd<N8TbQW4Av)3TMe*s}UIbM(8uYq);mmqNsX
zg{wM#JTeJLj61rLQS%W?hzakCH8u4tvmeX~R^r}tBtOpc5~t@8J@IKdN7ufyXIdQN
ztp9*vj@HFZVhU@I%@6J7t2o}Le^9Z)&zQSJ>Dhth5A9xcNI%f*PFy1FxYamU!fRpg
z=Os)hzF9CG-6E^~k~u?6W_7yN#s1UZ;x}(&xXx6^D#4Hzm9{f^%|mridFJWr=d#Wz
Gp$P#0tKPN%

literal 0
HcmV?d00001

diff --git a/briar-android/res/drawable-xhdpi/social_reply.png b/briar-android/res/drawable-xhdpi/social_reply.png
new file mode 100644
index 0000000000000000000000000000000000000000..1dc01d7df2c545b5cd55a8cd8da5768e7bccf80c
GIT binary patch
literal 1701
zcmaJ?c~BE)91V8`5o!<=L6=2QsAP9zLP+EaBtROH(m**zR6?>ygk;xbF+mvwN&y8g
z5M)|YMbtW2w2nnZQp64&MWNuJ2x27yDiswfI?`dR-5_B9knYUxcg%b5cYN=g-4!j4
zu(x%!rBEpLf=Hf(979a+C)VV*R<ZQ~IV>Ug2}BH*N@%5Ol){x`DJURNN;jhtR4UKf
z+KL8HC>AReu?a+iD2gM)lr*UcL(?g7l1-rm1nY3AEFC3)6m+vf6$p+tUI77xJP?d$
zieM2QhNdYZx2w^Z?c!M3_H-Fr4h9DS0XhyTphO8Npi^e3G#p(ZIHk)W=ca841g0QF
zdLZ~dsRU6p5QeEyfJuXWWw0Lt_%Ub*17<VWYXCZozz{-*V3scebC@g+i~!RQNJdl3
zQ#le|_;f6C6$qvg1kQmVtyW9ZuBBn>%@D$7vmuxc(doV<!dH`}BBVNBmBw>Mfrn~j
zY6VUxFcn}@l%`;rL?B3J`o07uJ}axzOt*<_7^IWp5JH1ZCCvauqW^~~m9uCKAwfUn
z`%z&{Y!;3}5>$g_s%7NCrFxn|;hZovDkU&=EQV#wbTK*&BQQ-Gh67<SOd0}+q%wud
zv@^vJi8un4hLEacsDKv;k{UFHLe62bS!@=9u$ZAR4?*|{n}M+TjL<M{I1^zpLXjCR
z50hmoQ57-6m4D#+&&f5JL5Y)*d8k^k4V8zhF(oinG)M8VT>QhiT$s+CQ*W9ppF0=7
zIk^y-3}iam{~Yzq7U>_;bk?}!V%GYoiuAjhG&Vi)^*i#uxCnUMSlzv6-wD&5xK7{f
z@RGR=uICQWjO$yTINGa-K?i(geo67t^IJ#GIwqw#^0Wh=JMp;}eL&As6UF=Z+Xn3V
z+lHeuI)3ZwVYK$#>G~>0Uz_hxSS`@%cRcN`>noQmF3h`G<K`b^plqZ-xh~CiMyoM%
zo!Mdc3Wm#_E%k=c#>GEhb<mfezB>}fNnYkpvEGnm{?-Wh%sZJx4FPvrJ~I#I4wq3D
z{{1C9|Jw^uL75BNJYLq6W?LLgx@TQ}0DA0K6*HvwDfn@pL2Tm$zP#)fU3Gl~ZwB#G
zk>wpRxhIUP6V@MZeNt~(iN734eAwR9W!wn{TX)MNb?wLO9QipL3R{KDON$Jn_?}CD
zel{_`sn;unb-<zF)q?z{gXY<m9alZas_&W;)U1k?04?$QWSx0mbI#b}`_^CV@Gv3i
z{-{qk%aQvy_fx>Pw$VtRkZ*A*j%ae}4O(&Qmp8eC$u;xL3Y(D^<9-J(UTG+Z>pak>
z#EOSqujKF4pKRw?RavgODtyp+^44DW=D4RsZbrnQ(W)xFnp$bKv#}l33q$BHyoLJC
zA$m{UI-8+K1wK_rl5_jtjQ7S)7OCHEtT4*!EIn71{^fW(Iy<tr4~mMhjj!spY50To
zr2B4Mf^FD+*X6@Oc06&ht!Kr(5U<ychUa*@xpUy;YM!vV?aVnz#?u7zwT}w&<_nK@
zO)OC5QH`0qM`}Dml}F1<Z<`GibRGTu@P2LoJA1uftHi&3|Dm&|Mb4q%uS@Vv9+Fyd
zV@p!>3;2-Fc!^u3R}_O+*4LSsvn)0A#`vNhs(6bqMHx|a+?m>MKXxNFBJJ{;?C+0^
zt4f6OBYC1-yNcSl@!n<U_I5_qRNtCMEzPN@JY>MB_Xo|qf8tW~V%;@b*-$}xlwm{t
zO`>V(?T40v{P<?~u@f%t)f?Aw|9S8_V0mZDs70AL&B}St?&`@FJ$x*>ufx@BK)nBX
zdPH(7#P=y2T_yI9uzA(6I%KGz<ieV-%ijIqeXVHLk?#$9esCYff->Q0Xjr(ZUTpet
N2>4>&nb73?e*s@?j?e%A

literal 0
HcmV?d00001

diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
index a6e7e3f321..d0d7ace2fd 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
@@ -88,8 +88,6 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 		// Bind to the service so we can wait for the DB to be opened
 		bindService(new Intent(BriarService.class.getName()),
 				serviceConnection, 0);
-		// Load the contact list from the DB
-		reloadContactList();
 
 		// Add some fake contacts to the database in a background thread
 		// FIXME: Remove this
@@ -118,6 +116,12 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
 		});
 	}
 
+	@Override
+	public void onResume() {
+		super.onResume();
+		reloadContactList();
+	}
+
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
index 1521492c93..45bc63639d 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
@@ -1,6 +1,6 @@
 package net.sf.briar.android.contact;
 
-import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_VERTICAL;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 import static android.widget.LinearLayout.HORIZONTAL;
 
@@ -35,7 +35,7 @@ implements OnItemClickListener {
 		Context ctx = getContext();
 		LinearLayout layout = new LinearLayout(ctx);
 		layout.setOrientation(HORIZONTAL);
-		layout.setGravity(CENTER);
+		layout.setGravity(CENTER_VERTICAL);
 
 		ImageView bulb = new ImageView(ctx);
 		bulb.setPadding(5, 5, 5, 5);
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
index 82ae8a74fe..5188bbbe7e 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
@@ -28,6 +28,8 @@ import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
 import android.widget.Button;
 import android.widget.LinearLayout;
 import android.widget.LinearLayout.LayoutParams;
@@ -36,7 +38,7 @@ import android.widget.ListView;
 import com.google.inject.Inject;
 
 public class ConversationActivity extends BriarActivity
-implements OnClickListener, DatabaseListener {
+implements DatabaseListener, OnClickListener, OnItemClickListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(ConversationActivity.class.getName());
@@ -48,6 +50,7 @@ implements OnClickListener, DatabaseListener {
 	@Inject @DatabaseExecutor private Executor dbExecutor;
 
 	private ConversationAdapter adapter = null;
+	private String contactName = null;
 	private volatile ContactId contactId = null;
 
 	@Override
@@ -55,12 +58,12 @@ implements OnClickListener, DatabaseListener {
 		super.onCreate(null);
 
 		Intent i = getIntent();
+		contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME");
+		if(contactName == null) throw new IllegalStateException();
+		setTitle(contactName);
 		int id = i.getIntExtra("net.sf.briar.CONTACT_ID", -1);
 		if(id == -1) throw new IllegalStateException();
 		contactId = new ContactId(id);
-		String contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME");
-		if(contactName == null) throw new IllegalStateException();
-		setTitle(contactName);
 
 		LinearLayout layout = new LinearLayout(this);
 		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
@@ -72,7 +75,7 @@ implements OnClickListener, DatabaseListener {
 		// Give me all the width and all the unused height
 		list.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT, 1f));
 		list.setAdapter(adapter);
-		list.setOnItemClickListener(adapter);
+		list.setOnItemClickListener(this);
 		layout.addView(list);
 
 		Button composeButton = new Button(this);
@@ -92,7 +95,11 @@ implements OnClickListener, DatabaseListener {
 		// Bind to the service so we can wait for the DB to be opened
 		bindService(new Intent(BriarService.class.getName()),
 				serviceConnection, 0);
-		// Load the message headers from the DB
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
 		reloadMessageHeaders();
 	}
 
@@ -103,10 +110,6 @@ implements OnClickListener, DatabaseListener {
 		unbindService(serviceConnection);
 	}
 
-	public void onClick(View view) {
-		// FIXME: Hook this button up to an activity
-	}
-
 	public void eventOccurred(DatabaseEvent e) {
 		if(e instanceof MessageAddedEvent) {
 			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
@@ -152,4 +155,20 @@ implements OnClickListener, DatabaseListener {
 			}
 		});
 	}
+
+	public void onClick(View view) {
+		// FIXME: Hook this button up to an activity
+	}
+
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		PrivateMessageHeader item = adapter.getItem(position);
+		Intent i = new Intent(this, ReadMessageActivity.class);
+		i.putExtra("net.sf.briar.CONTACT_NAME", contactName);
+		i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
+		i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType());
+		i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp());
+		i.putExtra("net.sf.briar.STARRED", item.isStarred());
+		startActivity(i);
+	}
 }
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
index 31fdd1dcc8..8f2b7a96b2 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
@@ -1,7 +1,7 @@
 package net.sf.briar.android.messages;
 
 import static android.graphics.Typeface.BOLD;
-import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_VERTICAL;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 import static android.widget.LinearLayout.HORIZONTAL;
 import static java.text.DateFormat.SHORT;
@@ -14,16 +14,13 @@ import android.content.Context;
 import android.text.format.DateUtils;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.LinearLayout.LayoutParams;
 import android.widget.TextView;
 
-class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader>
-implements OnItemClickListener {
+class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> {
 
 	ConversationAdapter(Context ctx) {
 		super(ctx, android.R.layout.simple_expandable_list_item_1,
@@ -36,12 +33,11 @@ implements OnItemClickListener {
 		Context ctx = getContext();
 		LinearLayout layout = new LinearLayout(ctx);
 		layout.setOrientation(HORIZONTAL);
-		layout.setGravity(CENTER);
+		layout.setGravity(CENTER_VERTICAL);
 
 		ImageView star = new ImageView(ctx);
 		star.setPadding(5, 5, 5, 5);
-		if(item.getStarred())
-			star.setImageResource(R.drawable.rating_important);
+		if(item.isStarred()) star.setImageResource(R.drawable.rating_important);
 		else star.setImageResource(R.drawable.rating_not_important);
 		layout.addView(star);
 
@@ -57,7 +53,8 @@ implements OnItemClickListener {
 		subject.setLayoutParams(new LayoutParams(WRAP_CONTENT, WRAP_CONTENT,
 				1));
 		subject.setTextSize(14);
-		if(!item.getRead()) subject.setTypeface(null, BOLD);
+		subject.setMaxLines(2);
+		if(!item.isRead()) subject.setTypeface(null, BOLD);
 		subject.setText(item.getSubject());
 		layout.addView(subject);
 
@@ -70,9 +67,4 @@ implements OnItemClickListener {
 
 		return layout;
 	}
-
-	public void onItemClick(AdapterView<?> parent, View view, int position,
-			long id) {
-		// FIXME
-	}
 }
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationItem.java b/briar-android/src/net/sf/briar/android/messages/ConversationItem.java
new file mode 100644
index 0000000000..2640bc105e
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationItem.java
@@ -0,0 +1,67 @@
+package net.sf.briar.android.messages;
+
+import net.sf.briar.api.db.PrivateMessageHeader;
+import net.sf.briar.api.messaging.MessageId;
+
+class ConversationItem {
+
+	private final PrivateMessageHeader header;
+	private final byte[] body;
+	private final boolean expanded;
+
+	ConversationItem(PrivateMessageHeader header) {
+		this.header = header;
+		body = null;
+		expanded = false;
+	}
+
+	// Collapse an existing item
+	ConversationItem(ConversationItem item) {
+		this.header = item.header;
+		body = null;
+		expanded = false;
+	}
+
+	// Expand an existing item
+	ConversationItem(ConversationItem item, byte[] body) {
+		this.header = item.header;
+		this.body = body;
+		expanded = true;
+	}
+
+	MessageId getId() {
+		return header.getId();
+	}
+
+	String getContentType() {
+		return header.getContentType();
+	}
+
+	String getSubject() {
+		return header.getSubject();
+	}
+
+	long getTimestamp() {
+		return header.getTimestamp();
+	}
+
+	boolean isRead() {
+		return header.isRead();
+	}
+
+	boolean isStarred() {
+		return header.isStarred();
+	}
+
+	boolean isIncoming() {
+		return header.isIncoming();
+	}
+
+	byte[] getBody() {
+		return body;
+	}
+
+	boolean isExpanded() {
+		return expanded;
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
index b54a41827e..a0d337641a 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
@@ -92,8 +92,6 @@ implements OnClickListener, DatabaseListener {
 		// Bind to the service so we can wait for the DB to be opened
 		bindService(new Intent(BriarService.class.getName()),
 				serviceConnection, 0);
-		// Load the message headers from the DB
-		reloadMessageHeaders();
 
 		// Add some fake messages to the database in a background thread
 		// FIXME: Remove this
@@ -115,13 +113,10 @@ implements OnClickListener, DatabaseListener {
 								"text/plain",
 								"First message is short".getBytes("UTF-8"));
 						db.addLocalPrivateMessage(m, contactId);
-						db.setReadFlag(m.getId(), true);
-						db.setStarredFlag(m.getId(), true);
-						Thread.sleep(1000);
 						m = messageFactory.createPrivateMessage(m.getId(),
 								"image/jpeg", new byte[1000]);
 						db.receiveMessage(contactId, m);
-						Thread.sleep(1000);
+						db.setReadFlag(m.getId(), true);
 						m = messageFactory.createPrivateMessage(m.getId(),
 								"text/plain",
 								("Third message is quite long to test line"
@@ -129,6 +124,7 @@ implements OnClickListener, DatabaseListener {
 								+ " all that fun stuff").getBytes("UTF-8"));
 						db.addLocalPrivateMessage(m, contactId);
 						db.setReadFlag(m.getId(), true);
+						db.setStarredFlag(m.getId(), true);
 					}
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
@@ -148,6 +144,12 @@ implements OnClickListener, DatabaseListener {
 		});
 	}
 
+	@Override
+	public void onResume() {
+		super.onResume();
+		reloadMessageHeaders();
+	}
+
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
index 234af5266e..42b68a5d0d 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
@@ -1,7 +1,7 @@
 package net.sf.briar.android.messages;
 
 import static android.graphics.Typeface.BOLD;
-import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_VERTICAL;
 import static android.view.Gravity.LEFT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 import static android.widget.LinearLayout.HORIZONTAL;
@@ -38,12 +38,11 @@ implements OnItemClickListener {
 		Context ctx = getContext();
 		LinearLayout layout = new LinearLayout(ctx);
 		layout.setOrientation(HORIZONTAL);
-		layout.setGravity(CENTER);
+		layout.setGravity(CENTER_VERTICAL);
 
 		ImageView star = new ImageView(ctx);
 		star.setPadding(5, 5, 5, 5);
-		if(item.getStarred())
-			star.setImageResource(R.drawable.rating_important);
+		if(item.isStarred()) star.setImageResource(R.drawable.rating_important);
 		else star.setImageResource(R.drawable.rating_not_important);
 		layout.addView(star);
 
@@ -61,7 +60,8 @@ implements OnItemClickListener {
 
 		TextView subject = new TextView(ctx);
 		subject.setTextSize(14);
-		if(!item.getRead()) subject.setTypeface(null, BOLD);
+		subject.setMaxLines(2);
+		if(!item.isRead()) subject.setTypeface(null, BOLD);
 		subject.setText(item.getSubject());
 		innerLayout.addView(subject);
 		layout.addView(innerLayout);
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
index 24b2e63a44..d0842efca0 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
@@ -23,10 +23,10 @@ class ConversationListItem {
 		subject = headers.get(0).getSubject();
 		timestamp = headers.get(0).getTimestamp();
 		length = headers.size();
-		boolean allRead = false, anyStarred = false;
+		boolean allRead = true, anyStarred = false;
 		for(PrivateMessageHeader h : headers) {
-			allRead &= h.getRead();
-			anyStarred |= h.getStarred();
+			allRead &= h.isRead();
+			anyStarred |= h.isStarred();
 		}
 		read = allRead;
 		starred = anyStarred;
@@ -48,11 +48,11 @@ class ConversationListItem {
 		return timestamp;
 	}
 
-	boolean getRead() {
+	boolean isRead() {
 		return read;
 	}
 
-	boolean getStarred() {
+	boolean isStarred() {
 		return starred;
 	}
 
diff --git a/briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java
new file mode 100644
index 0000000000..ea3f0dffd0
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java
@@ -0,0 +1,201 @@
+package net.sf.briar.android.messages;
+
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.Gravity.RIGHT;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+import java.io.UnsupportedEncodingException;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.BriarActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DatabaseExecutor;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.messaging.MessageId;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.TextView;
+
+import com.google.inject.Inject;
+
+public class ReadMessageActivity extends BriarActivity
+implements OnClickListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(ReadMessageActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private DatabaseComponent db;
+	@Inject @DatabaseExecutor private Executor dbExecutor;
+
+	private MessageId messageId = null;
+	private boolean starred = false;
+	private ImageButton starButton = null, replyButton = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		String contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME");
+		if(contactName == null) throw new IllegalStateException();
+		setTitle(contactName);
+		byte[] id = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID");
+		if(id == null) throw new IllegalStateException();
+		messageId = new MessageId(id);
+		String contentType = i.getStringExtra("net.sf.briar.CONTENT_TYPE");
+		if(contentType == null) throw new IllegalStateException();
+		long timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1);
+		if(timestamp == -1) throw new IllegalStateException();
+		starred = i.getBooleanExtra("net.sf.briar.STARRED", false);
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+		layout.setOrientation(VERTICAL);
+
+		LinearLayout header = new LinearLayout(this);
+		header.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+		header.setOrientation(HORIZONTAL);
+		header.setGravity(CENTER_VERTICAL);
+
+		starButton = new ImageButton(this);
+		starButton.setPadding(5, 5, 5, 5);
+		starButton.setBackgroundResource(0);
+		if(starred) starButton.setImageResource(R.drawable.rating_important);
+		else starButton.setImageResource(R.drawable.rating_not_important);
+		starButton.setOnClickListener(this);
+		header.addView(starButton);
+
+		replyButton = new ImageButton(this);
+		replyButton.setPadding(5, 5, 5, 5);
+		replyButton.setBackgroundResource(0);
+		replyButton.setImageResource(R.drawable.social_reply);
+		replyButton.setOnClickListener(this);
+		header.addView(replyButton);
+
+		TextView date = new TextView(this);
+		// Give me all the unused width
+		date.setLayoutParams(new LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1));
+		date.setTextSize(14);
+		date.setPadding(10, 0, 10, 0);
+		date.setGravity(RIGHT);
+		long now = System.currentTimeMillis();
+		date.setText(DateUtils.formatSameDayTime(timestamp, now, SHORT, SHORT));
+		header.addView(date);
+		layout.addView(header);
+
+		if(contentType.equals("text/plain")) {
+			// Load and display the message body
+			TextView content = new TextView(this);
+			content.setPadding(10, 10, 10, 10);
+			layout.addView(content);
+			loadMessageBody(messageId, content);
+		}
+
+		setContentView(layout);
+
+		// Bind to the service so we can wait for the DB to be opened
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	private void loadMessageBody(final MessageId id, final TextView view) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// Load the message body from the database
+					byte[] body = db.getMessageBody(id);
+					final String text = new String(body, "UTF-8");
+					// Display the message body
+					runOnUiThread(new Runnable() {
+						public void run() {
+							view.setText(text);
+						}
+					});
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				} catch(UnsupportedEncodingException e) {
+					throw new RuntimeException(e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		final MessageId id = messageId;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// Mark the message as read
+					db.setReadFlag(id, true);
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	@Override
+	public void onClick(View view) {
+		if(view == starButton) {
+			final MessageId id = messageId;
+			final boolean starredNow = !starred;
+			dbExecutor.execute(new Runnable() {
+				public void run() {
+					try {
+						db.setStarredFlag(id, starredNow);
+					} catch(DbException e) {
+						if(LOG.isLoggable(WARNING))
+							LOG.log(WARNING, e.toString(), e);
+					}
+				}
+			});
+			starred = starredNow;
+			if(starred)
+				starButton.setImageResource(R.drawable.rating_important);
+			else starButton.setImageResource(R.drawable.rating_not_important);
+		} else if(view == replyButton) {
+			// FIXME: Hook this up to an activity
+		}
+	}
+}
diff --git a/briar-api/src/net/sf/briar/api/db/MessageHeader.java b/briar-api/src/net/sf/briar/api/db/MessageHeader.java
index 2cbab3f51c..95a927d3e7 100644
--- a/briar-api/src/net/sf/briar/api/db/MessageHeader.java
+++ b/briar-api/src/net/sf/briar/api/db/MessageHeader.java
@@ -49,12 +49,12 @@ public abstract class MessageHeader {
 	}
 
 	/** Returns true if the message has been read. */
-	public boolean getRead() {
+	public boolean isRead() {
 		return read;
 	}
 
 	/** Returns true if the message has been starred. */
-	public boolean getStarred() {
+	public boolean isStarred() {
 		return starred;
 	}
 }
diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
index 3126208083..b59bdd71d1 100644
--- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
+++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java
@@ -1310,13 +1310,13 @@ public class H2DatabaseTest extends BriarTestCase {
 		GroupMessageHeader header = it.next();
 		if(messageId.equals(header.getId())) {
 			assertHeadersMatch(message, header);
-			assertTrue(header.getRead());
-			assertFalse(header.getStarred());
+			assertTrue(header.isRead());
+			assertFalse(header.isStarred());
 			messageFound = true;
 		} else if(messageId1.equals(header.getId())) {
 			assertHeadersMatch(message1, header);
-			assertFalse(header.getRead());
-			assertFalse(header.getStarred());
+			assertFalse(header.isRead());
+			assertFalse(header.isStarred());
 			message1Found = true;
 		} else {
 			fail();
@@ -1326,13 +1326,13 @@ public class H2DatabaseTest extends BriarTestCase {
 		header = it.next();
 		if(messageId.equals(header.getId())) {
 			assertHeadersMatch(message, header);
-			assertTrue(header.getRead());
-			assertFalse(header.getStarred());
+			assertTrue(header.isRead());
+			assertFalse(header.isStarred());
 			messageFound = true;
 		} else if(messageId1.equals(header.getId())) {
 			assertHeadersMatch(message1, header);
-			assertFalse(header.getRead());
-			assertFalse(header.getStarred());
+			assertFalse(header.isRead());
+			assertFalse(header.isStarred());
 			message1Found = true;
 		} else {
 			fail();
-- 
GitLab