From 4e5366509d9f913f22b58f1b61e8fd843d458797 Mon Sep 17 00:00:00 2001 From: akwizgran <michael@briarproject.org> Date: Sat, 23 Mar 2013 14:30:59 +0000 Subject: [PATCH] Android UI for blogs (restricted groups). --- briar-android/AndroidManifest.xml | 6 +- .../res/drawable-hdpi/social_blog.png | Bin 0 -> 948 bytes .../res/drawable-hdpi/social_new_blog.png | Bin 0 -> 1202 bytes .../res/drawable-mdpi/social_blog.png | Bin 0 -> 558 bytes .../res/drawable-mdpi/social_new_blog.png | Bin 0 -> 752 bytes .../res/drawable-xhdpi/social_blog.png | Bin 0 -> 918 bytes .../res/drawable-xhdpi/social_new_blog.png | Bin 0 -> 1207 bytes briar-android/res/values/color.xml | 1 + briar-android/res/values/strings.xml | 3 +- .../sf/briar/android/HomeScreenActivity.java | 19 ++- .../briar/android/groups/GroupActivity.java | 9 +- .../android/groups/GroupListActivity.java | 112 +++++++++++++++--- .../groups/WriteGroupMessageActivity.java | 4 + .../sf/briar/api/db/DatabaseComponent.java | 2 +- .../api/db/event/GroupMessageAddedEvent.java | 12 +- .../db/event/SubscriptionRemovedEvent.java | 12 +- .../src/net/sf/briar/api/messaging/Group.java | 5 + .../sf/briar/db/DatabaseComponentImpl.java | 17 ++- .../src/net/sf/briar/db/JdbcDatabase.java | 8 +- .../briar/messaging/MessageFactoryImpl.java | 8 +- .../net/sf/briar/messaging/MessageReader.java | 2 +- .../briar/messaging/MessageVerifierImpl.java | 2 +- .../sf/briar/messaging/PacketWriterImpl.java | 5 +- .../sf/briar/db/DatabaseComponentTest.java | 6 +- 24 files changed, 168 insertions(+), 65 deletions(-) create mode 100644 briar-android/res/drawable-hdpi/social_blog.png create mode 100644 briar-android/res/drawable-hdpi/social_new_blog.png create mode 100644 briar-android/res/drawable-mdpi/social_blog.png create mode 100644 briar-android/res/drawable-mdpi/social_new_blog.png create mode 100644 briar-android/res/drawable-xhdpi/social_blog.png create mode 100644 briar-android/res/drawable-xhdpi/social_new_blog.png diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index fc8fcd5289..ad48f4aea1 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -42,15 +42,15 @@ </activity> <activity android:name=".android.groups.GroupActivity" - android:label="@string/groups_title" > + android:label="@string/app_name" > </activity> <activity android:name=".android.groups.GroupListActivity" - android:label="@string/groups_title" > + android:label="@string/app_name" > </activity> <activity android:name=".android.groups.ReadGroupMessageActivity" - android:label="@string/groups_title" > + android:label="@string/app_name" > </activity> <activity android:name=".android.groups.WriteGroupMessageActivity" diff --git a/briar-android/res/drawable-hdpi/social_blog.png b/briar-android/res/drawable-hdpi/social_blog.png new file mode 100644 index 0000000000000000000000000000000000000000..dfafb709b7322d9a0ff679c975e1835ec1499966 GIT binary patch literal 948 zcmV;l155mgP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00001b5ch_0Itp) z=>Px&Zb?KzRA}Dqm`hI+Q5c4w>9jzRDnu?SD5)5u2}Vr_M)$gRr->W?nJX9m1rrx0 zZj43}7Am5kSU`f;A~%_-n|G3vFQ+g_$7(WfGD$O>9=_*%*B;1~D_5>uxpK8nLCSUA zz!9JVlz|Y~0XBg;@Eurh2S739x?4aj!$aqNWS%-O1uXn)0A=8p=e-Nm0D6ECeSQJF z6jUQCfH38{NqHbmI5zYQ0~ZCq0sL$yz!e!)(02-Y0zHY*ZcfE|*AJWlN<gh$NqiHS z16F`<O5QG@7Z}uO_sPhT;|UzV0<Z-<Xl23&Ej)~pU`_(3bn-jS-xCk50gr&?zZpVl z6f0pr0<$WuqsI6K&<TtItH6(c2>?@GSAiK|K-DR5JRSg5U`ZbNs{q}KeVUY3SKGjA z#s6`~6M^0h18-C8bKo2}30zVqPO4D6&*pU&xS>Q29GGq3Ue+GCtE!V!nWFQ#0-OUT z1op)wZ=%ntU{zC{O0uJtCBTGEJlLaojmTSpO0Ewg0FD`m0RjBX3SiP{Oo1^xFv(jM zL}!z*S`=u-fhg$nhb%ksrs8)6cqe0vo_zvml;~<=)FN=f9Ktar{8knKJ-|~vujKAG z8rGH-^GCH`Vo;>YQ}TFXTitlfnXwAmr<Mi4do`W7N%L9&=GC}598W~RkmEU5b#?5r z<FymOvn&A8jKdq6mqr!;Zhll0b~`3tDgwmu!l(Uo=s)emLyFmT8RCY<JaF29NB|;F zcr;X2?Qe>itN=pbj+#;xIInlh2BN0kkc`}MlknKo9!1mKrn3O(18zHwC)7W#>U5Wk zVcn|lyM;%rl6%<X&1~B7qL9+~4&b_swnNz#a22?0LSxPt5osK;su?>%T_DhEfWaoo zoIs2#?h~yDpy5DOJl|h2%`j}T?mz&(u4^Gm>aaMI>R02fP@(1On+KpP3xHska@tTl z+u45A7*_xec@p_gZ%g(Cu-Dp1N$RTKK*yee&3D>6tp->Fo|qd5x5350ea~;?KC?FR z{GjhOkWmw_9Oz;TT~q;FbCR>DFnMSi8cO<-$$`3O-$4&RJqv&sc&_p|uHkK3)8@7( zU#iX(OOl*Zo_&YSj1}oZpydFGz5tI?Tt$wUlk`ocp-Qy7Zk;w=6z9s7D_5@nPrm_Q W(eaa!`oEn30000<MNUMnLSTX!U8$)6 literal 0 HcmV?d00001 diff --git a/briar-android/res/drawable-hdpi/social_new_blog.png b/briar-android/res/drawable-hdpi/social_new_blog.png new file mode 100644 index 0000000000000000000000000000000000000000..ad2c5f31a36616193928b7cb7239a15f6b9b52ed GIT binary patch literal 1202 zcmV;j1Wo&iP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*e~ z4jUW6*Pq(}00c5gL_t(&-tCx8j}%oDhM%hbV0xs5i7=o8k_KWl!JsLE(Tyuz=*qZ} zgpL2fpTHmRAGlJ(h81Bki3<}q#>x;$MEMYOY#7E7k%pP>v8$TRTglDIEvA^7nrV_g zNhO`z^>N;N&U@Z-4@_)gr7Xx<p9b~-ZJ-6zfFaNadcc3c>No(X=d6DQq;kCGtS9E` z0hfWraRbl-{`aiAfJVS>;E;YV09OTSv|RvdIqRntfg<TQ(lrO16!<mZ@lFDGM^1&h zC)5?_%A9sNlxl4sa2#j=-JK%wC%`RW8F(P_P62y?S?%_WoNPFezyTNn1K@m>gbzwQ z+Yp$;z^mH%q4R$hp+~@Xz|wZ6P!UT-*uQ`qDy>J1^CMsva12-h9*z?LOnF@at^xa1 zodPGq0nh>NDk9qoU|P9PPHA;*9r#W8|FRQ_Kx=cr%>wT!&4I(fTT<ez3Z>5|UatWk zipYTjW*zvd>Ii(Us*_cjy7RjYybhcau=|F*nf;>*)-lzoq3?F91aL|_9&8|9hZL<q zCD#Wc01g@u`vu@}RRB&q!W0<C14G`HK<vs%t2+YPc0h#s{b!X*+*kfC1Ha4Jy63sT zX(gs|oVo+NX*S`Y2tTL-z;572UB4>a*VU{oDd%6(^O-}5Do?``iFK**F{j50^`34O z0RE8Zq&ecX2;7!%HJwN#zyT+6uJxqYEhlQP06$d$ph!Esrhe&&^55l0TWL2md}#|H zP89ByQRt#dd_Xz7Do5PZxD6b2Kx6<CPkM}0R_&SMT2%mQz*&h>2Y5qkO9n(&+K{!} zX-<0d^^Up`w<}cum;uf>!c)_SJKEj5#<6MD_ot;ts*?Mn;mwWBwijW}dK36SPTQtz z0K5ylZBpZwaUxMWVy$bwThjo6tp+fgL*@*`3FUoe%K|WRK(#&3FB>te<=j^afKPQT zh=vpvr&ImcM60IMa_xZ!z*H3gf~S<zk@DI0_AAD@5O~2u<f7A&QUJmNhnRb2ZJz<1 zdKxz0ZV$J5F7yca0f;<@^{h;O?KvaQnB!#PIneJlAQLw#Q50o;QV6WXaXi=z05qrq zc+VkcU1>6J1PzhCVK~t9ym!_Epl2fVg#_+P;Frw;NP$@8@r0VUE9y4aJ$&gnBbJPu zmp$*DGd)(Kfq<=o%>rp$h&=a!?>D%orHa%uDYfVk&ZfC<)stqaa~>%Tpopyk-xi!+ zT-eH~M96b);|QQ{8pX;+i=`}zqT?cTVg#}Ye5y8OUcHkQ$6g%A3tP#7&Di_ENB9T7 zYa;Q-o-ApqGVVBlzjcrqm<WVAK!q9x^i2fnT4$#LTm&w8N@WW;Cn5W$jF44oaU3Te zH7AN9m$<_?j)%`%6$+oF$V_x#0>H0L(>e%kIQFTJA%Fz<S=Hzv6B~1T0{o391E@)b Qs{jB107*qoM6N<$f_H`+>;M1& literal 0 HcmV?d00001 diff --git a/briar-android/res/drawable-mdpi/social_blog.png b/briar-android/res/drawable-mdpi/social_blog.png new file mode 100644 index 0000000000000000000000000000000000000000..22a330fec002129d3251a169101e84b3ef9b7be7 GIT binary patch literal 558 zcmV+}0@3}6P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800001b5ch_0Itp) z=>Px$=t)FDR9M69*3D{DVGsu3Cyk<lMq6#%XscDMS`<V?L8)DP0j^xS^OD>Og5HF< zQ$eICSP)4I5v<goh*YWRaWjG8kR~*#=pyebGxPntGxN@O!U!Xb@Q+dH5xa#6)KK6% zKH&=*SpC}s?qQ>gzd#jjEaKTP1gbq^*V3A?AX}8kAB}ZCqB=WTf%Eu)R@gPvF@>2_ zjm{Zg@wh~CC<6U(2G_AWk@<y(TeNQ5E$D|eyutxYrt{8)Hijlp#~d!;bWFrX^gE-0 zpE#KWdJ1obC2$vqaR>)PaC5j67iQ6|Vja_A?+v@JFvohtes}S0hHh@)c1c68@iy5D z!&cxb-bJ4qQQq+s-xC#CEVbieB(@m>Z4NpGokPl4z}biu_=ZQN=~zgUaevSXY&Fk~ z5aZMq{a(Z6bpA>g;cjaT=W#QHxE#KZ*dHQ=<9M0&jE8tTZ$ons5yBnJMwYW-ckrZ? zNIlsr(cVf)H`^j`1^bdtDq+v3HSZ(KHcphrvQyp3K?$_Gh_q6=XgOs%k^HT+w>Bt& zY8N3EA;KL)F9I(@-9@^59Z%Dm>F^i$ocy)4uQezEcz~sNdY8h^n2AzXOO>fd3-x=B whxr`WaV&oC!t)fH!jUwx&C$Q#5&m2J0fdHZBA-)gRsaA107*qoM6N<$g4gH$;Q#;t literal 0 HcmV?d00001 diff --git a/briar-android/res/drawable-mdpi/social_new_blog.png b/briar-android/res/drawable-mdpi/social_new_blog.png new file mode 100644 index 0000000000000000000000000000000000000000..039e5f8537e4480d6ce1e247862cd8a18908e452 GIT binary patch literal 752 zcmV<M0uTL(P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*e~ z4jeYiNmXe800MAHL_t(o!{wIEY7|irg}+YcN5xE>NXC_kMltH3A{S9m;;aR?g5c7P zh_B$r$8akM@g*d?L_0{(U_fLt5FsOp8j&Q%nRas;ic8Og?&+9?2YOMr@6V~KQ*|%= z?+85m4loLofEf4*d<DJ%b)dQ51RevsDf40=1X{o%Fjo|T(6eut@jelZJ;En6@0X%e zXap_-Uw}>7C7=R~1Cu5i$-3{rE01JP1opx?;3hC&$m{?wJESgi3iiS}@D3OT#>^fU zq>Y{lRDddQ2{@~Y*j2tQ>cBQ|+6eRv@S#@%4}s&rF<?Z3s{&VbpcP#c*a9YGACjF( zn0=o8CuL4sTDb+>^K|nb_-O1}uMxNod{Un43UAr`A5ugXy>wibGi?cUHE$IpoA{^! z=k+fJegHFGb<|8{JkJ|}PW4=u7{@#0dmV6Q&qj)HGnL^qa9cuL(byVrR3Zg92`rd3 zWr;WG4Xb%X2=D-yl9yAm6TJ4wSB%|IN&`<fnFw414jXL*vZu|Q&+@Vbobt*tiRf5f z0<9Eb2Hrs{#?w(VzG>E$@)8JBgcwVN`vRZ98>u@shi?J1X3m7hV_?IKm(054HpMwN z;+!jI5#bA9S*>?jw#B4E9eR<eD1~_K_&)H&IhT6#_IDsQ>v?Std280%C)&38w`Rr% z)IrHYpc9&1-h~Z)Y0tTKmJ}3?77#~K6c_O#>I&!FFmP94PlQ@kQ(N#HUy7n=zQ15^ z2X!+JnTzX)9KB+k4pk|I6W9Qrdij4O@jml<Oe>EFyFz*X?WHBrz|KL5Fc6X8pa={W i*$47qDa#Y>pW`pm9GgjZFSo7$0000<MNUMnLSTX-zB+OM literal 0 HcmV?d00001 diff --git a/briar-android/res/drawable-xhdpi/social_blog.png b/briar-android/res/drawable-xhdpi/social_blog.png new file mode 100644 index 0000000000000000000000000000000000000000..951a5d3f67822aadfe1d1401e4d348e929206d07 GIT binary patch literal 918 zcmV;H18Mw;P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF00001b5ch_0Itp) z=>Px&P)S5VRCwC$nM+R-Q5?m8l=sUNA;m|rkf5oLg^3$uT)E*35Ka65el7QgFTlbT zi7TTqnphVi1OY*yC17m;TNnH%X~^7rdxz<rf!>oeX=d)c&Yb`GpL+*9JUl!+JUl!+ zJUl!+IuBhDj=2ep0~0_N7y$-=I&cIW0u^8f*p>fPpmA9Ucop$lt3V0Z6rxgr9*5T| z$S)w{Be2~`1cZ@M1KtAbNfFQ;;h0A;zkoZyZD0fV+i3&@=m+irz2Xth3^R%ezLeSx z9R5ID9$*L<mF_SO<fKd3pbjj^xl>7iEyjQ;U>dk)aonPqW$H@67ByfC_y#NkC!!A1 zwP(c>eqVF~&XTbXd=&%C8OGlNj)lNWK|p|iz?vA|1Td%>n{THQk|H3$fv90vh|nov z2iQ*&0bO$AIba5u6j|4$&TV7zvAk`aCw!MOO0)|UfMQdujWhet0?!OB^0kyuVhVU6 zecptuJkUdtce#b6w<j{6(Y&x8;O9k4f?12Cw`VAaN#I9ZvDtMLo)kqa8)|vWV2pX- zOB@7b5^(+u@Vo^NSrRpb;fgHGq(T9QQj=woc%OygnfiN0W{axU_$jINfM39hgqiX= zvj0=yk0$ePh}0j1>>H34yzN>6o4|Xi)0!dsx03Ws{e2s_uejGiIo2rw2W?1tcSX(( zMQrX@s3{A4K2I)i6@^Vn&(z<mq5@UN*iSnF!Hq`k?=^A#u*H?$^OQjx1ayldKbCqv zk<QSF(BFqO57Vz8FAIEd-36L2K7S@p6wohgEs1+yRm?N`6my3)Avp{~(oLKOp@IaO zjAhtq0Pi&Cy{_0^7G<ekgwrIL1?E~9H>bF{4apK=C~D59>iOfY37EGW8|F1D8fB@b z>AQ;U6Rru!SdPsrY8p*VtD<o#O28Jym}*)T!bZ|gKwLFlRc!A|I{_t9il_wCEyt=N zZqCx|RNippnt%n%v0;;yF-`5N%x+Y%{m?Z56;8k1nXssNm?_P1RZDZMB;f(8z#Bz5 z)McOU<L0#HybVip+(~LKP~mi)KHt>pN>cN>Ir<IFYLU(`Z9Svy1Oy8ftF-<py9;=t s*}tjS9>p^}JUl!+JUl!+JUqhjA1ijp!(I^8WB>pF07*qoM6N<$f{omiMF0Q* literal 0 HcmV?d00001 diff --git a/briar-android/res/drawable-xhdpi/social_new_blog.png b/briar-android/res/drawable-xhdpi/social_new_blog.png new file mode 100644 index 0000000000000000000000000000000000000000..ee25de145dbcde7cb2f4ce316103adf866ee287e GIT binary patch literal 1207 zcmV;o1W5adP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF00001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i*e~ z4jLqGs)MHh00cKlL_t(|+U=T8ZxdG(#ea7Gk^ps@DCr+Vf}jxEE?Ui|s#tZys=KI_ z_yDZ=04&(^4UiCBSNQ@gx<W!iVu}z{gsP=Pl!Sx?2RTjCI`Oc`v9Q4JJwL}|&p2}= zOVP~u&Ac=Bo_p?fAVY=>84fUVmUEv1<G=(^01gAgKo$5O*aS9!HDFzTZvj!S5OA@@ zH5Y*eU|B>Z8vQY>Ijv8PjC;W9K_Z|L8QZ`w!1JUL&}TUpw3+DBz-Pb`@b5t*poSsf zb6`Lk(RIPJiQon0ZcX9KQh9(8;E1ZjI54Ry;e#qLueB3NfG@^?W55jXk;nILN-PtX z0>0P=R)ELA6JSTt;kw5uX~N626Ywq>&w)o0z)8pT$AO9n><I#D*ae<R@FsvmmTQaM zh=jfl-KVT*7!?t=NLT}2Cy9WZ9()ow0Zb{ZtIBg<n_SV`wrRqjN~5k;foWj2&epcm z`=0{7bCk#%N};Yrz>li)E)=w(uN2;;CY;_Yh4~4~i|GgcPMZ@fcsRXRj&zs;{)*c+ z$7O}56cJAxxxC^K#v<@AP6F~Vul++48gfg~&<IC0m`OwgY$_*93h_Y?!E@F34RwpA z(s;jH^MDt?Q<<64KD_@q;2(?UUs9-l+2Gu;O7Lo@BVZZ0t32Iyc>iXco~ypE0$&)O z>5#tLQ3A>_IK8t9=g452hYZnF03O8Q3)*dkT~5zc-?tP6ri$@8=>*gswAK2)E!A%{ zTp5TX4f;H<Jq~=MeBM!Ih+3%cqn6DK8OSRDzjnF`)ZcvW9p5mGeXMm$>iwai&loiH z4$nk#BaEn;bkqn<%h2RK%}xZ|wygWH;dn`rWqJ`Eb%F)pd=u-OH$2>uyhJ0+TGlhw z{P9i`Fz2z>oIz8bS|ZETj;|YzPjs4qyysd)Lr!DKX_GZhL<;y~woN&0GGT|4PC&bI zddqNpFzEy=kTSRksCukbwOHIqPq8y;L)-t~z-5HyRp*<*yrEecv*fNx?2Z_Yzo#WZ z5RAxPM?g6Y!}WGXzy^E2-MMhnvYBI+?`?UCW21YV0j>a-f$!tcfJNYELpoG-&i3Qx zjAh-(QykYU9cF(k&0Vv-zy^DB`eI$FTTss1dh|~$(;}N-`f5g0_lbpo+Q4Fw=6}l0 z0pB_%PP9ogV($i9K_p@HMG3f;lrq@!$O!0xJyhzt%H1Ssz`hX#!4P{h!kk5CirRPt zd=Lb|Nry0(!!TS)1p#t+m-L*Gfq*=32drmhj7%5+?f}=*LO@=9fr)^e-u_PAer$Uh zWN+w7S_t?L_(KlQ6ak}(l-e9&&5`oUX(HeSaMf|m*GiKL@Eh<`6aFL}1cYH2xs*K! zg8K7O7>2vPIhJ<h_A4@(5y}Y22spUt?}0t^$t+c9VQk(D1Z)Do%MGrOAwvcqyaDgW V1f(=2CaM4c002ovPDHLkV1k|fEA0RP literal 0 HcmV?d00001 diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml index 6252b617f5..986454bcf2 100644 --- a/briar-android/res/values/color.xml +++ b/briar-android/res/values/color.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> + <color name="home_screen_background">#FFFFFF</color> <color name="content_background">#FFFFFF</color> <color name="unread_background">#FFFFFF</color> <color name="horizontal_border">#CCCCCC</color> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 14258e0471..5881c29be0 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -44,7 +44,8 @@ <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="groups_title">Groups</string> <string name="compose_group_title">New Post</string> + <string name="blogs_title">Blogs</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 02cea07efb..3cf5159051 100644 --- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java +++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java @@ -89,8 +89,12 @@ public class HomeScreenActivity extends BriarActivity { groupsButton.setText(R.string.groups_button); groupsButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { - startActivity(new Intent(HomeScreenActivity.this, - GroupListActivity.class)); + Intent i = new Intent(HomeScreenActivity.this, + GroupListActivity.class); + i.putExtra("net.sf.briar.RESTRICTED", false); + i.putExtra("net.sf.briar.TITLE", + getResources().getString(R.string.groups_title)); + startActivity(i); } }); buttons.add(groupsButton); @@ -99,11 +103,16 @@ public class HomeScreenActivity extends BriarActivity { blogsButton.setLayoutParams(matchParent); blogsButton.setBackgroundResource(0); blogsButton.setCompoundDrawablesWithIntrinsicBounds(0, - R.drawable.social_share, 0, 0); + R.drawable.social_blog, 0, 0); blogsButton.setText(R.string.blogs_button); blogsButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { - // FIXME: Hook this button up to an activity + Intent i = new Intent(HomeScreenActivity.this, + GroupListActivity.class); + i.putExtra("net.sf.briar.RESTRICTED", true); + i.putExtra("net.sf.briar.TITLE", + getResources().getString(R.string.blogs_title)); + startActivity(i); } }); buttons.add(blogsButton); @@ -138,6 +147,8 @@ public class HomeScreenActivity extends BriarActivity { grid.setLayoutParams(matchParent); grid.setGravity(CENTER); grid.setPadding(5, 5, 5, 5); + grid.setBackgroundColor(getResources().getColor( + R.color.home_screen_background)); grid.setNumColumns(2); grid.setAdapter(new BaseAdapter() { diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java index deff171b85..43af87cf9d 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java @@ -51,6 +51,7 @@ OnClickListener, OnItemClickListener { private final BriarServiceConnection serviceConnection = new BriarServiceConnection(); + private boolean restricted = false; private String groupName = null; private GroupAdapter adapter = null; private ListView list = null; @@ -65,6 +66,7 @@ OnClickListener, OnItemClickListener { super.onCreate(null); Intent i = getIntent(); + restricted = i.getBooleanExtra("net.sf.briar.RESTRICTED", false); byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID"); if(id == null) throw new IllegalStateException(); groupId = new GroupId(id); @@ -194,7 +196,8 @@ OnClickListener, OnItemClickListener { public void eventOccurred(DatabaseEvent e) { if(e instanceof GroupMessageAddedEvent) { - if(((GroupMessageAddedEvent) e).getGroupId().equals(groupId)) { + GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; + if(g.getGroup().getId().equals(groupId)) { if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); loadHeaders(); } @@ -205,7 +208,8 @@ OnClickListener, OnItemClickListener { if(LOG.isLoggable(INFO)) LOG.info("Rating changed, reloading"); loadHeaders(); } else if(e instanceof SubscriptionRemovedEvent) { - if(((SubscriptionRemovedEvent) e).getGroupId().equals(groupId)) { + SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e; + if(s.getGroup().getId().equals(groupId)) { if(LOG.isLoggable(INFO)) LOG.info("Subscription removed"); finishOnUiThread(); } @@ -214,6 +218,7 @@ OnClickListener, OnItemClickListener { public void onClick(View view) { Intent i = new Intent(this, WriteGroupMessageActivity.class); + i.putExtra("net.sf.briar.RESTRICTED", restricted); i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes()); startActivity(i); } diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java index f1885f6102..70d797bb35 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java @@ -1,6 +1,8 @@ package net.sf.briar.android.groups; +import static android.view.Gravity.CENTER; import static android.view.Gravity.CENTER_HORIZONTAL; +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; @@ -26,6 +28,7 @@ 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.ContactId; import net.sf.briar.api.android.DatabaseUiExecutor; import net.sf.briar.api.crypto.CryptoComponent; @@ -67,6 +70,7 @@ implements OnClickListener, DatabaseListener { private GroupListAdapter adapter = null; private ListView list = null; + private ImageButton newGroupButton = null, composeButton = null; // Fields that are accessed from DB threads must be volatile @Inject private volatile CryptoComponent crypto; @@ -76,6 +80,7 @@ implements OnClickListener, DatabaseListener { @Inject private volatile AuthorFactory authorFactory; @Inject private volatile GroupFactory groupFactory; @Inject private volatile MessageFactory messageFactory; + private volatile boolean restricted = false; @Override public void onCreate(Bundle state) { @@ -85,6 +90,12 @@ implements OnClickListener, DatabaseListener { layout.setOrientation(VERTICAL); layout.setGravity(CENTER_HORIZONTAL); + Intent i = getIntent(); + restricted = i.getBooleanExtra("net.sf.briar.RESTRICTED", false); + String title = i.getStringExtra("net.sf.briar.TITLE"); + if(title == null) throw new IllegalStateException(); + setTitle(title); + adapter = new GroupListAdapter(this); list = new ListView(this); // Give me all the width and all the unused height @@ -95,11 +106,28 @@ implements OnClickListener, DatabaseListener { layout.addView(new HorizontalBorder(this)); - ImageButton newGroupButton = new ImageButton(this); + LinearLayout footer = new LinearLayout(this); + footer.setLayoutParams(CommonLayoutParams.MATCH_WRAP); + footer.setOrientation(HORIZONTAL); + footer.setGravity(CENTER); + footer.addView(new HorizontalSpace(this)); + + newGroupButton = new ImageButton(this); newGroupButton.setBackgroundResource(0); - newGroupButton.setImageResource(R.drawable.social_new_chat); + if(restricted) + newGroupButton.setImageResource(R.drawable.social_new_blog); + else newGroupButton.setImageResource(R.drawable.social_new_chat); newGroupButton.setOnClickListener(this); - layout.addView(newGroupButton); + footer.addView(newGroupButton); + footer.addView(new HorizontalSpace(this)); + + composeButton = new ImageButton(this); + composeButton.setBackgroundResource(0); + composeButton.setImageResource(R.drawable.content_new_email); + composeButton.setOnClickListener(this); + footer.addView(composeButton); + footer.addView(new HorizontalSpace(this)); + layout.addView(footer); setContentView(layout); @@ -146,7 +174,11 @@ implements OnClickListener, DatabaseListener { Group group1 = groupFactory.createGroup("Godwin's Lore"); db.subscribe(group1); db.setVisibility(group1.getId(), Arrays.asList(contactId)); - // Insert some text messages to the groups + Group group2 = groupFactory.createGroup( + "All Kids Love Blog", publicKey); + db.subscribe(group2); + db.setVisibility(group2.getId(), Arrays.asList(contactId)); + // Insert some text messages to the unrestricted groups for(int i = 0; i < 20; i++) { String body; if(i % 3 == 0) { @@ -197,6 +229,43 @@ implements OnClickListener, DatabaseListener { m = messageFactory.createAnonymousMessage(m.getId(), group1, "text/plain", body.getBytes("UTF-8")); db.addLocalGroupMessage(m); + // Insert some text messages to the restricted group + for(int i = 0; i < 20; i++) { + 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."; + } + now = System.currentTimeMillis(); + if(i % 5 == 0) { + m = messageFactory.createAnonymousMessage(null, + group2, privateKey, "text/plain", + body.getBytes("UTF-8")); + } else if(i % 5 == 2) { + m = messageFactory.createPseudonymousMessage(null, + group2, privateKey, author, privateKey, + "text/plain", body.getBytes("UTF-8")); + } else { + m = messageFactory.createPseudonymousMessage(null, + group2, privateKey, author1, privateKey, + "text/plain", body.getBytes("UTF-8")); + } + duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) { + LOG.info("Message creation took " + + duration + " ms"); + } + now = System.currentTimeMillis(); + if(Math.random() < 0.5) db.addLocalGroupMessage(m); + else db.receiveMessage(contactId, m); + db.setReadFlag(m.getId(), i % 4 == 0); + duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) { + LOG.info("Message storage took " + + duration + " ms"); + } + } } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -233,8 +302,8 @@ implements OnClickListener, DatabaseListener { new ArrayList<CountDownLatch>(); long now = System.currentTimeMillis(); for(Group g : db.getSubscriptions()) { - // Filter out restricted groups - if(g.getPublicKey() != null) continue; + // Filter out restricted/unrestricted groups + if(g.isRestricted() != restricted) continue; try { // Load the headers from the database Collection<GroupMessageHeader> headers = @@ -322,41 +391,52 @@ implements OnClickListener, DatabaseListener { } public void onClick(View view) { - startActivity(new Intent(this, WriteGroupMessageActivity.class)); + if(view == newGroupButton) { + // FIXME: Hook this button up to an activity + } else if(view == composeButton) { + Intent i = new Intent(this, WriteGroupMessageActivity.class); + i.putExtra("net.sf.briar.RESTRICTED", restricted); + startActivity(i); + } } public void eventOccurred(DatabaseEvent e) { if(e instanceof GroupMessageAddedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); - loadHeaders(((GroupMessageAddedEvent) e).getGroupId()); + Group g = ((GroupMessageAddedEvent) e).getGroup(); + if(g.isRestricted() == restricted) { + if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); + loadHeaders(g); + } } else if(e instanceof MessageExpiredEvent) { if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading"); loadHeaders(); // FIXME: Don't reload everything } else if(e instanceof SubscriptionRemovedEvent) { // Reload the group, expecting NoSuchSubscriptionException - if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); - loadHeaders(((SubscriptionRemovedEvent) e).getGroupId()); + Group g = ((SubscriptionRemovedEvent) e).getGroup(); + if(g.isRestricted() == restricted) { + if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); + loadHeaders(g); + } } } - private void loadHeaders(final GroupId g) { + private void loadHeaders(final Group g) { dbUiExecutor.execute(new Runnable() { public void run() { try { serviceConnection.waitForStartup(); long now = System.currentTimeMillis(); - Group group = db.getGroup(g); Collection<GroupMessageHeader> headers = - db.getMessageHeaders(g); + db.getMessageHeaders(g.getId()); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Partial load took " + duration + " ms"); CountDownLatch latch = new CountDownLatch(1); - displayHeaders(latch, group, headers); + displayHeaders(latch, g, headers); latch.await(); } catch(NoSuchSubscriptionException e) { if(LOG.isLoggable(INFO)) LOG.info("Subscription removed"); - removeGroup(g); + removeGroup(g.getId()); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java index 6dd8792ea6..38b8ce59da 100644 --- a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java @@ -53,6 +53,7 @@ implements OnClickListener, OnItemSelectedListener { new BriarServiceConnection(); @Inject private BundleEncrypter bundleEncrypter; + private boolean restricted = false; private GroupNameSpinnerAdapter adapter = null; private Spinner spinner = null; private ImageButton sendButton = null; @@ -71,6 +72,7 @@ implements OnClickListener, OnItemSelectedListener { super.onCreate(null); Intent i = getIntent(); + restricted = i.getBooleanExtra("net.sf.briar.RESTRICTED", false); byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID"); if(id != null) groupId = new GroupId(id); id = i.getByteArrayExtra("net.sf.briar.PARENT_ID"); @@ -123,6 +125,7 @@ implements OnClickListener, OnItemSelectedListener { serviceConnection, 0); } + // FIXME: If restricted, only load groups the user can post to private void loadGroupList() { dbExecutor.execute(new Runnable() { public void run() { @@ -144,6 +147,7 @@ implements OnClickListener, OnItemSelectedListener { runOnUiThread(new Runnable() { public void run() { for(Group g : groups) { + if(g.isRestricted() != restricted) continue; if(g.getId().equals(groupId)) { group = g; spinner.setSelection(adapter.getCount()); diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java index 3e466d1918..add29d7abd 100644 --- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java +++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java @@ -327,5 +327,5 @@ public interface DatabaseComponent { * Unsubscribes from the given group. Any messages belonging to the group * are deleted from the database. */ - void unsubscribe(GroupId g) throws DbException; + void unsubscribe(Group g) throws DbException; } diff --git a/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java index 0a66d81f4a..40c5510301 100644 --- a/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java +++ b/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java @@ -1,20 +1,20 @@ package net.sf.briar.api.db.event; -import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.Group; /** An event that is broadcast when a group message is added to the database. */ public class GroupMessageAddedEvent extends DatabaseEvent { - private final GroupId groupId; + private final Group group; private final boolean incoming; - public GroupMessageAddedEvent(GroupId groupId, boolean incoming) { - this.groupId = groupId; + public GroupMessageAddedEvent(Group group, boolean incoming) { + this.group = group; this.incoming = incoming; } - public GroupId getGroupId() { - return groupId; + public Group getGroup() { + return group; } public boolean isIncoming() { diff --git a/briar-api/src/net/sf/briar/api/db/event/SubscriptionRemovedEvent.java b/briar-api/src/net/sf/briar/api/db/event/SubscriptionRemovedEvent.java index df7f5bf934..2957304efc 100644 --- a/briar-api/src/net/sf/briar/api/db/event/SubscriptionRemovedEvent.java +++ b/briar-api/src/net/sf/briar/api/db/event/SubscriptionRemovedEvent.java @@ -1,17 +1,17 @@ package net.sf.briar.api.db.event; -import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.Group; /** An event that is broadcast when the user unsubscribes from a group. */ public class SubscriptionRemovedEvent extends DatabaseEvent { - private final GroupId groupId; + private final Group group; - public SubscriptionRemovedEvent(GroupId groupId) { - this.groupId = groupId; + public SubscriptionRemovedEvent(Group group) { + this.group = group; } - public GroupId getGroupId() { - return groupId; + public Group getGroup() { + return group; } } diff --git a/briar-api/src/net/sf/briar/api/messaging/Group.java b/briar-api/src/net/sf/briar/api/messaging/Group.java index 3e1bf6466f..71d260ac84 100644 --- a/briar-api/src/net/sf/briar/api/messaging/Group.java +++ b/briar-api/src/net/sf/briar/api/messaging/Group.java @@ -23,6 +23,11 @@ public class Group { return name; } + /** Returns true if the group is restricted. */ + public boolean isRestricted() { + return publicKey != null; + } + /** * If the group is restricted, returns the public key that is used to * authorise all messages sent to the group. Otherwise returns null. diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java index 99dc9c162a..f5184749ca 100644 --- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java +++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java @@ -287,10 +287,8 @@ DatabaseCleaner.Callback { } finally { contactLock.readLock().unlock(); } - if(added) { - GroupId g = m.getGroup().getId(); - callListeners(new GroupMessageAddedEvent(g, false)); - } + if(added) + callListeners(new GroupMessageAddedEvent(m.getGroup(), false)); } /** @@ -1357,7 +1355,7 @@ DatabaseCleaner.Callback { if(added) { Group g = m.getGroup(); if(g == null) callListeners(new PrivateMessageAddedEvent(c, true)); - else callListeners(new GroupMessageAddedEvent(g.getId(), true)); + else callListeners(new GroupMessageAddedEvent(g, true)); } } @@ -1855,7 +1853,7 @@ DatabaseCleaner.Callback { return added; } - public void unsubscribe(GroupId g) throws DbException { + public void unsubscribe(Group g) throws DbException { Collection<ContactId> affected; messageLock.writeLock().lock(); try { @@ -1863,10 +1861,11 @@ DatabaseCleaner.Callback { try { T txn = db.startTransaction(); try { - if(!db.containsSubscription(txn, g)) + GroupId id = g.getId(); + if(!db.containsSubscription(txn, id)) throw new NoSuchSubscriptionException(); - affected = db.getVisibility(txn, g); - db.removeSubscription(txn, g); + affected = db.getVisibility(txn, id); + db.removeSubscription(txn, id); db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java index 854fe74e73..c9834381a5 100644 --- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java +++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java @@ -823,7 +823,8 @@ abstract class JdbcDatabase implements Database<Connection> { ps = txn.prepareStatement(sql); ps.setBytes(1, g.getId().getBytes()); ps.setString(2, g.getName()); - ps.setBytes(3, g.getPublicKey()); + if(g.isRestricted()) ps.setBytes(3, g.getPublicKey()); + else ps.setNull(3, BINARY); int affected = ps.executeUpdate(); if(affected != 1) throw new DbStateException(); ps.close(); @@ -3029,9 +3030,8 @@ abstract class JdbcDatabase implements Database<Connection> { for(Group g : subs) { ps.setBytes(2, g.getId().getBytes()); ps.setString(3, g.getName()); - byte[] key = g.getPublicKey(); - if(key == null) ps.setNull(4, BINARY); - else ps.setBytes(4, key); + if(g.isRestricted()) ps.setBytes(4, g.getPublicKey()); + else ps.setNull(4, BINARY); ps.addBatch(); } int[] affectedBatch = ps.executeBatch(); diff --git a/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java b/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java index a66b3b6bf2..a6c93b06ed 100644 --- a/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java +++ b/briar-core/src/net/sf/briar/messaging/MessageFactoryImpl.java @@ -99,8 +99,7 @@ class MessageFactoryImpl implements MessageFactory { // Validate the arguments if((author == null) != (authorKey == null)) throw new IllegalArgumentException(); - if((group == null || group.getPublicKey() == null) - != (groupKey == null)) + if((group == null || !group.isRestricted()) != (groupKey == null)) throw new IllegalArgumentException(); if(contentType.getBytes("UTF-8").length > MAX_CONTENT_TYPE_LENGTH) throw new IllegalArgumentException(); @@ -182,9 +181,8 @@ class MessageFactoryImpl implements MessageFactory { private void writeGroup(Writer w, Group g) throws IOException { w.writeStructId(GROUP); w.writeString(g.getName()); - byte[] publicKey = g.getPublicKey(); - if(publicKey == null) w.writeNull(); - else w.writeBytes(publicKey); + if(g.isRestricted()) w.writeBytes(g.getPublicKey()); + else w.writeNull(); } private void writeAuthor(Writer w, Author a) throws IOException { diff --git a/briar-core/src/net/sf/briar/messaging/MessageReader.java b/briar-core/src/net/sf/briar/messaging/MessageReader.java index de0bf74ac5..73c0550516 100644 --- a/briar-core/src/net/sf/briar/messaging/MessageReader.java +++ b/briar-core/src/net/sf/briar/messaging/MessageReader.java @@ -93,7 +93,7 @@ class MessageReader implements StructReader<UnverifiedMessage> { int signedByGroup = (int) counting.getCount(); // Read the group's signature, if there is one byte[] groupSig = null; - if(group == null || group.getPublicKey() == null) r.readNull(); + if(group == null || !group.isRestricted()) r.readNull(); else groupSig = r.readBytes(MAX_SIGNATURE_LENGTH); // That's all, folks r.removeConsumer(counting); diff --git a/briar-core/src/net/sf/briar/messaging/MessageVerifierImpl.java b/briar-core/src/net/sf/briar/messaging/MessageVerifierImpl.java index 22abc26555..de57bbb3a1 100644 --- a/briar-core/src/net/sf/briar/messaging/MessageVerifierImpl.java +++ b/briar-core/src/net/sf/briar/messaging/MessageVerifierImpl.java @@ -46,7 +46,7 @@ class MessageVerifierImpl implements MessageVerifier { } // Verify the group's signature, if there is one Group group = m.getGroup(); - if(group != null && group.getPublicKey() != null) { + if(group != null && group.isRestricted()) { PublicKey k = keyParser.parsePublicKey(group.getPublicKey()); signature.initVerify(k); signature.update(raw, 0, m.getLengthSignedByGroup()); diff --git a/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java b/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java index 0e54cec9b9..067881cb56 100644 --- a/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java +++ b/briar-core/src/net/sf/briar/messaging/PacketWriterImpl.java @@ -133,9 +133,8 @@ class PacketWriterImpl implements PacketWriter { for(Group g : u.getGroups()) { w.writeStructId(GROUP); w.writeString(g.getName()); - byte[] publicKey = g.getPublicKey(); - if(publicKey == null) w.writeNull(); - else w.writeBytes(publicKey); + if(g.isRestricted()) w.writeBytes(g.getPublicKey()); + else w.writeNull(); } w.writeListEnd(); w.writeInt64(u.getVersion()); diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java index e59726b0f8..61bc729a9d 100644 --- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java +++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java @@ -204,8 +204,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { db.subscribe(group); // Second time - not called assertEquals(Collections.emptyList(), db.getMessageHeaders(groupId)); assertEquals(Arrays.asList(groupId), db.getSubscriptions()); - db.unsubscribe(groupId); // Listeners called - db.removeContact(contactId); // Listeners called + db.unsubscribe(group); + db.removeContact(contactId); db.removeListener(listener); db.close(); @@ -707,7 +707,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { } catch(NoSuchSubscriptionException expected) {} try { - db.unsubscribe(groupId); + db.unsubscribe(group); fail(); } catch(NoSuchSubscriptionException expected) {} -- GitLab