From 747a06d1ad634919d8e327d538d596c66e70d0c9 Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Tue, 12 Mar 2013 15:55:41 +0000
Subject: [PATCH] Android UI for group messages (anonymous text only, no
 moderation yet).

---
 briar-android/AndroidManifest.xml             |  22 +-
 .../res/drawable-hdpi/social_new_chat.png     | Bin 0 -> 716 bytes
 .../res/drawable-hdpi/social_reply_all.png    | Bin 0 -> 1652 bytes
 .../res/drawable-mdpi/social_new_chat.png     | Bin 0 -> 583 bytes
 .../res/drawable-mdpi/social_reply_all.png    | Bin 0 -> 1457 bytes
 .../res/drawable-xhdpi/social_new_chat.png    | Bin 0 -> 830 bytes
 .../res/drawable-xhdpi/social_reply_all.png   | Bin 0 -> 1929 bytes
 briar-android/res/values/color.xml            |   4 +-
 briar-android/res/values/strings.xml          |  17 +-
 .../sf/briar/android/HomeScreenActivity.java  |   4 +-
 .../android/contact/ContactComparator.java    |   4 +-
 .../android/contact/ContactListAdapter.java   |   8 +-
 .../android/contact/ContactListItem.java      |   4 +-
 .../briar/android/groups/GroupActivity.java   | 226 +++++++++++++
 .../sf/briar/android/groups/GroupAdapter.java |  92 ++++++
 .../android/groups/GroupListActivity.java     | 302 ++++++++++++++++++
 .../android/groups/GroupListAdapter.java      |  85 +++++
 .../briar/android/groups/GroupListItem.java   |  57 ++++
 .../groups/GroupNameSpinnerAdapter.java       |  36 +++
 .../groups/ReadGroupMessageActivity.java      | 288 +++++++++++++++++
 .../groups/WriteGroupMessageActivity.java     | 217 +++++++++++++
 .../android/invitation/ConnectionView.java    |   7 +-
 .../briar/android/invitation/WifiWidget.java  |   7 +-
 .../messages/ContactNameSpinnerAdapter.java   |   1 +
 .../messages/ConversationActivity.java        |  67 ++--
 .../android/messages/ConversationAdapter.java |  24 +-
 .../messages/ConversationListActivity.java    |   2 +-
 .../messages/ConversationListAdapter.java     |   8 +-
 .../messages/ConversationListItem.java        |   2 +-
 ...y.java => ReadPrivateMessageActivity.java} |  30 +-
 ....java => WritePrivateMessageActivity.java} |  25 +-
 .../android/widgets/HorizontalBorder.java     |   2 +-
 32 files changed, 1438 insertions(+), 103 deletions(-)
 create mode 100644 briar-android/res/drawable-hdpi/social_new_chat.png
 create mode 100644 briar-android/res/drawable-hdpi/social_reply_all.png
 create mode 100644 briar-android/res/drawable-mdpi/social_new_chat.png
 create mode 100644 briar-android/res/drawable-mdpi/social_reply_all.png
 create mode 100644 briar-android/res/drawable-xhdpi/social_new_chat.png
 create mode 100644 briar-android/res/drawable-xhdpi/social_reply_all.png
 create mode 100644 briar-android/src/net/sf/briar/android/groups/GroupActivity.java
 create mode 100644 briar-android/src/net/sf/briar/android/groups/GroupAdapter.java
 create mode 100644 briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
 create mode 100644 briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
 create mode 100644 briar-android/src/net/sf/briar/android/groups/GroupListItem.java
 create mode 100644 briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java
 create mode 100644 briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
 create mode 100644 briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java
 rename briar-android/src/net/sf/briar/android/messages/{ReadMessageActivity.java => ReadPrivateMessageActivity.java} (90%)
 rename briar-android/src/net/sf/briar/android/messages/{WriteMessageActivity.java => WritePrivateMessageActivity.java} (89%)

diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 0b0771da6e..e402626aff 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -40,6 +40,22 @@
 		    android:name=".android.contact.ContactListActivity"
 		    android:label="@string/contact_list_title" >
 		</activity>
+		<activity
+			android:name=".android.groups.GroupActivity"
+			android:label="@string/groups_title" >
+		</activity>
+		<activity
+			android:name=".android.groups.GroupListActivity"
+			android:label="@string/groups_title" >
+		</activity>
+		<activity
+			android:name=".android.groups.ReadGroupMessageActivity"
+			android:label="@string/groups_title" >
+		</activity>
+		<activity
+			android:name=".android.groups.WriteGroupMessageActivity"
+			android:label="@string/compose_group_title" >
+		</activity>
 		<activity
 			android:name=".android.invitation.AddContactActivity"
 			android:label="@string/add_contact_title" >
@@ -53,12 +69,12 @@
 			android:label="@string/messages_title" >
 		</activity>
 		<activity
-			android:name=".android.messages.ReadMessageActivity"
+			android:name="net.sf.briar.android.messages.ReadPrivateMessageActivity"
 			android:label="@string/messages_title" >
 		</activity>
 		<activity
-			android:name=".android.messages.WriteMessageActivity"
-			android:label="@string/compose_title" >
+			android:name="net.sf.briar.android.messages.WritePrivateMessageActivity"
+			android:label="@string/compose_message_title" >
 		</activity>
 	</application>
 </manifest>
diff --git a/briar-android/res/drawable-hdpi/social_new_chat.png b/briar-android/res/drawable-hdpi/social_new_chat.png
new file mode 100644
index 0000000000000000000000000000000000000000..a6a42eeb6779836698a0e2c213955518d2284373
GIT binary patch
literal 716
zcmV;-0yF)IP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00001b5ch_0Itp)
z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*e;
z5*8vPkK3&P00K-&L_t(&-tCydiqt?5hQDNDoK;UATtN^8z3<XjVFgbrEa<cN2)=;$
z1fCWjK)k9KK`3_Eg9kn6$%`ID(Vf}JOuW>{5YnB_CNVvjf+jGN(Es06^;cEHV1o@d
zSf3UF7-M#TYrrBPY%!1nM<Vjs_EMtH9pE-F*N;Lt*fH=L_~m*rS@%RhQLzI5Oho2B
z1oFb_6Z~1=m4eTzK1f16c_1^!{QXlztcemNc;62bhXORsi=}5cM!?7g-gf#f0@D^G
zH33o&60s`HL0#}R0x9quc-xS`UEl%m+tXXiy5L<NIM&s~rZvMbstevlU`tmM8=qE^
zeZkuZoB$7j{e}e2s26Qs$eV(9dElHXLldhiipx53S%<&~cmsUwu*JxMo4~EE2)qa0
zbzHpWz+O|iwpHTvA5`B~_9*i}2HXL5I}V|Xx>wXTfhBMixZbe`7D`~$Hi4BFGB&_s
zHr$dBK^bEr;Iba@HxZfnjFeGNqh;B?W8e{R0eGUa?hK(kKu4syVxJJ20N;DMR*AI#
z$~1h6Ep}c>Ktv|Sn8}dD_2ysO91=W%+ym&e(B20tqc?ynh1|<Ec}JQ9yT+JH1*M9C
zpCWRYoOY0CN$>Z7dzQKRSP9%OQKA(11RVC_fi<Q13VZ-&1rNl)wm!d<%-4zj-g^kW
z0A7|z<reT*3)z9Iko8SqVW~YQRgL8;<U?)dV%v2~5FT&|#`=Y>&-)wSPKQb$(yN{G
yRTEnhD1ilVq-ykI6A8?Kr#=OC$brHBcl!gr;iHsZJEg+_0000<MNUMnLSTZ_1UP>H

literal 0
HcmV?d00001

