From 863e1c6508899cbae28005deda405efad01b6777 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Fri, 20 Jul 2012 22:23:25 -0700 Subject: [PATCH] Fix the look of tabbed multi-contact selector. 1) Updated to ActionBar style. 2) Split out into fragments. 3) Switch to cursor loaders. --- AndroidManifest.xml | 10 +- res/drawable-hdpi/hairline_left.9.png | Bin 0 -> 1105 bytes res/drawable-hdpi/hairline_right.9.png | Bin 0 -> 1106 bytes res/drawable-hdpi/ic_menu_done_holo_dark.png | Bin 0 -> 1030 bytes res/drawable-hdpi/ic_tab_contacts.png | Bin 0 -> 1490 bytes res/drawable-hdpi/ic_tab_groups.png | Bin 0 -> 2152 bytes res/drawable-hdpi/ic_tab_recent.png | Bin 0 -> 1888 bytes res/drawable-mdpi/hairline_left.9.png | Bin 0 -> 1090 bytes res/drawable-mdpi/hairline_right.9.png | Bin 0 -> 1091 bytes res/drawable-mdpi/ic_menu_done_holo_dark.png | Bin 0 -> 694 bytes res/drawable-mdpi/ic_tab_contacts.png | Bin 0 -> 1059 bytes res/drawable-mdpi/ic_tab_groups.png | Bin 0 -> 1376 bytes res/drawable-mdpi/ic_tab_recent.png | Bin 0 -> 1173 bytes res/drawable-xhdpi/hairline_left.9.png | Bin 0 -> 1132 bytes res/drawable-xhdpi/hairline_right.9.png | Bin 0 -> 1130 bytes res/drawable-xhdpi/ic_menu_done_holo_dark.png | Bin 0 -> 1306 bytes res/drawable-xhdpi/ic_tab_contacts.png | Bin 0 -> 2070 bytes res/drawable-xhdpi/ic_tab_groups.png | Bin 0 -> 3076 bytes res/drawable-xhdpi/ic_tab_recent.png | Bin 0 -> 2757 bytes res/layout/contact_selection_activity.xml | 48 +-- res/menu/contact_selection.xml | 9 + res/menu/contact_selection_list.xml | 8 + .../securesms/ContactSelectionActivity.java | 162 ++++++--- .../ContactSelectionGroupsFragment.java | 231 ++++++++++++ ...java => ContactSelectionListFragment.java} | 238 +++++++------ ...va => ContactSelectionRecentFragment.java} | 174 +++++----- .../securesms/GroupSelectionListActivity.java | 236 ------------- .../securesms/contacts/ContactAccessor.java | 68 ++-- .../contacts/ContactAccessorNewApi.java | 215 ++++++------ .../contacts/ContactAccessorOldApi.java | 328 ------------------ .../securesms/recipients/Recipients.java | 56 +-- 31 files changed, 743 insertions(+), 1040 deletions(-) create mode 100644 res/drawable-hdpi/hairline_left.9.png create mode 100644 res/drawable-hdpi/hairline_right.9.png create mode 100644 res/drawable-hdpi/ic_menu_done_holo_dark.png create mode 100644 res/drawable-hdpi/ic_tab_contacts.png create mode 100644 res/drawable-hdpi/ic_tab_groups.png create mode 100644 res/drawable-hdpi/ic_tab_recent.png create mode 100644 res/drawable-mdpi/hairline_left.9.png create mode 100644 res/drawable-mdpi/hairline_right.9.png create mode 100644 res/drawable-mdpi/ic_menu_done_holo_dark.png create mode 100644 res/drawable-mdpi/ic_tab_contacts.png create mode 100644 res/drawable-mdpi/ic_tab_groups.png create mode 100644 res/drawable-mdpi/ic_tab_recent.png create mode 100644 res/drawable-xhdpi/hairline_left.9.png create mode 100644 res/drawable-xhdpi/hairline_right.9.png create mode 100644 res/drawable-xhdpi/ic_menu_done_holo_dark.png create mode 100644 res/drawable-xhdpi/ic_tab_contacts.png create mode 100644 res/drawable-xhdpi/ic_tab_groups.png create mode 100644 res/drawable-xhdpi/ic_tab_recent.png create mode 100644 res/menu/contact_selection.xml create mode 100644 res/menu/contact_selection_list.xml create mode 100644 src/org/thoughtcrime/securesms/ContactSelectionGroupsFragment.java rename src/org/thoughtcrime/securesms/{ContactSelectionListActivity.java => ContactSelectionListFragment.java} (76%) rename src/org/thoughtcrime/securesms/{ContactSelectionRecentActivity.java => ContactSelectionRecentFragment.java} (75%) delete mode 100644 src/org/thoughtcrime/securesms/GroupSelectionListActivity.java delete mode 100644 src/org/thoughtcrime/securesms/contacts/ContactAccessorOldApi.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a5db8bb207..e41c4179b8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -39,6 +39,11 @@ android:windowSoftInputMode="stateVisible" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"/> + + + @@ -51,11 +56,6 @@ - - - - - diff --git a/res/drawable-hdpi/hairline_left.9.png b/res/drawable-hdpi/hairline_left.9.png new file mode 100644 index 0000000000000000000000000000000000000000..5d9c7210ea997b48afeb5253a5211664f5c6bc77 GIT binary patch literal 1105 zcmbVLO-K|`93KlREulk!b$X14DB77fvmdiF?IycBySmkyts89=;V6j4cUbk_>RLjyDKWB$M2@BewdqrJ7U z;a~$rQH|*~IZM{vVd71Wi$qma?s=MT9{&>e0<8{rTxL8t7V-J{QcujGaKedfSkL zI)~bG>QGS?HM-?Eh?FHlU?8l3ve9R{QaMVm@k(UxU9&V;Gr`3uz3x;#(*Y8egFujh zJ{5)_2yqPO4+w&A6!4IPEaX{^_i=*c=OqY1^`VKHqZOpAoT_S(Q!LT!#4Pw)+ zCW@qtEh{$5F_1M3FRnFf7iZBQH#W6)bAvWwv&gju9F^3g&|CwP+-)u784_=#(~eGx zqV&m@I$$6Zr{yS3{xF)ZNstdijuQe<;9)4taUvgwF%HJ#koSv1La1?UhUJ80FclBQ z;#`a%lbq;>sbmPIQVCfWQ$ewYO`9%OOcm91b)s9vid$l(go6}pIXTPft93v}uZ1nQ z*RnyPGYB$@s+-=~E6;kgGII3GNJ}}E0oL*>>6_?7@!z<^;Y27EiY3IDkPOEIe!7a) z{wHTFafbDpzNE$b4LQ^m=^P>hR;8hu>UTuDd}$7~LBmdG~WdZ~FA3 gG~@7J}yFs>g2_e@=|oEPJvy5h&m)hkoI-3H@a&D;-P_=_x|twf4~3tjJCBl zRaNY%peU*;)vRR5x{bW-md)fF9tZwXjT1TCVfWyI>LN8DbF~%(Oie&E}vkbkgg8O33ic`6C8!g)| zqQgND&_D#x5g3G_dYQ zMFkj#5bP{yV#5381j%61K6w}b`jVnx|SDz@FMZTFWupe=7> z+soSyEq8?Jw5l1FFZ<_baT@7K*EZ?p{EEgJ_JQzk+|j5UNk$}DkoZJ2ULRsg zSp9!;#u8^(zd8PDmhutlK!3a1`b1bA9%PY;ami@h`8n1{#%d&`NZI0>`77-ePv}bd z-u&BNotZDx#JI#CtlYJ@zx#IG!su+~6m`9&?p^B8$C;*EhIx=`Qp2Qf$ZE?vZQkw*!f}r3SUVr6xZMpANj-`>g)_ l&Unq_t>KerzhCV*w}IL>(K0@Lx6}W7P^m<#azEZR@*8nUX0-qS literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_menu_done_holo_dark.png b/res/drawable-hdpi/ic_menu_done_holo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..750c77ef9377f5897a8219344114d343a4eabeef GIT binary patch literal 1030 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}trX+877l!}s{b%+Ad7K3vk;OpT z1B~5HX4`=T%L*LRfizez!?|}o;S3DSe4Z|jArXh)UiZ$3NfkNp@%~(EVda(}!xbK4 zfg3{>S|4=Xt#Mal(IdCmPVV`claDPi)qN)--WAoBgPKF=yt!oLll~)|o@D=aQZo-T(RT|L?g!E$;8lIU1H@HYu-h(Z5ikucz5| zTuH52d$2E$i|dDVjDD18?16TH%ns{hEw#lWo+_0~m&ES+9g@~A{Oiq?_eS zs=ILem+q|39gD+{nW<)6von4mYrcDlj?T{OyuGS9P60o%^FK{<6rZZQ^ICk{pOqp0 zFCqdCe6POj_t~=U)`t0?cyI4q&~QKRlHO*mX`*c{x#juWLm=+Ugv?Y^TRp26yUTv;aS5EW!)x`a-vvs09t0W( zncDs{e0BR1Yu} z7onvB${&m8^yNP)nbW)e(Yvynr^5s~3+7&b`>9>FPkO@Eb&EE9p1k(i`vPnS8=&|40UVXU>U;i3DYOw5_ zWw&3d$Wd$ZsXLEDXGRJC|5^3n?8T;}8wJ51yZ`E~*pah7Il{^H*@pAi+Vy+HMSjbg z`0wjtuG@Zl$)?j9-0gE?^!KSQULavKOM9E;`&i*^ksM5HG_B`(o zoO`A2s-^CF(Yw`>x4PE;*nhNsy-K?Ga!2uNz-*#g;u=wsl30>zm0Xkxq!^403=MS+ zEOZS_LJUo<3{9*|&2$aStqcriSS{B;(U6;;l9^VCTf@tA`6)mRk{}y`^V3So6N^$A k98>a>QWZRN6Vp?JQWH}u3s0un02MKKy85}Sb4q9e0OJDIx&QzG literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_tab_contacts.png b/res/drawable-hdpi/ic_tab_contacts.png new file mode 100644 index 0000000000000000000000000000000000000000..e5deb0112978ff9ea18a4caff72e506e75219758 GIT binary patch literal 1490 zcmV;@1ugoCP)X>3(h6otQi?i=XbQicL8B*41r;QQZE1`BxX+RGq4w_$!0Rg zMFq0DWN^iryqi+C3uJlL4=EVmE6>~0O0|Pw-{q54WpfT5^?fa~RQ`SSD^b)SFWq+T zHTxGmqDS?RR%oSGX|L6<|bmO`fyCor4b>gM~p53>HE&!R*J*Kl5YKW(%e`3RuyHFVl#y zdD#3yD}xqXQJf0Unv}n^d3w2$q$jAqaR*t{g2B6JsVkdiry>x9mi|kMM~J)Awp~zS~Bi>DjQuEkHB`&f!)*ZAm|i= z>pX<5%tg64=FyR0Ub?#vVY-G*x86CiEJm-3+mpqjt6c;r7*7g7@Aw>qZ-D%ohyt|6 zA)s)eQQKBPh_z8ggmB;zKx2FkiY^6eae;Hm1B=B=YsuJ-AMyNXgYDpINkw7g=jqNelbwW3T)_kXOX zYM;eYnHvv%HlLyf*Vz}eTK7lxmQ2N{6_TK+sHmJ3HERti7uV^D;CE{VBT8aC0x%o} zvpBouGd-;P?Y%!2;8He-O4vkXx4^VM4QsVZD}I}YQPZ~j<7yG8V2bhrJM@U|*RqDf zEn`TYVH&_Qp>O3QfiJXLD^IMzd)m~o+>uF~B?J|bH-|p)T+*F{aoZQ04*O3>Lfz6+ zb3H@w+O?JMkN&DEw8T!NG`{-VPzdZ0On&B}rGYQCOpj}g)@q%e(dtOag~E46VEts= zj;DDS4xk@pq)HO6%W86atjo`zOLqt&}e2TI+Y)sNsPl-77&sm z=>Z8OU7K=~OacZB12JwZ6`)D#0dI5xr*J7d3^KOYuXB7G8{aFrFE0x>B)88_7gyTG zk!N1?fAq(y!d%=c`^4d1PLt^LTdl?3(rM$Cc!y#Epx4gJ#y6v|kp?G$^B_68 zb7XQpS5!3;=M$8K@iWB*o1E5Q0U*}7*)o7j5_|tggW}G7;MHP|FK0z`fkzP~`Rviq zrbsBx1_2;o61-i#70=Q}!#|u~N2gauB*r!0MVo@^f&W&fs0F4v4qmE`GD^p) zh*Ioe9Xno75N5noQEX>KjX+hr0E&b{6cLOH0b`bIvKuyg`M$S*BqB=M>yxXnPI35NIgCH0z zgf?2cU;Ny>;;zGuG61Mz4fUK2lz&qO#XQ0I_VEc!*5?tqwQAmCw-2pY-#j|?r09#s z%kI_ea(^-r5d{^YT$@Pako_uyNkF5#DwEW>5*0@@CK&9ir@JRPh>eozIJ?h_&Lhi*E=FK=3dGg$%7Z{^AjYrg;tu$l}&r~x#?jF}a_URj%MrJ@56=cRn#1r!Y&ko@%yzdz5u83&ic886cxCpPv(W> zqZE!eV8JNfMh(vT3ErE!Eq2egX&=vSU7=exT%hc|K|j<@{-)Q*gIX!cii(O#Ow`m` z1nY0n9l1B38%jarLj5%L%ii1)em(0-kT`Eo_GAe&Yf6_6D)8e*k>2788^2N*4w59kUi!&v$+3dzb_mv zlG>i!nMpbYk*d%@aa*^oUpnEPn|s`RW0%o*SpI?5?a9wGse-Q2zQO)Pb^nef{hvN7 zymU&hDR%eV{^XWa0~+cc8Ep6T8b4Y2vnR`2wt9lSa@fTARvE1b0bB$n%Rvk!-rgtw zFn#0T!X*<1UL2?*f{VcbZQ2-Lzv`*q)!@`!*=r7MG~*J&0zA@Wa0oTMSUEp>Kn9e* zyEr&NTg2)(kqQ`zYIDO&>t6ZiC&L;C>R3i}UvTR214G@MsGrF+x9nZB`O4bs+ZHiB zSX%z8L8nX%pH5a&v^%sBQ6FE5>lRl%B%fVXd41lUr3fU~KlSkJ8qEs0IJ9T*uAMT?32B}^iZ!%CB9Wwjq$ykJ z%UNQrK6BKi1%n8nWHWW~tq1Dj4T+>}7Znv%bUQJ6iYY-5jqQJB!HWAozeVTUV*l6k zLgdI3x|T8AZ(kj*o7rmyHYt3hV{&}bcnA(IK54S;w*dv&-7npJ(Oc@{$2!i{VG0j& zDs3HBuI!bm`VVVjXy0{Z(@5YS@_6kKk|XNvt&o3k?XBb1sV?&?2^H{yBLYx2Kaj_9*PdtFNEJJ5qgn_WKj3c_#70ZQ^h zhY1w19}Mo;siy0;D8D8nx^A< zV>?V>7zldq&&>(M^6$UH1V$iAWYcc{zFg)zPS95hmScqv|Hu(?EE8sL}&O9Pgg4VMir=%0000bbVXQnWMOn=I%9HW zVRU5xGB7bQEigGPFga8)G&(XgIy5ycFgH3dF#n{$t^fc4C3HntbYx+4WjbwdWNBu3 z05UK!F)c7TEigG$F*G_dG&(RiEigAaFfeOm7bpM#02y>eSaefwW^{L9a%BKPWN%_+ eAW3auXJt}lVPtu6$z?nM0000mVj|`$JuJMBf<5(P52MWGBmD9zyVd9WO^bYQgggSy?PYHy#H zm0>{>NJ%330h-MU_uJ*g+g*9sz>xECuqQO6*k0FcJC*4`FnIm)>ns0yF|Z^&Gl4~- ze;^_o(+_?xV-ptQw0E8LU;6EhQ$rs+0Zp6smFe3SEiK7-(#}Q+b)BeqwYEpcm+C%H zKm}8oHE=+iwIOu&*2_C`u)qk(c=5n20J9_K*2|%@+Mvw?2P(5LDi8Ec%}2qk&WOm5 z)KnE=sni)2IQP}TL%(0GsGOENqJmXbG*T0>Se=1U=HE|#8Opy5$=!RjSsVNJKbr~N zM#&X5O8ZXjy1G?SLB594ZQ$9={{7mZ&E0!QzG9SR|4qIy5+1p@S+8otNKHj1q5>W= zP*FbozP2@PQ&a$wqwE(lhikM!uhwslu$Lq@uuzbaxq-LT@0@v~2vpz+pd1yWo$mkv-3U#n+OQi}5@lqMOJ z0b*ukljQUry1}P*O%_?Uv-o8B^x^-fG6qFggoDvB|ARErDi(OvI8<3i0Tz7g|IsV>_K;ZYo=ZEAy;Ko}YA2G{JTyZ|ZV zV^0G=9nG$4{Ust+;SbK23u^s=9pkL0ysp_W+QSG^Ndl-StrglFzFfCfQJE3*hM?3N z?03HzzNk&1b0^ng)W8-}P*LjEhA(S#YsEw&nFI?JrKTi&L0dX^W7OpGqnPnoK-ggZ zUgrEmTb#P1rHC3>f*MTcZfyx)s3}oYtf>v6tM3~=tSw`;&(BAc;yA`iKncGoc>*5BpI)X<17b;4f5TrfnV#zW2-R8kwWLR`UA&H zr%2avSVE+P)Nj_S+BjVOT(sc0<5+8ekSIS{aG)4hzIkrWaJ4o*P{E0m%q#awW<&q} zt#jbEQOW^tlw*?u^vXH2+(y3j$VAC(FdCBv6ChIbS7i(y(D$@)qL{S0+ zP?RzGw^70DEgGrTMtyJaL5u!)+hSE__3u-^HipjJ+Vx@%2BzI>eVGPeUd(CV6*{Ah z>hIrInT0iV+mhgmx+$;78#CXjoVOJloVGh>{gRLWY z*L3}Cts{bR$W(+2(hqOQ-QZpeSaefwW^{L9a%BKPWN%_+AW3au aXJt}lVPtu6$z?nM0000IZ@>lU`;#pKPV69sn@ z1P|hUClQ6=7P`p^@Ja|xWzHIGO7&{n9-benw-|zo`h#jZVk%mUL$v1j8&Sah;z!W^D)(df)b zMW30}WrH0$1_phF5?F|6z_+GsPw`XiCa*&G!8OlC7E2(DT?FDBjOZ`{ zK|r9CbqqHqG-x^ zUvqeYgS=%0acx?Aq>TQ!v7@zDX*!56BM(ozI;}^&zXPVZ+g&Iyq~0i}T$2_>n^Lhp zZ6TW!)D%nqaE56pP)tHWkYZ30VInCAvY3Hs0cJ8#jLK40>Tv9Y71T6LC*^oXNK<4^ zkfSi4OTc_StEzH7E_bj6+asE-qmHggb=%n3u2?1OB8_0Tg7H+R14(j&b@F1H`j7vu&{3ta*$I30J>5BjA^NrP>B@oJ;{{CfS!&u$- z^3Cfr%hlxrEpv9PdGBst*W7dG$HI#I=}_(3@X?mN&qh8)I%hkuH5`4IBxkx{w=D J=QCrozX2NBVl@B& literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/hairline_right.9.png b/res/drawable-mdpi/hairline_right.9.png new file mode 100644 index 0000000000000000000000000000000000000000..75f84a43e848489bf21f33e4331b0796f19e98e0 GIT binary patch literal 1091 zcmbVLOK8(z91l$97*5g4<_`Li;XqlNFXc90izw39FMLwH4&?KoM&1-p!^s*9+&ZjU06G1W1YN2)$O`5X;WR9o37 zmT)P1P}FQQsQNa+n&}WUMGX$s990`f7>uGZ!;*o(-u~10n6)aF(}B>T*JYJZ2_NA9kr*hL=#U}LNJ|dc9q!jnu#JQ zV`{3yutCU}rXN?s+QWJD$Bi|uz2dZkm^||ADOV%)DEGC%BzNlz`G&+B@sMkfqNo#! ztxcK8!Wl)P$)BKZ=py7|kY)KWjFY_&qwXhr)<`cniWf!T~c8j(>(dvL)#m2T* zu^k{6A|R`3hUK6A@~lLwAlEpJ^ptCxppjqESVJEQ|Hd7Q$u!8B2sibQ7!p zPtF+P4C6P)U(M3GA|2>&S6iPPR)+^!WMW)08V|O7IY7qB%P8?;?c=wruR9-uj%1;} zxO{f*YunGwpDy=28Ckg9pT4$nKHpBM-0?Hkz;!P*-bF8*(4<0t@5>i=Hf_Cfa!LC9eq?!4>)tuLRAt*ryH4GkdHdnb#l!oL9hu!z-tq2MPTbH#-JRoS Sz_y#c{#a*{!^-`{$ob!Cuv*Xn literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_menu_done_holo_dark.png b/res/drawable-mdpi/ic_menu_done_holo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7ec8c8f5cd0aa79672d2472adc501be0b83de537 GIT binary patch literal 694 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UOiAAEE)4(M`_JqL@;D1TB8!2v z2N=7Z%(epwmK8Xr18D^?ZvQoBE&~JOK2I0N5Rc<;r`TqT1d6oHpQ)&r$ED1;Vu!s;K;Y?)3>Be(EdfC**2)VA=v+B}=c0gW6Xsw!ObmaQOGNU0+SbmhVZweC2_cs-KV9 z)%XVAzJlpeZ~tsL%d%SM!`!_#Nv#&IqtCOR%Pu%KtyH>FtaD=L>TN!}acleH^pce3 zz4h?vo@maqjBR~r>58aFJ-V;WY9HuL|C^Kb;a9?*>eb3g+bqs1*{}H68(C*~TzJ~2 zWnU`yEK=(3JEAFeGiB2KGasjTH-F4hR?q!)Ynpj4dzHLG;i2{Sj3hbrJzX9dmOp=c zx3P1Z_mjt0)uhfUO_QmvAUQh^kM zk%6J1u7QQFfk}v=sgk4UOiAAEE)4(M`_JqL@;D1TB8!2v z2N=7Z%(epwmK8Xr18D^?ZvQoBE&~Izwx^3@h{y4(Q)6>NLIsZ37ni*+%gpxH&Rv@; zY`D=&gOS_ip{tbBkB&~U!*(AQ@}HXS>*JaA<*10MlxLS)a!04eMRCKIlS6|7U0i!F z3E8hw4bWh{rR6j|uRQwJ-shu5Y|63M6lljqq%V=hn-In_FmLhum78HGCga$algj%U9{hIz+%&4uKsZGJ?L*3K7J8$32 zWHUT@Fy@@6kNwQ8+f?%#`2^Wn4hkuUeXOW&-sCQ|X{y2OI7jyR`)W7ybsDcV+PLrA z*=#Gpi#=Sop02P958Tui_;I=Fr!zfAzcPes+6Re+alcw!=q2H;I?u;6&>%nX;iVIK zoSAG)4Tp^WNJ_o?*(IkJA$#)V8U_Y#4z}6L95!(F`U_8YE2tC@zsf1&dreVj`W%-Z zTOY2kFfl$T`1Wza^?QwTRt4+Mue08taKyMl$l!6F&Gvu26Xz=%s+iyI3|l4x_V=!p$-8lOUy6{+yZCROZ`Ul< z%)fKfaxblKPPLxZ9rn`Y%ja&{xII7KJz{@4ZC>ov-xI8CeZ)A!yI-4bFuEbzwfDK~ z<;UBsKi+*B&VO(9iucltOVuVbu|}nuW`AwbW3N~^~vw8jR#+DDa%ia%~NLEg9T>baqMiB2Pmp-jFritdoUK-~U<9eKuFIY_6l<+bzQT zKb%os{cne_SjH!vh!?iGvuwiizObz@HGaxh^Jn)mjljU-;(uSy+8X^A|IHn8Drw1b z^FtQE{G(dp8c~vxSdwa$T$Bo=7>o=I4RsAHfhfe##LCFT%EVOHz}(8f;Qx{X>rgc0 z=BH$)Rbpx|uz+aDP7&7tYLEok5S*V@Ql40p%HWuipOmWLnVXoN8kCxtQdxL1)dr}D N!PC{xWt~$(69DG&&g%dG literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_tab_groups.png b/res/drawable-mdpi/ic_tab_groups.png new file mode 100644 index 0000000000000000000000000000000000000000..c3cf14a1744e4d3b35c918e18add8506aaa0818b GIT binary patch literal 1376 zcmV-m1)utfP)kdg00002VoOIv0RM-N z%)bBt010qNS#tmY07w7;07w8v$!k6U000Sga6xAP001BW001BWhx(kI000DNNkl-+TJQkDR%rt*y@U*SqI^&d%9$ z-WT|vVm^S~q7@ZYm*ffnB>WSCqe5|kcgj+0K(Mdl=Y?_s)1wZKEqnUh)S~o=umn^{ zvGj?8*dvRO72U9`w?vonH@d8|-9=@yU;=O4pMqElpj4>ZWFd~p5fmhM?P4v@vy{K^ z9Lre2s>|=Cgag@9AOWG;DF84EfFgj&pg0LVfg7csg;7}_^c&0T@B>f~5I%Sl=AT(b z6e0>XLTAz^F&Kd@5Wt3jfNKXnnbgpE*1gT|UXlDduZek&Oww^?*`$*TowJf1w(a#} zy<;SUfNaIffW7{OZ~*3Ic5?)ejB{K|vfz z(_=Pum?2#4q7Kzj(@@t8xZ5W50`$Yiy9q>sVhed1I^Ye|G9V#kw|Wot9)f8DYPzce zS7LMDmvwqkLeMe0(Vo_nxBvK~POqf^5H_VlSIx})ZkIh`SN@ANUzyp~<@KR1_CO&Z z#J8#b1HQtV8BNdmioPCIm&NAZy#>jqP$>Yg0uWJm=fLgE?X|WN`yrp%V=BIy5{uE zq<-lrmUkc@!56>umFVHW|J2eF=}4JiA6WOz^74Ie#m=VB`(t}quqS3%+%$7=V_wQ` z_uK(fJmvKjf}!a#kGYSe?S>!go7nNW}u;}VD$G4j0-FC!`P7U#DE@aZ|k<} zbJPXf5`S~-+nIy2jHp{V5rmhol)A-XPU@SR3ek0NX5+pQljcUGcbqIczT7Roi)yxW zb!ytQ@YMDb2flml%IS$i#xno`K*Q#)xht}pu^KR&mL@Gx{SktxyMk9Oj~GU*ZGeQq zf-T^#+&ua)vf)Jt0>Xp|1B8VjGz1zE8m_gY_rvI9^sQH_ zgrY@@P6YIY8H1Lf0~!Vt3=jeM(1RcZ1{6&6C_OTF$1qr(?5{J0wv{eSaefwW^{L9a%BKP iWN%_+AW3auXJt}lVPtu6$z?nM0000k4UOiAAEE)4(M`_JqL@;D1TB8!2v z2N=7Z%(epwmK8Xr18D^?ZvQoBE&~JeTu&Fr5Rc<`r^fq4r%D{Fzi+u#^RR#h>n5XQ zrkqzlgvDchN@lD`&Nh1F!81#+F9}m4rN~~fupOw>jaYkwp zj15a>Jk%)Jvifa;K>Hube+!q|zOCGwusC4Xo4WHmf1aEC{_nZ(c9r*@A2{5(yund5 z>X&3l_8aDhObUx!IGY$YY!ea1RCF#EIX4-pCLT7vTeYi%(b?4K)YTmG6$(f4 z)I^LPRsC{)E!>vB@c*{_w7l6%4{Y3fYUBN=T$cL(IqRiEHS4rEa`ztebKz`MGoJB@ zEs9UP#$oT%&#~`qtLEtOJ!g*!iZhnT5ON7k$#*nKyrSAz5nsM_!j*jwl%Ky^e=nTn zqATa!s~_z+dv>v0+%$Fd{}&}{Y?JQAXD#Ze^PTvpnS)YH89|7h&wFKTSo za|2a^-FYvzM6Wdx&JuHBcRu=~YPqG^MTL%O%pH0yvr?ahtyY+|EsnDxdafb=4R1cx zn&Z`eALs65@7|PnJ}}Ii?|{~awKos`w&jdJxv=@0@v`XLm-Q9%Tb|!~U#c^yLdm}UHT}RnAa6gYNyVf2TGjH9dCDV7huF=zB zI&(j{XycO`45pkBj1dt@rL&V4S4sEGd6*OSNTO1&{Hj-0Wb?1J_wLs?l*r9K%oo5j z$K+w_LXYWN-z;4xfBL_H^DS2fnX;UVN7tzB3eujqbK4dhcR|&j{d$u(&bQZn`&3_;CY-;K z?NncSX|}{0t=`>aB+GNNnU0=Ys~5CV0`N6Z!Z9?*fR~zbs3*A;M ztC;A$%He_T?~SKqQ+K*?XT9}$BY$XJNCRh zUKF`@BfsiD_BXoMl^kY@u`)Kd zGPVSAtqcs9UPknwXvob^$xN$6*I;U8Yy#0BHZAZhP=h4MhT#0PlJdl&R0hYC{G?O` d&)mfH)S%SFl*+=BsWw1G44$rjF6*2UngAUK literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/hairline_left.9.png b/res/drawable-xhdpi/hairline_left.9.png new file mode 100644 index 0000000000000000000000000000000000000000..d2574bc98f4085ac4d203b0b8d1bab444a7cdc8c GIT binary patch literal 1132 zcmbVLO-$2Z7_OKYL=g{$fImr7F8;0k+Oc)56tb=>%s7~1#^FUu`@x#gAKMRCh$aeA zc<|tbn-{JoVhjmtJb6&R!iJg<4>oDPpT5uYyuY_|z1@us z2O9{2Xw38|dA#n!Z)g23{5`}Szk-)mlrExvXBbs94-!ek8G>ZS(neq&YR34bx3GgC zb`G0`A}VIjh`M9ZT4+NDmW$B@(a{ySnqG#89D*aJEm7+)UQwiJNYwdQ7G&KNESWtM z9_*j!E$98qF@XhUGX%@cX19oK zznUnVGA7VmhNS^xSz%lo);`L^-)?Mc?H9&f$mF5#jCne)$8cK>jC1#Iq0kU}BldYF zE{Zm)IQp0cZIn?Y3jd)E(+~ld04&S51D*r%1j`DX43aF6Wxz!RKE>BKw!^Y~I;P5T zPG*xBnP!D3P}6asswqVg)R<7iW^5m6whn8$Cf2QD*?(film|8Bcm>B9t#v@IDn^BU5U}f$8*@h*u{$^yN z>G{vrseQTS`8%Ge`D7iZrcyhPp;k~+b=|VYKzM5E`fADNNKe=CC ze7&}OAN+bacJQ`JsaD-y;^X;|`hilhIh_AYx>s3{ H2Pc04wn%A$ literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/hairline_right.9.png b/res/drawable-xhdpi/hairline_right.9.png new file mode 100644 index 0000000000000000000000000000000000000000..bb7948566d2249c6879e5746992b8eff4bf6d9e8 GIT binary patch literal 1130 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 - - - - - - - - - - - + + \ No newline at end of file diff --git a/res/menu/contact_selection.xml b/res/menu/contact_selection.xml new file mode 100644 index 0000000000..6f3145eb20 --- /dev/null +++ b/res/menu/contact_selection.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/res/menu/contact_selection_list.xml b/res/menu/contact_selection_list.xml new file mode 100644 index 0000000000..95ffa978f6 --- /dev/null +++ b/res/menu/contact_selection_list.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/org/thoughtcrime/securesms/ContactSelectionActivity.java b/src/org/thoughtcrime/securesms/ContactSelectionActivity.java index 40d9e57069..b486a5fd8d 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionActivity.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionActivity.java @@ -1,6 +1,6 @@ -/** +/** * Copyright (C) 2011 Whisper Systems - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -10,54 +10,66 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms; -import org.thoughtcrime.securesms.service.KeyCachingService; - -import android.app.TabActivity; import android.content.Intent; import android.os.Bundle; -import android.view.Window; -import android.widget.TabHost; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.ActionBar.Tab; +import com.actionbarsherlock.app.ActionBar.TabListener; +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.service.KeyCachingService; /** * Activity container for selecting a list of contacts. Provides a tab frame for * contact, group, and "recent contact" activity tabs. Used by ComposeMessageActivity * when selecting a list of contacts to address a message to. - * + * * @author Moxie Marlinspike * */ +public class ContactSelectionActivity extends SherlockFragmentActivity { -public class ContactSelectionActivity extends TabActivity implements TabHost.OnTabChangeListener { + private ContactSelectionListFragment contactsFragment; + private ContactSelectionGroupsFragment groupsFragment; + private ContactSelectionRecentFragment recentFragment; - private TabHost tabHost; + private Recipients recipients; @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); - requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.contact_selection_activity); + ActionBar actionBar = this.getSupportActionBar(); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + actionBar.setDisplayHomeAsUpEnabled(true); - tabHost = getTabHost(); - tabHost.setOnTabChangedListener(this); + setContentView(R.layout.contact_selection_activity); setupContactsTab(); setupGroupsTab(); setupRecentTab(); } - + @Override protected void onStart() { super.onStart(); registerPassphraseActivityStarted(); } - + @Override protected void onStop() { super.onStop(); @@ -65,48 +77,102 @@ public class ContactSelectionActivity extends TabActivity implements TabHost.OnT } @Override - public boolean onSearchRequested() { + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = this.getSupportMenuInflater(); + inflater.inflate(R.menu.contact_selection, menu); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_selection_finished: + case android.R.id.home: + handleSelectionFinished(); return true; + } + return false; } - + + private void handleSelectionFinished() { + recipients = contactsFragment.getSelectedContacts(); + recipients.append(recentFragment.getSelectedContacts()); + recipients.append(groupsFragment.getSelectedContacts()); + + Intent resultIntent = getIntent(); + resultIntent.putExtra("recipients", this.recipients); + + setResult(RESULT_OK, resultIntent); + + finish(); + } + + private ActionBar.Tab constructTab(final Fragment fragment) { + ActionBar actionBar = this.getSupportActionBar(); + ActionBar.Tab tab = actionBar.newTab(); + + tab.setTabListener(new TabListener(){ + @Override + public void onTabSelected(Tab tab, FragmentTransaction ignore) { + FragmentManager manager = ContactSelectionActivity.this.getSupportFragmentManager(); + FragmentTransaction ft = manager.beginTransaction(); + + ft.add(R.id.fragment_container, fragment); + ft.commit(); + } + + @Override + public void onTabUnselected(Tab tab, FragmentTransaction ignore) { + FragmentManager manager = ContactSelectionActivity.this.getSupportFragmentManager(); + FragmentTransaction ft = manager.beginTransaction(); + ft.remove(fragment); + ft.commit(); + } + @Override + public void onTabReselected(Tab tab, FragmentTransaction ft) {} + }); + + return tab; + } + + private void setupContactsTab() { + contactsFragment = (ContactSelectionListFragment)Fragment.instantiate(this, + ContactSelectionListFragment.class.getName()); + ActionBar.Tab contactsTab = constructTab(contactsFragment); + contactsTab.setIcon(R.drawable.ic_tab_contacts); + this.getSupportActionBar().addTab(contactsTab); + } + + private void setupGroupsTab() { + groupsFragment = (ContactSelectionGroupsFragment)Fragment.instantiate(this, + ContactSelectionGroupsFragment.class.getName()); + ActionBar.Tab groupsTab = constructTab(groupsFragment); + groupsTab.setIcon(R.drawable.ic_tab_groups); + this.getSupportActionBar().addTab(groupsTab); + } + + private void setupRecentTab() { + recentFragment = (ContactSelectionRecentFragment)Fragment.instantiate(this, + ContactSelectionRecentFragment.class.getName()); + + ActionBar.Tab recentTab = constructTab(recentFragment); + recentTab.setIcon(R.drawable.ic_tab_recent); + this.getSupportActionBar().addTab(recentTab); + } + private void registerPassphraseActivityStarted() { Intent intent = new Intent(this, KeyCachingService.class); intent.setAction(KeyCachingService.ACTIVITY_START_EVENT); - startService(intent); + startService(intent); } - + private void registerPassphraseActivityStopped() { Intent intent = new Intent(this, KeyCachingService.class); intent.setAction(KeyCachingService.ACTIVITY_STOP_EVENT); startService(intent); } - - private void setupGroupsTab() { - Intent intent = new Intent(this, GroupSelectionListActivity.class); - - tabHost.addTab(tabHost.newTabSpec("groups") - .setIndicator("Groups", getResources().getDrawable(android.R.drawable.ic_menu_share)) - .setContent(intent)); - } - - private void setupRecentTab() { - Intent intent = new Intent(this, ContactSelectionRecentActivity.class); - - tabHost.addTab(tabHost.newTabSpec("recent") - .setIndicator("Recent", getResources().getDrawable(android.R.drawable.ic_menu_call)) - .setContent(intent)); - } - - private void setupContactsTab() { - Intent intent = new Intent(this, ContactSelectionListActivity.class); - tabHost.addTab(tabHost.newTabSpec("contacts") - .setIndicator("Contacts", getResources().getDrawable(android.R.drawable.ic_menu_agenda)) - .setContent(intent)); - } - public void onTabChanged(String tabId) { - - } - + } diff --git a/src/org/thoughtcrime/securesms/ContactSelectionGroupsFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionGroupsFragment.java new file mode 100644 index 0000000000..94be129ac9 --- /dev/null +++ b/src/org/thoughtcrime/securesms/ContactSelectionGroupsFragment.java @@ -0,0 +1,231 @@ +/** + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms; + +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckedTextView; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.ListView; + +import com.actionbarsherlock.app.SherlockListFragment; + +import org.thoughtcrime.securesms.contacts.ContactAccessor; +import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; +import org.thoughtcrime.securesms.contacts.ContactAccessor.GroupData; +import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * An activity for selecting a list of "contact groups." Displayed + * by ContactSelectionActivity in a tabbed frame, and ultimately called + * by ComposeMessageActivity for selecting a list of recipients. + * + * @author Moxie Marlinspike + * + */ +public class ContactSelectionGroupsFragment extends SherlockListFragment + implements LoaderManager.LoaderCallbacks +{ + + private final HashMap selectedGroups = new HashMap(); + + @Override + public void onActivityCreated(Bundle icicle) { + super.onActivityCreated(icicle); + + initializeResources(); + initializeCursor(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.contact_selection_group_activity, container, false); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + ((GroupItemView)v).selected(); + } + + private void initializeCursor() { + setListAdapter(new GroupSelectionListAdapter(getActivity(), null)); + this.getLoaderManager().initLoader(0, null, this); + } + + private void initializeResources() { + this.getListView().setFocusable(true); + } + + public Recipients getSelectedContacts() { + List recipientList = new LinkedList(); + + for (GroupData groupData : selectedGroups.values()) { + List contactDataList = ContactAccessor.getInstance() + .getGroupMembership(getActivity(), groupData.id); + + Log.w("GroupSelectionListActivity", "Got contacts in group: " + contactDataList.size()); + + for (ContactData contactData : contactDataList) { + for (NumberData numberData : contactData.numbers) { + recipientList.add(new Recipient(contactData.name, numberData.number, null)); + } + } + } + + return new Recipients(recipientList); + } + + private void addGroup(GroupData groupData) { + selectedGroups.put(groupData.id, groupData); + } + + private void removeGroup(GroupData groupData) { + selectedGroups.remove(groupData.id); + } + + private class GroupSelectionListAdapter extends CursorAdapter { + + public GroupSelectionListAdapter(Context context, Cursor cursor) { + super(context, cursor); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + GroupItemView view = new GroupItemView(context); + bindView(view, context, cursor); + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + GroupData groupData = ContactAccessor.getInstance().getGroupData(getActivity(), cursor); + ((GroupItemView)view).set(groupData); + } + } + + private class GroupItemView extends LinearLayout { + private GroupData groupData; + private CheckedTextView name; + + public GroupItemView(Context context) { + super(context); + + LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + li.inflate(R.layout.contact_selection_group_item, this, true); + + this.name = (CheckedTextView)findViewById(R.id.name); + } + + public void selected() { + name.toggle(); + + if (name.isChecked()) { + addGroup(groupData); + } else { + removeGroup(groupData); + } + } + + public void set(GroupData groupData) { + this.groupData = groupData; + + if (selectedGroups.containsKey(groupData.id)) + this.name.setChecked(true); + else + this.name.setChecked(false); + + this.name.setText(groupData.name); + } + } + +// private class GroupAggregationHandler extends Handler implements Runnable { +// private List recipientList = new LinkedList(); +// private ProgressDialog progressDialog; +// private final Context context; +// +// public GroupAggregationHandler(Context context) { +// this.context = context; +// } +// +// public void run() { +// recipientList.clear(); +// +// for (GroupData groupData : selectedGroups.values()) { +// List contactDataList = ContactAccessor.getInstance() +// .getGroupMembership(getActivity(), groupData.id); +// +// Log.w("GroupSelectionListActivity", "Got contacts in group: " + contactDataList.size()); +// +// for (ContactData contactData : contactDataList) { +// for (NumberData numberData : contactData.numbers) { +// recipientList.add(new Recipient(contactData.name, numberData.number, null)); +// } +// } +// } +// +// this.obtainMessage().sendToTarget(); +// } +// +// public void aggregateContacts() { +// progressDialog = new ProgressDialog(context); +// progressDialog.setTitle("Aggregating Contacts"); +// progressDialog.setMessage("Aggregating group contacts..."); +// progressDialog.setCancelable(false); +// progressDialog.setIndeterminate(true); +// progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); +// progressDialog.show(); +// Log.w("GroupSelectionListActivity", "Showing group spinner..."); +// new Thread(this).start(); +// } +// +// @Override +// public void handleMessage(Message message) { +// progressDialog.dismiss(); +// +// listener.groupAggregationComplete(new Recipients(recipientList)); +// } +// } + + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) { + return ContactAccessor.getInstance().getCursorLoaderForContactGroups(getActivity()); + } + + @Override + public void onLoadFinished(Loader arg0, Cursor cursor) { + ((CursorAdapter)getListAdapter()).changeCursor(cursor); + } + + @Override + public void onLoaderReset(Loader arg0) { + ((CursorAdapter)getListAdapter()).changeCursor(null); + } +} diff --git a/src/org/thoughtcrime/securesms/ContactSelectionListActivity.java b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java similarity index 76% rename from src/org/thoughtcrime/securesms/ContactSelectionListActivity.java rename to src/org/thoughtcrime/securesms/ContactSelectionListFragment.java index b92fe23553..0b61655837 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionListActivity.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -1,6 +1,6 @@ -/** +/** * Copyright (C) 2011 Whisper Systems - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -10,35 +10,22 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; - -import org.thoughtcrime.securesms.contacts.ContactAccessor; -import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; -import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.Recipients; - import android.app.AlertDialog; -import android.app.ListActivity; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.database.Cursor; import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; import android.util.Log; -import android.view.KeyEvent; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CheckedTextView; @@ -47,85 +34,97 @@ import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; +import com.actionbarsherlock.app.SherlockListFragment; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +import org.thoughtcrime.securesms.contacts.ContactAccessor; +import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; +import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + /** * Activity for selecting a list of contacts. Displayed inside * a ContactSelectionActivity tab frame, and ultimately called by * ComposeMessageActivity for selecting a list of destination contacts. - * + * * @author Moxie Marlinspike * */ -public class ContactSelectionListActivity extends ListActivity { - - private final HashMap selectedContacts = new HashMap(); +public class ContactSelectionListFragment extends SherlockListFragment + implements LoaderManager.LoaderCallbacks +{ + + private final HashMap selectedContacts = new HashMap(); - private static final int MENU_OPTION_EXIT = 1; - private static final int MENU_OPTION_SELECT_ALL = 2; - private static final int MENU_OPTION_UNSELECT_ALL = 3; - @Override - protected void onCreate(Bundle icicle) { + public void onActivityCreated(Bundle icicle) { super.onCreate(icicle); - - setContentView(R.layout.contact_selection_list_activity); - initializeResources(); - displayContacts(); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - menu.clear(); - - menu.add(0, MENU_OPTION_EXIT, Menu.NONE, "Finished!").setIcon(android.R.drawable.ic_menu_set_as); - menu.add(0, MENU_OPTION_SELECT_ALL, Menu.NONE, "Select all").setIcon(android.R.drawable.ic_menu_add); - menu.add(0, MENU_OPTION_UNSELECT_ALL, Menu.NONE, "Unselect all").setIcon(android.R.drawable.ic_menu_revert); - return true; + initializeResources(); + initializeCursor(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.contact_selection_list_activity, container, false); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.contact_selection_list, menu); + super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - + switch (item.getItemId()) { - case MENU_OPTION_EXIT: saveAndExit(); return true; - case MENU_OPTION_SELECT_ALL: selectAll(); return true; - case MENU_OPTION_UNSELECT_ALL: unselectAll(); return true; + case R.id.menu_select_all: handleSelectAll(); return true; + case R.id.menu_unselect_all: handleUnselectAll(); return true; } - + + super.onOptionsItemSelected(item); return false; } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { - saveAndExit(); - return true; + + public Recipients getSelectedContacts() { + List recipientList = new LinkedList(); + + for (ContactData contactData : selectedContacts.values()) { + for (NumberData numberData : contactData.numbers) { + recipientList.add(new Recipient(contactData.name, numberData.number, null)); + } } - return super.onKeyDown(keyCode, event); + return new Recipients(recipientList); } - - private void unselectAll() { + + + private void handleUnselectAll() { selectedContacts.clear(); ((CursorAdapter)getListView().getAdapter()).notifyDataSetChanged(); } - - private void selectAll() { - Log.w("ContactSelectionListActivity", "Selecting all..."); + + private void handleSelectAll() { selectedContacts.clear(); Cursor cursor = null; - + try { - cursor = ContactAccessor.getInstance().getCursorForContactsWithNumbers(this); - + cursor = ContactAccessor.getInstance().getCursorForContactsWithNumbers(getActivity()); + while (cursor != null && cursor.moveToNext()) { - ContactData contactData = ContactAccessor.getInstance().getContactData(this, cursor); - + ContactData contactData = ContactAccessor.getInstance().getContactData(getActivity(), cursor); + if (contactData.numbers.isEmpty()) continue; else if (contactData.numbers.size() == 1) addSingleNumberContact(contactData); else addMultipleNumberContact(contactData, null); @@ -137,66 +136,47 @@ public class ContactSelectionListActivity extends ListActivity { ((CursorAdapter)getListView().getAdapter()).notifyDataSetChanged(); } - - private void saveAndExit() { - List recipientList = new LinkedList(); - - for (ContactData contactData : selectedContacts.values()) { - for (NumberData numberData : contactData.numbers) { - recipientList.add(new Recipient(contactData.name, numberData.number, null)); - } - } - - Intent resultIntent = getIntent(); - resultIntent.putExtra("recipients", new Recipients(recipientList)); - - if (getParent() == null) setResult(RESULT_OK, resultIntent); - else getParent().setResult(RESULT_OK, resultIntent); - - finish(); - } - + private void addSingleNumberContact(ContactData contactData) { selectedContacts.put(contactData.id, contactData); } - + private void removeContact(ContactData contactData) { selectedContacts.remove(contactData.id); } - + private void addMultipleNumberContact(ContactData contactData, CheckedTextView textView) { String[] options = new String[contactData.numbers.size()]; int i = 0; - + for (NumberData option : contactData.numbers) { options[i++] = option.type + " " + option.number; } - - AlertDialog.Builder builder = new AlertDialog.Builder(this); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle("Select for " + contactData.name); builder.setMultiChoiceItems(options, null, new DiscriminatorClickedListener(contactData)); builder.setPositiveButton("Ok", new DiscriminatorFinishedListener(contactData, textView)); builder.setOnCancelListener(new DiscriminatorFinishedListener(contactData, textView)); builder.show(); } - - private void displayContacts() { - Cursor cursor = ContactAccessor.getInstance().getCursorForContactsWithNumbers(this); - startManagingCursor(cursor); - setListAdapter(new ContactSelectionListAdapter(this, cursor)); + + private void initializeCursor() { + setListAdapter(new ContactSelectionListAdapter(getActivity(), null)); + this.getLoaderManager().initLoader(0, null, this); } - + private void initializeResources() { this.getListView().setFocusable(true); } @Override - protected void onListItemClick(ListView l, View v, int position, long id) { + public void onListItemClick(ListView l, View v, int position, long id) { ((ContactItemView)v).selected(); } - + private class ContactSelectionListAdapter extends CursorAdapter { - + public ContactSelectionListAdapter(Context context, Cursor c) { super(context, c); } @@ -213,9 +193,9 @@ public class ContactSelectionListActivity extends ListActivity { public void bindView(View view, Context context, Cursor cursor) { ContactData contactData = ContactAccessor.getInstance().getContactData(context, cursor); ((ContactItemView)view).set(contactData); - } - } - + } + } + private class ContactItemView extends RelativeLayout { private ContactData contactData; private CheckedTextView name; @@ -236,40 +216,40 @@ public class ContactSelectionListActivity extends ListActivity { public void selected() { name.toggle(); - + if (name.isChecked()) { if (contactData.numbers.size() == 1) addSingleNumberContact(contactData); else addMultipleNumberContact(contactData, name); } else { removeContact(contactData); - } + } } - + public void set(ContactData contactData) { this.contactData = contactData; - + if (selectedContacts.containsKey(contactData.id)) this.name.setChecked(true); else this.name.setChecked(false); - + this.name.setText(contactData.name); - + if (contactData.numbers.isEmpty()) { this.name.setEnabled(false); this.number.setText(""); this.label.setText(""); } else { this.number.setText(contactData.numbers.get(0).number); - this.label.setText(contactData.numbers.get(0).type); + this.label.setText(contactData.numbers.get(0).type); } } } - + private class DiscriminatorFinishedListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { private final ContactData contactData; private final CheckedTextView textView; - + public DiscriminatorFinishedListener(ContactData contactData, CheckedTextView textView) { this.contactData = contactData; this.textView = textView; @@ -277,14 +257,14 @@ public class ContactSelectionListActivity extends ListActivity { public void onClick(DialogInterface dialog, int which) { ContactData selected = selectedContacts.get(contactData.id); - + if (selected == null && textView != null) { if (textView != null) textView.setChecked(false); } else if (selected.numbers.size() == 0) { selectedContacts.remove(selected.id); if (textView != null) textView.setChecked(false); } - + if (textView == null) ((CursorAdapter)getListView().getAdapter()).notifyDataSetChanged(); } @@ -293,38 +273,52 @@ public class ContactSelectionListActivity extends ListActivity { onClick(dialog, 0); } } - + private class DiscriminatorClickedListener implements DialogInterface.OnMultiChoiceClickListener { private final ContactData contactData; - + public DiscriminatorClickedListener(ContactData contactData) { this.contactData = contactData; } - + public void onClick(DialogInterface dialog, int which, boolean isChecked) { Log.w("ContactSelectionListActivity", "Got checked: " + isChecked); - + ContactData existing = selectedContacts.get(contactData.id); - + if (existing == null) { Log.w("ContactSelectionListActivity", "No existing contact data, creating..."); if (!isChecked) throw new AssertionError("We shouldn't be unchecking data that doesn't exist."); - + existing = new ContactData(); existing.id = contactData.id; existing.name = contactData.name; existing.numbers = new LinkedList(); - + selectedContacts.put(existing.id, existing); } - + NumberData selectedData = contactData.numbers.get(which); - + if (!isChecked) existing.numbers.remove(selectedData); - else existing.numbers.add(selectedData); + else existing.numbers.add(selectedData); } } + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) { + return ContactAccessor.getInstance().getCursorLoaderForContactsWithNumbers(getActivity()); + } + + @Override + public void onLoadFinished(Loader arg0, Cursor cursor) { + ((CursorAdapter)getListAdapter()).changeCursor(cursor); + } + + @Override + public void onLoaderReset(Loader arg0) { + ((CursorAdapter)getListAdapter()).changeCursor(null); + } } diff --git a/src/org/thoughtcrime/securesms/ContactSelectionRecentActivity.java b/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java similarity index 75% rename from src/org/thoughtcrime/securesms/ContactSelectionRecentActivity.java rename to src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java index 28b008de33..81b1fefa64 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionRecentActivity.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionRecentFragment.java @@ -1,6 +1,6 @@ -/** +/** * Copyright (C) 2011 Whisper Systems - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -10,35 +10,21 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; - -import org.thoughtcrime.securesms.contacts.ContactAccessor; -import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; -import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.Recipients; -import org.thoughtcrime.securesms.util.RedPhoneCallTypes; - -import android.app.ListActivity; import android.content.Context; -import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.provider.CallLog.Calls; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; import android.text.format.DateUtils; -import android.util.Log; -import android.view.KeyEvent; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CheckedTextView; @@ -48,102 +34,82 @@ import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; +import com.actionbarsherlock.app.SherlockListFragment; + +import org.thoughtcrime.securesms.contacts.ContactAccessor; +import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; +import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.RedPhoneCallTypes; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + /** * Displays a list of recently used contacts for multi-select. Displayed * by the ContactSelectionActivity in a tab frame, and ultimately used by * ComposeMessageActivity for selecting destination message contacts. - * + * * @author Moxie Marlinspike * */ -public class ContactSelectionRecentActivity extends ListActivity { - - private final HashMap selectedContacts = new HashMap(); +public class ContactSelectionRecentFragment extends SherlockListFragment + implements LoaderManager.LoaderCallbacks +{ + + private final HashMap selectedContacts = new HashMap(); - private static final int MENU_OPTION_EXIT = 1; - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - setContentView(R.layout.contact_selection_recent_activity); + public void onActivityCreated(Bundle icicle) { + super.onActivityCreated(icicle); + initializeResources(); - displayContacts(); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - menu.clear(); - menu.add(0, MENU_OPTION_EXIT, Menu.NONE, "Finished!").setIcon(android.R.drawable.ic_menu_set_as); - return true; + initializeCursor(); } @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - - switch (item.getItemId()) { - case MENU_OPTION_EXIT: saveAndExit(); return true; - } - - return false; + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.contact_selection_recent_activity, container, false); } @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { - saveAndExit(); - return true; - } - - return super.onKeyDown(keyCode, event); + public void onListItemClick(ListView l, View v, int position, long id) { + ((CallItemView)v).selected(); } - - private void saveAndExit() { + + private void initializeCursor() { + setListAdapter(new ContactSelectionListAdapter(getActivity(), null)); + this.getLoaderManager().initLoader(0, null, this); + } + + public Recipients getSelectedContacts() { List recipientList = new LinkedList(); - + for (ContactData contactData : selectedContacts.values()) { for (NumberData numberData : contactData.numbers) { recipientList.add(new Recipient(contactData.name, numberData.number, null)); } } - - Intent resultIntent = getIntent(); - resultIntent.putExtra("recipients", new Recipients(recipientList)); - - if (getParent() == null) setResult(RESULT_OK, resultIntent); - else getParent().setResult(RESULT_OK, resultIntent); - - finish(); + + return new Recipients(recipientList); } - + private void addSingleNumberContact(ContactData contactData) { selectedContacts.put(contactData.id, contactData); } - + private void removeContact(ContactData contactData) { selectedContacts.remove(contactData.id); } - - private void displayContacts() { - Cursor cursor = getContentResolver().query(Calls.CONTENT_URI, null, null, null, Calls.DEFAULT_SORT_ORDER); - startManagingCursor(cursor); - setListAdapter(new ContactSelectionListAdapter(this, cursor)); - } - private void initializeResources() { this.getListView().setFocusable(true); } - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - ((CallItemView)v).selected(); - } - private class ContactSelectionListAdapter extends CursorAdapter { - + public ContactSelectionListAdapter(Context context, Cursor c) { super(context, c); } @@ -164,11 +130,11 @@ public class ContactSelectionRecentActivity extends ListActivity { String number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)); int type = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.TYPE)); long date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)); - + ((CallItemView)view).set(id, name, label, number, type, date); - } - } - + } + } + private class CallItemView extends RelativeLayout { private ContactData contactData; private Context context; @@ -176,7 +142,7 @@ public class ContactSelectionRecentActivity extends ListActivity { private TextView date; private TextView label; private TextView number; - private CheckedTextView line1; + private CheckedTextView line1; public CallItemView(Context context) { super(context); @@ -194,41 +160,59 @@ public class ContactSelectionRecentActivity extends ListActivity { public void selected() { line1.toggle(); - + if (line1.isChecked()) { addSingleNumberContact(contactData); } else { removeContact(contactData); - } + } } public void set(long id, String name, String label, String number, int type, long date) { if( name == null ) { - name = ContactAccessor.getInstance().getNameForNumber(ContactSelectionRecentActivity.this, number); + name = ContactAccessor.getInstance().getNameForNumber(getActivity(), number); } - + this.line1.setText((name == null || name.equals("")) ? number : name); this.number.setText((name == null || name.equals("")) ? "" : number); this.label.setText(label); this.date.setText(DateUtils.getRelativeDateTimeString(context, date, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE)); - + if (type == Calls.INCOMING_TYPE || type == RedPhoneCallTypes.INCOMING) callTypeIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_call_log_list_incoming_call)); else if (type == Calls.OUTGOING_TYPE || type == RedPhoneCallTypes.OUTGOING) callTypeIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_call_log_list_outgoing_call)); else if (type == Calls.MISSED_TYPE || type == RedPhoneCallTypes.MISSED) callTypeIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_call_log_list_missed_call)); - + this.contactData = new ContactData(); - + if (name != null) this.contactData.name = name; - + this.contactData.id = id; this.contactData.numbers = new LinkedList(); this.contactData.numbers.add(new NumberData(null, number)); - + if (selectedContacts.containsKey(id)) this.line1.setChecked(true); else this.line1.setChecked(false); } - } + } + + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) { + return new CursorLoader(getActivity(), Calls.CONTENT_URI, + null, null, null, + Calls.DEFAULT_SORT_ORDER); + } + + @Override + public void onLoadFinished(Loader arg0, Cursor cursor) { + ((CursorAdapter)getListAdapter()).changeCursor(cursor); + } + + @Override + public void onLoaderReset(Loader arg0) { + ((CursorAdapter)getListAdapter()).changeCursor(null); + } + } diff --git a/src/org/thoughtcrime/securesms/GroupSelectionListActivity.java b/src/org/thoughtcrime/securesms/GroupSelectionListActivity.java deleted file mode 100644 index 55dd415c9d..0000000000 --- a/src/org/thoughtcrime/securesms/GroupSelectionListActivity.java +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms; - -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; - -import org.thoughtcrime.securesms.contacts.ContactAccessor; -import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; -import org.thoughtcrime.securesms.contacts.ContactAccessor.GroupData; -import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.Recipients; - -import android.app.ListActivity; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.util.Log; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckedTextView; -import android.widget.CursorAdapter; -import android.widget.LinearLayout; -import android.widget.ListView; - -/** - * An activity for selecting a list of "contact groups." Displayed - * by ContactSelectionActivity in a tabbed frame, and ultimately called - * by ComposeMessageActivity for selecting a list of recipients. - * - * @author Moxie Marlinspike - * - */ -public class GroupSelectionListActivity extends ListActivity { - - private final HashMap selectedGroups = new HashMap(); - - private static final int MENU_OPTION_EXIT = 1; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - setContentView(R.layout.contact_selection_group_activity); - initializeResources(); - displayGroups(); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - menu.clear(); - menu.add(0, MENU_OPTION_EXIT, Menu.NONE, "Finished!").setIcon(android.R.drawable.ic_menu_set_as); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - - switch (item.getItemId()) { - case MENU_OPTION_EXIT: saveAndExit(); return true; - } - - return false; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { - saveAndExit(); - return true; - } - - return super.onKeyDown(keyCode, event); - } - - private void saveAndExit() { - GroupAggregationHandler aggregator = new GroupAggregationHandler(); - aggregator.aggregateContactsAndExit(); - } - - private void addGroup(GroupData groupData) { - selectedGroups.put(groupData.id, groupData); - } - - private void removeGroup(GroupData groupData) { - selectedGroups.remove(groupData.id); - } - - private void displayGroups() { - Cursor cursor = ContactAccessor.getInstance().getCursorForContactGroups(this); - - startManagingCursor(cursor); - setListAdapter(new GroupSelectionListAdapter(this, cursor)); - } - - private void initializeResources() { - this.getListView().setFocusable(true); - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - ((GroupItemView)v).selected(); - } - - private class GroupSelectionListAdapter extends CursorAdapter { - - public GroupSelectionListAdapter(Context context, Cursor cursor) { - super(context, cursor); - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - GroupItemView view = new GroupItemView(context); - bindView(view, context, cursor); - return view; - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - GroupData groupData = ContactAccessor.getInstance().getGroupData(GroupSelectionListActivity.this, cursor); - ((GroupItemView)view).set(groupData); - } - } - - private class GroupItemView extends LinearLayout { - private GroupData groupData; - private CheckedTextView name; - - public GroupItemView(Context context) { - super(context); - - LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - li.inflate(R.layout.contact_selection_group_item, this, true); - - this.name = (CheckedTextView)findViewById(R.id.name); - } - - public void selected() { - name.toggle(); - - if (name.isChecked()) { - addGroup(groupData); - } else { - removeGroup(groupData); - } - } - - public void set(GroupData groupData) { - this.groupData = groupData; - - if (selectedGroups.containsKey(groupData.id)) - this.name.setChecked(true); - else - this.name.setChecked(false); - - this.name.setText(groupData.name); - } - } - - private class GroupAggregationHandler extends Handler implements Runnable { - private List recipientList = new LinkedList(); - private ProgressDialog progressDialog; - - public GroupAggregationHandler() {} - - public void run() { - recipientList.clear(); - - for (GroupData groupData : selectedGroups.values()) { - List contactDataList = ContactAccessor.getInstance().getGroupMembership(GroupSelectionListActivity.this, groupData.id); - - Log.w("GroupSelectionListActivity", "Got contacts in group: " + contactDataList.size()); - - for (ContactData contactData : contactDataList) { - for (NumberData numberData : contactData.numbers) { - recipientList.add(new Recipient(contactData.name, numberData.number, null)); - } - } - } - - this.obtainMessage().sendToTarget(); - } - - public void aggregateContactsAndExit() { - progressDialog = new ProgressDialog(GroupSelectionListActivity.this); - progressDialog.setTitle("Aggregating Contacts"); - progressDialog.setMessage("Aggregating group contacts..."); - progressDialog.setCancelable(false); - progressDialog.setIndeterminate(true); - progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); - progressDialog.show(); - Log.w("GroupSelectionListActivity", "Showing group spinner..."); - new Thread(this).start(); - } - - @Override - public void handleMessage(Message message) { - progressDialog.dismiss(); - - Intent resultIntent = getIntent(); - resultIntent.putExtra("recipients", new Recipients(recipientList)); - - if (getParent() == null) setResult(RESULT_OK, resultIntent); - else getParent().setResult(RESULT_OK, resultIntent); - - finish(); - } - } -} diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java index 326c32456b..3a80393efa 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -1,6 +1,6 @@ -/** +/** * Copyright (C) 2011 Whisper Systems - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -10,25 +10,25 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms.contacts; -import java.util.LinkedList; -import java.util.List; - -import org.thoughtcrime.securesms.crypto.IdentityKey; - import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.os.Build; import android.os.Parcel; import android.os.Parcelable; +import android.support.v4.content.CursorLoader; + +import org.thoughtcrime.securesms.crypto.IdentityKey; + +import java.util.LinkedList; +import java.util.List; /** * Android changed their contacts API pretty heavily between @@ -36,37 +36,17 @@ import android.os.Parcelable; * API operations, using a singleton pattern that will Class.forName * the correct one so we don't trigger NoClassDefFound exceptions on * old platforms. - * + * * @author Moxie Marlinspike */ -public abstract class ContactAccessor { +public abstract class ContactAccessor { public static final int UNIQUE_ID = 0; public static final int DISPLAY_NAME = 1; - private static ContactAccessor sInstance; - - public static synchronized ContactAccessor getInstance() { - if (sInstance == null) { - String className; + private static final ContactAccessor sInstance = new ContactAccessorNewApi(); - if (Integer.parseInt(Build.VERSION.SDK) <= Build.VERSION_CODES.DONUT) - className = "ContactAccessorOldApi"; - else - className = "ContactAccessorNewApi"; - - try { - Class clazz = - Class.forName("org.thoughtcrime.securesms.contacts." + className ) - .asSubclass(ContactAccessor.class); - - sInstance = clazz.newInstance(); - - } catch (Exception e) { - throw new AssertionError(e); - } - } - + public static synchronized ContactAccessor getInstance() { return sInstance; } @@ -78,6 +58,8 @@ public abstract class ContactAccessor { public abstract List getNumbersForThreadSearchFilter(String constraint, ContentResolver contentResolver); public abstract List getGroupMembership(Context context, long groupId); public abstract Cursor getCursorForContactGroups(Context context); + public abstract CursorLoader getCursorLoaderForContactGroups(Context context); + public abstract CursorLoader getCursorLoaderForContactsWithNumbers(Context context); public abstract Cursor getCursorForContactsWithNumbers(Context context); public abstract GroupData getGroupData(Context context, Cursor cursor); public abstract ContactData getContactData(Context context, Cursor cursor); @@ -85,9 +67,9 @@ public abstract class ContactAccessor { public abstract CharSequence phoneTypeToString(Context mContext, int type, CharSequence label); public abstract String getNameForNumber(Context context, String number); public abstract Uri getContactsUri(); - + public static class NumberData implements Parcelable { - + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public NumberData createFromParcel(Parcel in) { return new NumberData(in); @@ -105,12 +87,12 @@ public abstract class ContactAccessor { this.type = type; this.number = number; } - + public NumberData(Parcel in) { number = in.readString(); type = in.readString(); } - + public int describeContents() { return 0; } @@ -120,14 +102,14 @@ public abstract class ContactAccessor { dest.writeString(type); } } - + public static class GroupData { public long id; public String name; } - + public static class ContactData implements Parcelable { - + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public ContactData createFromParcel(Parcel in) { return new ContactData(in); @@ -143,18 +125,18 @@ public abstract class ContactAccessor { public List numbers; public ContactData() {} - + public ContactData(Parcel in) { id = in.readLong(); name = in.readString(); numbers = new LinkedList(); in.readTypedList(numbers, NumberData.CREATOR); } - + public int describeContents() { return 0; } - + public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); dest.writeString(name); diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessorNewApi.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessorNewApi.java index 3c27b898ce..a8a36e3ab4 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessorNewApi.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactAccessorNewApi.java @@ -1,6 +1,6 @@ -/** +/** * Copyright (C) 2011 Whisper Systems - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -10,21 +10,12 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms.contacts; -import java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -import org.thoughtcrime.securesms.crypto.IdentityKey; -import org.thoughtcrime.securesms.crypto.InvalidKeyException; -import org.thoughtcrime.securesms.util.Base64; - import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -39,17 +30,27 @@ import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.PhoneLookup; import android.provider.ContactsContract.RawContacts; +import android.support.v4.content.CursorLoader; import android.telephony.PhoneNumberUtils; import android.util.Log; +import org.thoughtcrime.securesms.crypto.IdentityKey; +import org.thoughtcrime.securesms.crypto.InvalidKeyException; +import org.thoughtcrime.securesms.util.Base64; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + /** * Interface into the Android 2.x+ contacts operations. - * + * * @author Stuart Anderson */ public class ContactAccessorNewApi extends ContactAccessor { - + private static final String SORT_ORDER = Contacts.TIMES_CONTACTED + " DESC," + Contacts.DISPLAY_NAME + "," + Phone.TYPE; private static final String[] PROJECTION_PHONE = { @@ -65,22 +66,22 @@ public class ContactAccessorNewApi extends ContactAccessor { public List getNumbersForThreadSearchFilter(String constraint, ContentResolver contentResolver) { LinkedList numberList = new LinkedList(); Cursor cursor = null; - + try { - cursor = contentResolver.query(Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(constraint)), - null, null, null, null); - + cursor = contentResolver.query(Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(constraint)), + null, null, null, null); + while (cursor != null && cursor.moveToNext()) numberList.add(cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER))); - + } finally { if (cursor != null) cursor.close(); } - + return numberList; } - + @Override public Cursor getCursorForRecipientFilter(CharSequence constraint, ContentResolver mContentResolver) { String phone = ""; @@ -97,22 +98,22 @@ public class ContactAccessorNewApi extends ContactAccessor { } } } - + Uri uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(cons)); String selection = String.format("%s=%s OR %s=%s OR %s=%s", - Phone.TYPE, - Phone.TYPE_MOBILE, - Phone.TYPE, - Phone.TYPE_WORK_MOBILE, - Phone.TYPE, - Phone.TYPE_MMS); + Phone.TYPE, + Phone.TYPE_MOBILE, + Phone.TYPE, + Phone.TYPE_WORK_MOBILE, + Phone.TYPE, + Phone.TYPE_MMS); Cursor phoneCursor = mContentResolver.query(uri, - PROJECTION_PHONE, - null, - null, - SORT_ORDER); - + PROJECTION_PHONE, + null, + null, + SORT_ORDER); + if (phone.length() > 0) { @@ -139,9 +140,9 @@ public class ContactAccessorNewApi extends ContactAccessor { } else { return phoneCursor; } - + } - + @Override public CharSequence phoneTypeToString( Context mContext, int type, CharSequence label ) { return Phone.getTypeLabel(mContext.getResources(), type, label); @@ -156,50 +157,50 @@ public class ContactAccessorNewApi extends ContactAccessor { private long getContactIdFromLookupUri(Context context, Uri uri) { Cursor cursor = null; - + try { cursor = context.getContentResolver().query(uri, new String[] {ContactsContract.Contacts._ID}, null, null, null); - + if (cursor != null && cursor.moveToFirst()) return cursor.getLong(0); else return -1; - + } finally { if (cursor != null) cursor.close(); } } - + private ArrayList getRawContactIds(Context context, long contactId) { Cursor cursor = null; ArrayList rawContactIds = new ArrayList(); - + try { - cursor = context.getContentResolver().query(RawContacts.CONTENT_URI, new String[] {RawContacts._ID}, - RawContacts.CONTACT_ID + " = ?", new String[] {contactId+""}, - null); - + cursor = context.getContentResolver().query(RawContacts.CONTENT_URI, new String[] {RawContacts._ID}, + RawContacts.CONTACT_ID + " = ?", new String[] {contactId+""}, + null); + if (cursor == null) return rawContactIds; - + while (cursor.moveToNext()) { rawContactIds.add(new Long(cursor.getLong(0))); - } + } } finally { if (cursor != null) cursor.close(); } - + return rawContactIds; } - + @Override public void insertIdentityKey(Context context, Uri uri, IdentityKey identityKey) { long contactId = getContactIdFromLookupUri(context, uri); Log.w("ContactAccessorNewApi", "Got contact ID: " + contactId + " from uri: " + uri.toString()); ArrayList rawContactIds = getRawContactIds(context, contactId); - + for (long rawContactId : rawContactIds) { Log.w("ContactAccessorNewApi", "Inserting data for raw contact id: " + rawContactId); ContentValues contentValues = new ContentValues(); @@ -208,9 +209,9 @@ public class ContactAccessorNewApi extends ContactAccessor { contentValues.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM); contentValues.put(Im.CUSTOM_PROTOCOL, "TextSecure-IdentityKey"); contentValues.put(Im.DATA, Base64.encodeBytes(identityKey.serialize())); - - context.getContentResolver().insert(Data.CONTENT_URI, contentValues); - } + + context.getContentResolver().insert(Data.CONTENT_URI, contentValues); + } } @Override @@ -218,16 +219,16 @@ public class ContactAccessorNewApi extends ContactAccessor { long contactId = getContactIdFromLookupUri(context, uri); String selection = Im.CONTACT_ID + " = ? AND " + Im.PROTOCOL + " = ? AND " + Im.CUSTOM_PROTOCOL + " = ?"; String[] selectionArgs = new String[] {contactId+"", Im.PROTOCOL_CUSTOM+"", "TextSecure-IdentityKey"}; - + Cursor cursor = context.getContentResolver().query(Data.CONTENT_URI, null, selection, selectionArgs, null); - + try { if (cursor != null && cursor.moveToFirst()) { String data = cursor.getString(cursor.getColumnIndexOrThrow(Im.DATA)); - - if (data != null) + + if (data != null) return new IdentityKey(Base64.decode(data), 0); - + } } catch (InvalidKeyException e) { Log.w("ContactAccessorNewApi", e); @@ -246,10 +247,10 @@ public class ContactAccessorNewApi extends ContactAccessor { @Override public String getNameFromContact(Context context, Uri uri) { Cursor cursor = null; - + try { cursor = context.getContentResolver().query(uri, new String[] {Contacts.DISPLAY_NAME}, null, null, null); - + if (cursor != null && cursor.moveToFirst()) return cursor.getString(0); @@ -257,24 +258,24 @@ public class ContactAccessorNewApi extends ContactAccessor { if (cursor != null) cursor.close(); } - + return null; } - + private String getMobileNumberForId(Context context, long id) { Cursor cursor = null; - + try { - cursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ? AND " + Phone.TYPE + " = ?", - new String[] {id+"", Phone.TYPE_MOBILE+""}, null); - + cursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ? AND " + Phone.TYPE + " = ?", + new String[] {id+"", Phone.TYPE_MOBILE+""}, null); + if (cursor != null && cursor.moveToFirst()) return cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER)); } finally { if (cursor != null) cursor.close(); } - + return null; } @@ -282,29 +283,41 @@ public class ContactAccessorNewApi extends ContactAccessor { public NameAndNumber getNameAndNumberFromContact(Context context, Uri uri) { Log.w("ContactAccessorNewApi", "Get name and number from: " + uri.toString()); Cursor cursor = null; - + try { NameAndNumber results = new NameAndNumber(); cursor = context.getContentResolver().query(uri, new String[] {Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null); - + if (cursor != null && cursor.moveToFirst()) { results.name = cursor.getString(1); results.number = getMobileNumberForId(context, cursor.getLong(0)); return results; } - + } finally { if (cursor != null) cursor.close(); } - + return null; } @Override - public Cursor getCursorForContactsWithNumbers(Context context) { + public CursorLoader getCursorLoaderForContactsWithNumbers(Context context) { + Uri uri = ContactsContract.Contacts.CONTENT_URI; String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1"; - return context.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, selection, null, ContactsContract.Contacts.DISPLAY_NAME + " ASC"); + + return new CursorLoader(context, uri, null, selection, null, + ContactsContract.Contacts.DISPLAY_NAME + " ASC"); + } + + @Override + public Cursor getCursorForContactsWithNumbers(Context context) { + Uri uri = ContactsContract.Contacts.CONTENT_URI; + String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1"; + + return context.getContentResolver().query(uri, null, selection, null, + ContactsContract.Contacts.DISPLAY_NAME + " ASC"); } private ContactData getContactData(Context context, String displayName, long id) { @@ -312,52 +325,58 @@ public class ContactAccessorNewApi extends ContactAccessor { contactData.id = id; contactData.name = displayName; contactData.numbers = new LinkedList(); - + Cursor numberCursor = null; - + try { - numberCursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ?", - new String[] {contactData.id + ""}, null); - + numberCursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ?", + new String[] {contactData.id + ""}, null); + while (numberCursor != null && numberCursor.moveToNext()) - contactData.numbers.add(new NumberData(Phone.getTypeLabel(context.getResources(), - numberCursor.getInt(numberCursor.getColumnIndexOrThrow(Phone.TYPE)), - numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.LABEL))).toString(), - numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.NUMBER)))); + contactData.numbers.add(new NumberData(Phone.getTypeLabel(context.getResources(), + numberCursor.getInt(numberCursor.getColumnIndexOrThrow(Phone.TYPE)), + numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.LABEL))).toString(), + numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.NUMBER)))); } finally { if (numberCursor != null) numberCursor.close(); } - + return contactData; - + } - + @Override public ContactData getContactData(Context context, Cursor cursor) { - return getContactData(context, - cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME)), - cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID))); + return getContactData(context, + cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME)), + cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID))); } - + @Override public Cursor getCursorForContactGroups(Context context) { return context.getContentResolver().query(ContactsContract.Groups.CONTENT_URI, null, null, null, ContactsContract.Groups.TITLE + " ASC"); } + @Override + public CursorLoader getCursorLoaderForContactGroups(Context context) { + return new CursorLoader(context, ContactsContract.Groups.CONTENT_URI, + null, null, null, ContactsContract.Groups.TITLE + " ASC"); + } + @Override public List getGroupMembership(Context context, long groupId) { - LinkedList contacts = new LinkedList(); + LinkedList contacts = new LinkedList(); Cursor groupMembership = null; - + try { - String selection = ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID + " = ? AND " + + String selection = ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID + " = ? AND " + ContactsContract.CommonDataKinds.GroupMembership.MIMETYPE + " = ?"; String[] args = new String[] {groupId+"", ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE}; - + groupMembership = context.getContentResolver().query(Data.CONTENT_URI, null, selection, args, null); - + while (groupMembership != null && groupMembership.moveToNext()) { String displayName = groupMembership.getString(groupMembership.getColumnIndexOrThrow(Data.DISPLAY_NAME)); long contactId = groupMembership.getLong(groupMembership.getColumnIndexOrThrow(Data.CONTACT_ID)); @@ -368,16 +387,16 @@ public class ContactAccessorNewApi extends ContactAccessor { if (groupMembership != null) groupMembership.close(); } - + return contacts; } - + @Override public GroupData getGroupData(Context context, Cursor cursor) { GroupData groupData = new GroupData(); groupData.id = cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.Groups._ID)); groupData.name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups.TITLE)); - + return groupData; } @@ -393,7 +412,7 @@ public class ContactAccessorNewApi extends ContactAccessor { if (cursor != null) cursor.close(); } - + return null; } @@ -401,5 +420,5 @@ public class ContactAccessorNewApi extends ContactAccessor { public Uri getContactsUri() { return ContactsContract.Contacts.CONTENT_URI; } - + } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessorOldApi.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessorOldApi.java deleted file mode 100644 index 9c2d5113e8..0000000000 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessorOldApi.java +++ /dev/null @@ -1,328 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.contacts; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -import org.thoughtcrime.securesms.crypto.IdentityKey; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.MergeCursor; -import android.net.Uri; -import android.provider.Contacts; -import android.provider.Contacts.GroupMembership; -import android.provider.Contacts.People; -import android.provider.Contacts.Phones; -import android.telephony.PhoneNumberUtils; -import android.util.Log; -import android.widget.Toast; - - -/** - * A contact interface into the 1.x API for older clients. - * - * @author Stuart Anderson - */ - -public class ContactAccessorOldApi extends ContactAccessor { - - @SuppressWarnings("deprecation") - private static final String SORT_ORDER = Phones.NAME + "," + Phones.TYPE; - @SuppressWarnings("deprecation") - private static final String[] PROJECTION_PHONE = { - Phones._ID, // 0 - Phones.PERSON_ID, // 1 - Phones.TYPE, // 2 - Phones.NUMBER, // 3 - Phones.LABEL, // 4 - Phones.DISPLAY_NAME, // 5 - }; - - @SuppressWarnings("deprecation") - @Override - public Cursor getCursorForRecipientFilter(CharSequence constraint, - ContentResolver mContentResolver) { - String phone = ""; - String wherePhone = null; - - String cons = null; - if (constraint != null) { - cons = constraint.toString(); - - if (RecipientsAdapter.usefulAsDigits(cons)) { - phone = PhoneNumberUtils.convertKeypadLettersToDigits(cons); - if (phone.equals(cons)) { - phone = ""; - } else { - phone = phone.trim(); - } - } - } - - String filter = DatabaseUtils.sqlEscapeString(cons + '%'); - String filterLastName = DatabaseUtils.sqlEscapeString("% " + cons + '%'); - - StringBuilder s = new StringBuilder(); - s.append("((name LIKE "); - s.append(filter); - s.append(") OR (name LIKE "); - s.append(filterLastName); - s.append(") OR (REPLACE(REPLACE(REPLACE(REPLACE(number, ' ', ''), '(', ''), ')', ''), '-', '') LIKE "); - s.append(filter); - s.append("))"); - - wherePhone = s.toString(); - - Cursor phoneCursor = mContentResolver.query(Phones.CONTENT_URI, - PROJECTION_PHONE, - wherePhone, - null, - SORT_ORDER); - - //dumpCursor(phoneCursor); - - - if (phone.length() > 0) { - ArrayList result = new ArrayList(); - result.add(Integer.valueOf(-1)); // ID - result.add(Long.valueOf(-1)); // CONTACT_ID - result.add(Integer.valueOf(Phones.TYPE_CUSTOM)); // TYPE - result.add(phone); // NUMBER - - /* - * The "\u00A0" keeps Phone.getDisplayLabel() from deciding - * to display the default label ("Home") next to the transformation - * of the letters into numbers. - */ - result.add("\u00A0"); // LABEL - result.add(cons); // NAME - - ArrayList wrap = new ArrayList(); - wrap.add(result); - - ArrayListCursor translated = new ArrayListCursor(PROJECTION_PHONE, wrap); - - return new MergeCursor(new Cursor[] { translated, phoneCursor }); - } else { - return phoneCursor; - } - - } - - @SuppressWarnings("deprecation") - @Override - public CharSequence phoneTypeToString(Context mContext, int type, - CharSequence label) { - return Phones.getDisplayLabel(mContext, type, label); - } - - public static void dumpCursor( Cursor c ) { - c.moveToFirst(); - Log.d( "DC", "Begin:" ); - for( int i=0; i < c.getCount(); i++ ) { - String rowStr = ""; - for( int j=0; j < c.getColumnCount(); j++ ) { - rowStr = rowStr + c.getColumnName(j) + "=" + c.getString(j) +" "; - } - Log.d( "DC", rowStr + "\n" ); - c.moveToNext(); - } - } - - @Override - public Intent getIntentForContactSelection() { - return new Intent(Intent.ACTION_PICK, People.CONTENT_URI); - } - - @Override - public void insertIdentityKey(Context context, Uri uri, IdentityKey identityKey) { - Toast.makeText(context, "Sorry, reading and writing identity keys to the contacts database is not supported on Android 1.X", Toast.LENGTH_LONG).show(); - } - - @Override - public IdentityKey importIdentityKey(Context context, Uri uri) { - Toast.makeText(context, "Sorry, reading and writing identity keys to the contacts database is not supported on Android 1.X", Toast.LENGTH_LONG).show(); - return null; - } - - @Override - public String getNameFromContact(Context context, Uri uri) { - // TODO Auto-generated method stub - return null; - } - - @Override - public List getNumbersForThreadSearchFilter(String constraint, ContentResolver contentResolver) { - LinkedList numberList = new LinkedList(); - Cursor cursor = null; - - try { - cursor = contentResolver.query(Uri.withAppendedPath(Contacts.People.CONTENT_FILTER_URI, Uri.encode(constraint)), - null, null, null, null); - - while (cursor != null && cursor.moveToNext()) { - String number = cursor.getString(cursor.getColumnIndexOrThrow(Contacts.Phones.NUMBER)); - if (number != null) - numberList.add(number); - } - } finally { - if (cursor != null) - cursor.close(); - } - - return numberList; - } - - @Override - public NameAndNumber getNameAndNumberFromContact(Context context, Uri uri) { - Cursor cursor = null; - NameAndNumber result = new NameAndNumber(); - - try { - cursor = context.getContentResolver().query(uri, null, null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - result.name = cursor.getString(cursor.getColumnIndexOrThrow(People.NAME)); - result.number = cursor.getString(cursor.getColumnIndexOrThrow(People.NUMBER)); - - return result; - } - } finally { - if (cursor != null) - cursor.close(); - } - - return null; - } - - @Override - public Cursor getCursorForContactsWithNumbers(Context context) { - return context.getContentResolver().query(People.CONTENT_URI,new String[]{People._ID,People.DISPLAY_NAME}, - People.NUMBER + " NOT NULL", null, "UPPER( " + People.DISPLAY_NAME + " ) ASC"); - } - - @Override - public ContactData getContactData(Context context, Cursor cursor) { - ContactData contactData = new ContactData(); - contactData.id = cursor.getLong(cursor.getColumnIndexOrThrow(People._ID)); - contactData.name = cursor.getString(cursor.getColumnIndexOrThrow(People.DISPLAY_NAME)); - contactData.numbers = getNumberDataForPersonId(context, contactData.id); - - return contactData; - } - - @Override - public Cursor getCursorForContactGroups(Context context) { - return context.getContentResolver().query(Contacts.Groups.CONTENT_URI, null, null, null, Contacts.Groups.NAME + " ASC"); - } - - private LinkedList getNumberDataForPersonId(Context context, long personId) { - LinkedList numbers = new LinkedList(); - Cursor numberCursor = context.getContentResolver().query(Phones.CONTENT_URI, null, - Phones.PERSON_ID + " = ?", - new String[] {personId+""}, null); - try { - while (numberCursor != null && numberCursor.moveToNext()) { - numbers.add(new NumberData(Phones.getDisplayLabel(context, numberCursor.getInt(numberCursor.getColumnIndexOrThrow(Phones.TYPE)), "").toString(), - numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phones.NUMBER)))); - } - } finally { - if (numberCursor != null) - numberCursor.close(); - } - - return numbers; - } - - private ContactData getContactDataFromGroupMembership(Context context, Cursor cursor) { - ContactData contactData = new ContactData(); - contactData.id = cursor.getLong(cursor.getColumnIndexOrThrow(GroupMembership.PERSON_ID)); - - Cursor personCursor = context.getContentResolver().query(Uri.withAppendedPath(People.CONTENT_URI, contactData.id+""), null, null, null, null); - - try { - if (personCursor == null || !personCursor.moveToFirst()) - throw new AssertionError("Non-existent user in group?"); - - contactData.name = personCursor.getString(personCursor.getColumnIndexOrThrow(People.DISPLAY_NAME)); - contactData.numbers = getNumberDataForPersonId(context, contactData.id); - - return contactData; - - } finally { - if (personCursor != null) - personCursor.close(); - } - } - - @Override - public List getGroupMembership(Context context, long groupId) { - LinkedList contacts = new LinkedList(); - Cursor groupMembershipCursor = context.getContentResolver().query(Contacts.GroupMembership.CONTENT_URI, null, - GroupMembership.GROUP_ID + " = ?", - new String[] {groupId+""}, null); - - try { - while (groupMembershipCursor != null && groupMembershipCursor.moveToNext()) { - contacts.add(getContactDataFromGroupMembership(context, groupMembershipCursor)); - } - } finally { - if (groupMembershipCursor != null) - groupMembershipCursor.close(); - } - - return contacts; - } - - @Override - public GroupData getGroupData(Context context, Cursor cursor) { - GroupData groupData = new GroupData(); - groupData.id = cursor.getLong(cursor.getColumnIndexOrThrow(Contacts.Groups._ID)); - groupData.name = cursor.getString(cursor.getColumnIndexOrThrow(Contacts.Groups.NAME)); - - return groupData; - } - - @Override - public String getNameForNumber(Context context, String number) { - Cursor cursor = context.getContentResolver().query(Contacts.Phones.CONTENT_URI, null, - Phones.NUMBER + " = ?", - new String[] {number}, null); - - try { - if (cursor != null && cursor.moveToFirst()) - return cursor.getString(cursor.getColumnIndexOrThrow(Contacts.Phones.DISPLAY_NAME)); - } finally { - if (cursor != null) - cursor.close(); - } - - return null; - } - - @Override - public Uri getContactsUri() { - return Contacts.People.CONTENT_URI; - } - -} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/recipients/Recipients.java b/src/org/thoughtcrime/securesms/recipients/Recipients.java index b1d60ddd41..540313b9aa 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipients.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipients.java @@ -1,6 +1,6 @@ -/** +/** * Copyright (C) 2011 Whisper Systems - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or @@ -10,20 +10,20 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms.recipients; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; +import android.os.Parcel; +import android.os.Parcelable; import org.thoughtcrime.securesms.util.NumberUtil; -import android.os.Parcel; -import android.os.Parcelable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; public class Recipients implements Parcelable { @@ -37,73 +37,77 @@ public class Recipients implements Parcelable { } }; - + private List recipients; - + public Recipients(List recipients) { this.recipients = recipients; } - + public Recipients(Parcel in) { this.recipients = new ArrayList(); in.readTypedList(recipients, Recipient.CREATOR); } - + + public void append(Recipients recipients) { + this.recipients.addAll(recipients.getRecipientsList()); + } + public Recipients truncateToSingleRecipient() { assert(!this.recipients.isEmpty()); this.recipients = this.recipients.subList(0, 1); return this; } - + public boolean isEmailRecipient() { for (Recipient recipient : recipients) { if (NumberUtil.isValidEmail(recipient.getNumber())) - return true; + return true; } - + return false; } - + public boolean isEmpty() { return this.recipients.isEmpty(); } - + public boolean isSingleRecipient() { return this.recipients.size() == 1; } - + public Recipient getPrimaryRecipient() { if (!isEmpty()) return this.recipients.get(0); else return null; } - + public List getRecipientsList() { return this.recipients; } - + public String[] toNumberStringArray() { String[] recipientsArray = new String[recipients.size()]; Iterator iterator = recipients.iterator(); int i = 0; - + while (iterator.hasNext()) recipientsArray[i++] = iterator.next().getNumber(); - + return recipientsArray; } public String toShortString() { String fromString = ""; - + for (int i=0;i