diff --git a/briar-android/res/drawable-hdpi/social_reply_all.png b/briar-android/res/drawable-hdpi/social_reply_all.png
new file mode 100644
index 0000000000000000000000000000000000000000..377f6286ce9d88bcb8e7e4bc2b331d583f5cd6c3
GIT binary patch
literal 1652
zcmaJ?c~BE~6ked#h(|%HBG6_HDq_PPArX=qAP3QClYxMU&=y0oAt@vqvWtYsC8Cse
ztQB=Ecm?q&AmB-((AsGo8H5P6QmF@c&_FFhEuBH|m~IfTf0XXb?(g^ByzhJ0H?zf2
z@>R3lecS;6m@Qo`R!}G6d}(h|-}SB|Mbxp7jEpB^a1EKMG+}^1jVEKERIg0I6qr(-
zmEMf41OT_iy4ZL!UiJY`h3g@uGX^s2jT9OHR)(96N>wUGg2`Bl&cJ63He6zWIyIlM
zo+CqKMiHjft+tr37>hhsWl2?W)r{~kaHW|?5$G{e37Yk327+hiGsbv%)ZOWZ8Q@q5
znaXFpDk@$U1&VMJ267-INQJCG!4+%>Wg}cRmkF{E6oF9|jD!TC2#*uOLr`%1VNlsj
zY7I{zmW=13EIva^l13g3XJ%$XnZXclN`X->mkT2-n8gaBB7%r41F19z83_Lg1~Epc
zOgbZ}!wsO5QJIWqkbDMJ>8lp>#z|TOF+L`0V6a(fgi#1_wlonall_0FUOyR4kP7T|
zz5hu}#AX>WSb-6EhDk*&PUG)PW#oxWn3BXzu{fSKF~uk?PU3_XH-e%V4upa-rAlXT
zdd3hk8Bb~;NTorANyU5y#R2JbY91$4A`l=fHb;nvQ8W_evQcg%TPPApIA{o4h)!U|
zxGF=B8ORB&`Zbm_CDthhy^+c+#!R|Rm|9}O_25|3Jl)i_u%_f2$Ev5UC3s3KOcet=
zSNpG3Pq--kIIok!rHo1GV+Km^CQ8`bB^TEKfU83)7Q~uw^_SXr&JuWE>8)t$AEn(F
z3XPsW*b8l*@`5i<+CsT&{4VF+W86PA{Q+!D@OmS42?K(>L*~wHewP+d@=;4I8vMAj
zr}F6a(n?EpgQh;@VoAXJ^p5jwbzR?dRSg6-rGJ>vT%$<1Ujr-y-1A*GW+fg?V*Bi1
zK0I0E@XNVUekm%T@JU?tJf`cjd6%r+s5YZZp$#E-d7e5?v_FYDOLvdhpVV^0`^HkM
zSHD;9G|CD<@=9_nS(U=$BlX{BZ;p5T4D?)Wg!Ap$S=`oVKxgZ!?LX}*+vnQZdb4~(
zABVKua4Z3!p>C_R4<scU!=8qDOhfYWD^<aC;$Yg=`6CIs;=#>2{{@sc?$5T4OjY|l
zH%VUC!>}g@A6D(jZW^ux(j0q>>kc{W^;K;tJ{tdXMJl@pFYj$%KnI!&zO^4XyxMb9
zVt4K7>S|GUwEVi4HsQ>t!@_;$dTCjm;IQ=%cAe&Bn@6>5Uuwm*t@pMGeuwT(PZpls
zlDp(D^H()3+j6~mgRbVc?iS7;?WtOxcl!dZ!?mF$tio3D`Hb|!Gm?95#Pbw7Z7*wo
zdCMK!^@v!nDq7u6Zt)3QXH8&FL1)QahK7{AIDUFG-+gaufvoW?G2rHNBq@@$Xz564
z^?|qxn`%wzW!O4<-<&>=b1mtE?Ha7iVg99bs8}51G2@*rks^X=hpoQt{P{(n9h+6X
ztbcdhqQJ}V^?e5%`D8?He=fRaP+$1?v1ek=)vsqJ2mCk+Sl+hs_>O_ZwchlU%L5T@
z*~2cr;wJAC3jtx|gB{Vwu7pMhw{yM-hq*gOnQ22h(eAvncYIrYe1BWp7x?$D!=aC7
zMxTjzT2E)rS-vi8d+u`O+7pY#w@5|PqeN(E!=ag_9vm03eW-u2yn)#v6ftR_#&-M7
za6q!K?xu&Y-;3k6_KHTAJ?7pXi|w{DC&_(w6qFkPIfZ5gp4Pd%oPhw`#v#)0(Uy4U
Oe?S^37uO0m7W@NZe|=g2

literal 0
HcmV?d00001

diff --git a/briar-android/res/drawable-mdpi/social_new_chat.png b/briar-android/res/drawable-mdpi/social_new_chat.png
new file mode 100644
index 0000000000000000000000000000000000000000..e78580b8ec8bd72df2953c1cb27afbe201205552
GIT binary patch
literal 583
zcmV-N0=WH&P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800001b5ch_0Itp)
z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*e;
z5*Q}EVKO=Z00GBIL_t(o!|j&6N&`U@g}+V4BuYXGK}385%kc>;1q;DKAHl-X*2Wje
zTlfYd2!e%J=(P-15=AQ^c1qOj+T4X@GMnGsMbHDw!p_dzGv}UrXW$=y9RYx~b^~Yt
zVGc1OpayhA<klTD`rZUqfUg_~%mW?Z+BX;z=m8(VcgDeMz&+3sk*@EDQjY1Z0*^oo
z=vr&B)}|^D=gK(WMxm$keCxi%!G|qi9L6`m)dV}-)f|VObsyZ|A~KYK0iFSwK!KgO
zk~}Dn3hol<0Tx&uGq?fj>Z2ryfl^Kj?h*)rrb-)(S>Lbv4iew$y2i-hE`b_w4xEgd
zl!V9Gd%!jm$e2{14y;;hgBiMBeUgsqdSG3BFl&47O+z4_98e&PN5afut!)A&5qV2%
z#Z_%6wTzy5pyC+wz~`-4fzynSV*@-Ts$?!%ngawx<idAQWPB$Gi#`pL=mU|6MBW=w
zN<*0z4PZ-UkMvnnZ`?VXZwEOzei1m-oQFzaK{xxJ*mxPZG&#uK1&*Bj?*mJ~u~X`O
zlat{-eI9T-q=*z|!oc-hS|^+t?n$cVuEc%79XACj(AGO5xhs%@v$V@eQ6Bic_yJ)e
VbZitNe@p-X002ovPDHLkV1kog?|=XR

literal 0
HcmV?d00001

diff --git a/briar-android/res/drawable-mdpi/social_reply_all.png b/briar-android/res/drawable-mdpi/social_reply_all.png
new file mode 100644
index 0000000000000000000000000000000000000000..86334552edec456966f3b5bd4b9e00b73a180bd7
GIT binary patch
literal 1457
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(!p+gqz{tYX$-u?X(ACh=#L&{!#L3yw
z&D7A`#K{n**Cju>G&eP`1g19yq1O_pUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+dZnv1>
zG!Lpb1-Dzwaq86vIz}H9wMbD769T3m5EGtofgE_!Pt60S_ab1zKB?uz!N9<z?CIhd
zQgQ3ebX#v`M}f9t!&wq6fdV=#TLqdE#I3otKXNnIHU2;7^oR8y<I%7Un~XUHr2|=V
zMRbx96B8eXFJ82Og{3#}dU=A_wAAlKoZ{<F9-6FdUTyvU+?jK#eF91%PdM6{<VqPE
zxjQbn8_oP;wV!v7^gHwB_z$t2pKI*m16iyNZa8XoNGWStQ`rWlD&B3C2E27ystt4W
zo8>+%y_x2~CDOQ}mEq+c>6%|}_sn6qYWem~;IrS4?)WyGw6xsCCM@=R(W~Sa2P>65
zn#By#Z~mMw&s6d}v20$T(|T+5<AEEF>{U2&UdaDJo8VKH8D)OobZ%!pS3T_XWiKO-
z)_0kwUgz&LF&88XUpRPf_m-yHY=s*hEPT*4?e~^2d1s}vE2T=)+Ml$jsjj)ockq$N
z36b^FFZv(Uj$r0jSoWlBqU*AqVlvZXkBKarBNof?byA^0f>m37g0rCFkEcebzpZg?
zIh^}LW{%<X1qWVRL>-)T-m@~^zfokNSaK)#1ePZ}#|_SWHru-R2AA7GiT2oY%Y9XM
zY`s5IYNDo)s@@TPy+tdw8M}MVm0>;iRq*;G5$R;X!tH(;LXLjF4%*!??%2@lEGGAy
zV{Sue!P7D>pO*q&9mO@il@+{~`DeJlW8E$Ct!V8HHKm0&i@eH9x>;^+JK()_Kl_r7
zYUg>ya+efy&h2$^5Z%bv#<Pt_JoHHa^oh4C*p`dk-sr7eChPZiePpb|1*U(j5)8j4
W&2@O=)cFZih<dvExvX<aXaWFQ?+v5?

literal 0
HcmV?d00001

diff --git a/briar-android/res/drawable-xhdpi/social_new_chat.png b/briar-android/res/drawable-xhdpi/social_new_chat.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d5d9049252fe043a5b55b80cc8b5b8b8af37b3a
GIT binary patch
literal 830
zcmV-E1Ht@>P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF00001b5ch_0Itp)
z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*e;
z5*j37SuvCV00O>AL_t(|+U=S>i_}0E$A5dty?7Rah^r_FT8M=wGPNJU+OOgFu(KB|
zM9;!+AlqnTF^Gzd9BOmufmf|>%WigTo}6Ltb~Bl5HkoAp%@UY|Jik1zGYdgMK|w)5
zK|vi=FMFYsx&xdArk>li0qkk5-{mcgXRdDoSAfJ*!;RQvfG;*T%mO$ETm&Yb0$7vZ
zXTVz*025&BDS${qcUNou()OmfCAlp9@=vwagCaM!0YD_byG4y}1AwD14-Ax2#};U<
zTMOXm%L4<~hxiO&TGn`50Gk=w%dXjf9Pix%UMw;`&-HBp^n~(z;8#WQyCZOogZqCi
zYrGAB1b7bYSLDVIGo9N~ytjz)*({ppZdGV=*$*sgye)uHMF2*18=n^fM#5kRI9~yP
zb@>3MF5_(gM8LfYtHAHF0LmC|TL!Bfj^YBal2yMzS_b!lzq6YsE1vPUfc~;Vrf3;_
z0(`Ey>$?ivckCHTyI=w&Wn*ogP8wk5yhCj<I4SU=RcTTOfV2$W%EVt?L(c_y7qe;r
z5pcI5)1RpVz#1J_Y)QVg3LelFLzGh6lB^9d)LI{Q3IK3jnx!%D8hG3_06qS1u0*aO
z5X+d_Yq+uNbMludS}tb+`~XhV{KL^xGN2V{r>y{dkm+ygfd?Ee<3aJv=EAF*d;ZV|
z1OV<DXOef~ngg1uP2kc=)k#9u0GGvZ1DsJxUCSw%L#_3JF97<|<jgxnG2+eXQQHk^
zj20!)H^8IVqfp&m03hAIn`AtDk!>5|RmT@qwe?l9&mRGM%-<6<(!ccOdMG{3qR{{5
zMGlY{{|-FJS$aD&3w*Sg1&mKg(B@nqDY8EG^fr~jo@;Um8zN1XO;UFGb5!tv0Kl(S
z*hY~wOJiwtItSn<@JYJCgGx(BLz^Y1ZZ;&r03ZMe3hJ`{0Ve6U!bNUCRR91007*qo
IM6N<$f?o?}0RR91

literal 0
HcmV?d00001

diff --git a/briar-android/res/drawable-xhdpi/social_reply_all.png b/briar-android/res/drawable-xhdpi/social_reply_all.png
new file mode 100644
index 0000000000000000000000000000000000000000..f10a492c0229009a9b358cbd8ccae910ba21cdab
GIT binary patch
literal 1929
zcmaJ?eN<Cr7#{)&Bp?l)LTuN8f&{zwj)B|Ffn(!i!Y09v;s@$wyRbF4JGN^KMB~gM
znK}G`wD1Et5NHY_VCE-`0Hq==^bm9`1ObaAHJSujcO>Q?+CAsq_kEu8{C>~#ywC4F
zcW06`&fU$&jY6Tgi{nMf<hsoMy1YVuPpGnPlgoS}CY4CRGl*<dk5L2)JRJkX8Z;A2
z#!y90)-`M?h2j{XlBE)<l0-y~Ygni~hGo>~NHm4AG|H$$<!X!o(y>gHmd_l$aD@q|
z6ny5Ya0w*Q2{ENAK39*W<Vt1oT(z91U`9m(ON|Igpuq?fFlshw4TzD?oZ>~uz1<Bm
zfvFHe&1e2wRH`Hi5aN0a2xmc|awq}@A~-CV1MxV#5P%KA5D2qDXh|pxA>m692nL=$
zOfs8Zk%1(OVxQ(BEk0985IO_|v$L~V*<mbPp9#V|9uI`rAe$XZMuZx2v;=Al)f)V#
z8AO;tu2<;@6|M#BjA%N(ncy?YO8;#^qkBfHH9S2g^1whNssmvbWN&FYP$K#NP>tqU
zw1G&*{;T(e#0FW84g-@h1HM@=CkL0|Z%?H|gnA4maJ>x2H%*^nk`gCygA&&P!jy0p
z3`kJ9N^AE_AtVw+tThm*R*s29d?v}kQmGV3I5$=xfY?xYG$exI7?{U_c`=-5p&&LK
zUc!lnr?DbjzFC85iD|6jKP>!GtX&Km9hq5#=~eGwida3a0j8QpR4=S$Nh~icBKoC#
zPqB&@)-r_^z7z|R#enwFJ~!%V7pWin_L*==<C*j^Eva`sDQs>2L_K+5Cd49v%-A!s
zcCDH!@IE3*uJr~1liT}c05fu4S6r<uWlq=1griQ3N#`e2#+cZ<@wn%mf^ymEKNWMF
zcX`*D+|HWTw5rwb@C&yI3oF0>YE#Dkw6K|xXCpGgh9)laGBYB^Ou+0@uk&u)>2uLJ
zac8{lV4=k2`!{)AKl9d}DDPb{y|(q{Usi0lHBh(Bo~)oU=yd514im1v?ybW>$Ry*F
zCc0I86ZN?v=}xFt9M0crUT>;W-CcSWd2e8Q2(_uh2JF1@@#sC%Sd}kq`*f}#p~Lo5
zCc~q;GLKe#v@_RNR#K}F<dk~2yaCQC!Ph*Da7ik$)mL0}pTRzrcgbluXtdz%#1qA}
zRW2QojLJNZ*57DPPW>_76~JhBKspQ$HD+J4mvtv6n6%G#JX&`pCaC3)N=`8TGlhB=
zXO7=*m~-)*)39vf^WsrXVq_D4fN-r1h+1sx4}C<lRy6N>bFRZ~S!9#Z5|?KQso9rb
zYtA+gnQw3v-pOTo+-+*hmW60IN=0etA?RwRpLuj_uHbOvR=Q)~z(iTzae?7>VNc>M
z#-!yMH-?+gz24K|{LLd9E{KL#72_)#5A=>Mwyv?xAAo*TxP|z8AG^3gUeKPk7cvic
z_P3O#{p!fI3gNWn+@iYGKYD=8o6meSG43N82{>fYn|`L$oXCDKNdM}=@{-b5W$MWB
zOR=em+z(%)Rre40)NO|Z=W2SL?=RT79;;bZYFK!&({Cfa=V<(V*O5*~<$nM38r|$`
zalGdCuM-X$wmxnWE?)cB_`w<LDmE1SBIrVvwYxsdJ{*5}7W)p;Os)5n4gMLzNI!Ul
zz5=~;y5zTR#lr6RK;yh&=XEU!eRrE)vHHM|DA@A-?@_uX#&U}|6TBW2cy-9-o8neU
zV`rm9XFgpS`{l~*>RY+iVC$CeBCbl$nFNFWA;E{Qy9fTz7P%lZM_T07dCc+NpjTNE
z-tuw#<7ELgRDHGe>;=a+ko*cua75Aj&gp|WcY}H^*RdB#H}2BzSnlY`6}0&^e3s+&
zca?i>(SkzHtfDUx5AC7VugI;v)3bLEm42*a%L3ZKp2yrazV~fS8-LENdmepZW_FGE
z&}&(JlOKHU7xd;$oLglCLK(LBkQ!=Nb-=LG%CS(?xy?Frk*%)bZ)JbtX?md9e{o>q
z##x?wAN!UTmao=X6Rc`$N^fELZhiEn2h7gWP1~Y?5p&aZSIKybbMMyGX@SH0*0v$X
zfVba0&Kz@!4_vpS9I+jc&ij_vQtww_zW=3ENy`}eeP2WL?yr*;xy)9^ENb3RJwnU<
t#`$Ey&=ci}))udyp1*r#SpQLqBgJwut<u%v@U8vlD2|beYN9uq{sE<M_IdyS

literal 0
HcmV?d00001

diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml
index a55aadfa90..866b33e586 100644
--- a/briar-android/res/values/color.xml
+++ b/briar-android/res/values/color.xml
@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-	<color name="HorizontalBorder">#CCCCCC</color>
+	<color name="horizontal_border">#CCCCCC</color>
+	<color name="anonymous_author">#999999</color>
+	<color name="pseudonymous_author">#000000</color>
 </resources>
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 92486abf3b..14258e0471 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -11,14 +11,13 @@
 	<string name="quit_button">Quit</string>
 	<string name="contact_list_title">Contacts</string>
 	<string name="contact_connected">Connected</string>
-	<string name="contact_last_connected">Last connected &lt;br /&gt; %1$s</string>
-	<string name="search_button">Search</string>
+	<string name="format_contact_last_connected">Last connected &lt;br /&gt; %1$s</string>
 	<string name="add_contact_title">Add a Contact</string>
 	<string name="same_network">Briar can add contacts via Wi-Fi or Bluetooth.  To use Wi-Fi you must both be connected to the same network.</string>
 	<string name="wifi_not_available">Wi-Fi is not available on this device</string>
 	<string name="wifi_disabled">Wi-Fi is OFF</string>
 	<string name="wifi_disconnected">Wi-Fi is DISCONNECTED</string>
-	<string name="wifi_connected">Wi-Fi is CONNECTED to %1$s</string>
+	<string name="format_wifi_connected">Wi-Fi is CONNECTED to %1$s</string>
 	<string name="bluetooth_not_available">Bluetooth is not available on this device</string>
 	<string name="bluetooth_disabled">Bluetooth is OFF</string>
 	<string name="bluetooth_not_discoverable">Bluetooth is NOT DISCOVERABLE</string>
@@ -26,7 +25,7 @@
 	<string name="continue_button">Continue</string>
 	<string name="your_invitation_code">Your invitation code is</string>
 	<string name="enter_invitation_code">Please enter your contact\'s invitation code:</string>
-	<string name="connecting_wifi">Connecting via %1$s\u2026</string>
+	<string name="format_connecting_wifi">Connecting via %1$s\u2026</string>
 	<string name="connecting_bluetooth">Connecting via Bluetooth\u2026</string>
 	<string name="connection_failed">Connection failed</string>
 	<string name="check_same_network">Please check that you are both using the same network.</string>
@@ -41,7 +40,11 @@
 	<string name="enter_nickname">Please enter a nickname for this contact:</string>
 	<string name="done_button">Done</string>
 	<string name="messages_title">Messages</string>
-	<string name="message_from">From: %1$s</string>
-	<string name="compose_title">New Message</string>
-	<string name="message_to">To:</string>
+	<string name="format_from">From: %1$s</string>
+	<string name="format_to">To: %1$s</string>
+	<string name="compose_message_title">New Message</string>
+	<string name="to">To:</string>
+	<string name="groups_title">Groups</string>
+	<string name="anonymous">(Anonymous)</string>
+	<string name="compose_group_title">New Post</string>
 </resources>
diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
index 090ceca84d..02cea07efb 100644
--- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
+++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
@@ -12,6 +12,7 @@ import net.sf.briar.R;
 import net.sf.briar.android.BriarService.BriarBinder;
 import net.sf.briar.android.BriarService.BriarServiceConnection;
 import net.sf.briar.android.contact.ContactListActivity;
+import net.sf.briar.android.groups.GroupListActivity;
 import net.sf.briar.android.messages.ConversationListActivity;
 import net.sf.briar.android.widgets.CommonLayoutParams;
 import android.content.Intent;
@@ -88,7 +89,8 @@ public class HomeScreenActivity extends BriarActivity {
 			groupsButton.setText(R.string.groups_button);
 			groupsButton.setOnClickListener(new OnClickListener() {
 				public void onClick(View view) {
-					// FIXME: Hook this button up to an activity
+					startActivity(new Intent(HomeScreenActivity.this,
+							GroupListActivity.class));
 				}
 			});
 			buttons.add(groupsButton);
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java b/briar-android/src/net/sf/briar/android/contact/ContactComparator.java
index 6495bf6310..8e9207c6fc 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactComparator.java
@@ -7,7 +7,7 @@ class ContactComparator implements Comparator<ContactListItem> {
 	static final ContactComparator INSTANCE = new ContactComparator();
 
 	public int compare(ContactListItem a, ContactListItem b) {
-		return String.CASE_INSENSITIVE_ORDER.compare(a.contact.getName(),
-				b.contact.getName());
+		return String.CASE_INSENSITIVE_ORDER.compare(a.getContactName(),
+				b.getContactName());
 	}
 }
\ No newline at end of file
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 39cb91c6c1..bb10e399cf 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
@@ -8,7 +8,6 @@ import java.util.ArrayList;
 import net.sf.briar.R;
 import net.sf.briar.android.widgets.CommonLayoutParams;
 import android.content.Context;
-import android.content.res.Resources;
 import android.text.Html;
 import android.text.format.DateUtils;
 import android.view.View;
@@ -46,8 +45,9 @@ implements OnItemClickListener {
 		// Give me all the unused width
 		name.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
 		name.setTextSize(18);
+		name.setMaxLines(1);
 		name.setPadding(0, 10, 10, 10);
-		name.setText(item.getName());
+		name.setText(item.getContactName());
 		layout.addView(name);
 
 		TextView connected = new TextView(ctx);
@@ -56,8 +56,8 @@ implements OnItemClickListener {
 		if(item.isConnected()) {
 			connected.setText(R.string.contact_connected);
 		} else {
-			Resources res = ctx.getResources();
-			String format = res.getString(R.string.contact_last_connected);
+			String format = ctx.getResources().getString(
+					R.string.format_contact_last_connected);
 			long then = item.getLastConnected();
 			CharSequence ago = DateUtils.getRelativeTimeSpanString(then);
 			connected.setText(Html.fromHtml(String.format(format, ago)));
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
index cba805e863..0b030f2f96 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
@@ -6,7 +6,7 @@ import net.sf.briar.api.ContactId;
 // This class is not thread-safe
 class ContactListItem {
 
-	final Contact contact;
+	private final Contact contact;
 	private boolean connected;
 
 	ContactListItem(Contact contact, boolean connected) {
@@ -18,7 +18,7 @@ class ContactListItem {
 		return contact.getId();
 	}
 
-	String getName() {
+	String getContactName() {
 		return contact.getName();
 	}
 
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
new file mode 100644
index 0000000000..8ed287306a
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
@@ -0,0 +1,226 @@
+package net.sf.briar.android.groups;
+
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.AscendingHeaderComparator;
+import net.sf.briar.android.BriarActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalBorder;
+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.db.GroupMessageHeader;
+import net.sf.briar.api.db.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.SubscriptionAddedEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.Author;
+import net.sf.briar.api.messaging.GroupId;
+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.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class GroupActivity extends BriarActivity implements DatabaseListener,
+OnClickListener, OnItemClickListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(GroupActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private DatabaseComponent db;
+	@Inject @DatabaseExecutor private Executor dbExecutor;
+
+	private GroupId groupId = null;
+	private String groupName = null;
+	private GroupAdapter adapter = null;
+	private ListView list = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(id == null) throw new IllegalStateException();
+		groupId = new GroupId(id);
+		groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+		if(groupName == null) throw new IllegalStateException();
+		setTitle(groupName);
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(CommonLayoutParams.MATCH_MATCH);
+		layout.setOrientation(VERTICAL);
+		layout.setGravity(CENTER_HORIZONTAL);
+
+		adapter = new GroupAdapter(this);
+		list = new ListView(this);
+		// Give me all the width and all the unused height
+		list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
+		list.setAdapter(adapter);
+		list.setOnItemClickListener(this);
+		layout.addView(list);
+
+		layout.addView(new HorizontalBorder(this));
+
+		ImageButton composeButton = new ImageButton(this);
+		composeButton.setBackgroundResource(0);
+		composeButton.setImageResource(R.drawable.content_new_email);
+		composeButton.setOnClickListener(this);
+		layout.addView(composeButton);
+
+		setContentView(layout);
+
+		// Listen for messages and groups being added or removed
+		db.addListener(this);
+		// Bind to the service so we can wait for the DB to be opened
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		reloadMessageHeaders();
+	}
+
+	private void reloadMessageHeaders() {
+		final DatabaseComponent db = this.db;
+		final GroupId groupId = this.groupId;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// Load the message headers from the database
+					Collection<GroupMessageHeader> headers =
+							db.getMessageHeaders(groupId);
+					if(LOG.isLoggable(INFO))
+						LOG.info("Loaded " + headers.size() + " headers");
+					// Update the conversation
+					updateConversation(headers);
+				} 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();
+				}
+			}
+		});
+	}
+
+	private void updateConversation(
+			final Collection<GroupMessageHeader> headers) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				List<GroupMessageHeader> sort =
+						new ArrayList<GroupMessageHeader>(headers);
+				Collections.sort(sort, AscendingHeaderComparator.INSTANCE);
+				int firstUnread = -1;
+				adapter.clear();
+				for(GroupMessageHeader h : sort) {
+					if(firstUnread == -1 && !h.isRead())
+						firstUnread = adapter.getCount();
+					adapter.add(h);
+				}
+				if(firstUnread == -1) list.setSelection(adapter.getCount() - 1);
+				else list.setSelection(firstUnread);
+			}
+		});
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		db.removeListener(this);
+		unbindService(serviceConnection);
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof MessageAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+			reloadMessageHeaders();
+		} else if(e instanceof MessageExpiredEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
+			reloadMessageHeaders();
+		} else if(e instanceof SubscriptionAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
+			reloadMessageHeaders();
+		} else if(e instanceof SubscriptionRemovedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
+			reloadMessageHeaders();
+		}
+	}
+
+	public void onClick(View view) {
+		Intent i = new Intent(this, WriteGroupMessageActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+		i.putExtra("net.sf.briar.GROUP_NAME", groupName);
+		startActivity(i);
+	}
+
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		showMessage(position);
+	}
+
+	private void showMessage(int position) {
+		GroupMessageHeader item = adapter.getItem(position);
+		Intent i = new Intent(this, ReadGroupMessageActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+		i.putExtra("net.sf.briar.GROUP_NAME", groupName);
+		i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
+		Author author = item.getAuthor();
+		if(author == null) {
+			i.putExtra("net.sf.briar.ANONYMOUS", true);
+		} else {
+			i.putExtra("net.sf.briar.ANONYMOUS", false);
+			i.putExtra("net.sf.briar.AUTHOR_ID", author.getId().getBytes());
+			i.putExtra("net.sf.briar.AUTHOR_NAME", author.getName());
+		}
+		i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType());
+		i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp());
+		i.putExtra("net.sf.briar.FIRST", position == 0);
+		i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1);
+		startActivityForResult(i, position);
+	}
+
+	@Override
+	public void onActivityResult(int request, int result, Intent data) {
+		if(result == ReadGroupMessageActivity.RESULT_PREV) {
+			int position = request - 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		} else if(result == ReadGroupMessageActivity.RESULT_NEXT) {
+			int position = request + 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		}
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java
new file mode 100644
index 0000000000..17915c8d45
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java
@@ -0,0 +1,92 @@
+package net.sf.briar.android.groups;
+
+import static android.graphics.Typeface.BOLD;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+
+import java.util.ArrayList;
+
+import net.sf.briar.R;
+import net.sf.briar.android.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.messaging.Author;
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+class GroupAdapter extends ArrayAdapter<GroupMessageHeader> {
+
+	GroupAdapter(Context ctx) {
+		super(ctx, android.R.layout.simple_expandable_list_item_1,
+				new ArrayList<GroupMessageHeader>());
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		GroupMessageHeader item = getItem(position);
+		Context ctx = getContext();
+		// FIXME: Use a RelativeLayout
+		LinearLayout layout = new LinearLayout(ctx);
+		layout.setOrientation(HORIZONTAL);
+		layout.setGravity(CENTER_VERTICAL);
+
+		LinearLayout innerLayout = new LinearLayout(ctx);
+		// Give me all the unused width
+		innerLayout.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
+		innerLayout.setOrientation(VERTICAL);
+
+		Author author = item.getAuthor();
+
+		TextView name = new TextView(ctx);
+		name.setTextSize(18);
+		name.setMaxLines(1);
+		name.setPadding(10, 10, 10, 10);
+		Resources res = ctx.getResources();
+		if(author == null) {
+			name.setTextColor(res.getColor(R.color.anonymous_author));
+			name.setText(R.string.anonymous);
+		} else {
+			name.setTextColor(res.getColor(R.color.pseudonymous_author));
+			name.setText(author.getName());
+		}
+		innerLayout.addView(name);
+
+		if(item.getContentType().equals("text/plain")) {
+			TextView subject = new TextView(ctx);
+			subject.setTextSize(14);
+			subject.setMaxLines(2);
+			subject.setPadding(10, 0, 10, 10);
+			if(!item.isRead()) subject.setTypeface(null, BOLD);
+			subject.setText(item.getSubject());
+			innerLayout.addView(subject);
+		} else {
+			LinearLayout innerInnerLayout = new LinearLayout(ctx);
+			innerInnerLayout.setOrientation(HORIZONTAL);
+			ImageView attachment = new ImageView(ctx);
+			attachment.setPadding(10, 0, 10, 10);
+			attachment.setImageResource(R.drawable.content_attachment);
+			innerInnerLayout.addView(attachment);
+			innerInnerLayout.addView(new HorizontalSpace(ctx));
+			innerLayout.addView(innerInnerLayout);
+		}
+		layout.addView(innerLayout);
+
+		TextView date = new TextView(ctx);
+		date.setTextSize(14);
+		date.setPadding(0, 10, 10, 10);
+		long then = item.getTimestamp(), now = System.currentTimeMillis();
+		date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
+		layout.addView(date);
+
+		return layout;
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
new file mode 100644
index 0000000000..5c0c3beecf
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
@@ -0,0 +1,302 @@
+package net.sf.briar.android.groups;
+
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+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.android.DescendingHeaderComparator;
+import net.sf.briar.android.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalBorder;
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.CryptoComponent;
+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.db.GroupMessageHeader;
+import net.sf.briar.api.db.NoSuchSubscriptionException;
+import net.sf.briar.api.db.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.MessageAddedEvent;
+import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.SubscriptionAddedEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.Author;
+import net.sf.briar.api.messaging.AuthorFactory;
+import net.sf.briar.api.messaging.Group;
+import net.sf.briar.api.messaging.GroupFactory;
+import net.sf.briar.api.messaging.Message;
+import net.sf.briar.api.messaging.MessageFactory;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class GroupListActivity extends BriarActivity
+implements OnClickListener, DatabaseListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(GroupListActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private CryptoComponent crypto;
+	@Inject private DatabaseComponent db;
+	@Inject @DatabaseExecutor private Executor dbExecutor;
+	@Inject private AuthorFactory authorFactory;
+	@Inject private GroupFactory groupFactory;
+	@Inject private MessageFactory messageFactory;
+
+	private GroupListAdapter adapter = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(CommonLayoutParams.MATCH_MATCH);
+		layout.setOrientation(VERTICAL);
+		layout.setGravity(CENTER_HORIZONTAL);
+
+		adapter = new GroupListAdapter(this);
+		ListView list = new ListView(this);
+		// Give me all the width and all the unused height
+		list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
+		list.setAdapter(adapter);
+		list.setOnItemClickListener(adapter);
+		layout.addView(list);
+
+		layout.addView(new HorizontalBorder(this));
+
+		ImageButton newGroupButton = new ImageButton(this);
+		newGroupButton.setBackgroundResource(0);
+		newGroupButton.setImageResource(R.drawable.social_new_chat);
+		newGroupButton.setOnClickListener(this);
+		layout.addView(newGroupButton);
+
+		setContentView(layout);
+
+		// Listen for messages and groups being added or removed
+		db.addListener(this);
+		// Bind to the service so we can wait for the DB to be opened
+		bindService(new Intent(BriarService.class.getName()),
+				serviceConnection, 0);
+
+		// Add some fake messages to the database in a background thread
+		insertFakeMessages();
+	}
+
+	// FIXME: Remove this
+	private void insertFakeMessages() {
+		final DatabaseComponent db = this.db;
+		final GroupFactory groupFactory = this.groupFactory;
+		final MessageFactory messageFactory = this.messageFactory;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// If there are no groups in the DB, create some fake ones
+					Collection<Group> groups = db.getSubscriptions();
+					if(!groups.isEmpty()) return;
+					if(LOG.isLoggable(INFO))
+						LOG.info("Inserting fake groups and messages");
+					// We'll also need a contact to receive messages from
+					ContactId contactId = db.addContact("Dave");
+					// Finally, we'll need some authors for the messages
+					KeyPair keyPair = crypto.generateSignatureKeyPair();
+					byte[] publicKey = keyPair.getPublic().getEncoded();
+					PrivateKey privateKey = keyPair.getPrivate();
+					Author author = authorFactory.createAuthor("Batman",
+							publicKey);
+					Author author1 = authorFactory.createAuthor("Duckman",
+							publicKey);
+					// Insert some fake groups and make them visible
+					Group group = groupFactory.createGroup("DisneyLeaks");
+					db.subscribe(group);
+					db.setVisibility(group.getId(), Arrays.asList(contactId));
+					Group group1 = groupFactory.createGroup("Godwin's Lore");
+					db.subscribe(group1);
+					db.setVisibility(group1.getId(), Arrays.asList(contactId));
+					// Insert some text messages to the groups
+					for(int i = 0; i < 20; i++) {
+						String body;
+						if(i % 3 == 0) {
+							body = "Message " + i + " is short.";
+						} else { 
+							body = "Message " + i + " is long enough to wrap"
+									+ " onto a second line on some screens.";
+						}
+						Group g = i % 2 == 0 ? group : group1;
+						Message m;
+						if(i % 5 == 0) {
+							m = messageFactory.createAnonymousMessage(null, g,
+									"text/plain", body.getBytes("UTF-8"));
+						} else if(i % 5 == 2) {
+							m = messageFactory.createPseudonymousMessage(null,
+									g, author, privateKey, "text/plain",
+									body.getBytes("UTF-8"));
+						} else {
+							m = messageFactory.createPseudonymousMessage(null,
+									g, author1, privateKey, "text/plain",
+									body.getBytes("UTF-8"));
+						}
+						if(Math.random() < 0.5) db.addLocalGroupMessage(m);
+						else db.receiveMessage(contactId, m);
+						db.setReadFlag(m.getId(), i % 4 == 0);
+					}
+					// Insert a non-text message
+					Message m = messageFactory.createAnonymousMessage(null,
+							group, "image/jpeg", new byte[1000]);
+					db.receiveMessage(contactId, m);
+					// Insert a long text message
+					StringBuilder s = new StringBuilder();
+					for(int i = 0; i < 100; i++)
+						s.append("This is a very tedious message. ");
+					String body = s.toString();
+					m = messageFactory.createAnonymousMessage(m.getId(),
+							group1, "text/plain", body.getBytes("UTF-8"));
+					db.addLocalGroupMessage(m);
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(GeneralSecurityException 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(IOException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		reloadGroupList();
+	}
+
+	private void reloadGroupList() {
+		final DatabaseComponent db = this.db;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					// Wait for the service to be bound and started
+					serviceConnection.waitForStartup();
+					// Load the groups and message headers from the DB
+					if(LOG.isLoggable(INFO)) LOG.info("Loading groups");
+					Collection<Group> groups = db.getSubscriptions();
+					if(LOG.isLoggable(INFO))
+						LOG.info("Loaded " + groups.size() + " groups");
+					List<GroupListItem> items = new ArrayList<GroupListItem>();
+					for(Group g : groups) {
+						// Filter out restricted groups
+						if(g.getPublicKey() != null) continue;
+						Collection<GroupMessageHeader> headers;
+						try {
+							headers = db.getMessageHeaders(g.getId());
+						} catch(NoSuchSubscriptionException e) {
+							// We'll reload the list when we get the event
+							continue;
+						}
+						if(LOG.isLoggable(INFO))
+							LOG.info("Loaded " + headers.size() + " headers");
+						if(!headers.isEmpty())
+							items.add(createItem(g, headers));
+					}
+					// Update the group list
+					updateGroupList(Collections.unmodifiableList(items));
+				} 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();
+				}
+			}
+		});
+	}
+
+	private GroupListItem createItem(Group group,
+			Collection<GroupMessageHeader> headers) {
+		List<GroupMessageHeader> sort =
+				new ArrayList<GroupMessageHeader>(headers);
+		Collections.sort(sort, DescendingHeaderComparator.INSTANCE);
+		return new GroupListItem(group, sort);
+	}
+
+	private void updateGroupList(final Collection<GroupListItem> items) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				adapter.clear();
+				for(GroupListItem i : items) adapter.add(i);
+				adapter.sort(GroupComparator.INSTANCE);
+			}
+		});
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		db.removeListener(this);
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		startActivity(new Intent(this, WriteGroupMessageActivity.class));
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof MessageAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+			reloadGroupList();
+		} else if(e instanceof MessageExpiredEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
+			reloadGroupList();
+		} else if(e instanceof SubscriptionAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
+			reloadGroupList();
+		} else if(e instanceof SubscriptionRemovedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
+			reloadGroupList();
+		}
+	}
+
+	private static class GroupComparator implements Comparator<GroupListItem> {
+
+		private static final GroupComparator INSTANCE = new GroupComparator();
+
+		public int compare(GroupListItem a, GroupListItem b) {
+			return String.CASE_INSENSITIVE_ORDER.compare(a.getGroupName(),
+					b.getGroupName());
+		}
+	}
+}
\ No newline at end of file
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
new file mode 100644
index 0000000000..102a93a06c
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
@@ -0,0 +1,85 @@
+package net.sf.briar.android.groups;
+
+import static android.graphics.Typeface.BOLD;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.Gravity.LEFT;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+
+import java.util.ArrayList;
+
+import net.sf.briar.android.widgets.CommonLayoutParams;
+import net.sf.briar.util.StringUtils;
+import android.content.Context;
+import android.content.Intent;
+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.LinearLayout;
+import android.widget.TextView;
+
+class GroupListAdapter extends ArrayAdapter<GroupListItem>
+implements OnItemClickListener {
+
+	GroupListAdapter(Context ctx) {
+		super(ctx, android.R.layout.simple_expandable_list_item_1,
+				new ArrayList<GroupListItem>());
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		GroupListItem item = getItem(position);
+		Context ctx = getContext();
+		LinearLayout layout = new LinearLayout(ctx);
+		layout.setOrientation(HORIZONTAL);
+		layout.setGravity(CENTER_VERTICAL);
+
+		LinearLayout innerLayout = new LinearLayout(ctx);
+		// Give me all the unused width
+		innerLayout.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
+		innerLayout.setOrientation(VERTICAL);
+		innerLayout.setGravity(LEFT);
+
+		TextView name = new TextView(ctx);
+		name.setTextSize(18);
+		name.setMaxLines(1);
+		name.setPadding(10, 10, 10, 10);
+		int unread = item.getUnreadCount();
+		if(unread > 0) name.setText(item.getGroupName() + " (" + unread + ")");
+		else name.setText(item.getGroupName());
+		innerLayout.addView(name);
+
+		if(!StringUtils.isNullOrEmpty(item.getSubject())) {
+			TextView subject = new TextView(ctx);
+			subject.setTextSize(14);
+			subject.setMaxLines(2);
+			subject.setPadding(10, 0, 10, 10);
+			if(unread > 0) subject.setTypeface(null, BOLD);
+			subject.setText(item.getSubject());
+			innerLayout.addView(subject);
+		}
+		layout.addView(innerLayout);
+
+		TextView date = new TextView(ctx);
+		date.setTextSize(14);
+		date.setPadding(0, 10, 10, 10);
+		long then = item.getTimestamp(), now = System.currentTimeMillis();
+		date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
+		layout.addView(date);
+
+		return layout;
+	}
+
+	public void onItemClick(AdapterView<?> parent, View view, int position,
+			long id) {
+		GroupListItem item = getItem(position);
+		Intent i = new Intent(getContext(), GroupActivity.class);
+		i.putExtra("net.sf.briar.GROUP_ID", item.getGroupId().getBytes());
+		i.putExtra("net.sf.briar.GROUP_NAME", item.getGroupName());
+		getContext().startActivity(i);
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
new file mode 100644
index 0000000000..38a4513c21
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
@@ -0,0 +1,57 @@
+package net.sf.briar.android.groups;
+
+import java.util.Collections;
+import java.util.List;
+
+import net.sf.briar.android.DescendingHeaderComparator;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.messaging.Author;
+import net.sf.briar.api.messaging.Group;
+import net.sf.briar.api.messaging.GroupId;
+
+class GroupListItem {
+
+	private final Group group;
+	private final String author, subject;
+	private final long timestamp;
+	private final int unread;
+
+	GroupListItem(Group group, List<GroupMessageHeader> headers) {
+		if(headers.isEmpty()) throw new IllegalArgumentException();
+		this.group = group;
+		Collections.sort(headers, DescendingHeaderComparator.INSTANCE);
+		GroupMessageHeader newest = headers.get(0);
+		Author a = newest.getAuthor();
+		if(a == null) author = null;
+		else author = a.getName();
+		subject = newest.getSubject();
+		timestamp = newest.getTimestamp();
+		int unread = 0;
+		for(GroupMessageHeader h : headers) if(!h.isRead()) unread++;
+		this.unread = unread;
+	}
+
+	GroupId getGroupId() {
+		return group.getId();
+	}
+
+	String getGroupName() {
+		return group.getName();
+	}
+
+	String getAuthorName() {
+		return author;
+	}
+
+	String getSubject() {
+		return subject;
+	}
+
+	long getTimestamp() {
+		return timestamp;
+	}
+
+	int getUnreadCount() {
+		return unread;
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java
new file mode 100644
index 0000000000..2c16ebfaf8
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java
@@ -0,0 +1,36 @@
+package net.sf.briar.android.groups;
+
+import java.util.ArrayList;
+
+import net.sf.briar.api.messaging.Group;
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+
+class GroupNameSpinnerAdapter extends ArrayAdapter<Group>
+implements SpinnerAdapter {
+
+	GroupNameSpinnerAdapter(Context context) {
+		super(context, android.R.layout.simple_spinner_item,
+				new ArrayList<Group>());
+	}
+
+	@Override
+	public View getView(int position, View convertView, ViewGroup parent) {
+		TextView name = new TextView(getContext());
+		name.setTextSize(18);
+		name.setMaxLines(1);
+		name.setPadding(10, 10, 10, 10);
+		name.setText(getItem(position).getName());
+		return name;
+	}
+
+	@Override
+	public View getDropDownView(int position, View convertView,
+			ViewGroup parent) {
+		return getView(position, convertView, parent);
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
new file mode 100644
index 0000000000..d5750019ba
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
@@ -0,0 +1,288 @@
+package net.sf.briar.android.groups;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_VERTICAL;
+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.android.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalBorder;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.android.BundleEncrypter;
+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.AuthorId;
+import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.MessageId;
+import android.content.Intent;
+import android.content.res.Resources;
+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.ScrollView;
+import android.widget.TextView;
+
+import com.google.inject.Inject;
+
+public class ReadGroupMessageActivity extends BriarActivity
+implements OnClickListener {
+
+	static final int RESULT_REPLY = RESULT_FIRST_USER;
+	static final int RESULT_PREV = RESULT_FIRST_USER + 1;
+	static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
+
+	private static final Logger LOG =
+			Logger.getLogger(ReadGroupMessageActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private BundleEncrypter bundleEncrypter;
+	@Inject private DatabaseComponent db;
+	@Inject @DatabaseExecutor private Executor dbExecutor;
+
+	private GroupId groupId = null;
+	private MessageId messageId = null;
+	private AuthorId authorId = null;
+	private String authorName = null;
+	private boolean read;
+	private ImageButton readButton = null, prevButton = null, nextButton = null;
+	private ImageButton replyButton = null;
+	private TextView content = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(id == null) throw new IllegalStateException();
+		groupId = new GroupId(id);
+		String groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+		if(groupName == null) throw new IllegalStateException();
+		setTitle(groupName);
+		id = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID");
+		if(id == null) throw new IllegalStateException();
+		messageId = new MessageId(id);
+		boolean anonymous = i.getBooleanExtra("net.sf.briar.ANONYMOUS", false);
+		if(!anonymous) {
+			id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
+			if(id == null) throw new IllegalStateException();
+			authorId = new AuthorId(id);
+			authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME");
+			if(authorName == null) throw new IllegalStateException();
+		}
+		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();
+		boolean first = i.getBooleanExtra("net.sf.briar.FIRST", false);
+		boolean last = i.getBooleanExtra("net.sf.briar.LAST", false);
+
+		if(state != null && bundleEncrypter.decrypt(state)) {
+			read = state.getBoolean("net.sf.briar.READ");
+		} else {
+			read = false;
+			setReadInDatabase(true);
+		}
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		layout.setOrientation(VERTICAL);
+
+		ScrollView scrollView = new ScrollView(this);
+		// Give me all the width and all the unused height
+		scrollView.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
+
+		LinearLayout message = new LinearLayout(this);
+		message.setOrientation(VERTICAL);
+
+		LinearLayout header = new LinearLayout(this);
+		header.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		header.setOrientation(HORIZONTAL);
+		header.setGravity(CENTER_VERTICAL);
+
+		TextView author = new TextView(this);
+		// Give me all the unused width
+		author.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
+		author.setTextSize(18);
+		author.setMaxLines(1);
+		author.setPadding(10, 10, 10, 10);
+		Resources res = getResources();
+		if(authorName == null) {
+			author.setTextColor(res.getColor(R.color.anonymous_author));
+			author.setText(R.string.anonymous);
+		} else {
+			author.setTextColor(res.getColor(R.color.pseudonymous_author));
+			author.setText(authorName);
+		}
+		header.addView(author);
+
+		TextView date = new TextView(this);
+		date.setTextSize(14);
+		date.setPadding(0, 10, 10, 10);
+		long now = System.currentTimeMillis();
+		date.setText(DateUtils.formatSameDayTime(timestamp, now, SHORT, SHORT));
+		header.addView(date);
+		message.addView(header);
+
+		if(contentType.equals("text/plain")) {
+			// Load and display the message body
+			content = new TextView(this);
+			content.setPadding(10, 0, 10, 10);
+			message.addView(content);
+			loadMessageBody();
+		}
+		scrollView.addView(message);
+		layout.addView(scrollView);
+
+		layout.addView(new HorizontalBorder(this));
+
+		LinearLayout footer = new LinearLayout(this);
+		footer.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		footer.setOrientation(HORIZONTAL);
+		footer.setGravity(CENTER);
+
+		readButton = new ImageButton(this);
+		readButton.setBackgroundResource(0);
+		if(read) readButton.setImageResource(R.drawable.content_unread);
+		else readButton.setImageResource(R.drawable.content_read);
+		readButton.setOnClickListener(this);
+		footer.addView(readButton);
+		footer.addView(new HorizontalSpace(this));
+
+		prevButton = new ImageButton(this);
+		prevButton.setBackgroundResource(0);
+		prevButton.setImageResource(R.drawable.navigation_previous_item);
+		prevButton.setOnClickListener(this);
+		prevButton.setEnabled(!first);
+		footer.addView(prevButton);
+		footer.addView(new HorizontalSpace(this));
+
+		nextButton = new ImageButton(this);
+		nextButton.setBackgroundResource(0);
+		nextButton.setImageResource(R.drawable.navigation_next_item);
+		nextButton.setOnClickListener(this);
+		nextButton.setEnabled(!last);
+		footer.addView(nextButton);
+		footer.addView(new HorizontalSpace(this));
+
+		replyButton = new ImageButton(this);
+		replyButton.setBackgroundResource(0);
+		replyButton.setImageResource(R.drawable.social_reply_all);
+		replyButton.setOnClickListener(this);
+		footer.addView(replyButton);
+		layout.addView(footer);
+
+		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 setReadInDatabase(final boolean read) {
+		final DatabaseComponent db = this.db;
+		final MessageId messageId = this.messageId;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					db.setReadFlag(messageId, read);
+					setReadInUi(read);
+				} 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();
+				}
+			}
+		});
+	}
+
+	private void setReadInUi(final boolean read) {
+		runOnUiThread(new Runnable() {
+			public void run() {
+				ReadGroupMessageActivity.this.read = read;
+				if(read) readButton.setImageResource(R.drawable.content_unread);
+				else readButton.setImageResource(R.drawable.content_read);
+			}
+		});
+	}
+
+	private void loadMessageBody() {
+		final DatabaseComponent db = this.db;
+		final MessageId messageId = this.messageId;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					byte[] body = db.getMessageBody(messageId);
+					final String text = new String(body, "UTF-8");
+					runOnUiThread(new Runnable() {
+						public void run() {
+							content.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 onSaveInstanceState(Bundle state) {
+		state.putBoolean("net.sf.briar.READ", read);
+		bundleEncrypter.encrypt(state);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		if(view == readButton) {
+			setReadInDatabase(!read);
+		} else if(view == prevButton) {
+			setResult(RESULT_PREV);
+			finish();
+		} else if(view == nextButton) {
+			setResult(RESULT_NEXT);
+			finish();
+		} else if(view == replyButton) {
+			Intent i = new Intent(this, WriteGroupMessageActivity.class);
+			i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+			i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
+			startActivity(i);
+			setResult(RESULT_REPLY);
+			finish();
+		}
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java
new file mode 100644
index 0000000000..7a1a93c06f
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java
@@ -0,0 +1,217 @@
+package net.sf.briar.android.groups;
+
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.util.Collection;
+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.android.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.android.BundleEncrypter;
+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.Group;
+import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.Message;
+import net.sf.briar.api.messaging.MessageFactory;
+import net.sf.briar.api.messaging.MessageId;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.google.inject.Inject;
+
+public class WriteGroupMessageActivity extends BriarActivity
+implements OnClickListener, OnItemSelectedListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(WriteGroupMessageActivity.class.getName());
+
+	private final BriarServiceConnection serviceConnection =
+			new BriarServiceConnection();
+
+	@Inject private BundleEncrypter bundleEncrypter;
+	@Inject private DatabaseComponent db;
+	@Inject @DatabaseExecutor private Executor dbExecutor;
+	@Inject private MessageFactory messageFactory;
+
+	private Group group = null;
+	private GroupId groupId = null;
+	private MessageId parentId = null;
+	private GroupNameSpinnerAdapter adapter = null;
+	private Spinner spinner = null;
+	private ImageButton sendButton = null;
+	private EditText content = null;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(null);
+
+		Intent i = getIntent();
+		byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+		if(id != null) groupId = new GroupId(id);
+		id = i.getByteArrayExtra("net.sf.briar.PARENT_ID");
+		if(id != null) parentId = new MessageId(id);
+
+		LinearLayout layout = new LinearLayout(this);
+		layout.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		layout.setOrientation(VERTICAL);
+
+		LinearLayout actionBar = new LinearLayout(this);
+		actionBar.setLayoutParams(CommonLayoutParams.MATCH_WRAP);
+		actionBar.setOrientation(HORIZONTAL);
+		actionBar.setGravity(CENTER_VERTICAL);
+
+		TextView to = new TextView(this);
+		to.setTextSize(18);
+		to.setPadding(10, 10, 10, 10);
+		to.setText(R.string.to);
+		actionBar.addView(to);
+
+		adapter = new GroupNameSpinnerAdapter(this);
+		spinner = new Spinner(this);
+		spinner.setAdapter(adapter);
+		spinner.setOnItemSelectedListener(this);
+		loadContactNames();
+		actionBar.addView(spinner);
+
+		actionBar.addView(new HorizontalSpace(this));
+
+		sendButton = new ImageButton(this);
+		sendButton.setBackgroundResource(0);
+		sendButton.setImageResource(R.drawable.social_send_now);
+		sendButton.setEnabled(false);
+		sendButton.setOnClickListener(this);
+		actionBar.addView(sendButton);
+		layout.addView(actionBar);
+
+		content = new EditText(this);
+		content.setPadding(10, 10, 10, 10);
+		if(state != null && bundleEncrypter.decrypt(state)) {
+			Parcelable p = state.getParcelable("net.sf.briar.CONTENT");
+			if(p != null) content.onRestoreInstanceState(p);
+		}
+		layout.addView(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 loadContactNames() {
+		final DatabaseComponent db = this.db;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					final Collection<Group> groups = db.getSubscriptions();
+					runOnUiThread(new Runnable() {
+						public void run() {
+							for(Group g : groups) {
+								if(g.getId().equals(groupId)) {
+									group = g;
+									spinner.setSelection(adapter.getCount());
+								}
+								adapter.add(g);
+							}
+						}
+					});
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(InterruptedException e) {
+					LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				}
+			}
+		});
+	}
+
+	@Override
+	public void onSaveInstanceState(Bundle state) {
+		Parcelable p = content.onSaveInstanceState();
+		state.putParcelable("net.sf.briar.CONTENT", p);
+		bundleEncrypter.encrypt(state);
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		unbindService(serviceConnection);
+	}
+
+	public void onClick(View view) {
+		if(group == null) throw new IllegalStateException();
+		try {
+			storeMessage(content.getText().toString().getBytes("UTF-8"));
+		} catch(UnsupportedEncodingException e) {
+			throw new RuntimeException(e);
+		}
+		finish();
+	}
+
+	private void storeMessage(final byte[] body) {
+		final DatabaseComponent db = this.db;
+		final MessageFactory messageFactory = this.messageFactory;
+		final Group group = this.group;
+		final MessageId parentId = this.parentId;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					serviceConnection.waitForStartup();
+					Message m = messageFactory.createAnonymousMessage(parentId,
+							group, "text/plain", body);
+					db.addLocalGroupMessage(m);
+				} catch(DbException e) {
+					if(LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch(GeneralSecurityException e) {
+					throw new RuntimeException(e);
+				} catch(InterruptedException e) {
+					if(LOG.isLoggable(INFO))
+						LOG.info("Interrupted while waiting for service");
+					Thread.currentThread().interrupt();
+				} catch(IOException e) {
+					throw new RuntimeException(e);
+				}
+			}
+		});
+	}
+
+	public void onItemSelected(AdapterView<?> parent, View view, int position,
+			long id) {
+		group = adapter.getItem(position);
+		groupId = group.getId();
+		sendButton.setEnabled(true);
+	}
+
+	public void onNothingSelected(AdapterView<?> parent) {
+		group = null;
+		groupId = null;
+		sendButton.setEnabled(false);
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java b/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java
index 90be870c03..9daf3287f5 100644
--- a/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java
+++ b/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java
@@ -4,7 +4,6 @@ import static android.view.Gravity.CENTER;
 import static android.view.Gravity.CENTER_HORIZONTAL;
 import net.sf.briar.R;
 import android.content.Context;
-import android.content.res.Resources;
 import android.widget.LinearLayout;
 import android.widget.ProgressBar;
 import android.widget.TextView;
@@ -45,9 +44,9 @@ public class ConnectionView extends AddContactView {
 			innerLayout.addView(progress);
 
 			TextView connecting = new TextView(ctx);
-			Resources res = getResources();
-			String connectingVia = res.getString(R.string.connecting_wifi);
-			connecting.setText(String.format(connectingVia, networkName));
+			String format = getResources().getString(
+					R.string.format_connecting_wifi);
+			connecting.setText(String.format(format, networkName));
 			innerLayout.addView(connecting);
 
 			addView(innerLayout);
diff --git a/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java b/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java
index 1305acc566..8cb63950fe 100644
--- a/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java
+++ b/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java
@@ -7,7 +7,6 @@ import net.sf.briar.R;
 import net.sf.briar.android.widgets.CommonLayoutParams;
 import android.content.Context;
 import android.content.Intent;
-import android.content.res.Resources;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 import android.view.View;
@@ -70,9 +69,9 @@ public class WifiWidget extends LinearLayout implements OnClickListener {
 				ok.setImageResource(R.drawable.navigation_accept);
 				ok.setPadding(10, 10, 10, 10);
 				addView(ok);
-				Resources res = getResources();
-				String connected = res.getString(R.string.wifi_connected);
-				status.setText(String.format(connected, networkName));
+				String format = getResources().getString(
+						R.string.format_wifi_connected);
+				status.setText(String.format(format, networkName));
 				addView(status);
 				ImageButton settings = new ImageButton(ctx);
 				settings.setImageResource(R.drawable.action_settings);
diff --git a/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java b/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java
index 168d7aa81e..18c4d00e7d 100644
--- a/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java
+++ b/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java
@@ -22,6 +22,7 @@ implements SpinnerAdapter {
 	public View getView(int position, View convertView, ViewGroup parent) {
 		TextView name = new TextView(getContext());
 		name.setTextSize(18);
+		name.setMaxLines(1);
 		name.setPadding(10, 10, 10, 10);
 		name.setText(getItem(position).getName());
 		return name;
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 60d3869012..b382e920ee 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java
@@ -67,6 +67,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		contactId = new ContactId(id);
 		contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME");
 		if(contactName == null) throw new IllegalStateException();
+		setTitle(contactName);
 
 		LinearLayout layout = new LinearLayout(this);
 		layout.setLayoutParams(CommonLayoutParams.MATCH_MATCH);
@@ -104,23 +105,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		reloadMessageHeaders();
 	}
 
-	@Override
-	public void onDestroy() {
-		super.onDestroy();
-		db.removeListener(this);
-		unbindService(serviceConnection);
-	}
-
-	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof MessageAddedEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
-			reloadMessageHeaders();
-		} else if(e instanceof MessageExpiredEvent) {
-			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
-			reloadMessageHeaders();
-		}
-	}
-
 	private void reloadMessageHeaders() {
 		final DatabaseComponent db = this.db;
 		final ContactId contactId = this.contactId;
@@ -168,8 +152,38 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 		});
 	}
 
+	@Override
+	public void onActivityResult(int request, int result, Intent data) {
+		if(result == ReadPrivateMessageActivity.RESULT_PREV) {
+			int position = request - 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		} else if(result == ReadPrivateMessageActivity.RESULT_NEXT) {
+			int position = request + 1;
+			if(position >= 0 && position < adapter.getCount())
+				showMessage(position);
+		}
+	}
+
+	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		db.removeListener(this);
+		unbindService(serviceConnection);
+	}
+
+	public void eventOccurred(DatabaseEvent e) {
+		if(e instanceof MessageAddedEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+			reloadMessageHeaders();
+		} else if(e instanceof MessageExpiredEvent) {
+			if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
+			reloadMessageHeaders();
+		}
+	}
+
 	public void onClick(View view) {
-		Intent i = new Intent(this, WriteMessageActivity.class);
+		Intent i = new Intent(this, WritePrivateMessageActivity.class);
 		i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
 		startActivity(i);
 	}
@@ -181,28 +195,15 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
 
 	private void showMessage(int position) {
 		PrivateMessageHeader item = adapter.getItem(position);
-		Intent i = new Intent(this, ReadMessageActivity.class);
+		Intent i = new Intent(this, ReadPrivateMessageActivity.class);
 		i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
 		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.INCOMING", item.isIncoming());
 		i.putExtra("net.sf.briar.FIRST", position == 0);
 		i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1);
-		i.putExtra("net.sf.briar.STARRED", item.isStarred());
 		startActivityForResult(i, position);
 	}
-
-	@Override
-	public void onActivityResult(int request, int result, Intent data) {
-		if(result == ReadMessageActivity.RESULT_PREV) {
-			int position = request - 1;
-			if(position >= 0 && position < adapter.getCount())
-				showMessage(position);
-		} else if(result == ReadMessageActivity.RESULT_NEXT) {
-			int position = request + 1;
-			if(position >= 0 && position < adapter.getCount())
-				showMessage(position);
-		}
-	}
 }
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 056935ad82..40e9626dbc 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java
@@ -9,6 +9,7 @@ import java.util.ArrayList;
 
 import net.sf.briar.R;
 import net.sf.briar.android.widgets.CommonLayoutParams;
+import net.sf.briar.android.widgets.HorizontalSpace;
 import net.sf.briar.api.db.PrivateMessageHeader;
 import android.content.Context;
 import android.text.format.DateUtils;
@@ -34,23 +35,24 @@ class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> {
 		layout.setOrientation(HORIZONTAL);
 		layout.setGravity(CENTER_VERTICAL);
 
-		if(!item.getContentType().equals("text/plain")) {
+		if(item.getContentType().equals("text/plain")) {
+			TextView subject = new TextView(ctx);
+			// Give me all the unused width
+			subject.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
+			subject.setTextSize(14);
+			subject.setMaxLines(2);
+			subject.setPadding(10, 10, 10, 10);
+			if(!item.isRead()) subject.setTypeface(null, BOLD);
+			subject.setText(item.getSubject());
+			layout.addView(subject);
+		} else {
 			ImageView attachment = new ImageView(ctx);
 			attachment.setPadding(10, 10, 10, 10);
 			attachment.setImageResource(R.drawable.content_attachment);
 			layout.addView(attachment);
+			layout.addView(new HorizontalSpace(ctx));
 		}
 
-		TextView subject = new TextView(ctx);
-		// Give me all the unused width
-		subject.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
-		subject.setTextSize(14);
-		subject.setMaxLines(2);
-		subject.setPadding(10, 10, 10, 10);
-		if(!item.isRead()) subject.setTypeface(null, BOLD);
-		subject.setText(item.getSubject());
-		layout.addView(subject);
-
 		TextView date = new TextView(ctx);
 		date.setTextSize(14);
 		date.setPadding(0, 10, 10, 10);
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 5618057483..5eafcf3367 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
@@ -238,7 +238,7 @@ implements OnClickListener, DatabaseListener {
 	}
 
 	public void onClick(View view) {
-		startActivity(new Intent(this, WriteMessageActivity.class));
+		startActivity(new Intent(this, WritePrivateMessageActivity.class));
 	}
 
 	public void eventOccurred(DatabaseEvent e) {
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 542a14d52e..19066131bc 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
@@ -46,10 +46,12 @@ implements OnItemClickListener {
 
 		TextView name = new TextView(ctx);
 		name.setTextSize(18);
+		name.setMaxLines(1);
 		name.setPadding(10, 10, 10, 10);
 		int unread = item.getUnreadCount();
-		if(unread > 0) name.setText(item.getName() + " (" + unread + ")");
-		else name.setText(item.getName());
+		String contactName = item.getContactName();
+		if(unread > 0) name.setText(contactName + " (" + unread + ")");
+		else name.setText(contactName);
 		innerLayout.addView(name);
 
 		if(!StringUtils.isNullOrEmpty(item.getSubject())) {
@@ -78,7 +80,7 @@ implements OnItemClickListener {
 		ConversationListItem item = getItem(position);
 		Intent i = new Intent(getContext(), ConversationActivity.class);
 		i.putExtra("net.sf.briar.CONTACT_ID", item.getContactId().getInt());
-		i.putExtra("net.sf.briar.CONTACT_NAME", item.getName());
+		i.putExtra("net.sf.briar.CONTACT_NAME", item.getContactName());
 		getContext().startActivity(i);
 	}
 }
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 b168f7a9bc..eec30a91c8 100644
--- a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
@@ -30,7 +30,7 @@ class ConversationListItem {
 		return contact.getId();
 	}
 
-	String getName() {
+	String getContactName() {
 		return contact.getName();
 	}
 
diff --git a/briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
similarity index 90%
rename from briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java
rename to briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
index 04185e78f8..afe0944578 100644
--- a/briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java
@@ -37,7 +37,7 @@ import android.widget.TextView;
 
 import com.google.inject.Inject;
 
-public class ReadMessageActivity extends BriarActivity
+public class ReadPrivateMessageActivity extends BriarActivity
 implements OnClickListener {
 
 	static final int RESULT_REPLY = RESULT_FIRST_USER;
@@ -45,7 +45,7 @@ implements OnClickListener {
 	static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
 
 	private static final Logger LOG =
-			Logger.getLogger(ReadMessageActivity.class.getName());
+			Logger.getLogger(ReadPrivateMessageActivity.class.getName());
 
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
@@ -55,11 +55,9 @@ implements OnClickListener {
 	@Inject @DatabaseExecutor private Executor dbExecutor;
 
 	private ContactId contactId = null;
-	private String contactName = null;
 	private MessageId messageId = null;
-	private boolean first, last, starred, read;
-	private ImageButton readButton = null;
-	private ImageButton prevButton = null, nextButton = null;
+	private boolean read;
+	private ImageButton readButton = null, prevButton = null, nextButton = null;
 	private ImageButton replyButton = null;
 	private TextView content = null;
 
@@ -71,8 +69,9 @@ implements OnClickListener {
 		int cid = i.getIntExtra("net.sf.briar.CONTACT_ID", -1);
 		if(cid == -1) throw new IllegalStateException();
 		contactId = new ContactId(cid);
-		contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME");
+		String contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME");
 		if(contactName == null) throw new IllegalStateException();
+		setTitle(contactName);
 		byte[] mid = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID");
 		if(mid == null) throw new IllegalStateException();
 		messageId = new MessageId(mid);
@@ -80,14 +79,13 @@ implements OnClickListener {
 		if(contentType == null) throw new IllegalStateException();
 		long timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1);
 		if(timestamp == -1) throw new IllegalStateException();
-		first = i.getBooleanExtra("net.sf.briar.FIRST", false);
-		last = i.getBooleanExtra("net.sf.briar.LAST", false);
+		boolean incoming = i.getBooleanExtra("net.sf.briar.INCOMING", false);
+		boolean first = i.getBooleanExtra("net.sf.briar.FIRST", false);
+		boolean last = i.getBooleanExtra("net.sf.briar.LAST", false);
 
 		if(state != null && bundleEncrypter.decrypt(state)) {
-			starred = state.getBoolean("net.sf.briar.STARRED");
 			read = state.getBoolean("net.sf.briar.READ");
 		} else {
-			starred = i.getBooleanExtra("net.sf.briar.STARRED", false);
 			read = false;
 			setReadInDatabase(true);
 		}
@@ -112,8 +110,11 @@ implements OnClickListener {
 		// Give me all the unused width
 		name.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1);
 		name.setTextSize(18);
+		name.setMaxLines(1);
 		name.setPadding(10, 10, 10, 10);
-		String format = getResources().getString(R.string.message_from);
+		String format;
+		if(incoming) format = getResources().getString(R.string.format_from);
+		else format = getResources().getString(R.string.format_to);
 		name.setText(String.format(format, contactName));
 		header.addView(name);
 
@@ -204,7 +205,7 @@ implements OnClickListener {
 	private void setReadInUi(final boolean read) {
 		runOnUiThread(new Runnable() {
 			public void run() {
-				ReadMessageActivity.this.read = read;
+				ReadPrivateMessageActivity.this.read = read;
 				if(read) readButton.setImageResource(R.drawable.content_unread);
 				else readButton.setImageResource(R.drawable.content_read);
 			}
@@ -241,7 +242,6 @@ implements OnClickListener {
 
 	@Override
 	public void onSaveInstanceState(Bundle state) {
-		state.putBoolean("net.sf.briar.STARRED", starred);
 		state.putBoolean("net.sf.briar.READ", read);
 		bundleEncrypter.encrypt(state);
 	}
@@ -262,7 +262,7 @@ implements OnClickListener {
 			setResult(RESULT_NEXT);
 			finish();
 		} else if(view == replyButton) {
-			Intent i = new Intent(this, WriteMessageActivity.class);
+			Intent i = new Intent(this, WritePrivateMessageActivity.class);
 			i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
 			i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
 			startActivity(i);
diff --git a/briar-android/src/net/sf/briar/android/messages/WriteMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
similarity index 89%
rename from briar-android/src/net/sf/briar/android/messages/WriteMessageActivity.java
rename to briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
index 4673d8180e..ced62326ac 100644
--- a/briar-android/src/net/sf/briar/android/messages/WriteMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java
@@ -7,6 +7,7 @@ import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.security.GeneralSecurityException;
 import java.util.Collection;
 import java.util.concurrent.Executor;
@@ -42,11 +43,11 @@ import android.widget.TextView;
 
 import com.google.inject.Inject;
 
-public class WriteMessageActivity extends BriarActivity
+public class WritePrivateMessageActivity extends BriarActivity
 implements OnClickListener, OnItemSelectedListener {
 
 	private static final Logger LOG =
-			Logger.getLogger(WriteMessageActivity.class.getName());
+			Logger.getLogger(WritePrivateMessageActivity.class.getName());
 
 	private final BriarServiceConnection serviceConnection =
 			new BriarServiceConnection();
@@ -85,7 +86,7 @@ implements OnClickListener, OnItemSelectedListener {
 		TextView to = new TextView(this);
 		to.setTextSize(18);
 		to.setPadding(10, 10, 10, 10);
-		to.setText(R.string.message_to);
+		to.setText(R.string.to);
 		actionBar.addView(to);
 
 		adapter = new ContactNameSpinnerAdapter(this);
@@ -163,32 +164,36 @@ implements OnClickListener, OnItemSelectedListener {
 	public void onClick(View view) {
 		if(contactId == null) throw new IllegalStateException();
 		try {
-			byte[] body = content.getText().toString().getBytes("UTF-8");
-			storeMessage(messageFactory.createPrivateMessage(parentId,
-					"text/plain", body));
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		} catch(GeneralSecurityException e) {
+			storeMessage(content.getText().toString().getBytes("UTF-8"));
+		} catch(UnsupportedEncodingException e) {
 			throw new RuntimeException(e);
 		}
 		finish();
 	}
 
-	private void storeMessage(final Message m) {
+	private void storeMessage(final byte[] body) {
 		final DatabaseComponent db = this.db;
+		final MessageFactory messageFactory = this.messageFactory;
 		final ContactId contactId = this.contactId;
+		final MessageId parentId = this.parentId;
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
 					serviceConnection.waitForStartup();
+					Message m = messageFactory.createPrivateMessage(parentId,
+							"text/plain", body);
 					db.addLocalPrivateMessage(m, contactId);
 				} catch(DbException e) {
 					if(LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
+				} catch(GeneralSecurityException e) {
+					throw new RuntimeException(e);
 				} catch(InterruptedException e) {
 					if(LOG.isLoggable(INFO))
 						LOG.info("Interrupted while waiting for service");
 					Thread.currentThread().interrupt();
+				} catch(IOException e) {
+					throw new RuntimeException(e);
 				}
 			}
 		});
diff --git a/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java b/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java
index 958092f40c..c708f335ca 100644
--- a/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java
+++ b/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java
@@ -13,6 +13,6 @@ public class HorizontalBorder extends View {
 	public HorizontalBorder(Context ctx) {
 		super(ctx);
 		setLayoutParams(new LayoutParams(MATCH_PARENT, LINE_WIDTH));
-		setBackgroundColor(getResources().getColor(R.color.HorizontalBorder));
+		setBackgroundColor(getResources().getColor(R.color.horizontal_border));
 	}
 }
-- 
GitLab