From 2ecf12f7275ddd2cbc6b6c39d43754c67200c909 Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Wed, 4 Dec 2024 23:17:36 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...米便签开源代码的泛读报告.docx | Bin 0 -> 16698 bytes ...签开源代码的质量分析报告.docx | Bin 0 -> 13911 bytes src/net/micode/notes/data/Contact.java | 73 ++ src/net/micode/notes/data/Notes.java | 279 +++++ .../notes/data/NotesDatabaseHelper.java | 362 +++++++ src/net/micode/notes/data/NotesProvider.java | 305 ++++++ src/net/micode/notes/gtask/data/MetaData.java | 82 ++ src/net/micode/notes/gtask/data/Node.java | 101 ++ src/net/micode/notes/gtask/data/SqlData.java | 189 ++++ src/net/micode/notes/gtask/data/SqlNote.java | 505 +++++++++ src/net/micode/notes/gtask/data/Task.java | 351 +++++++ src/net/micode/notes/gtask/data/TaskList.java | 343 +++++++ .../exception/ActionFailureException.java | 33 + .../exception/NetworkFailureException.java | 33 + .../notes/gtask/remote/GTaskASyncTask.java | 123 +++ .../notes/gtask/remote/GTaskClient.java | 585 +++++++++++ .../notes/gtask/remote/GTaskManager.java | 800 +++++++++++++++ .../notes/gtask/remote/GTaskSyncService.java | 128 +++ src/net/micode/notes/model/Note.java | 253 +++++ src/net/micode/notes/model/WorkingNote.java | 368 +++++++ src/net/micode/notes/tool/BackupUtils.java | 344 +++++++ src/net/micode/notes/tool/DataUtils.java | 295 ++++++ .../micode/notes/tool/GTaskStringUtils.java | 113 +++ src/net/micode/notes/tool/ResourceParser.java | 181 ++++ .../micode/notes/ui/AlarmAlertActivity.java | 158 +++ .../micode/notes/ui/AlarmInitReceiver.java | 65 ++ src/net/micode/notes/ui/AlarmReceiver.java | 30 + src/net/micode/notes/ui/DateTimePicker.java | 485 +++++++++ .../micode/notes/ui/DateTimePickerDialog.java | 90 ++ src/net/micode/notes/ui/DropdownMenu.java | 61 ++ .../micode/notes/ui/FoldersListAdapter.java | 80 ++ src/net/micode/notes/ui/NoteEditActivity.java | 873 ++++++++++++++++ src/net/micode/notes/ui/NoteEditText.java | 217 ++++ src/net/micode/notes/ui/NoteItemData.java | 224 ++++ .../micode/notes/ui/NotesListActivity.java | 954 ++++++++++++++++++ src/net/micode/notes/ui/NotesListAdapter.java | 184 ++++ src/net/micode/notes/ui/NotesListItem.java | 122 +++ .../notes/ui/NotesPreferenceActivity.java | 388 +++++++ .../notes/widget/NoteWidgetProvider.java | 132 +++ .../notes/widget/NoteWidgetProvider_2x.java | 47 + .../notes/widget/NoteWidgetProvider_4x.java | 46 + 41 files changed, 10002 insertions(+) create mode 100644 doc/小米便签开源代码的泛读报告.docx create mode 100644 doc/小米便签开源代码的质量分析报告.docx create mode 100644 src/net/micode/notes/data/Contact.java create mode 100644 src/net/micode/notes/data/Notes.java create mode 100644 src/net/micode/notes/data/NotesDatabaseHelper.java create mode 100644 src/net/micode/notes/data/NotesProvider.java create mode 100644 src/net/micode/notes/gtask/data/MetaData.java create mode 100644 src/net/micode/notes/gtask/data/Node.java create mode 100644 src/net/micode/notes/gtask/data/SqlData.java create mode 100644 src/net/micode/notes/gtask/data/SqlNote.java create mode 100644 src/net/micode/notes/gtask/data/Task.java create mode 100644 src/net/micode/notes/gtask/data/TaskList.java create mode 100644 src/net/micode/notes/gtask/exception/ActionFailureException.java create mode 100644 src/net/micode/notes/gtask/exception/NetworkFailureException.java create mode 100644 src/net/micode/notes/gtask/remote/GTaskASyncTask.java create mode 100644 src/net/micode/notes/gtask/remote/GTaskClient.java create mode 100644 src/net/micode/notes/gtask/remote/GTaskManager.java create mode 100644 src/net/micode/notes/gtask/remote/GTaskSyncService.java create mode 100644 src/net/micode/notes/model/Note.java create mode 100644 src/net/micode/notes/model/WorkingNote.java create mode 100644 src/net/micode/notes/tool/BackupUtils.java create mode 100644 src/net/micode/notes/tool/DataUtils.java create mode 100644 src/net/micode/notes/tool/GTaskStringUtils.java create mode 100644 src/net/micode/notes/tool/ResourceParser.java create mode 100644 src/net/micode/notes/ui/AlarmAlertActivity.java create mode 100644 src/net/micode/notes/ui/AlarmInitReceiver.java create mode 100644 src/net/micode/notes/ui/AlarmReceiver.java create mode 100644 src/net/micode/notes/ui/DateTimePicker.java create mode 100644 src/net/micode/notes/ui/DateTimePickerDialog.java create mode 100644 src/net/micode/notes/ui/DropdownMenu.java create mode 100644 src/net/micode/notes/ui/FoldersListAdapter.java create mode 100644 src/net/micode/notes/ui/NoteEditActivity.java create mode 100644 src/net/micode/notes/ui/NoteEditText.java create mode 100644 src/net/micode/notes/ui/NoteItemData.java create mode 100644 src/net/micode/notes/ui/NotesListActivity.java create mode 100644 src/net/micode/notes/ui/NotesListAdapter.java create mode 100644 src/net/micode/notes/ui/NotesListItem.java create mode 100644 src/net/micode/notes/ui/NotesPreferenceActivity.java create mode 100644 src/net/micode/notes/widget/NoteWidgetProvider.java create mode 100644 src/net/micode/notes/widget/NoteWidgetProvider_2x.java create mode 100644 src/net/micode/notes/widget/NoteWidgetProvider_4x.java diff --git a/doc/小米便签开源代码的泛读报告.docx b/doc/小米便签开源代码的泛读报告.docx new file mode 100644 index 0000000000000000000000000000000000000000..92cdf698ed0aae8d6f97df93d97d27e6fe08e660 GIT binary patch literal 16698 zcmaKT19)c3(r#?q*2K2$iEZ1qoe3us+qP}n*2K0ZxtX)i`S;m#|9ks+l5c&z>aFUo z`nuMt?kYJcU=S#P-z`*jm;d+Ye;VZHg^{hHoP({MBc0qQ8Omn|#J|Y&lcx%-0096z zfdBvy{+mqS&W_g2+A1Snz&4K_A^4K&+?Rj72P`#630d`v_16WP{OO6B+Hn#!=tko< z<=aaxE0r&v8*A;4{Y8hE+ZTp7@ve6QnspM9=|zxO#$492X?F$QqbJOe@~N_Li`!&8 zhT@m~kG9pfi#ah^LV^&;tmgQ6ZN}8acw(3i5wN6Ur(&2AFUWO6c`8qO3u|Fyweh#4 zeKBE$eYB3MdLD$EokJhNE-Es=daEAJimUz5#Mp+zc88Qqhq-O z$}h#~p0SpVLkfTBl!})jDR&V0nsnE(-gyuBYm)(axZlj)3(Ox=+mThM%SfPgp~OTOyri?lrNy9?=h?m8&n5*YHi%umHyIJMYxLhD&<5un06zS*r_ zBj@|jI&7eDe1W~mrBxkUD%!1~RDG0DpU0dqp9YAJZkq; zveg-?%k~~8rP=i`x`zCxWu%!1G)+D&Xvd={dDIf9$+y$A&9Nq1^d$}Rp1l(x#w2VT_kpZ61 zlazd+BdTXE%*L?n42m+Zav9c0KcqA)q+I|)kJ_pQ-S&$mPWRUjzzm_8rr{*AhzoEX z-jywY<+p?RVnh%<*m<~TTLt+2m=7Dvcq)aokn$vBwfWVmr^(<@=QN)w$@utt2bpNb z*3i;iU8IRJTiinhdp0{tJEm9*kO@CKi=WNVd1Be-$gx$xuHO^zk0>o)c4A&w2-1DB zelFEDTYpWJ%3aEirjW|UA@LN}xfh#TkbZkq+e|!O-A&fZ82qU!!uJZ-=i0`@EkA)R zFqju6!K3b?jvk$ET!O|gDCJ16-**ws3tgNQf4$LPqyN(=jW+q`l%GaH{4@&kKaAq+ z=wxgC+bjifGE#q;<%96dyAIzu!Ae3QH73AKuKIvdzs?+Z#Aev0ROIc_rbLL2uhhNb-m}oasql(OiB`LC9B3SwCY1xzhY+kw* zdGsN|D2bl!d9(0vSO<>9UYlA={=%G`rm8Gg{#qk)?X&@2er=cm4gmvp892MoPIpQZ z_i`5zHjS)H;VPb3QQBbERo)U@qb1UnN_9I0hDmG^cPK~#%nc|!;Wzq1%n3Fx?M`4@ zZWhJu5)ZqwEuKv-D<LP=;3aw>cC0 znwE(&{5>I+>exef%&P&J>PfeLNl7;qw8ZKA5MJb$Gz3oV)3>oZ;NPnK-yIg{r)pho z9gO~PR^=1|&(*L10L#Vz0Pz1IaddLGGIso}`E%_FhkbFxPUIdgiWd;@n2n1ETj{Bg z^OPRsQwEc>6`ryJizX8iW#W>JJq|}gb_5V`A_zFU9guYhxb8K2>Q^))FOj4){C;d#SJd~jotg_M)aPHY`1HexAhY@zk=Cb>RjN!x3^!- zO~<&8k_Cz-!Si}PY+Q0{XB-R|jyu3(iQmqVB*Pqt)Ua=COayXBy}d!|R-^c&UsGR0 zH;-SqBJ+86X`O*^t3Qb_NADK9t#53tap4r3JQ-Dw&nhnT%OZhdPCb_+5EXf`MgkSS zCnBw|HY?n4z$ZaUK?|=>sKIbvF=vTIwp-EUG=gcT|A^*Z=Dd#W|LIs}-L1k6jgASg zmKi7$uSS{mB(K53gu{ka3U&brvtD`jUTFr^AL{u;Clj-iDPzcTCfFJ@z6WLy*-(I5 zm1}-rIy=4%kh&T9W{`r;?S6sov#@E=;8Clw|206Vq(a@AeKk01Q4p98QdW1Pi2JsP zf&I{e9giD$3da2^ed5hnYk!xqZDhWnwU>|1U1NXqWPU1;A{FK$7bN{TwW6Jk`&!>l zwdl5v+4dv&+1)-#Ec?EQ@7Ec=??-_f=Wtr|8Lww+2-J3lY+@(Cq0@;!aw9O7#X@2I-06% zYQs$FU&#U!kezQMz%SL^Gy$s3lwlOoL+N-ytOySvyWgAp`(2V)_?3(>D>MhOTfk!2 z6!`ofs)8S`0a`cFK^NGv$5ZH%$FXgI^UXVD(q2FOS;Z1rfBGe9<38H%X8R(C{z_v% z+3L6dAd}Tdo9@t>#5WJ3fL#F(Y+5m1?HHk*yP$DNae1sBtA7pDF5Cb-V?0M|8mmuR zrk%5%LqzhcH}46gq1O0u)M%=myO!!4Zlk$K+DfDT*}aGc@r zGs;u4Y<;6%FRif8;dKgI$Co&EZGUG)1}qVcf~ZMIbEeRMVLG&e=BpoohMfjE+N<`ZXP1#bL(NN3Fxjp;sG?|)I>gZ&`C=T3W+)yz zJC@#iC9cRqqFr3mI+p!mUzY2Jn2-lbj2|95jpi=7zIeJnzKn7O;&6LQLj(jV;}Yoz ze`_Z#a61}wp#a_Ck<>Tt-B5|8c;)a+cfi-_tf%p?q%9>y_n?eAU+->(*}_CA@1RN0 zmw#FOiuSDpWpo1&wp>r%1j0jQobHVpiRxo9GmYds_9h3k#p$^hhxZUr(;YOR{z;hr zszIL|+f+!gruYhi^Pck;55(6i6ZiM{!F zKYH_NZ{1SzIg~9ENhzaiy`9KXA?-J6t;~1vjpW6#C{XgFkf0PoAV9zD1qST*9B(ez z%DM=jZLYIHxLjVdf!aH-yq_!zzs~~7#TtmA<_()pRRDTYBCf20H@}b`{U^W?%LL%dG z>}x`@>ahIvdb!6%9}u!^9R}N|1*Y>7Ym^s@xteC>9%7I4#WqF;;r+RSWF`_Lg){2L z6!fKw&C27E)mGyp$r`%V%;xdrpmmw^N^_V<|Dae>h2Ii&RFVV-Gnv@Myl*>CP|qP3 z;gxr3PiHB*_)^Hx$_g*eFtbha(YfM1--!+WEo|w#okfF`uT@rQ*ZD>(7QWS1w~Y3l zuhE)>2VAHb&&wTW?Zk$K|6C;AH!G&*fTJ1EHImBJO97qviL6oe@fH`zY(Gir?PM1F89!7?FP!NVreA(xU=MG239HA4^ z6KVn?HJHViRK*mmr^yDOsAHn}EaksCSslKi&dYKnu%Qm8w3qNt?#qBWkl7N&V!JB} zSe46NYel!0sIl4so6H)KF92~9ho z+DkU+#O{jnR$vzKY^Vb!xXA8`H5B_Y1D}KXA4X97(wj;IroS<9Y^Xtkn@ZH!Y)a{+ zzb7FIj~XkxDNNvc!-^CC6f%+)VZbDp&|dO;*28HLB7~ok|2>}nuqg!koIc|4=R^rm zZVC-nnsauQ{v25LD%+<_lr;a;eHoD&^&#O;nNXiY`<&K4*!97C!|X zv)hmYp{xZt*NR6%4)MtYNhIA}zDU-D3DP^z{V(tgC%avep$MlL@ zyk{EcHe$MHc8Cu1sj$rh*1zoq7o(EnQ|hul$*D_k7#>fqY5FTzFkpJ6UutPZYAX7r z!n|1>K1knVwLy1?0Xrp^GtTm=7|liJQ_3ZRn-=lTd%x;0pclA9;RpTDDAi0OXPvHe z!7g*)8dbZ*)YK9xqoAOz%$^kgruy6qFWkX$RR6RH1Ez^up77%-yHt(*GMxsxGHBl0 z7c476*}<(KawobF#mf{*Z;w!x$o|^o3lxx`2!ir*ARV;*>q(A-6iyTzdOFPHHZ(KE zvKlIIdF4{b`%6_90x5t!K%oN$xCLx~ zX!JH7mLj(YInSM z`<~sWhAhY%swW_)0E1$;69v(Vt3Wzq16Jfa&@Q+~dd<>ba5W&5VW~fvxX)~?089!i zquNm#u9~bgaOA$r zwr2qNuDmewI6UxLBD64c zFXzTn5nC+p7o1GY;}C9Yub;CS=lUAgiJWt@tHW&0sht~8jX+2;%eghon;riAxtoy_ z^M!(|mzx~i|F$mT0x;|(Tj%Yy|GN^Pk^I+ zs!Qhty5{ixddh8Fwu6fdw?Sk=K{zMt+0w7F=ZZ+3g$XY63c-#P+;E^LOC^GA6Qk#J z5_J|hL0pNBvywA3wQwL?MB0S}Bn`xBk0EFAr7b8Q0c5njAIy~@x;hH+#1u1Rc-B8K=Cf(0oxxO1URs* zK>=5;X;yUI4);yDWS)l5#>+>$e7>(gzWTmCKP@9CpMKQKeyeyXS_#5>=0k{BXIAa~Z0nz)^&>t!S@mb&clH_C{oe2RSN!K_?Brx_ zWBRY~u0d78c8wjS6Q2H~XPT!*+&W3VDVCHhVZF)$%_KKIsu@PP zHy2w_F)*w>&EZ-#EG;-pJ@| z-(S(0V=j_W&N69--z~Nr#sogiHi{m#^6(i7COHM#ULjcWfL``O%Ee)six`Djh3$s3 zvMk5690s4x41*V}a8ZcBo!U|aVn*;Kh(&#A(W4CVAWVKtej)KsG3WLt9R<9@Tu@6> z?>J4j8LpI5HR@2v#aeH1)03LzZ<-_->yj%`6OB+7VI+Xf9lmW_ZhorYOId9=`V}ha zLp>jEq)8+JHuJ!0(DNN-Ku>au5Auq=cJ8zRT~W0Z?2sAt=B1Vj=L^Kt5tJ6HQ>Q93 zA{RvqueWi_YNKEa?I|fKypwG0Yb(J$>#Sp*@kIocgj$-Cm{|dO465{xg(T~F)6(se zrdpB@uScgmBuvEG8-&`fb>Mapl_>RlM9ARK+7|X zrs%Ol=D+G!p>hhl4K+N^#Q7aV^5j|jQ8mzIn`u=nBS*XW0IJg`t6LYn69g;)Wo!L7 zARb>W^A=H*%#NTh@a5ZBN^jtif>xoy)WUP0bo6`rIEb=lRSEzWMbYlv&{tr#&S;~0 zE%F(fs1FPurl~*VaVO(VF~+GJ!EiqSx)Jx%U=Xrx7>LwC~nAY`1yQC(sof% zHTpwBH4H#!1~{UH%UAuk9sT|G2ylm@mNvVSLWUdMYp{LuMicN>Yc5gg(*y7Q!{KM{ z`>E{yhlQOrMgB3>Eo%a1VmT|TUlEb|;WDu;Vbg;@yKvkb>#~9~mIrC2aXfc1%b2du zTWtG#jCLmDYWNa@(L;W*U3;DdK@c02J7eUvSVj><;jX2dCuaj-&R`eXY-1Zn649w+ z+=kB*I7Bihx3Jr3uN5coJ6FdgtlAh2nw>OP+6sF?Io;Ao zc&8H>`p(V|APf1*r4q13!@9z)Nt}U@o#~-?2pl~?PcgvRAw09QZYbgqf|FL}*s$Li zaP>$2XmBw!z3`|Zw7~Z)NhkzUdiL3N`(_>i$`Dj74vPwl_607zxdE(CmmCDSEB!>Z zkDMd@?tq@Mt%8{Brv@XV#)&Ja7vt!$#^w|z{*xsX6&cS#xyz})yTdHQNj!=ORN8j8 z8MdmdqlzlzM!iWSX`Cx~TSpMHf_`X+TVsK$CFPV}XeFm@G>LK(BFqIscEcV6+^H7F z^y!8Nirb+3x#EOl|kNs*yMIVQbS$tzj3)BS|JjAbS2~UBG zA-0}`sxPFHKk`dkQQf^mO;mE&B=Z+g-o|!BR^QdPb}~aX@!sazAK*uvY6&**>-k+l zO5W|VIe;fPrnfs5Mq0pA*{0ipC-BX#h-?Zqmye8ko_NYD`sC->aw&v`ZXbc|O*#k~ zNV&P^w%)=2-`ji8|8eh5X2#aWe{Ay2R9^N!eLe?BFaZEi|4H~Sa)y6x^{LC)VzVK1 zpdb0Fopsb0of0z=fj9E*F1jEIOGe;)p=lTkYtxEN#NCM4SIg#~sUOY(jL`9*SOpXJ zLo<7^OfM8yB=F48D z@I`ieCF~kQ;%T(6!7q>a3VW{MUZJEBjX3^EC&0kY(~y}+=&a-I{BdcE@1eN1v#_OBmnKf!w3w5LY;1Q^T<6e{K$p+B(re==4oUe0qi^k#u&?;xn8+B0~oxMFdg zTYO{TUl9wvbm{1jZ@dcmh$fv}Q@*#|VWGIyRqu6eqY29mUdo2i)$UcLyuVi&=(Us! zsiN6AEjV{6u=%9QCBdZ2ky=<4wQO7kVl;4Cg0xkl+IaD}@cI^DK~X9t!(b4urEJO2 zI^>2903{wr5>8(91uus%(H>f!al3zwT%Tlt*$kO@&z+SSMM}s*^eYU9gz?-5iAXiW zfzDRv)A4zv>E?Cd;SjtJkI&Q9Qx9DV@t6fVX^#2*K`7ma`%TK@W8LK@_seQDI@$YF z7N76)kg5%VRW3({>K6B})8{$Wm@cS%g?R={L%{Dg zXulv3=M1P4GKLm@>9X78C*oLa^( z96Gr`G8nk`gW*87BI#)BUlZuZjOWEvTVQ5eC1uz*J|Tj5ZZvST8(Pz!Z#ZG)t&gjw zLU4|~G_(l5#!3}OY14X;m!9yuB*ttJe<<`a+vOA#EEJMXS%R!apiRTZS+peah6m}- zcmU>%wkG*ubavAcgTWMM&1xe;A*;QNCqab#T7w}JPIO6@K>Gw(=Nk3`=gvPe>a<0; zEcrNMVTWBmm(NUxaBtL$+tMDAqTJQ-1(&eA04#q);VhYieAJb-?-`O!{AZbM7Q36S z(#YmPe~YOLdXNXIjeMqN6!`vbnIpDLd>RI8zeA)4-Q?xK%EwmZZRszs_jqnqeFQUS zjPY-4FITu#+@3KI1w0%DZn{T%O@5omPWo~y$C2c;7Y61a_zSWjkknekagEpP30X*( z_e6=kGaYACoZ;|lvDzGC#?Bq%{XzsB_;kvV_RX}zFxq8P zRr0}}6Rh(~8<>E`ue2mlBUVV*a7gzn*--JFT0+w}BzL2e!6JPW*rGxT9lt`vWFjSw=i&`66KHP#RrA!KoR@}_wcut{o?Z@In9P#fzzu_i-f1(J%K5lOKAmJfy6PSt zgc#QL;S9MijQ-Qv6~2J~3>l~lkdZk*kKqiK{wrkoJm_||Hnws4>qJP?aeXn)XRGuV ze7XB7dK`OmASz>UnX2+PNn?)FAsRa{68%X4MSu|E|c8mM&X1H&qS~=`@%2Cp9oubfzv|GwT*w_JT69Y(9Q%{W5A_xoyky-xIOq z?}cU?>bp=e<%vMoz%i%0Ga|gag3w&4kUPWWm_=nJws3 z4`j!=OE?@9>en@+!*xhup$GDWk+K)P0%}PgIhy$a=FJK3<~^ly&pp5hQip=n3cPkd z`%M}9XkLlXWHEm*-Ji>0G2brdlIbvs^o?g>A3^wp5$Hy6J0`^3HRR%FvGyaJf`(e{ z;SnaW4u%Fhx<7QSujLjV=jYuVUMON;z2&%!S=^YR(>?v>&3gk8MpEyvHNUJ$os)9d z*0iNZZoX>Q7y3qq-K9ZswLYNP6|jwly~m5~E}`ZLTe$P?&ds%xu;F?)b>$J_zBEAh zT@|OI%kpS8`15y_&~vNUrH#zAb%Ub{)wYpn+9P4|ZdbN!K*``(sND~mtw=#PkFERM zif@LYaagS`Z-Q@f^*qlnnQ)MmIw*+TE60im!;L{sJ)Z}aoJ^u2N`6VjFTebj9 zyyjo8@bBF2w>=|)n!1?=?#~xxSNq!)Vr2T1Zqp6j_HnH3-#S^2XE>Gqc(bxHlndM0{(m z^3dOj3BOmkb6|_Q8AehMo5=YT0?3SC%FK*#w4VZazh?W*_xUHYC}5Oyp>#8TW~b-P ze&5I%Jh?3|_sfGSfgSl*ALqAY?85=tNWBJX?eGFZe%(cdU!ikTI#b#ENQ1FglW%8i z9To|E??4CmG;Oz}XatzL->mcoe)Y}yzJ4tE#2veKb|!($FuO;lEH1f!Qyd+|5Rd=T zOH$LL&95O3E45E6){0YxP{FXlWKfX-T+Sh(d^+ryp;l`i+tMG0h{Uh_hoVY2)CiO$ zVwNr-0iVa+B`V0B0xlgxpW_Y#|LU`e%+Y z!}4cxwkNiqnI%!lT$uQHwW%1S*0Gbf&|{+Y%5fs$k4f&^3y*a%p!MK^@kG z)3$FWU-nJJAJ686UMvxqeJ9Ba@gcE{LN}+Y?F%#^?M!Wmn{sdz=kBveEss1aAfVHw z7Fp55T%@NH@mQEn%p!Fki{^xR%bfG&P0={S2x($OOc3cwCQU=j!=6G`Jfzaq_tw8= z5GPr7%r#MOAdHkUVv$M^wn@!0IJMT$Bnc7kEClTm*>)Q{L0})gupqXYEoo=n=$2Ne zu*M_t)?h)UnN*l55Yx57iC#a9PXG;3WQHCvr%)$L#L$ylK`2VGOP0X1bc_?cv6B`{#q~VpTJq-4p1JOOdRMn4WCCXO z(p@Ogsv3_t;Se%ha6lNYH`T?b@Z}7VoO3Q?&jK+(3f47lJwo~Xy6OW4lZEd^gt*xL zt*z-BpZ8PWT$B09Xf1fhQ9(iykDqZQ7Rd>EkHUwE?$yxe^D(IDSRaKvf0U+6(baiVC;4*da{jDkS=xUWz!>#n}$D zE63)M&|I7#zj*VOi$zUwp$%!mM-YOqZw1>O}lshaU6kT4&ajvhmw&Y$Mx8E zhgoNup*@^wJnXRWbCO27q%@Tyo@OX6-QKw5&)t(yh56NsFjUnWQz;?G-?=q) zdU?|iz1)n6>!pbo-kRoYyEr{==naP_j{{Vet%<*aM383Deex>K13qRy$ z@Ujl>z_aY`WQ}i@2;Y;f`+zEKxs?$^Q%AxF3A={aWtX_F$>RHsg54n++-kpR2Sz~4 zU4=0p_Tka8u-b#>@+K1a6O;LDg-U|w!vSiAxbEyzb>e4bM6j?w=#_T1?TN{4oja>ps&yS7Zyk4;ZC#7GF*{|rY^=%BX-jMzKl+-0n^J+E zW;PXHYjjtI`Fkis)+BzJeWrEeOPK)zs^2m>*ui^SkcdMdAkItHXL42V zk8rX(v_0g3B?mwf!z!dPCxc=nwTz{vDz}+srgu9t(cAn6YVMJ%$(!}HwIrE3c1rOA z@PH&zzB^)~+F_hYyO(1lxuGq*5LBm=LUD(8v9=%J=mD3jJ0^WDz0C$+l8p3Tt9rRV zzb>39mq4FjxgCdcVsITBQg{w>$eV(%djSfRRxdGH6 zxahm6)#b;{eqB|Wj7TX3v(#UCUB-ojFjUZlAeq9!+dXXc+Ako~B z>)d1xMptv|P<@9=SZp`K4X6n;iJttV&}O~+*j(q5XD01ShOK^m)J-Hc?;5pqX}#ka zIuInPs7!g0@E=d(?(xAjUt4DApz0{2GL_k-z9&nsTJhwCmJEv!1i{}DMB_m$npgXd87=Yrd}g*EGlpWcL+lCqiF^#&r~cNmSqQHs4O{M)uW$O>up35 z;(X8qcgYYUj8QVTkpIyeRDAah=O}$1d{+h9-woEGc4z$MHk?1V)Vy_^t_PZ; zo)=4)K!Bbjg9RO%c49!cIpq{sLR^8AmiEUqkz6EyStx&cA!B*ii9F$VgfLwika*#d zY2^Y5IslhnShxP{BeSH$iX?{mU@(gVQx5>x!UXxmfQ@G-kOW4ex*A5~4m4v=KCw^I0 znL3I@5T|FBuU4Y$qycJxsP}*%fs8VOU;#y-ciN(XV2JC>m1Rp z2JSh+;*DV+^egl~|Py`aUAe&uKb*wXven$UsRb*JvScjti3| z4o*(!>0xn^{|=P`QCFc27GU8Qi=vt! zPAP)#r+!E*qIyeUO^B{idqzFZ0vjRbiKb5o5#YD3m6cmEfpNbhh^y0p>`IeV6Wyj0 zA?&&<)pFS<}ZShFz#n z2&f-P$&rwn4MMuQ#mua{A*g{4gbY!ZQmq*FXTxE#$~{g3K?%;3*}c^)9a6GNc|Wje zFKq%Ax~5J&xyGca8A7tKVv=xKA!Yba(XCGfapS=6V}XzrGw55yVl0#K)sMharM>}} z_QNVNoFL}@wu-V30*&^3vuIcEGyy$iF2MJ)evjspQQ!oU6visZv9Zy8Tpn z#6FH@;1BnkDq;asfP(3nyNZ?0{WDdBc8987EaOt{AF0U&XduWQ@V<|6VdHUwEfZ<`5KmmH;Ju z42;y|4rJ;J84~A4m_^vmCQZ5e)e=u!Z>(V7>s1$pV@)dN#jkO=ZAiH}yR1WrCW#1o zt4vk9b2NjxRcK5uU;x{q^ofx_7p4yIe!d zQRQ&-@7sT|{d1#j?8k+%JTL&j!e{a|(tplmY@DqPj2%8t=YQ|CRj8`ltkEO^u$kWc;yQ}TIe#D{XVAizBr}QK>Fy;o zy42K#d0wDKN#v&2;yrYtj~9Wulh*xm&)$`;!Bc>!OGwEMS8iHfzLpk)U&)k3ZWT@n z@<0Ht>Vp>Qk7|zl+E~#G!Ib|J6{J8>v-nGnUbB3Oq~C>XfAj%oX{2jFAxQ1Zuw#r{ zM51z`;n0e36i0ajT0CLJi=d5F$i5xc=t2UmDdua+v(7)0%|b26#g_Bo`J z*N^C5;+qSR6K-p1o)k02oDLwz)9p9i+X_k&kZ>9Og(!`d5BMCV_2zw7@E? z7yMOT(A0rlJ1t3V{|zGi^zV);@RfGr7`X$9l8dL5snED77h@&8NHI^YupZvejys*5 zZx!TSA*fNi4Xk}G2f!4*7h!jWCKqugeXk>4PKQbRX)I%6CGxd;^;9do?_E+Z+2g@$ zwuq#+dzZ6j{LR-vGnCaKI-RbFyV%ohPudWlS;#=dhXIzL^vh^|`uTSmM8zmv@TJ02 z4WYxmSTx&)4<9&-!9Fwyma4wVvEd$8z0A=|CE1Og5{2 zF0XER4?4CFeC>RhzV-O4rnCK>3%r}kKMpdSNInX^3(40^>5pdHuZr>#Ufcm`9$GAZ zgJ8%mE%mBrdNQD0d6fXO4m$Fi4PA-Q4j4yur@LiZIoKKDl1Gtkt&w5oTVH(^T{SR+w6f_Mt?>H z^NduF`b;)Fah4haEurVQQ*nKBGp8-F5z>y`l{Y|6>i~yJ!~*OfQ-jYCEy(BBr%8&JDlCX-2H%FUd_<4mYfC9xy&?G$o9CC5T?(i(UfaT2k7O}=+zI0u0&-WGJ5b%E80Den48T`WF+WfJ`UrwKJaZH$r$J(_*b1+@(fCKSG z!ow?j0AMyDgaAY~ZRouKII%EKWJy9M?3y|`iy@kS9ixLo;%hXu`CIYlFcra|a0 z`&}Lp`5OnAr1EONy*@kF(ZT^n9!>{)N4}SQcZTgFNF~`J!Q2Y%@dVPA-dA8KjWT|) zBLeYdueYK%!944Sq%~CvA0iN$ei@8r&GbXLRPmy08(;{k>jehj#sOmRdO}@%ZS}Qq z(h%M%xsKIreDO7jNsdjIK|R$MHi(fqh3}5m`EkU}>*@*_``9Y=B+#5Em8|$wDV6M} zo~E^X@_JZ?-{ooNHaX{LMdr=@!|mv+@LuZ66W{+kfdK6D8KmoAZ1ra~BOnnG{;$t_ zJD*GCcap(hG_=1dE8;gKff+sv89}!Q$GzFsD2ZmRY=w_9#J&9pQ#N2X-eRR2ygdVb zo2x53t{fOmC%qhpJsh0^-EtDGub`5YVFTLX247@*ZZGuhp^2D00)?*fw}mlTCw>mR zgoEufCVFDmQPA>I9i{ALB&t8|)2kB3(`B%dgldh>kA!)|;EpJ3Zw=qg0jI_lEn|V2 zTUH93$<|oCuW{8^EI8rc^~4>9qdJsROeS+0fL0M=n)8B}1;99@nmF@LT>g+0ivq(e zfD1=s_@0im7JtSjr|(WZ^{mYRUeAqh=Jlq;?PPQlGR}=O$S&q|pC$Yf%hBwf2|s}z zkK;Xl0Q>*e`BA*D<^TdB~h?)z#Cvd=R;1NaaE>dt22^N7O1D=iD(%y^%>*b<#CPcJ}|Cz8`eky z3d$R^bdn9MGa@EbRjz+gbrCcMOq68#=cDV|SEr<(hz%S3l3LuDW+|a;6(v?RO^HE( z9qaRi4@t&R(RfzR*4XoD%-%ECv%oi0UmL1m`n~UAN-@AC-nY2somF=|DPnj$IuUbD zw_B>gLk%np1FiIukO)D&v3Jd7Abii@F$%Vs`l)FIxGN!hZiLWjr1LT-t-|%|$bbp! z=J^ltWJi$%8!3TeWLZ5n!BP(t+dDOG-r)b~rF)nJ3vEBE15-ZJSAJIqY6^XN=+C1_ zT_txrV@K`Z?@0}bZ8p91C?SuM-l4;m-!V9qltuK8jIA(_fH7V{$_LS(*-qD44mi#D+6DUZ)^5RXOp(}s&j@YixhwUp7m(C6szFcLorTS$2G+f z{bYapo5-anE>4)!Ob1dUqa#GV?{y4&>`|^KDS`nB=$mIKYl|+SjpP%&KUotx{&6nw zCU+=$({z8m_@-9*-e+nNAP@?`-{sDKTIkbA0Du4lpZ}e>{#Seb#o&LhQvB_O{%XG~ zr)Svz<8S^H`lna=jrmLH{r_P8wTAwG!hY9q|J8mA``!MlqWhnd{IjO}H}PNc$p0Jm z?+WifNBd{p@!#m4&!6`H2>nkL@}KZOD`fwMt9*Kqzv2H=FZ=(S^yfPIcl$l*8jydx zp#LA_w?q4@{f1z3{=;kiIoLlP+;8AtniBpe@c(jme{%XW72$7A`*{Bz!hiGl6aMG> z-QVy@mVd+l@wWFT{?7pIZ@diSzw!SF*Z#!+nSA{>zUK3F^Z!c4{uBOZVea4X`TyAL z?=s!L!2kP_`lt0`G5#(4KY4=x{GI=qDfqY4mVW~O&Ks1I0{z#jMf^-LNBA5X3eRuQ F{{gUsbC>`C literal 0 HcmV?d00001 diff --git a/doc/小米便签开源代码的质量分析报告.docx b/doc/小米便签开源代码的质量分析报告.docx new file mode 100644 index 0000000000000000000000000000000000000000..45f911a6901431655901987f1bdf9f2d53d75f8f GIT binary patch literal 13911 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 literal 0 HcmV?d00001 diff --git a/src/net/micode/notes/data/Contact.java b/src/net/micode/notes/data/Contact.java new file mode 100644 index 0000000..d97ac5d --- /dev/null +++ b/src/net/micode/notes/data/Contact.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + +import android.content.Context; +import android.database.Cursor; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Data; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import java.util.HashMap; + +public class Contact { + private static HashMap sContactCache; + private static final String TAG = "Contact"; + + private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER + + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" + + " AND " + Data.RAW_CONTACT_ID + " IN " + + "(SELECT raw_contact_id " + + " FROM phone_lookup" + + " WHERE min_match = '+')"; + + public static String getContact(Context context, String phoneNumber) { + if(sContactCache == null) { + sContactCache = new HashMap(); + } + + if(sContactCache.containsKey(phoneNumber)) { + return sContactCache.get(phoneNumber); + } + + String selection = CALLER_ID_SELECTION.replace("+", + PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); + Cursor cursor = context.getContentResolver().query( + Data.CONTENT_URI, + new String [] { Phone.DISPLAY_NAME }, + selection, + new String[] { phoneNumber }, + null); + + if (cursor != null && cursor.moveToFirst()) { + try { + String name = cursor.getString(0); + sContactCache.put(phoneNumber, name); + return name; + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, " Cursor get string error " + e.toString()); + return null; + } finally { + cursor.close(); + } + } else { + Log.d(TAG, "No contact matched with number:" + phoneNumber); + return null; + } + } +} diff --git a/src/net/micode/notes/data/Notes.java b/src/net/micode/notes/data/Notes.java new file mode 100644 index 0000000..f240604 --- /dev/null +++ b/src/net/micode/notes/data/Notes.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + +import android.net.Uri; +public class Notes { + public static final String AUTHORITY = "micode_notes"; + public static final String TAG = "Notes"; + public static final int TYPE_NOTE = 0; + public static final int TYPE_FOLDER = 1; + public static final int TYPE_SYSTEM = 2; + + /** + * Following IDs are system folders' identifiers + * {@link Notes#ID_ROOT_FOLDER } is default folder + * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder + * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records + */ + public static final int ID_ROOT_FOLDER = 0; + public static final int ID_TEMPARAY_FOLDER = -1; + public static final int ID_CALL_RECORD_FOLDER = -2; + public static final int ID_TRASH_FOLER = -3; + + public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; + public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; + public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; + public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type"; + public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; + public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; + + public static final int TYPE_WIDGET_INVALIDE = -1; + public static final int TYPE_WIDGET_2X = 0; + public static final int TYPE_WIDGET_4X = 1; + + public static class DataConstants { + public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; + public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; + } + + /** + * Uri to query all notes and folders + */ + public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); + + /** + * Uri to query data + */ + public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + + public interface NoteColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * The parent's id for note or folder + *

Type: INTEGER (long)

+ */ + public static final String PARENT_ID = "parent_id"; + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + + /** + * Alert date + *

Type: INTEGER (long)

+ */ + public static final String ALERTED_DATE = "alert_date"; + + /** + * Folder's name or text content of note + *

Type: TEXT

+ */ + public static final String SNIPPET = "snippet"; + + /** + * Note's widget id + *

Type: INTEGER (long)

+ */ + public static final String WIDGET_ID = "widget_id"; + + /** + * Note's widget type + *

Type: INTEGER (long)

+ */ + public static final String WIDGET_TYPE = "widget_type"; + + /** + * Note's background color's id + *

Type: INTEGER (long)

+ */ + public static final String BG_COLOR_ID = "bg_color_id"; + + /** + * For text note, it doesn't has attachment, for multi-media + * note, it has at least one attachment + *

Type: INTEGER

+ */ + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * Folder's count of notes + *

Type: INTEGER (long)

+ */ + public static final String NOTES_COUNT = "notes_count"; + + /** + * The file type: folder or note + *

Type: INTEGER

+ */ + public static final String TYPE = "type"; + + /** + * The last sync id + *

Type: INTEGER (long)

+ */ + public static final String SYNC_ID = "sync_id"; + + /** + * Sign to indicate local modified or not + *

Type: INTEGER

+ */ + public static final String LOCAL_MODIFIED = "local_modified"; + + /** + * Original parent id before moving into temporary folder + *

Type : INTEGER

+ */ + public static final String ORIGIN_PARENT_ID = "origin_parent_id"; + + /** + * The gtask id + *

Type : TEXT

+ */ + public static final String GTASK_ID = "gtask_id"; + + /** + * The version code + *

Type : INTEGER (long)

+ */ + public static final String VERSION = "version"; + } + + public interface DataColumns { + /** + * The unique ID for a row + *

Type: INTEGER (long)

+ */ + public static final String ID = "_id"; + + /** + * The MIME type of the item represented by this row. + *

Type: Text

+ */ + public static final String MIME_TYPE = "mime_type"; + + /** + * The reference id to note that this data belongs to + *

Type: INTEGER (long)

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * Created data for note or folder + *

Type: INTEGER (long)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * Latest modified date + *

Type: INTEGER (long)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * Data's content + *

Type: TEXT

+ */ + public static final String CONTENT = "content"; + + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + public static final String DATA1 = "data1"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * integer data type + *

Type: INTEGER

+ */ + public static final String DATA2 = "data2"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + public static final String DATA3 = "data3"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + public static final String DATA4 = "data4"; + + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific, used for + * TEXT data type + *

Type: TEXT

+ */ + public static final String DATA5 = "data5"; + } + + public static final class TextNote implements DataColumns { + /** + * Mode to indicate the text in check list mode or not + *

Type: Integer 1:check list mode 0: normal mode

+ */ + public static final String MODE = DATA1; + + public static final int MODE_CHECK_LIST = 1; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); + } + + public static final class CallNote implements DataColumns { + /** + * Call date for this record + *

Type: INTEGER (long)

+ */ + public static final String CALL_DATE = DATA1; + + /** + * Phone number for this record + *

Type: TEXT

+ */ + public static final String PHONE_NUMBER = DATA3; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); + } +} diff --git a/src/net/micode/notes/data/NotesDatabaseHelper.java b/src/net/micode/notes/data/NotesDatabaseHelper.java new file mode 100644 index 0000000..ffe5d57 --- /dev/null +++ b/src/net/micode/notes/data/NotesDatabaseHelper.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + + +public class NotesDatabaseHelper extends SQLiteOpenHelper { + private static final String DB_NAME = "note.db"; + + private static final int DB_VERSION = 4; + + public interface TABLE { + public static final String NOTE = "note"; + + public static final String DATA = "data"; + } + + private static final String TAG = "NotesDatabaseHelper"; + + private static NotesDatabaseHelper mInstance; + + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + ")"; + + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + + DataColumns.MIME_TYPE + " TEXT NOT NULL," + + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA1 + " INTEGER," + + DataColumns.DATA2 + " INTEGER," + + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + + ")"; + + private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + /** + * Increase folder's note count when move note to the folder + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_update "+ + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * Decrease folder's note count when move note from folder + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; + + /** + * Increase folder's note count when insert new note to the folder + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * Decrease folder's note count when delete note from the folder + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; + + /** + * Update note's content when insert data with type {@link DataConstants#NOTE} + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = + "CREATE TRIGGER update_note_content_on_insert " + + " AFTER INSERT ON " + TABLE.DATA + + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * Update note's content when data with {@link DataConstants#NOTE} type has changed + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER update_note_content_on_update " + + " AFTER UPDATE ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * Update note's content when data with {@link DataConstants#NOTE} type has deleted + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = + "CREATE TRIGGER update_note_content_on_delete " + + " AFTER delete ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * Delete datas belong to note which has been deleted + */ + private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = + "CREATE TRIGGER delete_data_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.DATA + + " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * Delete notes belong to folder which has been deleted + */ + private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = + "CREATE TRIGGER folder_delete_notes_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * Move notes belong to folder which has been moved to trash folder + */ + private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = + "CREATE TRIGGER folder_move_notes_on_trash " + + " AFTER UPDATE ON " + TABLE.NOTE + + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); + reCreateNoteTableTriggers(db); + createSystemFolder(db); + Log.d(TAG, "note table has been created"); + } + + private void reCreateNoteTableTriggers(SQLiteDatabase db) { + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash"); + + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); + db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); + db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); + } + + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + + /** + * call record foler for call notes + */ + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * root folder which is default folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * temporary folder which is used for moving note + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * create trash folder + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + public void createDataTable(SQLiteDatabase db) { + db.execSQL(CREATE_DATA_TABLE_SQL); + reCreateDataTableTriggers(db); + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); + Log.d(TAG, "data table has been created"); + } + + private void reCreateDataTableTriggers(SQLiteDatabase db) { + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete"); + + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + } + + static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + @Override + public void onCreate(SQLiteDatabase db) { + createNoteTable(db); + createDataTable(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; + boolean skipV2 = false; + + if (oldVersion == 1) { + upgradeToV2(db); + skipV2 = true; // this upgrade including the upgrade from v2 to v3 + oldVersion++; + } + + if (oldVersion == 2 && !skipV2) { + upgradeToV3(db); + reCreateTriggers = true; + oldVersion++; + } + + if (oldVersion == 3) { + upgradeToV4(db); + oldVersion++; + } + + if (reCreateTriggers) { + reCreateNoteTableTriggers(db); + reCreateDataTableTriggers(db); + } + + if (oldVersion != newVersion) { + throw new IllegalStateException("Upgrade notes database to version " + newVersion + + "fails"); + } + } + + private void upgradeToV2(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); + db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); + createNoteTable(db); + createDataTable(db); + } + + private void upgradeToV3(SQLiteDatabase db) { + // drop unused triggers + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update"); + // add a column for gtask id + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + + " TEXT NOT NULL DEFAULT ''"); + // add a trash system folder + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + private void upgradeToV4(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + + " INTEGER NOT NULL DEFAULT 0"); + } +} diff --git a/src/net/micode/notes/data/NotesProvider.java b/src/net/micode/notes/data/NotesProvider.java new file mode 100644 index 0000000..edb0a60 --- /dev/null +++ b/src/net/micode/notes/data/NotesProvider.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.data; + + +import android.app.SearchManager; +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.NotesDatabaseHelper.TABLE; + + +public class NotesProvider extends ContentProvider { + private static final UriMatcher mMatcher; + + private NotesDatabaseHelper mHelper; + + private static final String TAG = "NotesProvider"; + + private static final int URI_NOTE = 1; + private static final int URI_NOTE_ITEM = 2; + private static final int URI_DATA = 3; + private static final int URI_DATA_ITEM = 4; + + private static final int URI_SEARCH = 5; + private static final int URI_SEARCH_SUGGEST = 6; + + static { + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); + mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); + mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); + mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); + mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); + } + + /** + * x'0A' represents the '\n' character in sqlite. For title and content in the search result, + * we will trim '\n' and white space in order to show more information. + */ + private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," + + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," + + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," + + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + + private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION + + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.SNIPPET + " LIKE ?" + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + + @Override + public boolean onCreate() { + mHelper = NotesDatabaseHelper.getInstance(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + Cursor c = null; + SQLiteDatabase db = mHelper.getReadableDatabase(); + String id = null; + switch (mMatcher.match(uri)) { + case URI_NOTE: + c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); + c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_DATA: + c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_SEARCH: + case URI_SEARCH_SUGGEST: + if (sortOrder != null || projection != null) { + throw new IllegalArgumentException( + "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); + } + + String searchString = null; + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); + } + } else { + searchString = uri.getQueryParameter("pattern"); + } + + if (TextUtils.isEmpty(searchString)) { + return null; + } + + try { + searchString = String.format("%%%s%%", searchString); + c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, + new String[] { searchString }); + } catch (IllegalStateException ex) { + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + if (c != null) { + c.setNotificationUri(getContext().getContentResolver(), uri); + } + return c; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + SQLiteDatabase db = mHelper.getWritableDatabase(); + long dataId = 0, noteId = 0, insertedId = 0; + switch (mMatcher.match(uri)) { + case URI_NOTE: + insertedId = noteId = db.insert(TABLE.NOTE, null, values); + break; + case URI_DATA: + if (values.containsKey(DataColumns.NOTE_ID)) { + noteId = values.getAsLong(DataColumns.NOTE_ID); + } else { + Log.d(TAG, "Wrong data format without note id:" + values.toString()); + } + insertedId = dataId = db.insert(TABLE.DATA, null, values); + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + // Notify the note uri + if (noteId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + + // Notify the data uri + if (dataId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + + return ContentUris.withAppendedId(uri, insertedId); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean deleteData = false; + switch (mMatcher.match(uri)) { + case URI_NOTE: + selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; + count = db.delete(TABLE.NOTE, selection, selectionArgs); + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); + /** + * ID that smaller than 0 is system folder which is not allowed to + * trash + */ + long noteId = Long.valueOf(id); + if (noteId <= 0) { + break; + } + count = db.delete(TABLE.NOTE, + NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + break; + case URI_DATA: + count = db.delete(TABLE.DATA, selection, selectionArgs); + deleteData = true; + break; + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); + count = db.delete(TABLE.DATA, + DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + deleteData = true; + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + if (count > 0) { + if (deleteData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean updateData = false; + switch (mMatcher.match(uri)) { + case URI_NOTE: + increaseNoteVersion(-1, selection, selectionArgs); + count = db.update(TABLE.NOTE, values, selection, selectionArgs); + break; + case URI_NOTE_ITEM: + id = uri.getPathSegments().get(1); + increaseNoteVersion(Long.valueOf(id), selection, selectionArgs); + count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + break; + case URI_DATA: + count = db.update(TABLE.DATA, values, selection, selectionArgs); + updateData = true; + break; + case URI_DATA_ITEM: + id = uri.getPathSegments().get(1); + count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + updateData = true; + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + if (count > 0) { + if (updateData) { + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + getContext().getContentResolver().notifyChange(uri, null); + } + return count; + } + + private String parseSelection(String selection) { + return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); + } + + private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + StringBuilder sql = new StringBuilder(120); + sql.append("UPDATE "); + sql.append(TABLE.NOTE); + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 "); + + if (id > 0 || !TextUtils.isEmpty(selection)) { + sql.append(" WHERE "); + } + if (id > 0) { + sql.append(NoteColumns.ID + "=" + String.valueOf(id)); + } + if (!TextUtils.isEmpty(selection)) { + String selectString = id > 0 ? parseSelection(selection) : selection; + for (String args : selectionArgs) { + selectString = selectString.replaceFirst("\\?", args); + } + sql.append(selectString); + } + + mHelper.getWritableDatabase().execSQL(sql.toString()); + } + + @Override + public String getType(Uri uri) { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/src/net/micode/notes/gtask/data/MetaData.java b/src/net/micode/notes/gtask/data/MetaData.java new file mode 100644 index 0000000..3a2050b --- /dev/null +++ b/src/net/micode/notes/gtask/data/MetaData.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + + +public class MetaData extends Task { + private final static String TAG = MetaData.class.getSimpleName(); + + private String mRelatedGid = null; + + public void setMeta(String gid, JSONObject metaInfo) { + try { + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); + } catch (JSONException e) { + Log.e(TAG, "failed to put related gid"); + } + setNotes(metaInfo.toString()); + setName(GTaskStringUtils.META_NOTE_NAME); + } + + public String getRelatedGid() { + return mRelatedGid; + } + + @Override + public boolean isWorthSaving() { + return getNotes() != null; + } + + @Override + public void setContentByRemoteJSON(JSONObject js) { + super.setContentByRemoteJSON(js); + if (getNotes() != null) { + try { + JSONObject metaInfo = new JSONObject(getNotes().trim()); + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); + } catch (JSONException e) { + Log.w(TAG, "failed to get related gid"); + mRelatedGid = null; + } + } + } + + @Override + public void setContentByLocalJSON(JSONObject js) { + // this function should not be called + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } + + @Override + public JSONObject getLocalJSONFromContent() { + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } + + @Override + public int getSyncAction(Cursor c) { + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } + +} diff --git a/src/net/micode/notes/gtask/data/Node.java b/src/net/micode/notes/gtask/data/Node.java new file mode 100644 index 0000000..63950e0 --- /dev/null +++ b/src/net/micode/notes/gtask/data/Node.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; + +import org.json.JSONObject; + +public abstract class Node { + public static final int SYNC_ACTION_NONE = 0; + + public static final int SYNC_ACTION_ADD_REMOTE = 1; + + public static final int SYNC_ACTION_ADD_LOCAL = 2; + + public static final int SYNC_ACTION_DEL_REMOTE = 3; + + public static final int SYNC_ACTION_DEL_LOCAL = 4; + + public static final int SYNC_ACTION_UPDATE_REMOTE = 5; + + public static final int SYNC_ACTION_UPDATE_LOCAL = 6; + + public static final int SYNC_ACTION_UPDATE_CONFLICT = 7; + + public static final int SYNC_ACTION_ERROR = 8; + + private String mGid; + + private String mName; + + private long mLastModified; + + private boolean mDeleted; + + public Node() { + mGid = null; + mName = ""; + mLastModified = 0; + mDeleted = false; + } + + public abstract JSONObject getCreateAction(int actionId); + + public abstract JSONObject getUpdateAction(int actionId); + + public abstract void setContentByRemoteJSON(JSONObject js); + + public abstract void setContentByLocalJSON(JSONObject js); + + public abstract JSONObject getLocalJSONFromContent(); + + public abstract int getSyncAction(Cursor c); + + public void setGid(String gid) { + this.mGid = gid; + } + + public void setName(String name) { + this.mName = name; + } + + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } + + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } + + public String getGid() { + return this.mGid; + } + + public String getName() { + return this.mName; + } + + public long getLastModified() { + return this.mLastModified; + } + + public boolean getDeleted() { + return this.mDeleted; + } + +} diff --git a/src/net/micode/notes/gtask/data/SqlData.java b/src/net/micode/notes/gtask/data/SqlData.java new file mode 100644 index 0000000..d3ec3be --- /dev/null +++ b/src/net/micode/notes/gtask/data/SqlData.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.NotesDatabaseHelper.TABLE; +import net.micode.notes.gtask.exception.ActionFailureException; + +import org.json.JSONException; +import org.json.JSONObject; + + +public class SqlData { + private static final String TAG = SqlData.class.getSimpleName(); + + private static final int INVALID_ID = -99999; + + public static final String[] PROJECTION_DATA = new String[] { + DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, + DataColumns.DATA3 + }; + + public static final int DATA_ID_COLUMN = 0; + + public static final int DATA_MIME_TYPE_COLUMN = 1; + + public static final int DATA_CONTENT_COLUMN = 2; + + public static final int DATA_CONTENT_DATA_1_COLUMN = 3; + + public static final int DATA_CONTENT_DATA_3_COLUMN = 4; + + private ContentResolver mContentResolver; + + private boolean mIsCreate; + + private long mDataId; + + private String mDataMimeType; + + private String mDataContent; + + private long mDataContentData1; + + private String mDataContentData3; + + private ContentValues mDiffDataValues; + + public SqlData(Context context) { + mContentResolver = context.getContentResolver(); + mIsCreate = true; + mDataId = INVALID_ID; + mDataMimeType = DataConstants.NOTE; + mDataContent = ""; + mDataContentData1 = 0; + mDataContentData3 = ""; + mDiffDataValues = new ContentValues(); + } + + public SqlData(Context context, Cursor c) { + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(c); + mDiffDataValues = new ContentValues(); + } + + private void loadFromCursor(Cursor c) { + mDataId = c.getLong(DATA_ID_COLUMN); + mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); + mDataContent = c.getString(DATA_CONTENT_COLUMN); + mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); + mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); + } + + public void setContent(JSONObject js) throws JSONException { + long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; + if (mIsCreate || mDataId != dataId) { + mDiffDataValues.put(DataColumns.ID, dataId); + } + mDataId = dataId; + + String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE) + : DataConstants.NOTE; + if (mIsCreate || !mDataMimeType.equals(dataMimeType)) { + mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType); + } + mDataMimeType = dataMimeType; + + String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; + if (mIsCreate || !mDataContent.equals(dataContent)) { + mDiffDataValues.put(DataColumns.CONTENT, dataContent); + } + mDataContent = dataContent; + + long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; + if (mIsCreate || mDataContentData1 != dataContentData1) { + mDiffDataValues.put(DataColumns.DATA1, dataContentData1); + } + mDataContentData1 = dataContentData1; + + String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; + if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { + mDiffDataValues.put(DataColumns.DATA3, dataContentData3); + } + mDataContentData3 = dataContentData3; + } + + public JSONObject getContent() throws JSONException { + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + JSONObject js = new JSONObject(); + js.put(DataColumns.ID, mDataId); + js.put(DataColumns.MIME_TYPE, mDataMimeType); + js.put(DataColumns.CONTENT, mDataContent); + js.put(DataColumns.DATA1, mDataContentData1); + js.put(DataColumns.DATA3, mDataContentData3); + return js; + } + + public void commit(long noteId, boolean validateVersion, long version) { + + if (mIsCreate) { + if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { + mDiffDataValues.remove(DataColumns.ID); + } + + mDiffDataValues.put(DataColumns.NOTE_ID, noteId); + Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); + try { + mDataId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + } else { + if (mDiffDataValues.size() > 0) { + int result = 0; + if (!validateVersion) { + result = mContentResolver.update(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null); + } else { + result = mContentResolver.update(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, + " ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.VERSION + "=?)", new String[] { + String.valueOf(noteId), String.valueOf(version) + }); + } + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); + } + } + } + + mDiffDataValues.clear(); + mIsCreate = false; + } + + public long getId() { + return mDataId; + } +} diff --git a/src/net/micode/notes/gtask/data/SqlNote.java b/src/net/micode/notes/gtask/data/SqlNote.java new file mode 100644 index 0000000..79a4095 --- /dev/null +++ b/src/net/micode/notes/gtask/data/SqlNote.java @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.appwidget.AppWidgetManager; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.tool.ResourceParser; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + + +public class SqlNote { + private static final String TAG = SqlNote.class.getSimpleName(); + + private static final int INVALID_ID = -99999; + + public static final String[] PROJECTION_NOTE = new String[] { + NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE, + NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID, + NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID, + NoteColumns.VERSION + }; + + public static final int ID_COLUMN = 0; + + public static final int ALERTED_DATE_COLUMN = 1; + + public static final int BG_COLOR_ID_COLUMN = 2; + + public static final int CREATED_DATE_COLUMN = 3; + + public static final int HAS_ATTACHMENT_COLUMN = 4; + + public static final int MODIFIED_DATE_COLUMN = 5; + + public static final int NOTES_COUNT_COLUMN = 6; + + public static final int PARENT_ID_COLUMN = 7; + + public static final int SNIPPET_COLUMN = 8; + + public static final int TYPE_COLUMN = 9; + + public static final int WIDGET_ID_COLUMN = 10; + + public static final int WIDGET_TYPE_COLUMN = 11; + + public static final int SYNC_ID_COLUMN = 12; + + public static final int LOCAL_MODIFIED_COLUMN = 13; + + public static final int ORIGIN_PARENT_ID_COLUMN = 14; + + public static final int GTASK_ID_COLUMN = 15; + + public static final int VERSION_COLUMN = 16; + + private Context mContext; + + private ContentResolver mContentResolver; + + private boolean mIsCreate; + + private long mId; + + private long mAlertDate; + + private int mBgColorId; + + private long mCreatedDate; + + private int mHasAttachment; + + private long mModifiedDate; + + private long mParentId; + + private String mSnippet; + + private int mType; + + private int mWidgetId; + + private int mWidgetType; + + private long mOriginParent; + + private long mVersion; + + private ContentValues mDiffNoteValues; + + private ArrayList mDataList; + + public SqlNote(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = true; + mId = INVALID_ID; + mAlertDate = 0; + mBgColorId = ResourceParser.getDefaultBgId(context); + mCreatedDate = System.currentTimeMillis(); + mHasAttachment = 0; + mModifiedDate = System.currentTimeMillis(); + mParentId = 0; + mSnippet = ""; + mType = Notes.TYPE_NOTE; + mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + mOriginParent = 0; + mVersion = 0; + mDiffNoteValues = new ContentValues(); + mDataList = new ArrayList(); + } + + public SqlNote(Context context, Cursor c) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(c); + mDataList = new ArrayList(); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + mDiffNoteValues = new ContentValues(); + } + + public SqlNote(Context context, long id) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(id); + mDataList = new ArrayList(); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + mDiffNoteValues = new ContentValues(); + + } + + private void loadFromCursor(long id) { + Cursor c = null; + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(id) + }, null); + if (c != null) { + c.moveToNext(); + loadFromCursor(c); + } else { + Log.w(TAG, "loadFromCursor: cursor = null"); + } + } finally { + if (c != null) + c.close(); + } + } + + private void loadFromCursor(Cursor c) { + mId = c.getLong(ID_COLUMN); + mAlertDate = c.getLong(ALERTED_DATE_COLUMN); + mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = c.getLong(CREATED_DATE_COLUMN); + mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); + mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); + mParentId = c.getLong(PARENT_ID_COLUMN); + mSnippet = c.getString(SNIPPET_COLUMN); + mType = c.getInt(TYPE_COLUMN); + mWidgetId = c.getInt(WIDGET_ID_COLUMN); + mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); + mVersion = c.getLong(VERSION_COLUMN); + } + + private void loadDataContent() { + Cursor c = null; + mDataList.clear(); + try { + c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, + "(note_id=?)", new String[] { + String.valueOf(mId) + }, null); + if (c != null) { + if (c.getCount() == 0) { + Log.w(TAG, "it seems that the note has not data"); + return; + } + while (c.moveToNext()) { + SqlData data = new SqlData(mContext, c); + mDataList.add(data); + } + } else { + Log.w(TAG, "loadDataContent: cursor = null"); + } + } finally { + if (c != null) + c.close(); + } + } + + public boolean setContent(JSONObject js) { + try { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + Log.w(TAG, "cannot set system folder"); + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // for folder we can only update the snnipet and type + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) { + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; + if (mIsCreate || mId != id) { + mDiffNoteValues.put(NoteColumns.ID, id); + } + mId = id; + + long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note + .getLong(NoteColumns.ALERTED_DATE) : 0; + if (mIsCreate || mAlertDate != alertDate) { + mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); + } + mAlertDate = alertDate; + + int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note + .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); + if (mIsCreate || mBgColorId != bgColorId) { + mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); + } + mBgColorId = bgColorId; + + long createDate = note.has(NoteColumns.CREATED_DATE) ? note + .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); + if (mIsCreate || mCreatedDate != createDate) { + mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); + } + mCreatedDate = createDate; + + int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note + .getInt(NoteColumns.HAS_ATTACHMENT) : 0; + if (mIsCreate || mHasAttachment != hasAttachment) { + mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); + } + mHasAttachment = hasAttachment; + + long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note + .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); + if (mIsCreate || mModifiedDate != modifiedDate) { + mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); + } + mModifiedDate = modifiedDate; + + long parentId = note.has(NoteColumns.PARENT_ID) ? note + .getLong(NoteColumns.PARENT_ID) : 0; + if (mIsCreate || mParentId != parentId) { + mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); + } + mParentId = parentId; + + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + + int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) + : AppWidgetManager.INVALID_APPWIDGET_ID; + if (mIsCreate || mWidgetId != widgetId) { + mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); + } + mWidgetId = widgetId; + + int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note + .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; + if (mIsCreate || mWidgetType != widgetType) { + mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); + } + mWidgetType = widgetType; + + long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note + .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; + if (mIsCreate || mOriginParent != originParent) { + mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); + } + mOriginParent = originParent; + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + SqlData sqlData = null; + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + for (SqlData temp : mDataList) { + if (dataId == temp.getId()) { + sqlData = temp; + } + } + } + + if (sqlData == null) { + sqlData = new SqlData(mContext); + mDataList.add(sqlData); + } + + sqlData.setContent(data); + } + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } + return true; + } + + public JSONObject getContent() { + try { + JSONObject js = new JSONObject(); + + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + + JSONObject note = new JSONObject(); + if (mType == Notes.TYPE_NOTE) { + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.ALERTED_DATE, mAlertDate); + note.put(NoteColumns.BG_COLOR_ID, mBgColorId); + note.put(NoteColumns.CREATED_DATE, mCreatedDate); + note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment); + note.put(NoteColumns.MODIFIED_DATE, mModifiedDate); + note.put(NoteColumns.PARENT_ID, mParentId); + note.put(NoteColumns.SNIPPET, mSnippet); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.WIDGET_ID, mWidgetId); + note.put(NoteColumns.WIDGET_TYPE, mWidgetType); + note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + + JSONArray dataArray = new JSONArray(); + for (SqlData sqlData : mDataList) { + JSONObject data = sqlData.getContent(); + if (data != null) { + dataArray.put(data); + } + } + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.SNIPPET, mSnippet); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + } + + return js; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + return null; + } + + public void setParentId(long id) { + mParentId = id; + mDiffNoteValues.put(NoteColumns.PARENT_ID, id); + } + + public void setGtaskId(String gid) { + mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); + } + + public void setSyncId(long syncId) { + mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); + } + + public void resetLocalModified() { + mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); + } + + public long getId() { + return mId; + } + + public long getParentId() { + return mParentId; + } + + public String getSnippet() { + return mSnippet; + } + + public boolean isNoteType() { + return mType == Notes.TYPE_NOTE; + } + + public void commit(boolean validateVersion) { + if (mIsCreate) { + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + mDiffNoteValues.remove(NoteColumns.ID); + } + + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); + try { + mId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + if (mId == 0) { + throw new IllegalStateException("Create thread id failed"); + } + + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, false, -1); + } + } + } else { + if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { + Log.e(TAG, "No such note"); + throw new IllegalStateException("Try to update note with invalid id"); + } + if (mDiffNoteValues.size() > 0) { + mVersion ++; + int result = 0; + if (!validateVersion) { + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?)", new String[] { + String.valueOf(mId) + }); + } else { + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[] { + String.valueOf(mId), String.valueOf(mVersion) + }); + } + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); + } + } + + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); + } + } + } + + // refresh local info + loadFromCursor(mId); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + + mDiffNoteValues.clear(); + mIsCreate = false; + } +} diff --git a/src/net/micode/notes/gtask/data/Task.java b/src/net/micode/notes/gtask/data/Task.java new file mode 100644 index 0000000..6a19454 --- /dev/null +++ b/src/net/micode/notes/gtask/data/Task.java @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +public class Task extends Node { + private static final String TAG = Task.class.getSimpleName(); + + private boolean mCompleted; + + private String mNotes; + + private JSONObject mMetaInfo; + + private Task mPriorSibling; + + private TaskList mParent; + + public Task() { + super(); + mCompleted = false; + mNotes = null; + mPriorSibling = null; + mParent = null; + mMetaInfo = null; + } + + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // action_id + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // index + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); + + // entity_delta + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_TASK); + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + // parent_id + js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); + + // dest_parent_type + js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + + // list_id + js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); + + // prior_sibling_id + if (mPriorSibling != null) { + js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate task-create jsonobject"); + } + + return js; + } + + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // action_id + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // id + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // entity_delta + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate task-update jsonobject"); + } + + return js; + } + + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + // id + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // last_modified + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // name + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + // notes + if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { + setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); + } + + // deleted + if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { + setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); + } + + // completed + if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { + setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to get task content from jsonobject"); + } + } + } + + public void setContentByLocalJSON(JSONObject js) { + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) + || !js.has(GTaskStringUtils.META_HEAD_DATA)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + try { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { + Log.e(TAG, "invalid type"); + return; + } + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + setName(data.getString(DataColumns.CONTENT)); + break; + } + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } + + public JSONObject getLocalJSONFromContent() { + String name = getName(); + try { + if (mMetaInfo == null) { + // new task created from web + if (name == null) { + Log.w(TAG, "the note seems to be an empty one"); + return null; + } + + JSONObject js = new JSONObject(); + JSONObject note = new JSONObject(); + JSONArray dataArray = new JSONArray(); + JSONObject data = new JSONObject(); + data.put(DataColumns.CONTENT, name); + dataArray.put(data); + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + return js; + } else { + // synced task + JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + data.put(DataColumns.CONTENT, getName()); + break; + } + } + + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + return mMetaInfo; + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } + + public void setMetaInfo(MetaData metaData) { + if (metaData != null && metaData.getNotes() != null) { + try { + mMetaInfo = new JSONObject(metaData.getNotes()); + } catch (JSONException e) { + Log.w(TAG, e.toString()); + mMetaInfo = null; + } + } + } + + public int getSyncAction(Cursor c) { + try { + JSONObject noteInfo = null; + if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) { + noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + } + + if (noteInfo == null) { + Log.w(TAG, "it seems that note meta has been deleted"); + return SYNC_ACTION_UPDATE_REMOTE; + } + + if (!noteInfo.has(NoteColumns.ID)) { + Log.w(TAG, "remote note id seems to be deleted"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + // validate the note id now + if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { + Log.w(TAG, "note id doesn't match"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // there is no local update + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // no update both side + return SYNC_ACTION_NONE; + } else { + // apply remote to local + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // validate gtask id + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + return SYNC_ACTION_ERROR; + } + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // local modification only + return SYNC_ACTION_UPDATE_REMOTE; + } else { + return SYNC_ACTION_UPDATE_CONFLICT; + } + } + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; + } + + public boolean isWorthSaving() { + return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) + || (getNotes() != null && getNotes().trim().length() > 0); + } + + public void setCompleted(boolean completed) { + this.mCompleted = completed; + } + + public void setNotes(String notes) { + this.mNotes = notes; + } + + public void setPriorSibling(Task priorSibling) { + this.mPriorSibling = priorSibling; + } + + public void setParent(TaskList parent) { + this.mParent = parent; + } + + public boolean getCompleted() { + return this.mCompleted; + } + + public String getNotes() { + return this.mNotes; + } + + public Task getPriorSibling() { + return this.mPriorSibling; + } + + public TaskList getParent() { + return this.mParent; + } + +} diff --git a/src/net/micode/notes/gtask/data/TaskList.java b/src/net/micode/notes/gtask/data/TaskList.java new file mode 100644 index 0000000..4ea21c5 --- /dev/null +++ b/src/net/micode/notes/gtask/data/TaskList.java @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + + +public class TaskList extends Node { + private static final String TAG = TaskList.class.getSimpleName(); + + private int mIndex; + + private ArrayList mChildren; + + public TaskList() { + super(); + mChildren = new ArrayList(); + mIndex = 1; + } + + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // action_id + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // index + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); + + // entity_delta + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-create jsonobject"); + } + + return js; + } + + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // action_id + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // id + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // entity_delta + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-update jsonobject"); + } + + return js; + } + + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + // id + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // last_modified + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // name + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to get tasklist content from jsonobject"); + } + } + } + + public void setContentByLocalJSON(JSONObject js) { + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + try { + JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + + if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + String name = folder.getString(NoteColumns.SNIPPET); + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); + } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT); + else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_CALL_NOTE); + else + Log.e(TAG, "invalid system folder"); + } else { + Log.e(TAG, "error type"); + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } + + public JSONObject getLocalJSONFromContent() { + try { + JSONObject js = new JSONObject(); + JSONObject folder = new JSONObject(); + + String folderName = getName(); + if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) + folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), + folderName.length()); + folder.put(NoteColumns.SNIPPET, folderName); + if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) + || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) + folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + else + folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + + js.put(GTaskStringUtils.META_HEAD_NOTE, folder); + + return js; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } + + public int getSyncAction(Cursor c) { + try { + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // there is no local update + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // no update both side + return SYNC_ACTION_NONE; + } else { + // apply remote to local + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // validate gtask id + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + return SYNC_ACTION_ERROR; + } + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // local modification only + return SYNC_ACTION_UPDATE_REMOTE; + } else { + // for folder conflicts, just apply local modification + return SYNC_ACTION_UPDATE_REMOTE; + } + } + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; + } + + public int getChildTaskCount() { + return mChildren.size(); + } + + public boolean addChildTask(Task task) { + boolean ret = false; + if (task != null && !mChildren.contains(task)) { + ret = mChildren.add(task); + if (ret) { + // need to set prior sibling and parent + task.setPriorSibling(mChildren.isEmpty() ? null : mChildren + .get(mChildren.size() - 1)); + task.setParent(this); + } + } + return ret; + } + + public boolean addChildTask(Task task, int index) { + if (index < 0 || index > mChildren.size()) { + Log.e(TAG, "add child task: invalid index"); + return false; + } + + int pos = mChildren.indexOf(task); + if (task != null && pos == -1) { + mChildren.add(index, task); + + // update the task list + Task preTask = null; + Task afterTask = null; + if (index != 0) + preTask = mChildren.get(index - 1); + if (index != mChildren.size() - 1) + afterTask = mChildren.get(index + 1); + + task.setPriorSibling(preTask); + if (afterTask != null) + afterTask.setPriorSibling(task); + } + + return true; + } + + public boolean removeChildTask(Task task) { + boolean ret = false; + int index = mChildren.indexOf(task); + if (index != -1) { + ret = mChildren.remove(task); + + if (ret) { + // reset prior sibling and parent + task.setPriorSibling(null); + task.setParent(null); + + // update the task list + if (index != mChildren.size()) { + mChildren.get(index).setPriorSibling( + index == 0 ? null : mChildren.get(index - 1)); + } + } + } + return ret; + } + + public boolean moveChildTask(Task task, int index) { + + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "move child task: invalid index"); + return false; + } + + int pos = mChildren.indexOf(task); + if (pos == -1) { + Log.e(TAG, "move child task: the task should in the list"); + return false; + } + + if (pos == index) + return true; + return (removeChildTask(task) && addChildTask(task, index)); + } + + public Task findChildTaskByGid(String gid) { + for (int i = 0; i < mChildren.size(); i++) { + Task t = mChildren.get(i); + if (t.getGid().equals(gid)) { + return t; + } + } + return null; + } + + public int getChildTaskIndex(Task task) { + return mChildren.indexOf(task); + } + + public Task getChildTaskByIndex(int index) { + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "getTaskByIndex: invalid index"); + return null; + } + return mChildren.get(index); + } + + public Task getChilTaskByGid(String gid) { + for (Task task : mChildren) { + if (task.getGid().equals(gid)) + return task; + } + return null; + } + + public ArrayList getChildTaskList() { + return this.mChildren; + } + + public void setIndex(int index) { + this.mIndex = index; + } + + public int getIndex() { + return this.mIndex; + } +} diff --git a/src/net/micode/notes/gtask/exception/ActionFailureException.java b/src/net/micode/notes/gtask/exception/ActionFailureException.java new file mode 100644 index 0000000..15504be --- /dev/null +++ b/src/net/micode/notes/gtask/exception/ActionFailureException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.exception; + +public class ActionFailureException extends RuntimeException { + private static final long serialVersionUID = 4425249765923293627L; + + public ActionFailureException() { + super(); + } + + public ActionFailureException(String paramString) { + super(paramString); + } + + public ActionFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} diff --git a/src/net/micode/notes/gtask/exception/NetworkFailureException.java b/src/net/micode/notes/gtask/exception/NetworkFailureException.java new file mode 100644 index 0000000..b08cfb1 --- /dev/null +++ b/src/net/micode/notes/gtask/exception/NetworkFailureException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.exception; + +public class NetworkFailureException extends Exception { + private static final long serialVersionUID = 2107610287180234136L; + + public NetworkFailureException() { + super(); + } + + public NetworkFailureException(String paramString) { + super(paramString); + } + + public NetworkFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} diff --git a/src/net/micode/notes/gtask/remote/GTaskASyncTask.java b/src/net/micode/notes/gtask/remote/GTaskASyncTask.java new file mode 100644 index 0000000..b3b61e7 --- /dev/null +++ b/src/net/micode/notes/gtask/remote/GTaskASyncTask.java @@ -0,0 +1,123 @@ + +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesListActivity; +import net.micode.notes.ui.NotesPreferenceActivity; + + +public class GTaskASyncTask extends AsyncTask { + + private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + + public interface OnCompleteListener { + void onComplete(); + } + + private Context mContext; + + private NotificationManager mNotifiManager; + + private GTaskManager mTaskManager; + + private OnCompleteListener mOnCompleteListener; + + public GTaskASyncTask(Context context, OnCompleteListener listener) { + mContext = context; + mOnCompleteListener = listener; + mNotifiManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + mTaskManager = GTaskManager.getInstance(); + } + + public void cancelSync() { + mTaskManager.cancelSync(); + } + + public void publishProgess(String message) { + publishProgress(new String[] { + message + }); + } + + private void showNotification(int tickerId, String content) { + Notification notification = new Notification(R.drawable.notification, mContext + .getString(tickerId), System.currentTimeMillis()); + notification.defaults = Notification.DEFAULT_LIGHTS; + notification.flags = Notification.FLAG_AUTO_CANCEL; + PendingIntent pendingIntent; + if (tickerId != R.string.ticker_success) { + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesPreferenceActivity.class), 0); + + } else { + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesListActivity.class), 0); + } + notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, + pendingIntent); + mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); + } + + @Override + protected Integer doInBackground(Void... unused) { + publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity + .getSyncAccountName(mContext))); + return mTaskManager.sync(mContext, this); + } + + @Override + protected void onProgressUpdate(String... progress) { + showNotification(R.string.ticker_syncing, progress[0]); + if (mContext instanceof GTaskSyncService) { + ((GTaskSyncService) mContext).sendBroadcast(progress[0]); + } + } + + @Override + protected void onPostExecute(Integer result) { + if (result == GTaskManager.STATE_SUCCESS) { + showNotification(R.string.ticker_success, mContext.getString( + R.string.success_sync_account, mTaskManager.getSyncAccount())); + NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis()); + } else if (result == GTaskManager.STATE_NETWORK_ERROR) { + showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network)); + } else if (result == GTaskManager.STATE_INTERNAL_ERROR) { + showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal)); + } else if (result == GTaskManager.STATE_SYNC_CANCELLED) { + showNotification(R.string.ticker_cancel, mContext + .getString(R.string.error_sync_cancelled)); + } + if (mOnCompleteListener != null) { + new Thread(new Runnable() { + + public void run() { + mOnCompleteListener.onComplete(); + } + }).start(); + } + } +} diff --git a/src/net/micode/notes/gtask/remote/GTaskClient.java b/src/net/micode/notes/gtask/remote/GTaskClient.java new file mode 100644 index 0000000..c67dfdf --- /dev/null +++ b/src/net/micode/notes/gtask/remote/GTaskClient.java @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.gtask.data.Node; +import net.micode.notes.gtask.data.Task; +import net.micode.notes.gtask.data.TaskList; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.ui.NotesPreferenceActivity; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + + +public class GTaskClient { + private static final String TAG = GTaskClient.class.getSimpleName(); + + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + + private static GTaskClient mInstance = null; + + private DefaultHttpClient mHttpClient; + + private String mGetUrl; + + private String mPostUrl; + + private long mClientVersion; + + private boolean mLoggedin; + + private long mLastLoginTime; + + private int mActionId; + + private Account mAccount; + + private JSONArray mUpdateArray; + + private GTaskClient() { + mHttpClient = null; + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + mClientVersion = -1; + mLoggedin = false; + mLastLoginTime = 0; + mActionId = 1; + mAccount = null; + mUpdateArray = null; + } + + public static synchronized GTaskClient getInstance() { + if (mInstance == null) { + mInstance = new GTaskClient(); + } + return mInstance; + } + + public boolean login(Activity activity) { + // we suppose that the cookie would expire after 5 minutes + // then we need to re-login + final long interval = 1000 * 60 * 5; + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; + } + + // need to re-login after account switch + if (mLoggedin + && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity + .getSyncAccountName(activity))) { + mLoggedin = false; + } + + if (mLoggedin) { + Log.d(TAG, "already logged in"); + return true; + } + + mLastLoginTime = System.currentTimeMillis(); + String authToken = loginGoogleAccount(activity, false); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + // login with custom domain if necessary + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() + .endsWith("googlemail.com"))) { + StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); + int index = mAccount.name.indexOf('@') + 1; + String suffix = mAccount.name.substring(index); + url.append(suffix + "/"); + mGetUrl = url.toString() + "ig"; + mPostUrl = url.toString() + "r/ig"; + + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; + } + } + + // try to login with google official url + if (!mLoggedin) { + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + if (!tryToLoginGtask(activity, authToken)) { + return false; + } + } + + mLoggedin = true; + return true; + } + + private String loginGoogleAccount(Activity activity, boolean invalidateToken) { + String authToken; + AccountManager accountManager = AccountManager.get(activity); + Account[] accounts = accountManager.getAccountsByType("com.google"); + + if (accounts.length == 0) { + Log.e(TAG, "there is no available google account"); + return null; + } + + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; + break; + } + } + if (account != null) { + mAccount = account; + } else { + Log.e(TAG, "unable to get an account with the same name in the settings"); + return null; + } + + // get the token now + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, + "goanna_mobile", null, activity, null, null); + try { + Bundle authTokenBundle = accountManagerFuture.getResult(); + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); + } + } catch (Exception e) { + Log.e(TAG, "get auth token failed"); + authToken = null; + } + + return authToken; + } + + private boolean tryToLoginGtask(Activity activity, String authToken) { + if (!loginGtask(authToken)) { + // maybe the auth token is out of date, now let's invalidate the + // token and try again + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + if (!loginGtask(authToken)) { + Log.e(TAG, "login gtask failed"); + return false; + } + } + return true; + } + + private boolean loginGtask(String authToken) { + int timeoutConnection = 10000; + int timeoutSocket = 15000; + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + mHttpClient = new DefaultHttpClient(httpParameters); + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // login gtask + try { + String loginUrl = mGetUrl + "?auth=" + authToken; + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = null; + response = mHttpClient.execute(httpGet); + + // get the cookie now + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + if (!hasAuthCookie) { + Log.w(TAG, "it seems that there is no auth cookie"); + } + + // get the client version + String resString = getResponseContent(response.getEntity()); + String jsBegin = "_setup("; + String jsEnd = ")}"; + int begin = resString.indexOf(jsBegin); + int end = resString.lastIndexOf(jsEnd); + String jsString = null; + if (begin != -1 && end != -1 && begin < end) { + jsString = resString.substring(begin + jsBegin.length(), end); + } + JSONObject js = new JSONObject(jsString); + mClientVersion = js.getLong("v"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } catch (Exception e) { + // simply catch all exceptions + Log.e(TAG, "httpget gtask_url failed"); + return false; + } + + return true; + } + + private int getActionId() { + return mActionId++; + } + + private HttpPost createHttpPost() { + HttpPost httpPost = new HttpPost(mPostUrl); + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + httpPost.setHeader("AT", "1"); + return httpPost; + } + + private String getResponseContent(HttpEntity entity) throws IOException { + String contentEncoding = null; + if (entity.getContentEncoding() != null) { + contentEncoding = entity.getContentEncoding().getValue(); + Log.d(TAG, "encoding: " + contentEncoding); + } + + InputStream input = entity.getContent(); + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { + input = new GZIPInputStream(entity.getContent()); + } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + Inflater inflater = new Inflater(true); + input = new InflaterInputStream(entity.getContent(), inflater); + } + + try { + InputStreamReader isr = new InputStreamReader(input); + BufferedReader br = new BufferedReader(isr); + StringBuilder sb = new StringBuilder(); + + while (true) { + String buff = br.readLine(); + if (buff == null) { + return sb.toString(); + } + sb = sb.append(buff); + } + } finally { + input.close(); + } + } + + private JSONObject postRequest(JSONObject js) throws NetworkFailureException { + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + HttpPost httpPost = createHttpPost(); + try { + LinkedList list = new LinkedList(); + list.add(new BasicNameValuePair("r", js.toString())); + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); + httpPost.setEntity(entity); + + // execute the post + HttpResponse response = mHttpClient.execute(httpPost); + String jsString = getResponseContent(response.getEntity()); + return new JSONObject(jsString); + + } catch (ClientProtocolException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (IOException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("unable to convert response content to jsonobject"); + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("error occurs when posting request"); + } + } + + public void createTask(Task task) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + + // action_list + actionList.put(task.getCreateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + JSONObject jsResponse = postRequest(jsPost); + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create task: handing jsonobject failed"); + } + } + + public void createTaskList(TaskList tasklist) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + + // action_list + actionList.put(tasklist.getCreateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + JSONObject jsResponse = postRequest(jsPost); + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create tasklist: handing jsonobject failed"); + } + } + + public void commitUpdate() throws NetworkFailureException { + if (mUpdateArray != null) { + try { + JSONObject jsPost = new JSONObject(); + + // action_list + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); + mUpdateArray = null; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("commit update: handing jsonobject failed"); + } + } + } + + public void addUpdateNode(Node node) throws NetworkFailureException { + if (node != null) { + // too many update items may result in an error + // set max to 10 items + if (mUpdateArray != null && mUpdateArray.length() > 10) { + commitUpdate(); + } + + if (mUpdateArray == null) + mUpdateArray = new JSONArray(); + mUpdateArray.put(node.getUpdateAction(getActionId())); + } + } + + public void moveTask(Task task, TaskList preParent, TaskList curParent) + throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + JSONObject action = new JSONObject(); + + // action_list + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); + if (preParent == curParent && task.getPriorSibling() != null) { + // put prioring_sibing_id only if moving within the tasklist and + // it is not the first one + action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); + } + action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); + action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); + if (preParent != curParent) { + // put the dest_list only if moving between tasklists + action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); + } + actionList.put(action); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("move task: handing jsonobject failed"); + } + } + + public void deleteNode(Node node) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + + // action_list + node.setDeleted(true); + actionList.put(node.getUpdateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); + mUpdateArray = null; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("delete node: handing jsonobject failed"); + } + } + + public JSONArray getTaskLists() throws NetworkFailureException { + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + try { + HttpGet httpGet = new HttpGet(mGetUrl); + HttpResponse response = null; + response = mHttpClient.execute(httpGet); + + // get the task list + String resString = getResponseContent(response.getEntity()); + String jsBegin = "_setup("; + String jsEnd = ")}"; + int begin = resString.indexOf(jsBegin); + int end = resString.lastIndexOf(jsEnd); + String jsString = null; + if (begin != -1 && end != -1 && begin < end) { + jsString = resString.substring(begin + jsBegin.length(), end); + } + JSONObject js = new JSONObject(jsString); + return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); + } catch (ClientProtocolException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (IOException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task lists: handing jasonobject failed"); + } + } + + public JSONArray getTaskList(String listGid) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + JSONObject action = new JSONObject(); + + // action_list + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); + action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); + actionList.put(action); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + JSONObject jsResponse = postRequest(jsPost); + return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task list: handing jsonobject failed"); + } + } + + public Account getSyncAccount() { + return mAccount; + } + + public void resetUpdateArray() { + mUpdateArray = null; + } +} diff --git a/src/net/micode/notes/gtask/remote/GTaskManager.java b/src/net/micode/notes/gtask/remote/GTaskManager.java new file mode 100644 index 0000000..d2b4082 --- /dev/null +++ b/src/net/micode/notes/gtask/remote/GTaskManager.java @@ -0,0 +1,800 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.data.MetaData; +import net.micode.notes.gtask.data.Node; +import net.micode.notes.gtask.data.SqlNote; +import net.micode.notes.gtask.data.Task; +import net.micode.notes.gtask.data.TaskList; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + + +public class GTaskManager { + private static final String TAG = GTaskManager.class.getSimpleName(); + + public static final int STATE_SUCCESS = 0; + + public static final int STATE_NETWORK_ERROR = 1; + + public static final int STATE_INTERNAL_ERROR = 2; + + public static final int STATE_SYNC_IN_PROGRESS = 3; + + public static final int STATE_SYNC_CANCELLED = 4; + + private static GTaskManager mInstance = null; + + private Activity mActivity; + + private Context mContext; + + private ContentResolver mContentResolver; + + private boolean mSyncing; + + private boolean mCancelled; + + private HashMap mGTaskListHashMap; + + private HashMap mGTaskHashMap; + + private HashMap mMetaHashMap; + + private TaskList mMetaList; + + private HashSet mLocalDeleteIdMap; + + private HashMap mGidToNid; + + private HashMap mNidToGid; + + private GTaskManager() { + mSyncing = false; + mCancelled = false; + mGTaskListHashMap = new HashMap(); + mGTaskHashMap = new HashMap(); + mMetaHashMap = new HashMap(); + mMetaList = null; + mLocalDeleteIdMap = new HashSet(); + mGidToNid = new HashMap(); + mNidToGid = new HashMap(); + } + + public static synchronized GTaskManager getInstance() { + if (mInstance == null) { + mInstance = new GTaskManager(); + } + return mInstance; + } + + public synchronized void setActivityContext(Activity activity) { + // used for getting authtoken + mActivity = activity; + } + + public int sync(Context context, GTaskASyncTask asyncTask) { + if (mSyncing) { + Log.d(TAG, "Sync is in progress"); + return STATE_SYNC_IN_PROGRESS; + } + mContext = context; + mContentResolver = mContext.getContentResolver(); + mSyncing = true; + mCancelled = false; + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + + try { + GTaskClient client = GTaskClient.getInstance(); + client.resetUpdateArray(); + + // login google task + if (!mCancelled) { + if (!client.login(mActivity)) { + throw new NetworkFailureException("login google task failed"); + } + } + + // get the task list from google + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); + initGTaskList(); + + // do content sync work + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing)); + syncContent(); + } catch (NetworkFailureException e) { + Log.e(TAG, e.toString()); + return STATE_NETWORK_ERROR; + } catch (ActionFailureException e) { + Log.e(TAG, e.toString()); + return STATE_INTERNAL_ERROR; + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return STATE_INTERNAL_ERROR; + } finally { + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + mSyncing = false; + } + + return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS; + } + + private void initGTaskList() throws NetworkFailureException { + if (mCancelled) + return; + GTaskClient client = GTaskClient.getInstance(); + try { + JSONArray jsTaskLists = client.getTaskLists(); + + // init meta list first + mMetaList = null; + for (int i = 0; i < jsTaskLists.length(); i++) { + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + + if (name + .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { + mMetaList = new TaskList(); + mMetaList.setContentByRemoteJSON(object); + + // load meta data + JSONArray jsMetas = client.getTaskList(gid); + for (int j = 0; j < jsMetas.length(); j++) { + object = (JSONObject) jsMetas.getJSONObject(j); + MetaData metaData = new MetaData(); + metaData.setContentByRemoteJSON(object); + if (metaData.isWorthSaving()) { + mMetaList.addChildTask(metaData); + if (metaData.getGid() != null) { + mMetaHashMap.put(metaData.getRelatedGid(), metaData); + } + } + } + } + } + + // create meta list if not existed + if (mMetaList == null) { + mMetaList = new TaskList(); + mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META); + GTaskClient.getInstance().createTaskList(mMetaList); + } + + // init task list + for (int i = 0; i < jsTaskLists.length(); i++) { + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + + if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) + && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META)) { + TaskList tasklist = new TaskList(); + tasklist.setContentByRemoteJSON(object); + mGTaskListHashMap.put(gid, tasklist); + mGTaskHashMap.put(gid, tasklist); + + // load tasks + JSONArray jsTasks = client.getTaskList(gid); + for (int j = 0; j < jsTasks.length(); j++) { + object = (JSONObject) jsTasks.getJSONObject(j); + gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + Task task = new Task(); + task.setContentByRemoteJSON(object); + if (task.isWorthSaving()) { + task.setMetaInfo(mMetaHashMap.get(gid)); + tasklist.addChildTask(task); + mGTaskHashMap.put(gid, task); + } + } + } + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("initGTaskList: handing JSONObject failed"); + } + } + + private void syncContent() throws NetworkFailureException { + int syncType; + Cursor c = null; + String gid; + Node node; + + mLocalDeleteIdMap.clear(); + + if (mCancelled) { + return; + } + + // for local deleted note + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id=?)", new String[] { + String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) + }, null); + if (c != null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); + } + + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + } + } else { + Log.w(TAG, "failed to query trash folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // sync folder first + syncFolder(); + + // for note existing in database + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c != null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); + } else { + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + // remote delete + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c); + } + } else { + Log.w(TAG, "failed to query existing note in database"); + } + + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // go through remaining items + Iterator> iter = mGTaskHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + node = entry.getValue(); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + } + + // mCancelled can be set by another thread, so we neet to check one by + // one + // clear local delete table + if (!mCancelled) { + if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + throw new ActionFailureException("failed to batch-delete local deleted notes"); + } + } + + // refresh local sync id + if (!mCancelled) { + GTaskClient.getInstance().commitUpdate(); + refreshLocalSyncId(); + } + + } + + private void syncFolder() throws NetworkFailureException { + Cursor c = null; + String gid; + Node node; + int syncType; + + if (mCancelled) { + return; + } + + // for root folder + try { + c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); + if (c != null) { + c.moveToNext(); + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); + mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); + // for system folder, only update remote name if necessary + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) + doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); + } else { + doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); + } + } else { + Log.w(TAG, "failed to query root folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // for call-note folder + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(Notes.ID_CALL_RECORD_FOLDER) + }, null); + if (c != null) { + if (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); + mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); + // for system folder, only update remote name if + // necessary + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_CALL_NOTE)) + doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); + } else { + doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); + } + } + } else { + Log.w(TAG, "failed to query call note folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // for local existing folders + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c != null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); + } else { + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + // remote delete + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c); + } + } else { + Log.w(TAG, "failed to query existing folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // for remote add folders + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + gid = entry.getKey(); + node = entry.getValue(); + if (mGTaskHashMap.containsKey(gid)) { + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + } + } + + if (!mCancelled) + GTaskClient.getInstance().commitUpdate(); + } + + private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + MetaData meta; + switch (syncType) { + case Node.SYNC_ACTION_ADD_LOCAL: + addLocalNode(node); + break; + case Node.SYNC_ACTION_ADD_REMOTE: + addRemoteNode(node, c); + break; + case Node.SYNC_ACTION_DEL_LOCAL: + meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN)); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); + } + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + break; + case Node.SYNC_ACTION_DEL_REMOTE: + meta = mMetaHashMap.get(node.getGid()); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); + } + GTaskClient.getInstance().deleteNode(node); + break; + case Node.SYNC_ACTION_UPDATE_LOCAL: + updateLocalNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_REMOTE: + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_CONFLICT: + // merging both modifications maybe a good idea + // right now just use local update simply + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_NONE: + break; + case Node.SYNC_ACTION_ERROR: + default: + throw new ActionFailureException("unkown sync action type"); + } + } + + private void addLocalNode(Node node) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote; + if (node instanceof TaskList) { + if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) { + sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER); + } else if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) { + sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER); + } else { + sqlNote = new SqlNote(mContext); + sqlNote.setContent(node.getLocalJSONFromContent()); + sqlNote.setParentId(Notes.ID_ROOT_FOLDER); + } + } else { + sqlNote = new SqlNote(mContext); + JSONObject js = node.getLocalJSONFromContent(); + try { + if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.has(NoteColumns.ID)) { + long id = note.getLong(NoteColumns.ID); + if (DataUtils.existInNoteDatabase(mContentResolver, id)) { + // the id is not available, have to create a new one + note.remove(NoteColumns.ID); + } + } + } + + if (js.has(GTaskStringUtils.META_HEAD_DATA)) { + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { + // the data id is not available, have to create + // a new one + data.remove(DataColumns.ID); + } + } + } + + } + } catch (JSONException e) { + Log.w(TAG, e.toString()); + e.printStackTrace(); + } + sqlNote.setContent(js); + + Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); + if (parentId == null) { + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot add local node"); + } + sqlNote.setParentId(parentId.longValue()); + } + + // create the local node + sqlNote.setGtaskId(node.getGid()); + sqlNote.commit(false); + + // update gid-nid mapping + mGidToNid.put(node.getGid(), sqlNote.getId()); + mNidToGid.put(sqlNote.getId(), node.getGid()); + + // update meta + updateRemoteMeta(node.getGid(), sqlNote); + } + + private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote; + // update the note locally + sqlNote = new SqlNote(mContext, c); + sqlNote.setContent(node.getLocalJSONFromContent()); + + Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid()) + : new Long(Notes.ID_ROOT_FOLDER); + if (parentId == null) { + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot update local node"); + } + sqlNote.setParentId(parentId.longValue()); + sqlNote.commit(true); + + // update meta info + updateRemoteMeta(node.getGid(), sqlNote); + } + + private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + Node n; + + // update remotely + if (sqlNote.isNoteType()) { + Task task = new Task(); + task.setContentByLocalJSON(sqlNote.getContent()); + + String parentGid = mNidToGid.get(sqlNote.getParentId()); + if (parentGid == null) { + Log.e(TAG, "cannot find task's parent tasklist"); + throw new ActionFailureException("cannot add remote task"); + } + mGTaskListHashMap.get(parentGid).addChildTask(task); + + GTaskClient.getInstance().createTask(task); + n = (Node) task; + + // add meta + updateRemoteMeta(task.getGid(), sqlNote); + } else { + TaskList tasklist = null; + + // we need to skip folder if it has already existed + String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX; + if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) + folderName += GTaskStringUtils.FOLDER_DEFAULT; + else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER) + folderName += GTaskStringUtils.FOLDER_CALL_NOTE; + else + folderName += sqlNote.getSnippet(); + + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + String gid = entry.getKey(); + TaskList list = entry.getValue(); + + if (list.getName().equals(folderName)) { + tasklist = list; + if (mGTaskHashMap.containsKey(gid)) { + mGTaskHashMap.remove(gid); + } + break; + } + } + + // no match we can add now + if (tasklist == null) { + tasklist = new TaskList(); + tasklist.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().createTaskList(tasklist); + mGTaskListHashMap.put(tasklist.getGid(), tasklist); + } + n = (Node) tasklist; + } + + // update local note + sqlNote.setGtaskId(n.getGid()); + sqlNote.commit(false); + sqlNote.resetLocalModified(); + sqlNote.commit(true); + + // gid-id mapping + mGidToNid.put(n.getGid(), sqlNote.getId()); + mNidToGid.put(sqlNote.getId(), n.getGid()); + } + + private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + + // update remotely + node.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(node); + + // update meta + updateRemoteMeta(node.getGid(), sqlNote); + + // move task if necessary + if (sqlNote.isNoteType()) { + Task task = (Task) node; + TaskList preParentList = task.getParent(); + + String curParentGid = mNidToGid.get(sqlNote.getParentId()); + if (curParentGid == null) { + Log.e(TAG, "cannot find task's parent tasklist"); + throw new ActionFailureException("cannot update remote task"); + } + TaskList curParentList = mGTaskListHashMap.get(curParentGid); + + if (preParentList != curParentList) { + preParentList.removeChildTask(task); + curParentList.addChildTask(task); + GTaskClient.getInstance().moveTask(task, preParentList, curParentList); + } + } + + // clear local modified flag + sqlNote.resetLocalModified(); + sqlNote.commit(true); + } + + private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException { + if (sqlNote != null && sqlNote.isNoteType()) { + MetaData metaData = mMetaHashMap.get(gid); + if (metaData != null) { + metaData.setMeta(gid, sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(metaData); + } else { + metaData = new MetaData(); + metaData.setMeta(gid, sqlNote.getContent()); + mMetaList.addChildTask(metaData); + mMetaHashMap.put(gid, metaData); + GTaskClient.getInstance().createTask(metaData); + } + } + } + + private void refreshLocalSyncId() throws NetworkFailureException { + if (mCancelled) { + return; + } + + // get the latest gtask list + mGTaskHashMap.clear(); + mGTaskListHashMap.clear(); + mMetaHashMap.clear(); + initGTaskList(); + + Cursor c = null; + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c != null) { + while (c.moveToNext()) { + String gid = c.getString(SqlNote.GTASK_ID_COLUMN); + Node node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + ContentValues values = new ContentValues(); + values.put(NoteColumns.SYNC_ID, node.getLastModified()); + mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + c.getLong(SqlNote.ID_COLUMN)), values, null, null); + } else { + Log.e(TAG, "something is missed"); + throw new ActionFailureException( + "some local items don't have gid after sync"); + } + } + } else { + Log.w(TAG, "failed to query local note to refresh sync id"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + } + + public String getSyncAccount() { + return GTaskClient.getInstance().getSyncAccount().name; + } + + public void cancelSync() { + mCancelled = true; + } +} diff --git a/src/net/micode/notes/gtask/remote/GTaskSyncService.java b/src/net/micode/notes/gtask/remote/GTaskSyncService.java new file mode 100644 index 0000000..cca36f7 --- /dev/null +++ b/src/net/micode/notes/gtask/remote/GTaskSyncService.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.gtask.remote; + +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +public class GTaskSyncService extends Service { + public final static String ACTION_STRING_NAME = "sync_action_type"; + + public final static int ACTION_START_SYNC = 0; + + public final static int ACTION_CANCEL_SYNC = 1; + + public final static int ACTION_INVALID = 2; + + public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; + + public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; + + public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; + + private static GTaskASyncTask mSyncTask = null; + + private static String mSyncProgress = ""; + + private void startSync() { + if (mSyncTask == null) { + mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + public void onComplete() { + mSyncTask = null; + sendBroadcast(""); + stopSelf(); + } + }); + sendBroadcast(""); + mSyncTask.execute(); + } + } + + private void cancelSync() { + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + @Override + public void onCreate() { + mSyncTask = null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Bundle bundle = intent.getExtras(); + if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { + case ACTION_START_SYNC: + startSync(); + break; + case ACTION_CANCEL_SYNC: + cancelSync(); + break; + default: + break; + } + return START_STICKY; + } + return super.onStartCommand(intent, flags, startId); + } + + @Override + public void onLowMemory() { + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + public IBinder onBind(Intent intent) { + return null; + } + + public void sendBroadcast(String msg) { + mSyncProgress = msg; + Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); + intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); + intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); + sendBroadcast(intent); + } + + public static void startSync(Activity activity) { + GTaskManager.getInstance().setActivityContext(activity); + Intent intent = new Intent(activity, GTaskSyncService.class); + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); + activity.startService(intent); + } + + public static void cancelSync(Context context) { + Intent intent = new Intent(context, GTaskSyncService.class); + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); + context.startService(intent); + } + + public static boolean isSyncing() { + return mSyncTask != null; + } + + public static String getProgressString() { + return mSyncProgress; + } +} diff --git a/src/net/micode/notes/model/Note.java b/src/net/micode/notes/model/Note.java new file mode 100644 index 0000000..6706cf6 --- /dev/null +++ b/src/net/micode/notes/model/Note.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.model; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.net.Uri; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; + +import java.util.ArrayList; + + +public class Note { + private ContentValues mNoteDiffValues; + private NoteData mNoteData; + private static final String TAG = "Note"; + /** + * Create a new note id for adding a new note to databases + */ + public static synchronized long getNewNoteId(Context context, long folderId) { + // Create a new note in the database + ContentValues values = new ContentValues(); + long createdTime = System.currentTimeMillis(); + values.put(NoteColumns.CREATED_DATE, createdTime); + values.put(NoteColumns.MODIFIED_DATE, createdTime); + values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + values.put(NoteColumns.PARENT_ID, folderId); + Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + + long noteId = 0; + try { + noteId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + noteId = 0; + } + if (noteId == -1) { + throw new IllegalStateException("Wrong note id:" + noteId); + } + return noteId; + } + + public Note() { + mNoteDiffValues = new ContentValues(); + mNoteData = new NoteData(); + } + + public void setNoteValue(String key, String value) { + mNoteDiffValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + public void setTextData(String key, String value) { + mNoteData.setTextData(key, value); + } + + public void setTextDataId(long id) { + mNoteData.setTextDataId(id); + } + + public long getTextDataId() { + return mNoteData.mTextDataId; + } + + public void setCallDataId(long id) { + mNoteData.setCallDataId(id); + } + + public void setCallData(String key, String value) { + mNoteData.setCallData(key, value); + } + + public boolean isLocalModified() { + return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); + } + + public boolean syncNote(Context context, long noteId) { + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + + if (!isLocalModified()) { + return true; + } + + /** + * In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and + * {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the + * note data info + */ + if (context.getContentResolver().update( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, + null) == 0) { + Log.e(TAG, "Update note error, should not happen"); + // Do not return, fall through + } + mNoteDiffValues.clear(); + + if (mNoteData.isLocalModified() + && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { + return false; + } + + return true; + } + + private class NoteData { + private long mTextDataId; + + private ContentValues mTextDataValues; + + private long mCallDataId; + + private ContentValues mCallDataValues; + + private static final String TAG = "NoteData"; + + public NoteData() { + mTextDataValues = new ContentValues(); + mCallDataValues = new ContentValues(); + mTextDataId = 0; + mCallDataId = 0; + } + + boolean isLocalModified() { + return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; + } + + void setTextDataId(long id) { + if(id <= 0) { + throw new IllegalArgumentException("Text data id should larger than 0"); + } + mTextDataId = id; + } + + void setCallDataId(long id) { + if (id <= 0) { + throw new IllegalArgumentException("Call data id should larger than 0"); + } + mCallDataId = id; + } + + void setCallData(String key, String value) { + mCallDataValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + void setTextData(String key, String value) { + mTextDataValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + Uri pushIntoContentResolver(Context context, long noteId) { + /** + * Check for safety + */ + if (noteId <= 0) { + throw new IllegalArgumentException("Wrong note id:" + noteId); + } + + ArrayList operationList = new ArrayList(); + ContentProviderOperation.Builder builder = null; + + if(mTextDataValues.size() > 0) { + mTextDataValues.put(DataColumns.NOTE_ID, noteId); + if (mTextDataId == 0) { + mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, + mTextDataValues); + try { + setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new text data fail with noteId" + noteId); + mTextDataValues.clear(); + return null; + } + } else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mTextDataId)); + builder.withValues(mTextDataValues); + operationList.add(builder.build()); + } + mTextDataValues.clear(); + } + + if(mCallDataValues.size() > 0) { + mCallDataValues.put(DataColumns.NOTE_ID, noteId); + if (mCallDataId == 0) { + mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, + mCallDataValues); + try { + setCallDataId(Long.valueOf(uri.getPathSegments().get(1))); + } catch (NumberFormatException e) { + Log.e(TAG, "Insert new call data fail with noteId" + noteId); + mCallDataValues.clear(); + return null; + } + } else { + builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mCallDataId)); + builder.withValues(mCallDataValues); + operationList.add(builder.build()); + } + mCallDataValues.clear(); + } + + if (operationList.size() > 0) { + try { + ContentProviderResult[] results = context.getContentResolver().applyBatch( + Notes.AUTHORITY, operationList); + return (results == null || results.length == 0 || results[0] == null) ? null + : ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + return null; + } + } + return null; + } + } +} diff --git a/src/net/micode/notes/model/WorkingNote.java b/src/net/micode/notes/model/WorkingNote.java new file mode 100644 index 0000000..be081e4 --- /dev/null +++ b/src/net/micode/notes/model/WorkingNote.java @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.model; + +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.tool.ResourceParser.NoteBgResources; + + +public class WorkingNote { + // Note for the working note + private Note mNote; + // Note Id + private long mNoteId; + // Note content + private String mContent; + // Note mode + private int mMode; + + private long mAlertDate; + + private long mModifiedDate; + + private int mBgColorId; + + private int mWidgetId; + + private int mWidgetType; + + private long mFolderId; + + private Context mContext; + + private static final String TAG = "WorkingNote"; + + private boolean mIsDeleted; + + private NoteSettingChangedListener mNoteSettingStatusListener; + + public static final String[] DATA_PROJECTION = new String[] { + DataColumns.ID, + DataColumns.CONTENT, + DataColumns.MIME_TYPE, + DataColumns.DATA1, + DataColumns.DATA2, + DataColumns.DATA3, + DataColumns.DATA4, + }; + + public static final String[] NOTE_PROJECTION = new String[] { + NoteColumns.PARENT_ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + NoteColumns.MODIFIED_DATE + }; + + private static final int DATA_ID_COLUMN = 0; + + private static final int DATA_CONTENT_COLUMN = 1; + + private static final int DATA_MIME_TYPE_COLUMN = 2; + + private static final int DATA_MODE_COLUMN = 3; + + private static final int NOTE_PARENT_ID_COLUMN = 0; + + private static final int NOTE_ALERTED_DATE_COLUMN = 1; + + private static final int NOTE_BG_COLOR_ID_COLUMN = 2; + + private static final int NOTE_WIDGET_ID_COLUMN = 3; + + private static final int NOTE_WIDGET_TYPE_COLUMN = 4; + + private static final int NOTE_MODIFIED_DATE_COLUMN = 5; + + // New note construct + private WorkingNote(Context context, long folderId) { + mContext = context; + mAlertDate = 0; + mModifiedDate = System.currentTimeMillis(); + mFolderId = folderId; + mNote = new Note(); + mNoteId = 0; + mIsDeleted = false; + mMode = 0; + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + } + + // Existing note construct + private WorkingNote(Context context, long noteId, long folderId) { + mContext = context; + mNoteId = noteId; + mFolderId = folderId; + mIsDeleted = false; + mNote = new Note(); + loadNote(); + } + + private void loadNote() { + Cursor cursor = mContext.getContentResolver().query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, + null, null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN); + mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); + mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); + mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); + mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); + } + cursor.close(); + } else { + Log.e(TAG, "No note with id:" + mNoteId); + throw new IllegalArgumentException("Unable to find note with id " + mNoteId); + } + loadNoteData(); + } + + private void loadNoteData() { + Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, + DataColumns.NOTE_ID + "=?", new String[] { + String.valueOf(mNoteId) + }, null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + String type = cursor.getString(DATA_MIME_TYPE_COLUMN); + if (DataConstants.NOTE.equals(type)) { + mContent = cursor.getString(DATA_CONTENT_COLUMN); + mMode = cursor.getInt(DATA_MODE_COLUMN); + mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); + } else if (DataConstants.CALL_NOTE.equals(type)) { + mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); + } else { + Log.d(TAG, "Wrong note type with type:" + type); + } + } while (cursor.moveToNext()); + } + cursor.close(); + } else { + Log.e(TAG, "No data with id:" + mNoteId); + throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId); + } + } + + public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, + int widgetType, int defaultBgColorId) { + WorkingNote note = new WorkingNote(context, folderId); + note.setBgColorId(defaultBgColorId); + note.setWidgetId(widgetId); + note.setWidgetType(widgetType); + return note; + } + + public static WorkingNote load(Context context, long id) { + return new WorkingNote(context, id, 0); + } + + public synchronized boolean saveNote() { + if (isWorthSaving()) { + if (!existInDatabase()) { + if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { + Log.e(TAG, "Create new note fail with id:" + mNoteId); + return false; + } + } + + mNote.syncNote(mContext, mNoteId); + + /** + * Update widget content if there exist any widget of this note + */ + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE + && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + return true; + } else { + return false; + } + } + + public boolean existInDatabase() { + return mNoteId > 0; + } + + private boolean isWorthSaving() { + if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) + || (existInDatabase() && !mNote.isLocalModified())) { + return false; + } else { + return true; + } + } + + public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { + mNoteSettingStatusListener = l; + } + + public void setAlertDate(long date, boolean set) { + if (date != mAlertDate) { + mAlertDate = date; + mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); + } + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onClockAlertChanged(date, set); + } + } + + public void markDeleted(boolean mark) { + mIsDeleted = mark; + if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onWidgetChanged(); + } + } + + public void setBgColorId(int id) { + if (id != mBgColorId) { + mBgColorId = id; + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onBackgroundColorChanged(); + } + mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); + } + } + + public void setCheckListMode(int mode) { + if (mMode != mode) { + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); + } + mMode = mode; + mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); + } + } + + public void setWidgetType(int type) { + if (type != mWidgetType) { + mWidgetType = type; + mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); + } + } + + public void setWidgetId(int id) { + if (id != mWidgetId) { + mWidgetId = id; + mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); + } + } + + public void setWorkingText(String text) { + if (!TextUtils.equals(mContent, text)) { + mContent = text; + mNote.setTextData(DataColumns.CONTENT, mContent); + } + } + + public void convertToCallNote(String phoneNumber, long callDate) { + mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); + mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); + mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); + } + + public boolean hasClockAlert() { + return (mAlertDate > 0 ? true : false); + } + + public String getContent() { + return mContent; + } + + public long getAlertDate() { + return mAlertDate; + } + + public long getModifiedDate() { + return mModifiedDate; + } + + public int getBgColorResId() { + return NoteBgResources.getNoteBgResource(mBgColorId); + } + + public int getBgColorId() { + return mBgColorId; + } + + public int getTitleBgResId() { + return NoteBgResources.getNoteTitleBgResource(mBgColorId); + } + + public int getCheckListMode() { + return mMode; + } + + public long getNoteId() { + return mNoteId; + } + + public long getFolderId() { + return mFolderId; + } + + public int getWidgetId() { + return mWidgetId; + } + + public int getWidgetType() { + return mWidgetType; + } + + public interface NoteSettingChangedListener { + /** + * Called when the background color of current note has just changed + */ + void onBackgroundColorChanged(); + + /** + * Called when user set clock + */ + void onClockAlertChanged(long date, boolean set); + + /** + * Call when user create note from widget + */ + void onWidgetChanged(); + + /** + * Call when switch between check list mode and normal mode + * @param oldMode is previous mode before change + * @param newMode is new mode + */ + void onCheckListModeChanged(int oldMode, int newMode); + } +} diff --git a/src/net/micode/notes/tool/BackupUtils.java b/src/net/micode/notes/tool/BackupUtils.java new file mode 100644 index 0000000..39f6ec4 --- /dev/null +++ b/src/net/micode/notes/tool/BackupUtils.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +import android.content.Context; +import android.database.Cursor; +import android.os.Environment; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; + + +public class BackupUtils { + private static final String TAG = "BackupUtils"; + // Singleton stuff + private static BackupUtils sInstance; + + public static synchronized BackupUtils getInstance(Context context) { + if (sInstance == null) { + sInstance = new BackupUtils(context); + } + return sInstance; + } + + /** + * Following states are signs to represents backup or restore + * status + */ + // Currently, the sdcard is not mounted + public static final int STATE_SD_CARD_UNMOUONTED = 0; + // The backup file not exist + public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; + // The data is not well formated, may be changed by other programs + public static final int STATE_DATA_DESTROIED = 2; + // Some run-time exception which causes restore or backup fails + public static final int STATE_SYSTEM_ERROR = 3; + // Backup or restore success + public static final int STATE_SUCCESS = 4; + + private TextExport mTextExport; + + private BackupUtils(Context context) { + mTextExport = new TextExport(context); + } + + private static boolean externalStorageAvailable() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + public int exportToText() { + return mTextExport.exportToText(); + } + + public String getExportedTextFileName() { + return mTextExport.mFileName; + } + + public String getExportedTextFileDir() { + return mTextExport.mFileDirectory; + } + + private static class TextExport { + private static final String[] NOTE_PROJECTION = { + NoteColumns.ID, + NoteColumns.MODIFIED_DATE, + NoteColumns.SNIPPET, + NoteColumns.TYPE + }; + + private static final int NOTE_COLUMN_ID = 0; + + private static final int NOTE_COLUMN_MODIFIED_DATE = 1; + + private static final int NOTE_COLUMN_SNIPPET = 2; + + private static final String[] DATA_PROJECTION = { + DataColumns.CONTENT, + DataColumns.MIME_TYPE, + DataColumns.DATA1, + DataColumns.DATA2, + DataColumns.DATA3, + DataColumns.DATA4, + }; + + private static final int DATA_COLUMN_CONTENT = 0; + + private static final int DATA_COLUMN_MIME_TYPE = 1; + + private static final int DATA_COLUMN_CALL_DATE = 2; + + private static final int DATA_COLUMN_PHONE_NUMBER = 4; + + private final String [] TEXT_FORMAT; + private static final int FORMAT_FOLDER_NAME = 0; + private static final int FORMAT_NOTE_DATE = 1; + private static final int FORMAT_NOTE_CONTENT = 2; + + private Context mContext; + private String mFileName; + private String mFileDirectory; + + public TextExport(Context context) { + TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); + mContext = context; + mFileName = ""; + mFileDirectory = ""; + } + + private String getFormat(int id) { + return TEXT_FORMAT[id]; + } + + /** + * Export the folder identified by folder id to text + */ + private void exportFolderToText(String folderId, PrintStream ps) { + // Query notes belong to this folder + Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] { + folderId + }, null); + + if (notesCursor != null) { + if (notesCursor.moveToFirst()) { + do { + // Print note's last modified date + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), + notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); + // Query data belong to this note + String noteId = notesCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (notesCursor.moveToNext()); + } + notesCursor.close(); + } + } + + /** + * Export note identified by id to a print stream + */ + private void exportNoteToText(String noteId, PrintStream ps) { + Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, + DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { + noteId + }, null); + + if (dataCursor != null) { + if (dataCursor.moveToFirst()) { + do { + String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); + if (DataConstants.CALL_NOTE.equals(mimeType)) { + // Print phone number + String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER); + long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE); + String location = dataCursor.getString(DATA_COLUMN_CONTENT); + + if (!TextUtils.isEmpty(phoneNumber)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + phoneNumber)); + } + // Print call date + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat + .format(mContext.getString(R.string.format_datetime_mdhm), + callDate))); + // Print call attachment location + if (!TextUtils.isEmpty(location)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + location)); + } + } else if (DataConstants.NOTE.equals(mimeType)) { + String content = dataCursor.getString(DATA_COLUMN_CONTENT); + if (!TextUtils.isEmpty(content)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + content)); + } + } + } while (dataCursor.moveToNext()); + } + dataCursor.close(); + } + // print a line separator between note + try { + ps.write(new byte[] { + Character.LINE_SEPARATOR, Character.LETTER_NUMBER + }); + } catch (IOException e) { + Log.e(TAG, e.toString()); + } + } + + /** + * Note will be exported as text which is user readable + */ + public int exportToText() { + if (!externalStorageAvailable()) { + Log.d(TAG, "Media was not mounted"); + return STATE_SD_CARD_UNMOUONTED; + } + + PrintStream ps = getExportToTextPrintStream(); + if (ps == null) { + Log.e(TAG, "get print stream error"); + return STATE_SYSTEM_ERROR; + } + // First export folder and its notes + Cursor folderCursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, + "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " + + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR " + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null); + + if (folderCursor != null) { + if (folderCursor.moveToFirst()) { + do { + // Print folder's name + String folderName = ""; + if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) { + folderName = mContext.getString(R.string.call_record_folder_name); + } else { + folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET); + } + if (!TextUtils.isEmpty(folderName)) { + ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName)); + } + String folderId = folderCursor.getString(NOTE_COLUMN_ID); + exportFolderToText(folderId, ps); + } while (folderCursor.moveToNext()); + } + folderCursor.close(); + } + + // Export notes in root's folder + Cursor noteCursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, + NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + + "=0", null, null); + + if (noteCursor != null) { + if (noteCursor.moveToFirst()) { + do { + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), + noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); + // Query data belong to this note + String noteId = noteCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (noteCursor.moveToNext()); + } + noteCursor.close(); + } + ps.close(); + + return STATE_SUCCESS; + } + + /** + * Get a print stream pointed to the file {@generateExportedTextFile} + */ + private PrintStream getExportToTextPrintStream() { + File file = generateFileMountedOnSDcard(mContext, R.string.file_path, + R.string.file_name_txt_format); + if (file == null) { + Log.e(TAG, "create file to exported failed"); + return null; + } + mFileName = file.getName(); + mFileDirectory = mContext.getString(R.string.file_path); + PrintStream ps = null; + try { + FileOutputStream fos = new FileOutputStream(file); + ps = new PrintStream(fos); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (NullPointerException e) { + e.printStackTrace(); + return null; + } + return ps; + } + } + + /** + * Generate the text file to store imported data + */ + private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { + StringBuilder sb = new StringBuilder(); + sb.append(Environment.getExternalStorageDirectory()); + sb.append(context.getString(filePathResId)); + File filedir = new File(sb.toString()); + sb.append(context.getString( + fileNameFormatResId, + DateFormat.format(context.getString(R.string.format_date_ymd), + System.currentTimeMillis()))); + File file = new File(sb.toString()); + + try { + if (!filedir.exists()) { + filedir.mkdir(); + } + if (!file.exists()) { + file.createNewFile(); + } + return file; + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } +} + + diff --git a/src/net/micode/notes/tool/DataUtils.java b/src/net/micode/notes/tool/DataUtils.java new file mode 100644 index 0000000..2a14982 --- /dev/null +++ b/src/net/micode/notes/tool/DataUtils.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; + +import java.util.ArrayList; +import java.util.HashSet; + + +public class DataUtils { + public static final String TAG = "DataUtils"; + public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + if (ids.size() == 0) { + Log.d(TAG, "no id is in the hashset"); + return true; + } + + ArrayList operationList = new ArrayList(); + for (long id : ids) { + if(id == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Don't delete system folder root"); + continue; + } + ContentProviderOperation.Builder builder = ContentProviderOperation + .newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + operationList.add(builder.build()); + } + try { + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); + return false; + } + return true; + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } + return false; + } + + public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, desFolderId); + values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); + } + + public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, + long folderId) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + + ArrayList operationList = new ArrayList(); + for (long id : ids) { + ContentProviderOperation.Builder builder = ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + builder.withValue(NoteColumns.PARENT_ID, folderId); + builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); + operationList.add(builder.build()); + } + + try { + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); + return false; + } + return true; + } catch (RemoteException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } catch (OperationApplicationException e) { + Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } + return false; + } + + /** + * Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}} + */ + public static int getUserFolderCount(ContentResolver resolver) { + Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI, + new String[] { "COUNT(*)" }, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)}, + null); + + int count = 0; + if(cursor != null) { + if(cursor.moveToFirst()) { + try { + count = cursor.getInt(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "get folder count failed:" + e.toString()); + } finally { + cursor.close(); + } + } + } + return count; + } + + public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, + new String [] {String.valueOf(type)}, + null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, null, null, null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), + null, null, null, null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null, + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.SNIPPET + "=?", + new String[] { name }, null); + boolean exist = false; + if(cursor != null) { + if(cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + } + + public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, + new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, + NoteColumns.PARENT_ID + "=?", + new String[] { String.valueOf(folderId) }, + null); + + HashSet set = null; + if (c != null) { + if (c.moveToFirst()) { + set = new HashSet(); + do { + try { + AppWidgetAttribute widget = new AppWidgetAttribute(); + widget.widgetId = c.getInt(0); + widget.widgetType = c.getInt(1); + set.add(widget); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, e.toString()); + } + } while (c.moveToNext()); + } + c.close(); + } + return set; + } + + public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { CallNote.PHONE_NUMBER }, + CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", + new String [] { String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE }, + null); + + if (cursor != null && cursor.moveToFirst()) { + try { + return cursor.getString(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Get call number fails " + e.toString()); + } finally { + cursor.close(); + } + } + return ""; + } + + public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { CallNote.NOTE_ID }, + CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" + + CallNote.PHONE_NUMBER + ",?)", + new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber }, + null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + try { + return cursor.getLong(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Get call note id fails " + e.toString()); + } + } + cursor.close(); + } + return 0; + } + + public static String getSnippetById(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, + new String [] { NoteColumns.SNIPPET }, + NoteColumns.ID + "=?", + new String [] { String.valueOf(noteId)}, + null); + + if (cursor != null) { + String snippet = ""; + if (cursor.moveToFirst()) { + snippet = cursor.getString(0); + } + cursor.close(); + return snippet; + } + throw new IllegalArgumentException("Note is not found with id: " + noteId); + } + + public static String getFormattedSnippet(String snippet) { + if (snippet != null) { + snippet = snippet.trim(); + int index = snippet.indexOf('\n'); + if (index != -1) { + snippet = snippet.substring(0, index); + } + } + return snippet; + } +} diff --git a/src/net/micode/notes/tool/GTaskStringUtils.java b/src/net/micode/notes/tool/GTaskStringUtils.java new file mode 100644 index 0000000..666b729 --- /dev/null +++ b/src/net/micode/notes/tool/GTaskStringUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +public class GTaskStringUtils { + + public final static String GTASK_JSON_ACTION_ID = "action_id"; + + public final static String GTASK_JSON_ACTION_LIST = "action_list"; + + public final static String GTASK_JSON_ACTION_TYPE = "action_type"; + + public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; + + public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; + + public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; + + public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; + + public final static String GTASK_JSON_CREATOR_ID = "creator_id"; + + public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; + + public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; + + public final static String GTASK_JSON_COMPLETED = "completed"; + + public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; + + public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; + + public final static String GTASK_JSON_DELETED = "deleted"; + + public final static String GTASK_JSON_DEST_LIST = "dest_list"; + + public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; + + public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; + + public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; + + public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; + + public final static String GTASK_JSON_GET_DELETED = "get_deleted"; + + public final static String GTASK_JSON_ID = "id"; + + public final static String GTASK_JSON_INDEX = "index"; + + public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; + + public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; + + public final static String GTASK_JSON_LIST_ID = "list_id"; + + public final static String GTASK_JSON_LISTS = "lists"; + + public final static String GTASK_JSON_NAME = "name"; + + public final static String GTASK_JSON_NEW_ID = "new_id"; + + public final static String GTASK_JSON_NOTES = "notes"; + + public final static String GTASK_JSON_PARENT_ID = "parent_id"; + + public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; + + public final static String GTASK_JSON_RESULTS = "results"; + + public final static String GTASK_JSON_SOURCE_LIST = "source_list"; + + public final static String GTASK_JSON_TASKS = "tasks"; + + public final static String GTASK_JSON_TYPE = "type"; + + public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; + + public final static String GTASK_JSON_TYPE_TASK = "TASK"; + + public final static String GTASK_JSON_USER = "user"; + + public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; + + public final static String FOLDER_DEFAULT = "Default"; + + public final static String FOLDER_CALL_NOTE = "Call_Note"; + + public final static String FOLDER_META = "METADATA"; + + public final static String META_HEAD_GTASK_ID = "meta_gid"; + + public final static String META_HEAD_NOTE = "meta_note"; + + public final static String META_HEAD_DATA = "meta_data"; + + public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; + +} diff --git a/src/net/micode/notes/tool/ResourceParser.java b/src/net/micode/notes/tool/ResourceParser.java new file mode 100644 index 0000000..1ad3ad6 --- /dev/null +++ b/src/net/micode/notes/tool/ResourceParser.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +import android.content.Context; +import android.preference.PreferenceManager; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesPreferenceActivity; + +public class ResourceParser { + + public static final int YELLOW = 0; + public static final int BLUE = 1; + public static final int WHITE = 2; + public static final int GREEN = 3; + public static final int RED = 4; + + public static final int BG_DEFAULT_COLOR = YELLOW; + + public static final int TEXT_SMALL = 0; + public static final int TEXT_MEDIUM = 1; + public static final int TEXT_LARGE = 2; + public static final int TEXT_SUPER = 3; + + public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; + + public static class NoteBgResources { + private final static int [] BG_EDIT_RESOURCES = new int [] { + R.drawable.edit_yellow, + R.drawable.edit_blue, + R.drawable.edit_white, + R.drawable.edit_green, + R.drawable.edit_red + }; + + private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] { + R.drawable.edit_title_yellow, + R.drawable.edit_title_blue, + R.drawable.edit_title_white, + R.drawable.edit_title_green, + R.drawable.edit_title_red + }; + + public static int getNoteBgResource(int id) { + return BG_EDIT_RESOURCES[id]; + } + + public static int getNoteTitleBgResource(int id) { + return BG_EDIT_TITLE_RESOURCES[id]; + } + } + + public static int getDefaultBgId(Context context) { + if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { + return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); + } else { + return BG_DEFAULT_COLOR; + } + } + + public static class NoteItemBgResources { + private final static int [] BG_FIRST_RESOURCES = new int [] { + R.drawable.list_yellow_up, + R.drawable.list_blue_up, + R.drawable.list_white_up, + R.drawable.list_green_up, + R.drawable.list_red_up + }; + + private final static int [] BG_NORMAL_RESOURCES = new int [] { + R.drawable.list_yellow_middle, + R.drawable.list_blue_middle, + R.drawable.list_white_middle, + R.drawable.list_green_middle, + R.drawable.list_red_middle + }; + + private final static int [] BG_LAST_RESOURCES = new int [] { + R.drawable.list_yellow_down, + R.drawable.list_blue_down, + R.drawable.list_white_down, + R.drawable.list_green_down, + R.drawable.list_red_down, + }; + + private final static int [] BG_SINGLE_RESOURCES = new int [] { + R.drawable.list_yellow_single, + R.drawable.list_blue_single, + R.drawable.list_white_single, + R.drawable.list_green_single, + R.drawable.list_red_single + }; + + public static int getNoteBgFirstRes(int id) { + return BG_FIRST_RESOURCES[id]; + } + + public static int getNoteBgLastRes(int id) { + return BG_LAST_RESOURCES[id]; + } + + public static int getNoteBgSingleRes(int id) { + return BG_SINGLE_RESOURCES[id]; + } + + public static int getNoteBgNormalRes(int id) { + return BG_NORMAL_RESOURCES[id]; + } + + public static int getFolderBgRes() { + return R.drawable.list_folder; + } + } + + public static class WidgetBgResources { + private final static int [] BG_2X_RESOURCES = new int [] { + R.drawable.widget_2x_yellow, + R.drawable.widget_2x_blue, + R.drawable.widget_2x_white, + R.drawable.widget_2x_green, + R.drawable.widget_2x_red, + }; + + public static int getWidget2xBgResource(int id) { + return BG_2X_RESOURCES[id]; + } + + private final static int [] BG_4X_RESOURCES = new int [] { + R.drawable.widget_4x_yellow, + R.drawable.widget_4x_blue, + R.drawable.widget_4x_white, + R.drawable.widget_4x_green, + R.drawable.widget_4x_red + }; + + public static int getWidget4xBgResource(int id) { + return BG_4X_RESOURCES[id]; + } + } + + public static class TextAppearanceResources { + private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] { + R.style.TextAppearanceNormal, + R.style.TextAppearanceMedium, + R.style.TextAppearanceLarge, + R.style.TextAppearanceSuper + }; + + public static int getTexAppearanceResource(int id) { + /** + * HACKME: Fix bug of store the resource id in shared preference. + * The id may larger than the length of resources, in this case, + * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} + */ + if (id >= TEXTAPPEARANCE_RESOURCES.length) { + return BG_DEFAULT_FONT_SIZE; + } + return TEXTAPPEARANCE_RESOURCES[id]; + } + + public static int getResourcesSize() { + return TEXTAPPEARANCE_RESOURCES.length; + } + } +} diff --git a/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/net/micode/notes/ui/AlarmAlertActivity.java new file mode 100644 index 0000000..85723be --- /dev/null +++ b/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; + +import java.io.IOException; + + +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + private long mNoteId; + private String mSnippet; + private static final int SNIPPET_PREW_MAX_LEN = 60; + MediaPlayer mPlayer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + + final Window win = getWindow(); + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + } + + Intent intent = getIntent(); + + try { + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + mPlayer = new MediaPlayer(); + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + playAlarmSound(); + } else { + finish(); + } + } + + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + private void playAlarmSound() { + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + mPlayer.setDataSource(this, url); + mPlayer.prepare(); + mPlayer.setLooping(true); + mPlayer.start(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.app_name); + dialog.setMessage(mSnippet); + dialog.setPositiveButton(R.string.notealert_ok, this); + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + } + dialog.show().setOnDismissListener(this); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + startActivity(intent); + break; + default: + break; + } + } + + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); + } + + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + } + } +} diff --git a/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/net/micode/notes/ui/AlarmInitReceiver.java new file mode 100644 index 0000000..f221202 --- /dev/null +++ b/src/net/micode/notes/ui/AlarmInitReceiver.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class AlarmInitReceiver extends BroadcastReceiver { + + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + + private static final int COLUMN_ID = 0; + private static final int COLUMN_ALERTED_DATE = 1; + + @Override + public void onReceive(Context context, Intent intent) { + long currentDate = System.currentTimeMillis(); + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + null); + + if (c != null) { + if (c.moveToFirst()) { + do { + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + Intent sender = new Intent(context, AlarmReceiver.class); + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + c.close(); + } + } +} diff --git a/src/net/micode/notes/ui/AlarmReceiver.java b/src/net/micode/notes/ui/AlarmReceiver.java new file mode 100644 index 0000000..54e503b --- /dev/null +++ b/src/net/micode/notes/ui/AlarmReceiver.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class AlarmReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + intent.setClass(context, AlarmAlertActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } +} diff --git a/src/net/micode/notes/ui/DateTimePicker.java b/src/net/micode/notes/ui/DateTimePicker.java new file mode 100644 index 0000000..496b0cd --- /dev/null +++ b/src/net/micode/notes/ui/DateTimePicker.java @@ -0,0 +1,485 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import net.micode.notes.R; + + +import android.content.Context; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.NumberPicker; + +public class DateTimePicker extends FrameLayout { + + private static final boolean DEFAULT_ENABLE_STATE = true; + + private static final int HOURS_IN_HALF_DAY = 12; + private static final int HOURS_IN_ALL_DAY = 24; + private static final int DAYS_IN_ALL_WEEK = 7; + private static final int DATE_SPINNER_MIN_VAL = 0; + private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + private static final int MINUT_SPINNER_MIN_VAL = 0; + private static final int MINUT_SPINNER_MAX_VAL = 59; + private static final int AMPM_SPINNER_MIN_VAL = 0; + private static final int AMPM_SPINNER_MAX_VAL = 1; + + private final NumberPicker mDateSpinner; + private final NumberPicker mHourSpinner; + private final NumberPicker mMinuteSpinner; + private final NumberPicker mAmPmSpinner; + private Calendar mDate; + + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + + private boolean mIsAm; + + private boolean mIs24HourView; + + private boolean mIsEnabled = DEFAULT_ENABLE_STATE; + + private boolean mInitialising; + + private OnDateTimeChangedListener mOnDateTimeChangedListener; + + private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + updateDateControl(); + onDateTimeChanged(); + } + }; + + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + if (!mIs24HourView) { + if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || + oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } else { + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); + mDate.set(Calendar.HOUR_OF_DAY, newHour); + onDateTimeChanged(); + if (isDateChanged) { + setCurrentYear(cal.get(Calendar.YEAR)); + setCurrentMonth(cal.get(Calendar.MONTH)); + setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); + } + } + }; + + private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + int offset = 0; + if (oldVal == maxValue && newVal == minValue) { + offset += 1; + } else if (oldVal == minValue && newVal == maxValue) { + offset -= 1; + } + if (offset != 0) { + mDate.add(Calendar.HOUR_OF_DAY, offset); + mHourSpinner.setValue(getCurrentHour()); + updateDateControl(); + int newHour = getCurrentHourOfDay(); + if (newHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + updateAmPmControl(); + } else { + mIsAm = true; + updateAmPmControl(); + } + } + mDate.set(Calendar.MINUTE, newVal); + onDateTimeChanged(); + } + }; + + private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + mIsAm = !mIsAm; + if (mIsAm) { + mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); + } else { + mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); + } + updateAmPmControl(); + onDateTimeChanged(); + } + }; + + public interface OnDateTimeChangedListener { + void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute); + } + + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); + } + + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + mDate = Calendar.getInstance(); + mInitialising = true; + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + inflate(context, R.layout.datetime_picker, this); + + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + mHourSpinner = (NumberPicker) findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); + mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); + mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + + String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); + mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); + mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); + mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); + mAmPmSpinner.setDisplayedValues(stringsForAmPm); + mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); + + // update controls to initial state + updateDateControl(); + updateHourControl(); + updateAmPmControl(); + + set24HourView(is24HourView); + + // set to current time + setCurrentDate(date); + + setEnabled(isEnabled()); + + // set the content descriptions + mInitialising = false; + } + + @Override + public void setEnabled(boolean enabled) { + if (mIsEnabled == enabled) { + return; + } + super.setEnabled(enabled); + mDateSpinner.setEnabled(enabled); + mMinuteSpinner.setEnabled(enabled); + mHourSpinner.setEnabled(enabled); + mAmPmSpinner.setEnabled(enabled); + mIsEnabled = enabled; + } + + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * Get the current date in millis + * + * @return the current date in millis + */ + public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); + } + + /** + * Set the current date + * + * @param date The current date in millis + */ + public void setCurrentDate(long date) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(date); + setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); + } + + /** + * Set the current date + * + * @param year The current year + * @param month The current month + * @param dayOfMonth The current dayOfMonth + * @param hourOfDay The current hourOfDay + * @param minute The current minute + */ + public void setCurrentDate(int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + setCurrentYear(year); + setCurrentMonth(month); + setCurrentDay(dayOfMonth); + setCurrentHour(hourOfDay); + setCurrentMinute(minute); + } + + /** + * Get current year + * + * @return The current year + */ + public int getCurrentYear() { + return mDate.get(Calendar.YEAR); + } + + /** + * Set current year + * + * @param year The current year + */ + public void setCurrentYear(int year) { + if (!mInitialising && year == getCurrentYear()) { + return; + } + mDate.set(Calendar.YEAR, year); + updateDateControl(); + onDateTimeChanged(); + } + + /** + * Get current month in the year + * + * @return The current month in the year + */ + public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); + } + + /** + * Set current month in the year + * + * @param month The month in the year + */ + public void setCurrentMonth(int month) { + if (!mInitialising && month == getCurrentMonth()) { + return; + } + mDate.set(Calendar.MONTH, month); + updateDateControl(); + onDateTimeChanged(); + } + + /** + * Get current day of the month + * + * @return The day of the month + */ + public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); + } + + /** + * Set current day of the month + * + * @param dayOfMonth The day of the month + */ + public void setCurrentDay(int dayOfMonth) { + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDateControl(); + onDateTimeChanged(); + } + + /** + * Get current hour in 24 hour mode, in the range (0~23) + * @return The current hour in 24 hour mode + */ + public int getCurrentHourOfDay() { + return mDate.get(Calendar.HOUR_OF_DAY); + } + + private int getCurrentHour() { + if (mIs24HourView){ + return getCurrentHourOfDay(); + } else { + int hour = getCurrentHourOfDay(); + if (hour > HOURS_IN_HALF_DAY) { + return hour - HOURS_IN_HALF_DAY; + } else { + return hour == 0 ? HOURS_IN_HALF_DAY : hour; + } + } + } + + /** + * Set current hour in 24 hour mode, in the range (0~23) + * + * @param hourOfDay + */ + public void setCurrentHour(int hourOfDay) { + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + if (!mIs24HourView) { + if (hourOfDay >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (hourOfDay > HOURS_IN_HALF_DAY) { + hourOfDay -= HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (hourOfDay == 0) { + hourOfDay = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(hourOfDay); + onDateTimeChanged(); + } + + /** + * Get currentMinute + * + * @return The Current Minute + */ + public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); + } + + /** + * Set current minute + */ + public void setCurrentMinute(int minute) { + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(minute); + mDate.set(Calendar.MINUTE, minute); + onDateTimeChanged(); + } + + /** + * @return true if this is in 24 hour view else false. + */ + public boolean is24HourView () { + return mIs24HourView; + } + + /** + * Set whether in 24 hour or AM/PM mode. + * + * @param is24HourView True for 24 hour mode. False for AM/PM mode. + */ + public void set24HourView(boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + int hour = getCurrentHourOfDay(); + updateHourControl(); + setCurrentHour(hour); + updateAmPmControl(); + } + + private void updateDateControl() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); + mDateSpinner.setDisplayedValues(null); + for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { + cal.add(Calendar.DAY_OF_YEAR, 1); + mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); + } + mDateSpinner.setDisplayedValues(mDateDisplayValues); + mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); + mDateSpinner.invalidate(); + } + + private void updateAmPmControl() { + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } + } + + private void updateHourControl() { + if (mIs24HourView) { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); + } else { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); + } + } + + /** + * Set the callback that indicates the 'Set' button has been pressed. + * @param callback the callback, if null will do nothing + */ + public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + mOnDateTimeChangedListener = callback; + } + + private void onDateTimeChanged() { + if (mOnDateTimeChangedListener != null) { + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } + } +} diff --git a/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/net/micode/notes/ui/DateTimePickerDialog.java new file mode 100644 index 0000000..2c47ba4 --- /dev/null +++ b/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.util.Calendar; + +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + private Calendar mDate = Calendar.getInstance(); + private boolean mIs24HourView; + private OnDateTimeSetListener mOnDateTimeSetListener; + private DateTimePicker mDateTimePicker; + + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + public DateTimePickerDialog(Context context, long date) { + super(context); + mDateTimePicker = new DateTimePicker(context); + setView(mDateTimePicker); + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + mDate.set(Calendar.YEAR, year); + mDate.set(Calendar.MONTH, month); + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + mDate.set(Calendar.MINUTE, minute); + updateTitle(mDate.getTimeInMillis()); + } + }); + mDate.setTimeInMillis(date); + mDate.set(Calendar.SECOND, 0); + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + setButton(context.getString(R.string.datetime_dialog_ok), this); + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + set24HourView(DateFormat.is24HourFormat(this.getContext())); + updateTitle(mDate.getTimeInMillis()); + } + + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + private void updateTitle(long date) { + int flag = + DateUtils.FORMAT_SHOW_YEAR | + DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_SHOW_TIME; + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener != null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } + +} \ No newline at end of file diff --git a/src/net/micode/notes/ui/DropdownMenu.java b/src/net/micode/notes/ui/DropdownMenu.java new file mode 100644 index 0000000..613dc74 --- /dev/null +++ b/src/net/micode/notes/ui/DropdownMenu.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +public class DropdownMenu { + private Button mButton; + private PopupMenu mPopupMenu; + private Menu mMenu; + + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; + mButton.setBackgroundResource(R.drawable.dropdown_icon); + mPopupMenu = new PopupMenu(context, mButton); + mMenu = mPopupMenu.getMenu(); + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} diff --git a/src/net/micode/notes/ui/FoldersListAdapter.java b/src/net/micode/notes/ui/FoldersListAdapter.java new file mode 100644 index 0000000..96b77da --- /dev/null +++ b/src/net/micode/notes/ui/FoldersListAdapter.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class FoldersListAdapter extends CursorAdapter { + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + ((FolderListItem) view).bind(folderName); + } + } + + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + } + + private class FolderListItem extends LinearLayout { + private TextView mName; + + public FolderListItem(Context context) { + super(context); + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + public void bind(String name) { + mName.setText(name); + } + } + +} diff --git a/src/net/micode/notes/ui/NoteEditActivity.java b/src/net/micode/notes/ui/NoteEditActivity.java new file mode 100644 index 0000000..96a9ff8 --- /dev/null +++ b/src/net/micode/notes/ui/NoteEditActivity.java @@ -0,0 +1,873 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Paint; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; +import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + private class HeadViewHolder { + public TextView tvModified; + + public ImageView ivAlertIcon; + + public TextView tvAlertDate; + + public ImageView ibSetBgColor; + } + + private static final Map sBgSelectorBtnsMap = new HashMap(); + static { + sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); + sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED); + sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE); + sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN); + sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE); + } + + private static final Map sBgSelectorSelectionMap = new HashMap(); + static { + sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select); + sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select); + sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select); + sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select); + sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select); + } + + private static final Map sFontSizeBtnsMap = new HashMap(); + static { + sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE); + sFontSizeBtnsMap.put(R.id.ll_font_small, ResourceParser.TEXT_SMALL); + sFontSizeBtnsMap.put(R.id.ll_font_normal, ResourceParser.TEXT_MEDIUM); + sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER); + } + + private static final Map sFontSelectorSelectionMap = new HashMap(); + static { + sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); + } + + private static final String TAG = "NoteEditActivity"; + + private HeadViewHolder mNoteHeaderHolder; + + private View mHeadViewPanel; + + private View mNoteBgColorSelector; + + private View mFontSizeSelector; + + private EditText mNoteEditor; + + private View mNoteEditorPanel; + + private WorkingNote mWorkingNote; + + private SharedPreferences mSharedPrefs; + private int mFontSizeId; + + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; + + public static final String TAG_CHECKED = String.valueOf('\u221A'); + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); + + private LinearLayout mEditTextList; + + private String mUserQuery; + private Pattern mPattern; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.note_edit); + + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); + return; + } + initResources(); + } + + /** + * Current activity may be killed when the memory is low. Once it is killed, for another time + * user load this activity, we should restore the former state + */ + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); + if (!initActivityState(intent)) { + finish(); + return; + } + Log.d(TAG, "Restoring from killed activity"); + } + } + + private boolean initActivityState(Intent intent) { + /** + * If the user specified the {@link Intent#ACTION_VIEW} but not provided with id, + * then jump to the NotesListActivity + */ + mWorkingNote = null; + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + + /** + * Starting from the searched result + */ + if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + } + + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + showToast(R.string.error_note_not_exist); + finish(); + return false; + } else { + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load note failed with note id" + noteId); + finish(); + return false; + } + } + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + } else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { + // New note + long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, + Notes.TYPE_WIDGET_INVALIDE); + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, + ResourceParser.getDefaultBgId(this)); + + // Parse call-record note + String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); + if (callDate != 0 && phoneNumber != null) { + if (TextUtils.isEmpty(phoneNumber)) { + Log.w(TAG, "The call record number is null"); + } + long noteId = 0; + if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), + phoneNumber, callDate)) > 0) { + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load call note failed with note id" + noteId); + finish(); + return false; + } + } else { + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, + widgetType, bgResId); + mWorkingNote.convertToCallNote(phoneNumber, callDate); + } + } else { + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, + bgResId); + } + + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } else { + Log.e(TAG, "Intent not specified action, should not support"); + finish(); + return false; + } + mWorkingNote.setOnSettingStatusChangedListener(this); + return true; + } + + @Override + protected void onResume() { + super.onResume(); + initNoteScreen(); + } + + private void initNoteScreen() { + mNoteEditor.setTextAppearance(this, TextAppearanceResources + .getTexAppearanceResource(mFontSizeId)); + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } + for (Integer id : sBgSelectorSelectionMap.keySet()) { + findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); + } + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + + mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, + mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR)); + + /** + * TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker + * is not ready + */ + showAlertHeader(); + } + + private void showAlertHeader() { + if (mWorkingNote.hasClockAlert()) { + long time = System.currentTimeMillis(); + if (time > mWorkingNote.getAlertDate()) { + mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); + } else { + mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( + mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); + } + mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); + } else { + mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); + }; + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + /** + * For new note without note id, we should firstly save it to + * generate a id. If the editing note is not worth saving, there + * is no id which is equivalent to create new note + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mNoteBgColorSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } + + if (mFontSizeSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mFontSizeSelector, ev)) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return super.dispatchTouchEvent(ev); + } + + private boolean inRangeOfView(View view, MotionEvent ev) { + int []location = new int[2]; + view.getLocationOnScreen(location); + int x = location[0]; + int y = location[1]; + if (ev.getX() < x + || ev.getX() > (x + view.getWidth()) + || ev.getY() < y + || ev.getY() > (y + view.getHeight())) { + return false; + } + return true; + } + + private void initResources() { + mHeadViewPanel = findViewById(R.id.note_title); + mNoteHeaderHolder = new HeadViewHolder(); + mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); + mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); + mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); + mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); + mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); + mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + mNoteEditorPanel = findViewById(R.id.sv_note_edit); + mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); + for (int id : sBgSelectorBtnsMap.keySet()) { + ImageView iv = (ImageView) findViewById(id); + iv.setOnClickListener(this); + } + + mFontSizeSelector = findViewById(R.id.font_size_selector); + for (int id : sFontSizeBtnsMap.keySet()) { + View view = findViewById(id); + view.setOnClickListener(this); + }; + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); + /** + * HACKME: Fix bug of store the resource id in shared preference. + * The id may larger than the length of resources, in this case, + * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} + */ + if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; + } + mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); + } + + @Override + protected void onPause() { + super.onPause(); + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + clearSettingState(); + } + + private void updateWidget() { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + Log.e(TAG, "Unspported widget type"); + return; + } + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + mWorkingNote.getWidgetId() + }); + + sendBroadcast(intent); + setResult(RESULT_OK, intent); + } + + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.btn_set_bg_color) { + mNoteBgColorSelector.setVisibility(View.VISIBLE); + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + - View.VISIBLE); + } else if (sBgSelectorBtnsMap.containsKey(id)) { + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.GONE); + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); + mNoteBgColorSelector.setVisibility(View.GONE); + } else if (sFontSizeBtnsMap.containsKey(id)) { + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + mFontSizeId = sFontSizeBtnsMap.get(id); + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setTextAppearance(this, + TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + } + mFontSizeSelector.setVisibility(View.GONE); + } + } + + @Override + public void onBackPressed() { + if(clearSettingState()) { + return; + } + + saveNote(); + super.onBackPressed(); + } + + private boolean clearSettingState() { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return false; + } + + public void onBackgroundColorChanged() { + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.VISIBLE); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if (isFinishing()) { + return true; + } + clearSettingState(); + menu.clear(); + if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_note_edit, menu); + } else { + getMenuInflater().inflate(R.menu.note_edit, menu); + } + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); + } else { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode); + } + if (mWorkingNote.hasClockAlert()) { + menu.findItem(R.id.menu_alert).setVisible(false); + } else { + menu.findItem(R.id.menu_delete_remind).setVisible(false); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_new_note: + createNewNote(); + break; + case R.id.menu_delete: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_note)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteCurrentNote(); + finish(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.menu_font_size: + mFontSizeSelector.setVisibility(View.VISIBLE); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + break; + case R.id.menu_list_mode: + mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? + TextNote.MODE_CHECK_LIST : 0); + break; + case R.id.menu_share: + getWorkingText(); + sendTo(this, mWorkingNote.getContent()); + break; + case R.id.menu_send_to_desktop: + sendToDesktop(); + break; + case R.id.menu_alert: + setReminder(); + break; + case R.id.menu_delete_remind: + mWorkingNote.setAlertDate(0, false); + break; + default: + break; + } + return true; + } + + private void setReminder() { + DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); + d.setOnDateTimeSetListener(new OnDateTimeSetListener() { + public void OnDateTimeSet(AlertDialog dialog, long date) { + mWorkingNote.setAlertDate(date , true); + } + }); + d.show(); + } + + /** + * Share note to apps that support {@link Intent#ACTION_SEND} action + * and {@text/plain} type + */ + private void sendTo(Context context, String info) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, info); + intent.setType("text/plain"); + context.startActivity(intent); + } + + private void createNewNote() { + // Firstly, save current editing notes + saveNote(); + + // For safety, start a new NoteEditActivity + finish(); + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + startActivity(intent); + } + + private void deleteCurrentNote() { + if (mWorkingNote.existInDatabase()) { + HashSet ids = new HashSet(); + long id = mWorkingNote.getNoteId(); + if (id != Notes.ID_ROOT_FOLDER) { + ids.add(id); + } else { + Log.d(TAG, "Wrong note id, should not happen"); + } + if (!isSyncMode()) { + if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { + Log.e(TAG, "Delete Note error"); + } + } else { + if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + } + mWorkingNote.markDeleted(true); + } + + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + + public void onClockAlertChanged(long date, boolean set) { + /** + * User could set clock to an unsaved note, so before setting the + * alert clock, we should save the note first + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + if (mWorkingNote.getNoteId() > 0) { + Intent intent = new Intent(this, AlarmReceiver.class); + intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + showAlertHeader(); + if(!set) { + alarmManager.cancel(pendingIntent); + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + } + } else { + /** + * There is the condition that user has input nothing (the note is + * not worthy saving), we have no note id, remind the user that he + * should input something + */ + Log.e(TAG, "Clock alert setting error"); + showToast(R.string.error_note_empty_for_clock); + } + } + + public void onWidgetChanged() { + updateWidget(); + } + + public void onEditTextDelete(int index, String text) { + int childCount = mEditTextList.getChildCount(); + if (childCount == 1) { + return; + } + + for (int i = index + 1; i < childCount; i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i - 1); + } + + mEditTextList.removeViewAt(index); + NoteEditText edit = null; + if(index == 0) { + edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( + R.id.et_edit_text); + } else { + edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( + R.id.et_edit_text); + } + int length = edit.length(); + edit.append(text); + edit.requestFocus(); + edit.setSelection(length); + } + + public void onEditTextEnter(int index, String text) { + /** + * Should not happen, check for debug + */ + if(index > mEditTextList.getChildCount()) { + Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); + } + + View view = getListItem(text, index); + mEditTextList.addView(view, index); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.requestFocus(); + edit.setSelection(0); + for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i); + } + } + + private void switchToListMode(String text) { + mEditTextList.removeAllViews(); + String[] items = text.split("\n"); + int index = 0; + for (String item : items) { + if(!TextUtils.isEmpty(item)) { + mEditTextList.addView(getListItem(item, index)); + index++; + } + } + mEditTextList.addView(getListItem("", index)); + mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); + + mNoteEditor.setVisibility(View.GONE); + mEditTextList.setVisibility(View.VISIBLE); + } + + private Spannable getHighlightQueryResult(String fullText, String userQuery) { + SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); + if (!TextUtils.isEmpty(userQuery)) { + mPattern = Pattern.compile(userQuery); + Matcher m = mPattern.matcher(fullText); + int start = 0; + while (m.find(start)) { + spannable.setSpan( + new BackgroundColorSpan(this.getResources().getColor( + R.color.user_query_highlight)), m.start(), m.end(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + start = m.end(); + } + } + return spannable; + } + + private View getListItem(String item, int index) { + View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); + final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item)); + cb.setOnCheckedChangeListener(new OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } else { + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + } + } + }); + + if (item.startsWith(TAG_CHECKED)) { + cb.setChecked(true); + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + item = item.substring(TAG_CHECKED.length(), item.length()).trim(); + } else if (item.startsWith(TAG_UNCHECKED)) { + cb.setChecked(false); + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); + } + + edit.setOnTextViewChangeListener(this); + edit.setIndex(index); + edit.setText(getHighlightQueryResult(item, mUserQuery)); + return view; + } + + public void onTextChange(int index, boolean hasText) { + if (index >= mEditTextList.getChildCount()) { + Log.e(TAG, "Wrong index, should not happen"); + return; + } + if(hasText) { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE); + } else { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); + } + } + + public void onCheckListModeChanged(int oldMode, int newMode) { + if (newMode == TextNote.MODE_CHECK_LIST) { + switchToListMode(mNoteEditor.getText().toString()); + } else { + if (!getWorkingText()) { + mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", + "")); + } + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mEditTextList.setVisibility(View.GONE); + mNoteEditor.setVisibility(View.VISIBLE); + } + } + + private boolean getWorkingText() { + boolean hasChecked = false; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { + sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + hasChecked = true; + } else { + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + } + } + } + mWorkingNote.setWorkingText(sb.toString()); + } else { + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + } + return hasChecked; + } + + private boolean saveNote() { + getWorkingText(); + boolean saved = mWorkingNote.saveNote(); + if (saved) { + /** + * There are two modes from List view to edit view, open one note, + * create/edit a node. Opening node requires to the original + * position in the list when back from edit view, while creating a + * new node requires to the top of the list. This code + * {@link #RESULT_OK} is used to identify the create/edit state + */ + setResult(RESULT_OK); + } + return saved; + } + + private void sendToDesktop() { + /** + * Before send message to home, we should make sure that current + * editing note is exists in databases. So, for new note, firstly + * save it + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + if (mWorkingNote.getNoteId() > 0) { + Intent sender = new Intent(); + Intent shortcutIntent = new Intent(this, NoteEditActivity.class); + shortcutIntent.setAction(Intent.ACTION_VIEW); + shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, + makeShortcutIconTitle(mWorkingNote.getContent())); + sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); + sender.putExtra("duplicate", true); + sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); + showToast(R.string.info_note_enter_desktop); + sendBroadcast(sender); + } else { + /** + * There is the condition that user has input nothing (the note is + * not worthy saving), we have no note id, remind the user that he + * should input something + */ + Log.e(TAG, "Send to desktop error"); + showToast(R.string.error_note_empty_for_send_to_desktop); + } + } + + private String makeShortcutIconTitle(String content) { + content = content.replace(TAG_CHECKED, ""); + content = content.replace(TAG_UNCHECKED, ""); + return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, + SHORTCUT_ICON_TITLE_MAX_LEN) : content; + } + + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); + } + + private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show(); + } +} diff --git a/src/net/micode/notes/ui/NoteEditText.java b/src/net/micode/notes/ui/NoteEditText.java new file mode 100644 index 0000000..2afe2a8 --- /dev/null +++ b/src/net/micode/notes/ui/NoteEditText.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; + private int mIndex; + private int mSelectionStartBeforeDelete; + + private static final String SCHEME_TEL = "tel:" ; + private static final String SCHEME_HTTP = "http:" ; + private static final String SCHEME_EMAIL = "mailto:" ; + + private static final Map sSchemaActionResMap = new HashMap(); + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + } + + /** + * Call by the {@link NoteEditActivity} to delete or add edit text + */ + public interface OnTextViewChangeListener { + /** + * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens + * and the text is null + */ + void onEditTextDelete(int index, String text); + + /** + * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} + * happen + */ + void onEditTextEnter(int index, String text); + + /** + * Hide or show item option when text change + */ + void onTextChange(int index, boolean hasText); + } + + private OnTextViewChangeListener mOnTextViewChangeListener; + + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + public void setIndex(int index) { + mIndex = index; + } + + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + + int x = (int) event.getX(); + int y = (int) event.getY(); + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + Selection.setSelection(getText(), off); + break; + } + + return super.onTouchEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch(keyCode) { + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener != null) { + if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + @Override + protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + for(String schema: sSchemaActionResMap.keySet()) { + if(urls[0].getURL().indexOf(schema) >= 0) { + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + defaultResId = R.string.note_link_other; + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // goto a new intent + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); + } +} diff --git a/src/net/micode/notes/ui/NoteItemData.java b/src/net/micode/notes/ui/NoteItemData.java new file mode 100644 index 0000000..0f5a878 --- /dev/null +++ b/src/net/micode/notes/ui/NoteItemData.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +import net.micode.notes.data.Contact; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; + + +public class NoteItemData { + static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, + NoteColumns.HAS_ATTACHMENT, + NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, + NoteColumns.PARENT_ID, + NoteColumns.SNIPPET, + NoteColumns.TYPE, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + }; + + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + private long mId; + private long mAlertDate; + private int mBgColorId; + private long mCreatedDate; + private boolean mHasAttachment; + private long mModifiedDate; + private int mNotesCount; + private long mParentId; + private String mSnippet; + private int mType; + private int mWidgetId; + private int mWidgetType; + private String mName; + private String mPhoneNumber; + + private boolean mIsLastItem; + private boolean mIsFirstItem; + private boolean mIsOnlyOneItem; + private boolean mIsOneNoteFollowingFolder; + private boolean mIsMultiNotesFollowingFolder; + + public NoteItemData(Context context, Cursor cursor) { + mId = cursor.getLong(ID_COLUMN); + mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); + mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + mParentId = cursor.getLong(PARENT_ID_COLUMN); + mSnippet = cursor.getString(SNIPPET_COLUMN); + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + mType = cursor.getInt(TYPE_COLUMN); + mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + mPhoneNumber = ""; + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + if (!TextUtils.isEmpty(mPhoneNumber)) { + mName = Contact.getContact(context, mPhoneNumber); + if (mName == null) { + mName = mPhoneNumber; + } + } + } + + if (mName == null) { + mName = ""; + } + checkPostion(cursor); + } + + private void checkPostion(Cursor cursor) { + mIsLastItem = cursor.isLast() ? true : false; + mIsFirstItem = cursor.isFirst() ? true : false; + mIsOnlyOneItem = (cursor.getCount() == 1); + mIsMultiNotesFollowingFolder = false; + mIsOneNoteFollowingFolder = false; + + if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { + int position = cursor.getPosition(); + if (cursor.moveToPrevious()) { + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true; + } else { + mIsOneNoteFollowingFolder = true; + } + } + if (!cursor.moveToNext()) { + throw new IllegalStateException("cursor move to previous but can't move back"); + } + } + } + } + + public boolean isOneFollowingFolder() { + return mIsOneNoteFollowingFolder; + } + + public boolean isMultiFollowingFolder() { + return mIsMultiNotesFollowingFolder; + } + + public boolean isLast() { + return mIsLastItem; + } + + public String getCallName() { + return mName; + } + + public boolean isFirst() { + return mIsFirstItem; + } + + public boolean isSingle() { + return mIsOnlyOneItem; + } + + public long getId() { + return mId; + } + + public long getAlertDate() { + return mAlertDate; + } + + public long getCreatedDate() { + return mCreatedDate; + } + + public boolean hasAttachment() { + return mHasAttachment; + } + + public long getModifiedDate() { + return mModifiedDate; + } + + public int getBgColorId() { + return mBgColorId; + } + + public long getParentId() { + return mParentId; + } + + public int getNotesCount() { + return mNotesCount; + } + + public long getFolderId () { + return mParentId; + } + + public int getType() { + return mType; + } + + public int getWidgetType() { + return mWidgetType; + } + + public int getWidgetId() { + return mWidgetId; + } + + public String getSnippet() { + return mSnippet; + } + + public boolean hasAlert() { + return (mAlertDate > 0); + } + + public boolean isCallRecord() { + return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); + } + + public static int getNoteType(Cursor cursor) { + return cursor.getInt(TYPE_COLUMN); + } +} diff --git a/src/net/micode/notes/ui/NotesListActivity.java b/src/net/micode/notes/ui/NotesListActivity.java new file mode 100644 index 0000000..e843aec --- /dev/null +++ b/src/net/micode/notes/ui/NotesListActivity.java @@ -0,0 +1,954 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.tool.BackupUtils; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; + +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + + private static final int MENU_FOLDER_DELETE = 0; + + private static final int MENU_FOLDER_VIEW = 1; + + private static final int MENU_FOLDER_CHANGE_NAME = 2; + + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + + private enum ListEditState { + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + }; + + private ListEditState mState; + + private BackgroundQueryHandler mBackgroundQueryHandler; + + private NotesListAdapter mNotesListAdapter; + + private ListView mNotesListView; + + private Button mAddNewNote; + + private boolean mDispatch; + + private int mOriginY; + + private int mDispatchY; + + private TextView mTitleBar; + + private long mCurrentFolderId; + + private ContentResolver mContentResolver; + + private ModeCallback mModeCallBack; + + private static final String TAG = "NotesListActivity"; + + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + + private NoteItemData mFocusNoteDataItem; + + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + + NoteColumns.NOTES_COUNT + ">0)"; + + private final static int REQUEST_CODE_OPEN_NODE = 102; + private final static int REQUEST_CODE_NEW_NODE = 103; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + + /** + * Insert an introduction when user firstly use this application + */ + setAppInfoFromRawRes(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + private void setAppInfoFromRawRes() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + StringBuilder sb = new StringBuilder(); + InputStream in = null; + try { + in = getResources().openRawResource(R.raw.introduction); + if (in != null) { + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + char [] buf = new char[1024]; + int len = 0; + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); + } + } else { + Log.e(TAG, "Read introduction file error"); + return; + } + } catch (IOException e) { + e.printStackTrace(); + return; + } finally { + if(in != null) { + try { + in.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + note.setWorkingText(sb.toString()); + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + Log.e(TAG, "Save introduction note error"); + return; + } + } + } + + @Override + protected void onStart() { + super.onStart(); + startAsyncNotesListQuery(); + } + + private void initResources() { + mContentResolver = this.getContentResolver(); + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mNotesListView = (ListView) findViewById(R.id.notes_list); + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), + null, false); + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); + mNotesListView.setOnItemLongClickListener(this); + mNotesListAdapter = new NotesListAdapter(this); + mNotesListView.setAdapter(mNotesListAdapter); + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + mAddNewNote.setOnClickListener(this); + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + mDispatch = false; + mDispatchY = 0; + mOriginY = 0; + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + mState = ListEditState.NOTE_LIST; + mModeCallBack = new ModeCallback(); + } + + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; + private ActionMode mActionMode; + private MenuItem mMoveMenu; + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + getMenuInflater().inflate(R.menu.note_list_options, menu); + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + mMoveMenu = menu.findItem(R.id.move); + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + mActionMode = mode; + mNotesListAdapter.setChoiceMode(true); + mNotesListView.setLongClickable(false); + mAddNewNote.setVisibility(View.GONE); + + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + mode.setCustomView(customView); + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ + public boolean onMenuItemClick(MenuItem item) { + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + updateMenu(); + return true; + } + + }); + return true; + } + + private void updateMenu() { + int selectedCount = mNotesListAdapter.getSelectedCount(); + // Update dropdown menu + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item != null) { + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + public void onDestroyActionMode(ActionMode mode) { + mNotesListAdapter.setChoiceMode(false); + mNotesListView.setLongClickable(true); + mAddNewNote.setVisibility(View.VISIBLE); + } + + public void finishActionMode() { + mActionMode.finish(); + } + + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + + public boolean onMenuItemClick(MenuItem item) { + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), + Toast.LENGTH_SHORT).show(); + return true; + } + + switch (item.getItemId()) { + case R.id.delete: + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.move: + startQueryDestinationFolders(); + break; + default: + return false; + } + return true; + } + } + + private class NewNoteOnTouchListener implements OnTouchListener { + + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + int newNoteViewHeight = mAddNewNote.getHeight(); + int start = screenHeight - newNoteViewHeight; + int eventY = start + (int) event.getY(); + /** + * Minus TitleBar's height + */ + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + /** + * HACKME:When click the transparent part of "New Note" button, dispatch + * the event to the list view behind this button. The transparent part of + * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) + * and the line top of the button. The coordinate based on left of the "New + * Note" button. The 94 represents maximum height of the transparent part. + * Notice that, if the background of the button changes, the formula should + * also change. This is very bad, just for the UI designer's strong requirement. + */ + if (event.getY() < (event.getX() * (-0.12) + 94)) { + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + if (view != null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + mOriginY = (int) event.getY(); + mDispatchY = eventY; + event.setLocation(event.getX(), mDispatchY); + mDispatch = true; + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + case MotionEvent.ACTION_MOVE: { + if (mDispatch) { + mDispatchY += (int) event.getY() - mOriginY; + event.setLocation(event.getX(), mDispatchY); + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + default: { + if (mDispatch) { + event.setLocation(event.getX(), mDispatchY); + mDispatch = false; + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + return false; + } + + }; + + private void startAsyncNotesListQuery() { + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { + String.valueOf(mCurrentFolderId) + }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + } + + private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + switch (token) { + case FOLDER_NOTE_LIST_QUERY_TOKEN: + mNotesListAdapter.changeCursor(cursor); + break; + case FOLDER_LIST_QUERY_TOKEN: + if (cursor != null && cursor.getCount() > 0) { + showFolderListMenu(cursor); + } else { + Log.e(TAG, "Query folder failed"); + } + break; + default: + return; + } + } + } + + private void showFolderListMenu(Cursor cursor) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + mModeCallBack.finishActionMode(); + } + }); + builder.show(); + } + + private void createNewNote() { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + + private void batchDelete() { + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + if (!isSyncMode()) { + // if not synced, delete notes directly + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // in sync mode, we'll move the deleted note into the trash + // folder + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + return widgets; + } + + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + mModeCallBack.finishActionMode(); + } + }.execute(); + } + + private void deleteFolder(long folderId) { + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + HashSet ids = new HashSet(); + ids.add(folderId); + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, + folderId); + if (!isSyncMode()) { + // if not synced, delete folder directly + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // in sync mode, we'll move the deleted folder into the trash folder + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + } + + private void openNode(NoteItemData data) { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } + + private void openFolder(NoteItemData data) { + mCurrentFolderId = data.getId(); + startAsyncNotesListQuery(); + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mState = ListEditState.CALL_RECORD_FOLDER; + mAddNewNote.setVisibility(View.GONE); + } else { + mState = ListEditState.SUB_FOLDER; + } + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mTitleBar.setText(R.string.call_record_folder_name); + } else { + mTitleBar.setText(data.getSnippet()); + } + mTitleBar.setVisibility(View.VISIBLE); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.btn_new_note: + createNewNote(); + break; + default: + break; + } + } + + private void showSoftInput() { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + } + + private void hideSoftInput(View view) { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showCreateOrModifyFolderDialog(final boolean create) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); + showSoftInput(); + if (!create) { + if (mFocusNoteDataItem != null) { + etName.setText(mFocusNoteDataItem.getSnippet()); + builder.setTitle(getString(R.string.menu_folder_change_name)); + } else { + Log.e(TAG, "The long click data item is null"); + return; + } + } else { + etName.setText(""); + builder.setTitle(this.getString(R.string.menu_create_folder)); + } + + builder.setPositiveButton(android.R.string.ok, null); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + hideSoftInput(etName); + } + }); + + final Dialog dialog = builder.setView(view).show(); + final Button positive = (Button)dialog.findViewById(android.R.id.button1); + positive.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + hideSoftInput(etName); + String name = etName.getText().toString(); + if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { + Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), + Toast.LENGTH_LONG).show(); + etName.setSelection(0, etName.length()); + return; + } + if (!create) { + if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + + "=?", new String[] { + String.valueOf(mFocusNoteDataItem.getId()) + }); + } + } else if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + dialog.dismiss(); + } + }); + + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } + /** + * When the name edit text is null, disable the positive button + */ + etName.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // TODO Auto-generated method stub + + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } else { + positive.setEnabled(true); + } + } + + public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void onBackPressed() { + switch (mState) { + case SUB_FOLDER: + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + startAsyncNotesListQuery(); + mTitleBar.setVisibility(View.GONE); + break; + case CALL_RECORD_FOLDER: + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + mAddNewNote.setVisibility(View.VISIBLE); + mTitleBar.setVisibility(View.GONE); + startAsyncNotesListQuery(); + break; + case NOTE_LIST: + super.onBackPressed(); + break; + default: + break; + } + } + + private void updateWidget(int appWidgetId, int appWidgetType) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + if (appWidgetType == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + Log.e(TAG, "Unspported widget type"); + return; + } + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + appWidgetId + }); + + sendBroadcast(intent); + setResult(RESULT_OK, intent); + } + + private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + if (mFocusNoteDataItem != null) { + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + } + } + }; + + @Override + public void onContextMenuClosed(Menu menu) { + if (mNotesListView != null) { + mNotesListView.setOnCreateContextMenuListener(null); + } + super.onContextMenuClosed(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); + return false; + } + switch (item.getItemId()) { + case MENU_FOLDER_VIEW: + openFolder(mFocusNoteDataItem); + break; + case MENU_FOLDER_DELETE: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_folder)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteFolder(mFocusNoteDataItem.getId()); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case MENU_FOLDER_CHANGE_NAME: + showCreateOrModifyFolderDialog(false); + break; + default: + break; + } + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.clear(); + if (mState == ListEditState.NOTE_LIST) { + getMenuInflater().inflate(R.menu.note_list, menu); + // set sync or sync_cancel + menu.findItem(R.id.menu_sync).setTitle( + GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); + } else if (mState == ListEditState.SUB_FOLDER) { + getMenuInflater().inflate(R.menu.sub_folder, menu); + } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_record_folder, menu); + } else { + Log.e(TAG, "Wrong state:" + mState); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_new_folder: { + showCreateOrModifyFolderDialog(true); + break; + } + case R.id.menu_export_text: { + exportNoteToText(); + break; + } + case R.id.menu_sync: { + if (isSyncMode()) { + if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + GTaskSyncService.startSync(this); + } else { + GTaskSyncService.cancelSync(this); + } + } else { + startPreferenceActivity(); + } + break; + } + case R.id.menu_setting: { + startPreferenceActivity(); + break; + } + case R.id.menu_new_note: { + createNewNote(); + break; + } + case R.id.menu_search: + onSearchRequested(); + break; + default: + break; + } + return true; + } + + @Override + public boolean onSearchRequested() { + startSearch(null, false, null /* appData */, false); + return true; + } + + private void exportNoteToText() { + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + new AsyncTask() { + + @Override + protected Integer doInBackground(Void... unused) { + return backup.exportToText(); + } + + @Override + protected void onPostExecute(Integer result) { + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_unmounted)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SUCCESS) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.success_sdcard_export)); + builder.setMessage(NotesListActivity.this.getString( + R.string.format_exported_file_location, backup + .getExportedTextFileName(), backup.getExportedTextFileDir())); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_export)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + } + + }.execute(); + } + + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + + private void startPreferenceActivity() { + Activity from = getParent() != null ? getParent() : this; + Intent intent = new Intent(from, NotesPreferenceActivity.class); + from.startActivityIfNeeded(intent, -1); + } + + private class OnListItemClickListener implements OnItemClickListener { + + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (view instanceof NotesListItem) { + NoteItemData item = ((NotesListItem) view).getItemData(); + if (mNotesListAdapter.isInChoiceMode()) { + if (item.getType() == Notes.TYPE_NOTE) { + position = position - mNotesListView.getHeaderViewsCount(); + mModeCallBack.onItemCheckedStateChanged(null, position, id, + !mNotesListAdapter.isSelectedItem(position)); + } + return; + } + + switch (mState) { + case NOTE_LIST: + if (item.getType() == Notes.TYPE_FOLDER + || item.getType() == Notes.TYPE_SYSTEM) { + openFolder(item); + } else if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + Log.e(TAG, "Wrong note type in NOTE_LIST"); + } + break; + case SUB_FOLDER: + case CALL_RECORD_FOLDER: + if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + Log.e(TAG, "Wrong note type in SUB_FOLDER"); + } + break; + default: + break; + } + } + } + + } + + private void startQueryDestinationFolders() { + String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + selection = (mState == ListEditState.NOTE_LIST) ? selection: + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + + mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, + null, + Notes.CONTENT_NOTE_URI, + FoldersListAdapter.PROJECTION, + selection, + new String[] { + String.valueOf(Notes.TYPE_FOLDER), + String.valueOf(Notes.ID_TRASH_FOLER), + String.valueOf(mCurrentFolderId) + }, + NoteColumns.MODIFIED_DATE + " DESC"); + } + + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (view instanceof NotesListItem) { + mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { + if (mNotesListView.startActionMode(mModeCallBack) != null) { + mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else { + Log.e(TAG, "startActionMode fails"); + } + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } + } + return false; + } +} diff --git a/src/net/micode/notes/ui/NotesListAdapter.java b/src/net/micode/notes/ui/NotesListAdapter.java new file mode 100644 index 0000000..51c9cb9 --- /dev/null +++ b/src/net/micode/notes/ui/NotesListAdapter.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import net.micode.notes.data.Notes; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + + +public class NotesListAdapter extends CursorAdapter { + private static final String TAG = "NotesListAdapter"; + private Context mContext; + private HashMap mSelectedIndex; + private int mNotesCount; + private boolean mChoiceMode; + + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; + }; + + public NotesListAdapter(Context context) { + super(context, null); + mSelectedIndex = new HashMap(); + mContext = context; + mNotesCount = 0; + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { + NoteItemData itemData = new NoteItemData(context, cursor); + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); + } + } + + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); + notifyDataSetChanged(); + } + + public boolean isInChoiceMode() { + return mChoiceMode; + } + + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); + mChoiceMode = mode; + } + + public void selectAll(boolean checked) { + Cursor cursor = getCursor(); + for (int i = 0; i < getCount(); i++) { + if (cursor.moveToPosition(i)) { + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + setCheckedItem(i, checked); + } + } + } + } + + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Long id = getItemId(position); + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + } + } + } + + return itemSet; + } + + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Cursor c = (Cursor) getItem(position); + if (c != null) { + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + itemSet.add(widget); + /** + * Don't close cursor here, only the adapter could close it + */ + } else { + Log.e(TAG, "Invalid cursor"); + return null; + } + } + } + return itemSet; + } + + public int getSelectedCount() { + Collection values = mSelectedIndex.values(); + if (null == values) { + return 0; + } + Iterator iter = values.iterator(); + int count = 0; + while (iter.hasNext()) { + if (true == iter.next()) { + count++; + } + } + return count; + } + + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount != 0 && checkedCount == mNotesCount); + } + + public boolean isSelectedItem(final int position) { + if (null == mSelectedIndex.get(position)) { + return false; + } + return mSelectedIndex.get(position); + } + + @Override + protected void onContentChanged() { + super.onContentChanged(); + calcNotesCount(); + } + + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); + } + + private void calcNotesCount() { + mNotesCount = 0; + for (int i = 0; i < getCount(); i++) { + Cursor c = (Cursor) getItem(i); + if (c != null) { + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { + mNotesCount++; + } + } else { + Log.e(TAG, "Invalid cursor"); + return; + } + } + } +} diff --git a/src/net/micode/notes/ui/NotesListItem.java b/src/net/micode/notes/ui/NotesListItem.java new file mode 100644 index 0000000..1221e80 --- /dev/null +++ b/src/net/micode/notes/ui/NotesListItem.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser.NoteItemBgResources; + + +public class NotesListItem extends LinearLayout { + private ImageView mAlert; + private TextView mTitle; + private TextView mTime; + private TextView mCallName; + private NoteItemData mItemData; + private CheckBox mCheckBox; + + public NotesListItem(Context context) { + super(context); + inflate(context, R.layout.note_item, this); + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); + mTitle = (TextView) findViewById(R.id.tv_title); + mTime = (TextView) findViewById(R.id.tv_time); + mCallName = (TextView) findViewById(R.id.tv_name); + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + } + + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setChecked(checked); + } else { + mCheckBox.setVisibility(View.GONE); + } + + mItemData = data; + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mCallName.setVisibility(View.GONE); + mAlert.setVisibility(View.VISIBLE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + mAlert.setImageResource(R.drawable.call_record); + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + mCallName.setVisibility(View.VISIBLE); + mCallName.setText(data.getCallName()); + mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } else { + mCallName.setVisibility(View.GONE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + + if (data.getType() == Notes.TYPE_FOLDER) { + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); + mAlert.setVisibility(View.GONE); + } else { + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } + } + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + setBackground(data); + } + + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); + if (data.getType() == Notes.TYPE_NOTE) { + if (data.isSingle() || data.isOneFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } else if (data.isLast()) { + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } else { + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + } else { + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + public NoteItemData getItemData() { + return mItemData; + } +} diff --git a/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/net/micode/notes/ui/NotesPreferenceActivity.java new file mode 100644 index 0000000..07c5f7e --- /dev/null +++ b/src/net/micode/notes/ui/NotesPreferenceActivity.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; + + +public class NotesPreferenceActivity extends PreferenceActivity { + public static final String PREFERENCE_NAME = "notes_preferences"; + + public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + + public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + + private static final String AUTHORITIES_FILTER_KEY = "authorities"; + + private PreferenceCategory mAccountCategory; + + private GTaskReceiver mReceiver; + + private Account[] mOriAccounts; + + private boolean mHasAddedAccount; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + /* using the app icon for navigation */ + getActionBar().setDisplayHomeAsUpEnabled(true); + + addPreferencesFromResource(R.xml.preferences); + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + mReceiver = new GTaskReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + registerReceiver(mReceiver, filter); + + mOriAccounts = null; + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + getListView().addHeaderView(header, null, true); + } + + @Override + protected void onResume() { + super.onResume(); + + // need to set sync account automatically if user has added a new + // account + if (mHasAddedAccount) { + Account[] accounts = getGoogleAccounts(); + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + for (Account accountNew : accounts) { + boolean found = false; + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + if (!found) { + setSyncAccount(accountNew.name); + break; + } + } + } + } + + refreshUI(); + } + + @Override + protected void onDestroy() { + if (mReceiver != null) { + unregisterReceiver(mReceiver); + } + super.onDestroy(); + } + + private void loadAccountPreference() { + mAccountCategory.removeAll(); + + Preference accountPref = new Preference(this); + final String defaultAccount = getSyncAccountName(this); + accountPref.setTitle(getString(R.string.preferences_account_title)); + accountPref.setSummary(getString(R.string.preferences_account_summary)); + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + if (!GTaskSyncService.isSyncing()) { + if (TextUtils.isEmpty(defaultAccount)) { + // the first time to set account + showSelectAccountAlertDialog(); + } else { + // if the account has already been set, we need to promp + // user about the risk + showChangeAccountConfirmAlertDialog(); + } + } else { + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; + } + }); + + mAccountCategory.addPreference(accountPref); + } + + private void loadSyncButton() { + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + + // set button state + if (GTaskSyncService.isSyncing()) { + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); + } + }); + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); + } + }); + } + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + + // set last sync time + if (GTaskSyncService.isSyncing()) { + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime != 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + lastSyncTimeView.setVisibility(View.GONE); + } + } + } + + private void refreshUI() { + loadAccountPreference(); + loadSyncButton(); + } + + private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + dialogBuilder.setCustomTitle(titleView); + dialogBuilder.setPositiveButton(null, null); + + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this); + + mOriAccounts = accounts; + mHasAddedAccount = false; + + if (accounts.length > 0) { + CharSequence[] items = new CharSequence[accounts.length]; + final CharSequence[] itemMapping = items; + int checkedItem = -1; + int index = 0; + for (Account account : accounts) { + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + items[index++] = account.name; + } + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); + dialog.dismiss(); + refreshUI(); + } + }); + } + + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); + + final AlertDialog dialog = dialogBuilder.show(); + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mHasAddedAccount = true; + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + }); + startActivityForResult(intent, -1); + dialog.dismiss(); + } + }); + } + + private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + dialogBuilder.setCustomTitle(titleView); + + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + showSelectAccountAlertDialog(); + } else if (which == 1) { + removeSyncAccount(); + refreshUI(); + } + } + }); + dialogBuilder.show(); + } + + private Account[] getGoogleAccounts() { + AccountManager accountManager = AccountManager.get(this); + return accountManager.getAccountsByType("com.google"); + } + + private void setSyncAccount(String account) { + if (!getSyncAccountName(this).equals(account)) { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // clean up last sync time + setLastSyncTime(this, 0); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } + } + + private void removeSyncAccount() { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + editor.commit(); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + } + + public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + + public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + editor.commit(); + } + + public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); + } + + private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + refreshUI(); + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + } + + } + } + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, NotesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + default: + return false; + } + } +} diff --git a/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/net/micode/notes/widget/NoteWidgetProvider.java new file mode 100644 index 0000000..ec6f819 --- /dev/null +++ b/src/net/micode/notes/widget/NoteWidgetProvider.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.widget; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +public abstract class NoteWidgetProvider extends AppWidgetProvider { + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.BG_COLOR_ID, + NoteColumns.SNIPPET + }; + + public static final int COLUMN_ID = 0; + public static final int COLUMN_BG_COLOR_ID = 1; + public static final int COLUMN_SNIPPET = 2; + + private static final String TAG = "NoteWidgetProvider"; + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + for (int i = 0; i < appWidgetIds.length; i++) { + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + } + + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); + } + + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + for (int i = 0; i < appWidgetIds.length; i++) { + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + int bgId = ResourceParser.getDefaultBgId(context); + String snippet = ""; + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c != null && c.moveToFirst()) { + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + intent.setAction(Intent.ACTION_VIEW); + } else { + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + if (c != null) { + c.close(); + } + + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + /** + * Generate the pending intent to start host for the widget + */ + PendingIntent pendingIntent = null; + if (privacyMode) { + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + rv.setTextViewText(R.id.widget_text, snippet); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + protected abstract int getBgResourceId(int bgId); + + protected abstract int getLayoutId(); + + protected abstract int getWidgetType(); +} diff --git a/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/net/micode/notes/widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..adcb2f7 --- /dev/null +++ b/src/net/micode/notes/widget/NoteWidgetProvider_2x.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + + +public class NoteWidgetProvider_2x extends NoteWidgetProvider { + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); + } + + @Override + protected int getLayoutId() { + return R.layout.widget_2x; + } + + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + } + + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_2X; + } +} diff --git a/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/net/micode/notes/widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..c12a02e --- /dev/null +++ b/src/net/micode/notes/widget/NoteWidgetProvider_4x.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + + +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); + } + + protected int getLayoutId() { + return R.layout.widget_4x; + } + + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_4X; + } +} -- 2.34.1 From 1d2f5c1d550dcf1fba214a8556f90591433a325d Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Wed, 4 Dec 2024 23:25:49 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/net/micode/notes/gtask/data/MetaData.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/net/micode/notes/gtask/data/MetaData.java b/src/net/micode/notes/gtask/data/MetaData.java index 3a2050b..b0bbe62 100644 --- a/src/net/micode/notes/gtask/data/MetaData.java +++ b/src/net/micode/notes/gtask/data/MetaData.java @@ -18,7 +18,7 @@ package net.micode.notes.gtask.data; import android.database.Cursor; import android.util.Log; - +import javafx.concurrent.Task; import net.micode.notes.tool.GTaskStringUtils; import org.json.JSONException; @@ -27,7 +27,7 @@ import org.json.JSONObject; public class MetaData extends Task { private final static String TAG = MetaData.class.getSimpleName(); - +//调用getSimpleName ()函数,将得到类的简写名称存入字符串TAG中 private String mRelatedGid = null; public void setMeta(String gid, JSONObject metaInfo) { -- 2.34.1 From c17b8df8b72339f706ac6dd9431e93d3d7e1e3f4 Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Fri, 6 Dec 2024 21:28:35 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E6=B3=A8=E9=87=8Aing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/net/micode/notes/gtask/data/MetaData.java | 15 ++++++++++++--- src/net/micode/notes/gtask/data/Node.java | 1 + src/net/micode/notes/gtask/data/SqlData.java | 9 ++++++++- src/net/micode/notes/gtask/data/SqlNote.java | 4 ++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/net/micode/notes/gtask/data/MetaData.java b/src/net/micode/notes/gtask/data/MetaData.java index b0bbe62..0789be1 100644 --- a/src/net/micode/notes/gtask/data/MetaData.java +++ b/src/net/micode/notes/gtask/data/MetaData.java @@ -31,33 +31,39 @@ public class MetaData extends Task { private String mRelatedGid = null; public void setMeta(String gid, JSONObject metaInfo) { +//调用JSONObject库函数put (),Task类中的setNotes ()和setName ()函数生成元数据库 + try { metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); } catch (JSONException e) { - Log.e(TAG, "failed to put related gid"); + Log.e(TAG, "failed to put related gid");//输出错误信息 } setNotes(metaInfo.toString()); setName(GTaskStringUtils.META_NOTE_NAME); } public String getRelatedGid() { +//获取相关联的Gid return mRelatedGid; } @Override public boolean isWorthSaving() { +//判断当前数据是否为空,若为空则返回真即值得保存 return getNotes() != null; } @Override public void setContentByRemoteJSON(JSONObject js) { +//使用远程json数据对象设置元数据内容 super.setContentByRemoteJSON(js); if (getNotes() != null) { try { JSONObject metaInfo = new JSONObject(getNotes().trim()); mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); } catch (JSONException e) { - Log.w(TAG, "failed to get related gid"); + Log.w(TAG, "failed to get related gid");//输出警告信息 + mRelatedGid = null; } } @@ -66,16 +72,19 @@ public class MetaData extends Task { @Override public void setContentByLocalJSON(JSONObject js) { // this function should not be called - throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); +//使用本地json数据对象设置元数据内容 + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");//传递非法参数异常 } @Override public JSONObject getLocalJSONFromContent() { +//从元数据内容中获取本地json对象 throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); } @Override public int getSyncAction(Cursor c) { +//获取同步动作状态 throw new IllegalAccessError("MetaData:getSyncAction should not be called"); } diff --git a/src/net/micode/notes/gtask/data/Node.java b/src/net/micode/notes/gtask/data/Node.java index 63950e0..48dee83 100644 --- a/src/net/micode/notes/gtask/data/Node.java +++ b/src/net/micode/notes/gtask/data/Node.java @@ -21,6 +21,7 @@ import android.database.Cursor; import org.json.JSONObject; public abstract class Node { +//定义了各种用于表征同步状态的常量 public static final int SYNC_ACTION_NONE = 0; public static final int SYNC_ACTION_ADD_REMOTE = 1; diff --git a/src/net/micode/notes/gtask/data/SqlData.java b/src/net/micode/notes/gtask/data/SqlData.java index d3ec3be..b502e04 100644 --- a/src/net/micode/notes/gtask/data/SqlData.java +++ b/src/net/micode/notes/gtask/data/SqlData.java @@ -36,6 +36,7 @@ import org.json.JSONObject; public class SqlData { +//得到类的简写名称存入字符串TAG中 private static final String TAG = SqlData.class.getSimpleName(); private static final int INVALID_ID = -99999; @@ -72,6 +73,7 @@ public class SqlData { private ContentValues mDiffDataValues; public SqlData(Context context) { +//构造函数,用于初始化数据 mContentResolver = context.getContentResolver(); mIsCreate = true; mDataId = INVALID_ID; @@ -83,6 +85,7 @@ public class SqlData { } public SqlData(Context context, Cursor c) { +//构造函数,用于初始化数据 mContentResolver = context.getContentResolver(); mIsCreate = false; loadFromCursor(c); @@ -90,6 +93,7 @@ public class SqlData { } private void loadFromCursor(Cursor c) { +//从当前的光标处将五列的数据加载到该类的对象 mDataId = c.getLong(DATA_ID_COLUMN); mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); mDataContent = c.getString(DATA_CONTENT_COLUMN); @@ -98,6 +102,7 @@ public class SqlData { } public void setContent(JSONObject js) throws JSONException { +//设置用于共享的数据 long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; if (mIsCreate || mDataId != dataId) { mDiffDataValues.put(DataColumns.ID, dataId); @@ -131,6 +136,7 @@ public class SqlData { } public JSONObject getContent() throws JSONException { +//获取共享的数据内容 if (mIsCreate) { Log.e(TAG, "it seems that we haven't created this in database yet"); return null; @@ -145,7 +151,7 @@ public class SqlData { } public void commit(long noteId, boolean validateVersion, long version) { - +//把当前造作所做的修改保存到数据库 if (mIsCreate) { if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { mDiffDataValues.remove(DataColumns.ID); @@ -184,6 +190,7 @@ public class SqlData { } public long getId() { +//获取当前id return mDataId; } } diff --git a/src/net/micode/notes/gtask/data/SqlNote.java b/src/net/micode/notes/gtask/data/SqlNote.java index 79a4095..c10e219 100644 --- a/src/net/micode/notes/gtask/data/SqlNote.java +++ b/src/net/micode/notes/gtask/data/SqlNote.java @@ -39,6 +39,7 @@ import java.util.ArrayList; public class SqlNote { +//得到类的简写名称存入字符串TAG中 private static final String TAG = SqlNote.class.getSimpleName(); private static final int INVALID_ID = -99999; @@ -123,6 +124,7 @@ public class SqlNote { private ArrayList mDataList; public SqlNote(Context context) { +//构造函数 mContext = context; mContentResolver = context.getContentResolver(); mIsCreate = true; @@ -144,6 +146,7 @@ public class SqlNote { } public SqlNote(Context context, Cursor c) { +//构造函数 mContext = context; mContentResolver = context.getContentResolver(); mIsCreate = false; @@ -167,6 +170,7 @@ public class SqlNote { } private void loadFromCursor(long id) { +// Cursor c = null; try { c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", -- 2.34.1 From 72cd45f1a2f883b319b4a657ce5c0b2a68c2165e Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Sat, 21 Dec 2024 13:54:38 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E6=B3=9B=E8=AF=BB=E6=8A=A5=E5=91=8A3?= =?UTF-8?q?=E7=BC=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...米便签开源代码的泛读报告.docx | Bin 16698 -> 229030 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/小米便签开源代码的泛读报告.docx b/doc/小米便签开源代码的泛读报告.docx index 92cdf698ed0aae8d6f97df93d97d27e6fe08e660..14a9a9d0fd33256d7dc9a07aac7f0d3103cdfaf3 100644 GIT binary patch literal 229030 zcmeFXgL7?9^gS5cww=7#cJiVZ+qP}nwrv|Pc3y1THs^l6GgC8DGylPFZq>QB&aFPZ zyZiL+z1G@WUK$h(4G01V3J3^@7-*4_CM_Nq2xt`o2nZDj3Pel9&eqw))>%*4!`{S6 zhu+=Bny3&Ageo5h1knEf|NJk!0?jFta)XRWVlR@PB8R^$wbhA&GaGjsf~C+RYA$3n zVf&2#6nuG&$l?~z*!Z_*N#0HMysRcr26ctZtx*P-Xa8C?**3#gKa(;k1NXuec(PT; zP*_zoH!IMW4!^W@_hrXo;2VlwWXG@$J+O`6RD( zbN{?1!%?x;b6@7oT?n2BMbX6VujY$z7`8I>nk1MvMrh}28Z0?OPByOM@rR8jr;Z9W zbPRvT+b-FVq~7>xcLi3#WoTcF$P6j>gDtBXlqp(vTpHv7Yw0*52#xuS{fWkEM}{EL z`vIZ2(H1naLj^HGUjFk{ETkw(Hi-V!X2dOc(n^xTC*uuR1(h2e1x{?-oVuGZ5j;bA zUf51#%5<;fxweiVPUs%t#`aWg@%5YDRyRdLHR3j5af7<>+sdVO@&-P$s0% zo4_C9sUDsU8j|@fUC~on<1b(l=8u3Z#Kel$pKYp0`o)$o0v3;xaTbx~tRDO&+EG?_ zwf;sbp@xNRxZ0}S4iELsKq=vw*;9Nq`(3cC_j|*yA=2NrQeQ}k6KH8Pr(y&6hVy({ zQXziF_aT=@k8$j^;FX~7U0Uu@`hMZX#E~JGBzb>qe4l< zN&-s@_RgrOaYW%!K;d!S=ae%%*&m-~!PRRV=WaZljKfYy&b!BK?E+RL5>?yO-GR>t*MK>>lI%i7xM8t7zVZ8i^CPS&a zo=6g-u_`0-CU?68_=BR5&K&k!SJ)g#_!Aa+QyS()Hj*By_8Ex;6nS6!s-PH7ykc;4 zuAi!)CwWyr`BqozTtE4_T*;v>(95K4h>+eZ#c?G=xb*9s_PUZ2Eas1lX$s8NiK(pyE zGH-Hd27^nN;n%AL;kv+*>Io-m)uA|VWRU~G=F!MB$rI~mbT8$1y`=tQ=%|IJYdFQfN!p@pdRVRAH6|pF!qm;j;bZqzJOFSL?hV*X{gg z@#RES3pDhOrr0pEi>$P^wq6VGFY$v1rySa*d>&rJax3=RGeQXHn2Ablj%;LJ=Kl z6>Cl;EsGi~-f2uZjLd(67i&=+$9!16a#eyY+2f9E$N$o_um7~)3xk1CW|+`_FV_j{ zs^gT_5RR?{7n&`5GnA8C=bh_m!IVHCR{F7t6+ygYE={(5ELVMxNl&Zysd|~Q%=Qaz zv~t)wp<8@5t8^{W54qC6*Pb@!k5k*(Szmeoj64_N#>Cl?E1)bfqAw6Lkpn{v(XeeK z4>2;$J6tR`NQGq~Ph0po(mB!=$`JoZ-@~R&Stg37zHpD#lN$X7=i!7bj0ic2f_(s5 ztI*-h6MT$>#tZREcAXFLnu^Q`l|le2k{LU|)nYKjQ%`?~c)qD}T()Fo9U9vmvZVgpGr z%px6_Ws4Oi{Vb|;@71~r$c7d}$#;+@=;Q1D3+&|e5R}97Vi{Kp@dtN>!}7$h_UqB) z>k_dgjvkDGG9IrPO)!ES$3P~L6r>J2dX1UIIa)<$lTUv9&U{I$m3AQYlf~q^waTPT z-hJtx^5?9Fgs4~be$Ld%rj*Q3j>T$;cJKfZPW{)R=~qnGlXO}eq237rYk)KKL428H zFkYGtGrmwh?G>BhDA-gmGf5`SndYVvy-DRY;ij+$9JB43o6UjTRZpK2@D0xU!NaGV z&6lT*)kB`yPPva)t;5^Z@&|8XrJqBS17Ac3Xc!F``8i<>T)9e85OX9on1SfRuYL*U zzO>xbXo}(uy(Vs7)pt6W6!nOY{4aOxT5bU)zPYjvJpBV}yY8I=(rjB;SrepKwo&;A zY$pnslHgNw_se+nQp78UE?;pXuXFdAK_-N)mPyR>w!V$+^~xT%q$-W8Ppt?M79eEB z1PqZGD7Hz(e6E=pNGCO#($zp zdf)Rx8n7&BF^p0-T{eDJsy0luP^T6WTZYBvgMk@#80Q8XA{$GH`C2vQs(}S=+efHDebYTU_ zQ99cRw`X6)TTPIzfT_7(pmB3fO4l9ulSmjS708vPLCT%dz5{dWSj*CHmb^ka!qNB* zJ?rl+IazHWuz0##;z95+HBK=#ZtXQUCam%^MAtI;k&-S{!JbpHWjI)zXj+k^x^b=$ zbV(NK8RWK#W(ws)6Lcw!dO6o;)mj)VXYV!7yARYbj6kzSBI=Q{bYS7_Jm~6mW?~0C z8Qump`|CQv2_W~74U268SD*JfS9@74F)?7j5Rgd38TEN7kiaR3z$_Z}koMIn$m}gW z_v_lfsp}jKFNJM+-|HJ(NRjM_&cTF3xmo;?IjWq4*vrpD@e$YI1Z7~uxnx=Xa6Ke@ z@g9M_RWS_c>tWM$y6hU2wF<8+Rv$KVCrDFqxeg!jhi@+nQYo*qWy z4Gkaei7`1PF3_lLR%&sb%@F2DE8Aqhe0PU^&Mf+%y~xcy{$Yj1a(xNh{3n4a+WGq3 z>3*^>8e_*{M2gfcu-g9gE%2}R?(y;@rf%pXriS*3!H!H(ItnOcDAZ>D(LMa4%c3Bi z7%9sVBjj8eJ5Xb+t7MS{J!~81`m?6@+v>nibp2O${^OeJ)!Y*25aQpylNvN}#@)7u z$(L-A1cJo1wOMw7P9$l9CBGwqK8)qg@27=-y*4pW2$O;VJABuvj<&dY)eOg{ORXJ@0`jOp z!TK>VR32+_7Jb>&cd2qd$IAw7S40vWPS4(xt)MGQsp>y@U51j~Y*3BgVpm}=R+#I_ zYMY4O23GnPNp7ue*DJl~5ExjxR`3Q_X zMlwAk$6?UVS$;fNJ;3py>ekUxL;MgwLU3X`uth%=C=iI=LH#haM9f9 zXbZ7G`*X9VN!9p}ul_p(wJQX~H!xHkk9$hKKmAGX5^bofv;*Oa zrm+r4_<~s$op{gEMI$}>^<)`X#fV!&;YyI}Q7?RTNGqA3#*};O6vgJLgZnJU4TkI7 zUa5^?LPgjamk}-Zd7s|Iy^a*%{vWBMgmIRAzG^%_R?fXXoNeO#@TK?R?V``?Nl=T; z)}@?A@ZHo(4Jb;)`c|Pq5=TLhF*u3Rx-!qWX@?sWO5;ap3ct#?yo^q=9$Id8s{NjH zzP3iSUt*nI?(=6qD}{qkySGa()rr}AB6?k zD_q-`K2vunO=~m|oUhd2;Po+w8Vh0AIeTRlL)f>o{kl^gX>Dh1Yd3O|QH*@vqx&S~ zr|r45)wJpKm`4!o+F%KhuavTnlPkx_^ee!DNkT*roaBzPc6JH~#>F|HCIP2HSSDy< zNkz#(dvvR^O?A9;lsV-bOSPS?R@eP~c*pW&$MW;OWz7BOR`zRh(x$!Z`{h*?2vo)S zbL=7`$qQrk@sHWbxY-8o&u%w(MtjZ=59RZWI~~eX%d|Al%eGXj1MqM#RjKk2Q$?BK z8J4DFRW2sCUF7a1Od!>u@G!(<2O4`kW;>*GD-!8#qr0E8S>LO{l z{7T7C8KrmN_#0Ss z>>}U?>kXA0+b2USgGx=?d^U5T~cWcs6Cq(;ds=aPCp#J-LfPka~&qG7pAOvn69wMfG%cL^z6Z0TIKkn-ryW#pZ5*HATYQ#ZIG4RYO z;ox7JC0Nwpk_S{7jDsRsDn7*UZtFnz;Q>bC>-ivbpW_-HR9+e9qJuSzk!%G)!bSr` z10*unmWk5e1T$-?YY*le# z2-yR}AF|=wZSYYHDxr)NdQ16UyZkbp_0%5T&7n^Aj+JEr=NKI;RPiPZSBuObIH7C<_b!p_!3`7uFMu3O^KakYRK8!})hs3^jruLWk$+L51rWiMj9 zVCppr)RMf zg(M?wG@ss7YE7k%q(%plh7}GOJRH)Np74+gip|AP`b1avkQw7-J&N>{X`qrMlg&#! zcNnx@*kt3bEj4Yc+79wYijaxYm`{MxQ})40TDDF>cgkbUD5awtF0?+9>4x43LJE6M zR~r-erk91XKdLX&=jaeNMaF|I&xKRGbN_)QiZ+3V!D9_|>6mL56ps`iZ(IAmeIi*^ zJLiR}(RVWKsn%~*leBo~_IOj%02{Ig01gBE$RqE>(kQQvLtErUgK_h zAJQ(eRuOf2$y*{BsirOLuOhLY>wQSztE~f{nuc?fG(F{*ih`wrX~Vgiwm=sRecD^q zXa_4;%^2u!x3uI}l@!w?4c;DY>d3zB`}<9c3THFnl1V6h1oU(u6>D=s8b(En*|Y6m zy2ti+ffwi}L*Qny4@4#@TyKhOhox^)V{(7}yAOXRIYNPCpekomtbjTF6 z%A!1BcYYk+{sz*bKo;5Aba5EpN@VU4E+UrfC*`9W)+Mlb zDIQc4i#xP=`Gm&fQ>iCu&CdZ36nVP=g@bS7*gh%T_Hgv^I;dWCKAJDdp?qoO*s0aw zZ|k{>TMv2I&o*6?O@Ne%$dwwviVw477jKxetJIKWvG{vkuOGu!VIl16jYJM3u3~-k!FQEKw~Nb z`xjV$20b9Cq&^zh25KP|#E3n4*c4#@%zk}cBQ?y^a&GFlWSZ5cbjFfs)F|80dv9KW z?J=(U#ZY_>WIpCTqwN2N2R!8~w*uuA3!H^qdpR3?>=!k^3A0=&T*VAtm2DS^mB?qK zF=a-pyQK9TXWX+768CYh(YBV^{fih%#frnCg$Ud7JB5xOjF5QnpAbiuI_q$t8!I^Xod zS0O1YkbTIMD$}KZuPABlYvUBTA*eoKs94Mm4O;rgBswd`?v;bk=1x$JH5#0ATVV&J zP8K%gT`xuuT>K`xp}4%cKx2%`Nh7A$g5m+7Ezc$_TT3IQSO>2U<1Cp1)z#ucN z7c^LB%~)rr%ggw1@+*hC7*!JoW%B|KLAd6F6bok!j79zo%hB3H*cK9h_C^2`$$2A9 z_ER`TKf~`MWbBw_npBliljN4~grON&TF86LcI|{k4rCU=kf2h_rLXSXK-M>sr4m)) zclVV#WJXq2GTksJWwG1{kZcGv#cag?EKd``6!CCSE_|e#AMeIcm#IcRNK#a9m|4>Ak3tl0%sWs_}&zJL0 zV)R(aZ+7(ybZP?MBOzH7BK9U{&~WM>8&o!UnE0Y_TLrr5IMxt$Qr{^)9vF&X#MPLU z@<1%Rs8lPculC@C48lAV0s0a)XkWY;$Iqg%J5pKWWli>2VyIx2D=y!m{KCnE4UM{9 zCy5*aY`sXonjM=1NZV%OV_2YS>{wnQcl#9RJjrcCrEFltKZ~#Yhay(SZ%Fj*(sW~PvRXt7EdZ&*|rJVCDcXO@&w2?++Yt>flWn2N;h}In&TaY$M8%M$%4J^-PC&LX^ zggQ{Hfjh{Mv){Kdyl%f`Lbr23w`wJxs`{)2I#H4#?ZHdRj(0G}maSaz{%O2dSt;>}1S^lM`-Mbt}iTDj0li;&Ba zOh}7#*c8Vx6!zD%)rAI5z8Q}G$(&AuiW?;=e|?W{D+j;1NloG{YiN(JJEX_M0YVod z8~TPGO|ahpiF8Pxy<=H!o*FvYUovA3H||6KK7Ds|hxo|Pqm--^wT@bJZH2;jwOF)! zl2aVX=?PEpIuwNWCsZ*}8gFIGXZNg}o z-_BBjdVUDLY$^JuN z1+!i{{U~bIN$9IF&LVf-X1mm$m{DNN)WY_4^qqy3RxNnTUa}tk2j9?lH;bBczN1)6GZh@W8Tx2s8*bw3Y&Ql z#c;wfFW{HpB=C-0C_%i zF`E}*v^Z!kwamYLmHK;VCoWPbt+)9r$ zG(ue?%|PQ@9=QZ9!#3s~^uloe10-i80vSG_@Q*DQE&JMK-R$Fx6E$u(nt5{Zh`5sl zjF5YNWQr*RJ@X8%Kg?3bTzI&7tsu5JU;s|SrIT6)vA;MESt!(E1o4oM73!K$r4+Vo zc%(!mj%>=Fo0Uh{8NI^L@(r#mY{X(~UPXzZ)ZN@cvjMJ>)02bsk~qEDA(Pr)V2n0&Gs)djRI3$03*3E=XmTZKS4F#J))lx)F3gj|kHviBk5xxOQIM@o)6$Ev_ z6k`^Gsg+lb=Kh4CmbIJ>SXhgID=-e8DX>zI4VlK0$J>N1h)^b^%c2{NDJ4-{x`(*E=|-99CL_8|I19D$JT<(X4ge7^bfn*9$y=V?WR5X#fIvR8@#3 zSy61kv}#FWEE%n1$v4{)^j7oD2FL!i9-+lAwaQ|JDrrU*aVjIA~^dFaLP z{qP^m_GdNMDd25t8M$0{#&7sR(~6P z58p)=@ExUAeZ@JK1u_xaGq#|c<0({BA~1wGdSJ*7@~TRM>Y_7T$ZODE>}IG&eU6Yb z;`BvIgv`ou)~gKM&g@2)uR> zxa~obC}9?ufJaz3oViRUX&Yo&tKGKztsP^GQTDRdX^LjhuDQ4qZfj6Y&0pmQIoSMd z)IB*aE&$ew?4+Blr*VGXCL!SfV?9sgreM1@Shs&9Dzd9YQi&rJ5eT{oz)51#A`fM5 zVfizZx0}AY>V^!7jRj}s@Bw^qiFKF@`ME66+xH+;*2H`;FwknjwJPP7OUytj_*$%%f5gSlKLAppTYcD3ZaBM2Kx33w0JUm-y*yRAiki^pT`zQ@Q zO}w$HE;`BXEkXf=@nHjvE2)A5Rowl=B(}op%Y-8>a0TQ2r`0y ziu`{sEBU`C%Ky(26JS7%7@+n4+Mn)}Nz4DKkqY?^n(Ix?Q6n50FGXv#Q94y^7;P;l z$F`Vf_uDBTjuk;o2)T8voy!@_N#ND{E~j`0xhS5)mOcm;%XduOA;oG zS;&OL+zL9H8nu!2M#~Cv7GGs@LE<<^Z^UN!G1M_>B|k7+3NEWBWt1XFV>7Ik;1>z= zO7+`x^ObM*{g`{FUio~M(5H-D39asn8QMMN4K0Za7iL?pND!Y z&FxoDNfZ#!e-}F7-~ZW^HYUav1`HN924*Hq^!B!95%O{p@Gv<4Jq7<;QdAKL2)F@o z%|iWG!`rch5b57zME{Y_PPih9`t8|Y6&)g!F{eAW{->jhr`9-sKr<+bm!#lzCo;pO&n zeckc%vuuAN_c1fYVP-m$9ntOibpCX)@^CngYT(}3*!cZ^QP=J9ATBQ62}WYR6&ho; zV#n)wxw=HV5?vt4qE@Rt!g>ecaAzfx#NV6if-jVST+3BSN+K&79T}ZS9)(!UrR#QR zUS9NpSEm;@flf|OW9h+1OG6_S?yo8V59EN~MA9ga9KcD^SgY3+O9LaamCO!Ok=c*u zC_1IBz=c6>IF2+M8Ay>2OeqM&RY=c#iWUPYB&oLKe#n(sRkW-<=RSK%hCa8U;_Td; zNFF&lJWP?spjTz;GSumQXILN{5^Muh02pV&6AxH{88Bg5d-+$3)yi!akDIL#(!8ji zczi>dRXf){iL6mx3DF{&(8xSVxM!z=uDjpcy$M(WiLfp31C-OyXgR5GNb%wC*l?<1 zx`{a`{QE*<=uSze?DUL`wU;x@^QGU^Y0B*M?5e8|Z{(zZp^!k->U4P=&JP1X!=A4< z{M@>DY%M)OI|-Z&B^~xdWwXM%KlaKE^o}Oe;x_mI54Z>ZEs;)3t<&z=Z4<~(dOUrW zA3lVsri3v|3RprdGG`WkZ#wz6?E@*5Mnj&3?={c=iYrdEi4-0iYdhC%@zdK=^dKhi zH1JcS(h`?lckEMiyxSD%#4-U`hL4HS8iaw z4$O*HglO=ENhDDYsd3f$xiPX1mV%D5OL=dg#gK*qmP@8+bv&$s_@f>*eKI)65yr=+Mwmp;*-QP9LF(Jpzq(yL}F~i#qgE z?|}o1tN89zM$=9VUW?sMKNTBLQ2Cdi~XU9hm(JG3PmHGE*B^$D2a)QuGgC$D>`ll zlqZMdNnr^1nq96oHJU9GX$*0iDfBuxeMuCum5L=&mpiy@8*O%@lWp6cu*Q}r5Yr|` zVBHcxux9E8!J){QxC_tAS~$yfkn8(CCqQ+So(yUyXo77gGdV%$kTti96-tl@c>dGC zueE{!AYRXxT#koOsTl<<*V{d~LaT035fQ*i`(E7zCqogatn_z-P{F()>C8Kemm`?D z9)${n?>Bz;!|A?Ju&lIB6SRu`~%3 zFBU6M%WF)hGXdkJ#i}Xaa!zJ@*~!o3#^V#Jj20ZirZz?Ya!jhcUIitk-pSN-B-TRt_b8&P)P|(w zf|~{Hn@pz%oL>h*V6)h526o;aPVCm3&-Mnx3TT8wvU%LDmus|WmCKO`2@#Ft?Du2w zd7iJgcnk)DJsyq?wqqbOje0zv-1+9RY)2e|V1Xa!3&mjCx+}1o1L6dJ&TkWnOr+8R zkLns;m?=F623+?G{aexFPx6Um7Dulh$5De57tY7nu*P+&YDVv%SBuYNyg^Bo75dM7@g!z)?r zb}sCPP|xdYeV?k1B+0#%0J4tVK&0Mi{2R_w9=gl*YQ5D)tD1}+;6G32OS__UZ1j4) z-go=MMEs{IE*Hzg>m;WrixD>1r|F#O6?P&UghGHEnLZbYx`~ya*#;%SjRq$ZaxoA_VsHH-jQOlAkscfXr%y>j{fHeJ6K z$vshXMBzMG;|7_Jl944=c^Yn__vGrb1>bOo(V$kT_LqexglPvJFTuMFn*o5D9~n(wX!g$0yBy{~BhOQ<}=7?W{NGJM^Ok zfspfanQaG#?)qdx;Sc!SwgSdm47*fY-DD74jJ49X(*tQ8MdM{U8h9o0gSi8ZWez1t zVFFNpXH$ttjYJ6i$$Y=~+#xt$$Ce8OS=HvW5-0LTT21XdxG*5i+?p-Es;Pm+pxQmp zlX;kVZad!F51QUU6=C)88X-tHha{*a~@iH#C9KM;nk$OAE0!kbiX=RQGW^V z5PKj?D-lDbxTu#G*Zv)gaCN>j3{13|8yP`etZy)nYBCrh7ew)|AsYV&cA0Y*PVNwt z(OZ*xB^mja7Gzd|UZ)ohH|MpzJ=^rsUIRN5F`SF<%UN-aIfuJB%*9OqlTt2|+o=C0 z3UH7hh6ckU!TP?jt8XAjAdZfG8M8#Om#ds9uhQ>EMNGitN5Kg7n7$W#~>q z#RLWy%-j^6^2SHMQXv9UTAF5mGJ-?{k?J#m3956Mu*}1{)Go+v&t`}o1QBubntQKy z6!hSLq=f>^;FX05$sUHy9uXHNIpE_HmxZs@#c40OQF>_g7XxcF+l$8-7} zl~V2-(7tLUgXx>QWVCWz!C0G?-s}!ew_k^R`P8 zhwn*Knk}){`jETnee&E|rv|2e%V06{(`q&z7#gqp-icbOQh}Dl2FW^W+xfA7cOF6Y z@f<+c7nEwW`G{>m^^KrbhBEPYy75o$*Dh6cFl_ZHw4FY^53>ceaKAzQ7#fs_)u+V2 zhk_y&Mrmi>`;L84mZ6V>t)7Oa05b%SL;}{))}~`c+Z<`Km|R8@oEJrB)T>o(amhcN|!D!|)yxtjn}9ic*iY@e4HF+~$XT;fzPeXil&-?tC8dUHCeqfNgYR35ue z=KF)>2ET7eq@8^9Z;AC^6z092A*W2OJc3-FaBt~zHPI~tAZ}fY2BxWcrOK;RRzulm zwVkM&&m~l2$HcKx<<}Z7EG#%UHOHyukGn!oMycUr*4`iZfoZbab@Ki4escZ;6sG9( z{ps-t^0lnjNrjbW=;=48*H9=XolNC=IRyPj%CO4r)Djrmsv>A?J1e@aMF;DB{dFu5 zgsqJnSSFou?`cJEbgPU%5wbOfUboZvZQg{i1Yk7CATwQ7-^)A^T3eA|F%|x;c(YYFD_=q2~^3{)D$4r znO?28M5=RwrE&bptw$FkQoH6&CNFIe_`Dso>-!4z=y%)k;qd$R{tVCn)d9JPDziSD zH|O?ckAD;sqx+3&g%*YJ>m9&Jpg-4|%=#YZrT^>G((=4N2y(~s;@2l?o6A9>t#spo zs5TN%{f^(y*W=kj#n+z<=A+dHBR%iC;fWO5Iaqt9cL1D#K)?&Po8e^Tc}xQs#{j|M z_vZZOwBP*;Xi8l4UpRg^o<10j3)_uf-ye?hSqTc%3>W$vkxn7FvD4+o;0|-JalO^$ zcD>nQI*s$WATfKO$QnIpuEj4U(4IIP`PA$4;rn)k87tp36s{r3BAln^UPFwp!bovu!|$Ma&j zMi0@o207Cb&lqtL16{UK%U~X&j8kU4+&QmO;-XVjd=t zc8_%ej2Vfgll)nQasP~Vg5PGJQz?KQxz!1hxD|jq`Zdv2f3;e#fq;YMAZdyNP+2_% zpYy4_Imx+6b@oJ+b;i7OD*Zo;>iACrD8L!#`0;!tok~|BUjULi_}2;mS)`?;gnfk4 z^xIDI{Jxw1@mnpHBPV-)HUYqhJg(Q{DKaW*kl*!M6Qn1(siyf7C%v{P&RVKZhOLT z6I2@nVBBg001?BD_G7If{W8xh0)>_t~R=OVYx~gq*N(aD7M@00Vvi= zjTWXglw-A89f6LSX0!R6a@{tkWUjeF+;3X~Xns8Mp442Ue-0&tB6ESd^^}k#`%utfCr}L^hVd4ShIdzYFHd^PD_Fpce{UTvn zJDb8-bF8tPXB{qHmO-8_DEkLbqKuj7ZoD=dtu=am{F*?^DFvuZP+Dj*moZ$LK$gW4vm z)!N)uRCCsfR*bAxqjACMskhegS&somDGa9G;tzEz0oc6-pn4b$h{<_H0%YFrRwc0JEa#zU!HVp}zSYg5Q zr=*OdQtwasPvUQOjdg4*QTC0s+#$xqUBZFo=ohTtr`pZZGz+Fvx-G6>@%^ARd{up} zTcLWTG8bR!Z7@fZ;HoHue&0_WPA9p3oqqj+{cc#>xTp55H!EkTJ-_=C+d18NPW|kD zUcWd}i!nqayzizNUk%&KwsJ}*vr|*q9S*r zrAt<#_OBu!+MWI_eP~n>J&ibZu!lvvRWztnu9KUPozC1>4l87o&mtnN7JzuhgOobG z1Qk}--%?9wN{;0OWEdy?_VXrHyMOLZ(#MR_VXU+M>ff)V^yy>N>N2`i3yJ=%Q3`u@ z>wg!hH)$n%@#JW&R_W;^|Lu_kh464drXUPB?ztnK{RJ@mu6w%hM-*VySbC^C(j59D z7R+V>V1F|#-0{DcOs4IHGwL<#6P#ziF43y(+TArs5CB3Te|@ugiXNVq!+xd~|a1g|LFgNn0(A8#-+5Vx$7QwlrQIfdsv3b~O&HDD&@aWBY0U4)k zz!q-X>jBzsoWPg&hfU2*?*#%6+QuZaKPuCg)mmXj+L<~^kKi{*``YiE#WklBU%lqT z!*Qh^FJ8?`W1_Aam250v9~4yMJv`hAvg$*Nr@5mHR+Et^hQ82g>NVlwju)%tT0+Lt zm@Tj8%qW6#bwZ!#qeBZ8ItfyurH&SzQUAjW*-}_Py+{3*?d@Xyy!Jf5udU=WmMbNK zKT?jTvwM`eZ-_E%yl<;+J2E*;7d>(Oe13a@@r}1Aq7wAYF(yPfQ`;5|#=OuEqKpgw z#yFjxXg=nkWkjeniy0}_1IDwQV#2B9>w%V?dR|72Q0apHnr=`?Qbz`upoO`(&0fy# zS%X%4W72Df$87DctD{Nx>(fFw>^d{bz3wYB;BJ3(LRxh75Ic7;IThC7M&mDpbwI>& z>#6(K&&~JxHe|E*7uMu|-i0J9j zs*3`9O5U128TW))MnWSJ8c!+EpIX4sCS&f>1TF?W-Uxx~M|xhiVNHW2fL_;myq>Fn zCiN9!HSDvW)~)LLiywzE^@cj!RS|!Iu$Lz~(4E1G-L&$ZN)%kV#18YHQ*^i<1t&Ywv1n^*IZ-i~|RKG7sUe%~F}Y+PS$wixfN zV(PjBAMoA|Ns*+$&_$#u;W-EW`IU{>|63gnl+gE;7z0ltjzD%tpB#9moss=7@Dt$e zoRSX#LKnyDf#>s@*%J&%IRFZLy`AK>e+B9W1`@jO0(-MtE>hvKmZrtva)cW1QHcD& zF%UzwoBr)h_03DijH z*71nat9kd=kD1`4He3jf;PJ23BZ>s{oIt-1_g8_D(N#4-X1PTzDGU9MlZ6-(b_>Cz zjt3!d7wKnE7T)T@T;AO^L|ixt4h~*}#bC&0`Q`s>Lv&fI)%Ie$OE1Z@Ww+yGS<7x% zzlaiKTgYS&9U?tH(;uW5T929XPi`=W_B)(Jia)M9e6OZZ&LhfYMs_wFw$Rt2lE_3n zscUnC!ykqK%eVU@11$s~mtVNvnyuCv_9WS!XAVML_L4y!J^bJO4e(O1(6*cH3TeZE zJ87G)TDZresMCLGvTVF^jG%Spz{vSt|N0!yY-Azzg(%Fr?2%$<`|DuZKW^m^HU{Zs zEq(h=nJyme&iIPT|P9t_&R**Vd9=gjASsosy#l;jL2)+kP+%z zM;-i77#HCcH0jsIP`?$$@xu^?_m7=%XWF+$R+v|**6bqU@hgewPmYPhlV?W`WMj$A zyH?VMP6I;d_ed79j*)5y)3i_epp|>kE^x<_CUM_GZg01v|iw7B^NP& zE+)JUFjd3`C-^4OS&6aw0(!|sC@<1D-utnymz_Uh$O8%^+Gb5qqI3I&vqK8VLs4v| zz-?l@+7rM}NM1#)R55liz#+(?vhDr!V9Q|MLGn<_^g%}qY;V*?wnKpkF*GdL@h-?!cDxzwLk=+aMe%IeA~||`pQ3HkUfP65DgeX*oA>oSvwrZ zSK*W(c?aaXS1ja7MY$Ifn<#^|LDwV;wb1|s{uTsy+AQN!XrS4%_LzIy=uQ{}FXcZm z0s$aK#G_KHh5si;2m-_i*jrAT|HKG9kf{h_O2t~F|HStIAg4miT?b`APf%Q7tO6S_ zAEFvaQ~tsTF#pp4i~~fUJfpwmp$1eOP&Iy!4%X4(C3swu^{)ZUZDVhq1%x=le~@P5 zkqe`O}Je#TF{*sD9h%v?~u4jRE$c))@;L?wigxh?kj$8X}P^M zS`djSf6aRGi3gl`elF#*Tv?&Ope;6li8Z;l9RWna2^kw8N>D(Ogf%e8IUs1=^0f3i25Jz#yE zU?TTFlqmn~=iX1GR1FP{sHf9GL{Fq7(yGaXV$sR4`~Ma?#l|*Uw%Y{zyt2!PYv>k! zbEIag56RXbdnWBlhMk2)X(S>%&OyB#S}u#p>}lOaWZQj7lW%SSV>fZ}{prGN zCVLH#(>gpf-Z>heW&vpx)8Fp)z~!*L1W>}cLNOaHNjn$_Ihl=)su!azV^Fg2*lPRe z4o)XSG;vK{NwHV|`=OhM!%&QCS@8dn>0VI-0KE544)+s)wSG@6{__x88SGXaF}17} zG4J)3XL>Nz<0WeTTK*(9gVpNTA(OEaVT=T=wPEsH_?G{UP zr2PMlu(u3oqwTu3ae@=v-Q69E1Zk1tQoO~ByA^kb7I&B8?(R-YDems>@=mY&`F?-= zOGpL+Gv~SYTF2VMs-=+KM+k5(82Cy-BjBV|cEiqNJn$>95^%y9d2{V8Cayh@)vV|L0&PKa}haI9y>->C|UH(71bjI{POP^oN0~ zzvr<3D+xeuXSr|grrU+w z45n~?x%QTgCW5DV_jJs?zj9rxlqm#@i2e-JfY6Z!2}9(*Bq>0HPD+H_5flsu1&AhR z#CGku#hOjFE6{)pKrTkInUfrhf`9twC;sZ)$k&{{R26Y zOw9H?WEAC%5C-HFz|B#4E0+pIw}>vetkiGYr^TB#^gN*D8KBSh#(L{VlzV3~iN9|# zP5vl|M{$M)c?)XbJrN+58WH9v-HQx5gvf{vo}H7Gf?{73GsV6R95#pv4N;Y2beRA; zyo|U%lmI8Mjrp2E>`rV9o{xvx>JZHUh1%Y)NI zf67i5n%1xn(X^l)I)Kfm+ZQBYt3R2?tWnP^F_WJx<9a84>s3yWc8(juF!gHQom#g? zcDkt3yuSaALxBF@CTa&K9!AV(qtkA3u+`DpWHmz)MRSc?bXG$k^|r%2PC}oDnVO2< zMJVR@H%l~Nf>0nN_^oF7^O;wgYijUO1B8vH{tr}K$Gw2doC^e7*zb2``5?oFKnJS@ z*=a)JE(3d6(gts6EN?C|>Zppgtbp||CMw2KEE0F_7|Fa#6Ge5nT4kdcAuDE6e(J5V0H%BqGq$NoiOxtR#Fa6nM0pF;5UWNv?N-?(d&~e%Wg0 z!+rwv7KGqp`g$ph!W~aWWFCxP%Ny4lh+vFbNb2wsqOtd5B$axV;6+dsa}+im*4V>Y zHg3>0ghEF;4xdRf)hetxj-Z3)O`0VNe<9h9rK7seA${cwvJC5286{pp$a z1Kj;ElccmXY4CcSG<_N4csvc&@sjgdGoW^ls;g^v1KMB_Am)AwbxZNxz{lh69i;vW z8zwK+(MV>((MW-UtkWh5E3UWA5LIGPss3qJW!MnZ>qRNRfLb1GG(>GpHXKh>hY4*} zxqUD-FA=2bI?h~`FTxO2b7Aoql#j?r6kEMk$|)OTA+lXaCp&>l{(Vc%SXfI=XKF@X ziKpd^WVKA_Z`Qv81@B+|h0CfH1w2roi2Ft!c}#+GaNg^TE3*NTgrv5#ixI{v4h1#t z7QB5&w~GQrr|p(;1uCRzAQdw`nm|%m6lJ0$c)kx5sHRXW&INt9O(OyoyYmpn?}Xj) zxFuCcG1BBIX}b2oO`xeTZRpT3RVYwRBEsrT|2DURThTK7792><$jHd!Rs?VBBsf}d zoRBZ?aotj9O(!oe+G`*V7}yZ?pmE`#6Xd-99!ldhxB%;;UwT_Jdy}%vyhjtu{pV>gi> zW=M5*(dq0|5>+0QVCbUKftw*~kGnrt9rU!wtbH{Sl7$zjzh>0C&re`x`b^}+qgTN4 z1&$_CnbO(TX_j-ShOfN*fVza-(HUHSm6dOw(byh19g!P#_ie_#Byd>fv&$CeN~N;w z22tVC?%^9rX8O+$A0aabt74S&k2)XKztS?GR_u@6M+(dYWt9+6)&#NWrO>O5LZWtc)~0P;#AUCUzxY6_fopN7KuO|&qpDEfBZl4Y@oH>CMA zSprYC>V`h}&HO&mVzxx(=5P^yg6D-&Y5SY`svDltb~twQBF2gER)_W2s?<|I=E4^+ z@e|UGu8$=*96GsJdD~Qt&P=mFPpIFWIwn;Hfp^{(7B@_kA1l~MslHIVS@~ZJK>>6- zuy7*gPy8@I9TZZhF&GH0MgppPJ}~4S4_2vc><1%u=gnT)5-MQwr6bWiM+BlEO=gJ_ z``qjjE#dfEr`x0&^N|i>Ch42iDHeG+mlzPl$KOyU^1$2E1E#v(qL zHE~7C^{b||{XD_Oy>&CGtKi`L-5w`bnI1>`vsy797Plnwm?1!t%6V1XEPLM??QYfU zY~wNsDAc|eIH#Xvqy_wB&a;4O75t>+1n~!J8Jo~!BQ7F97_ZAEC>Wx2am{IGH*@kv ziDFyO^SVibRkp*d%BDUrr2Q-}FhJz(((`^V1m1DTRtKY;6Y(jDkNw_Su<}t=RBbTJyd&5fLTlg;eF&9mF9KApv z-QmK@;p2bUyHl%-(&Vi}kbj+|I(Xp=)JOy(KoDC$;-sq4y2R8D<_omR@GW5$+6z!R zxefFvJJAamzo&2efV4gh4VV8x!nu?KJ{dPrTYJu3k0v31kh(61u291DR}4|?=Wz~t zQy#4X#;eGDzZgskM)S2LdTb~cui%rCX>Q!d_Uv!L9(HxCG-5RJ*R0!+TU;6Suentm zhjIps*2MI1JVDrr&0z`JDMh?8N(Ghj$W_(&)m0pMSgG(0{GGifFG`vD@O(?F=>m(! z8r@Hy+_M8n?LoI(+EnoJSV*okQaR7N+ZJp|+=wHW^m^j}mz&cBLP&z!1M>g~%0p3vQU=54)+?>NqN16vO7ikgfT4py zD%77rK(dKCpvpxblBaH?M{j+$`t5E7aCdau-D~7I0Yrt{G%={VJ z^rF{mm(9}4;Z6Bce&h?L+c1@lZ?LI0E}IcQQdAxVno=nVeV=e$X?1zC7XeCA#!vMn z;Us@lHT|~c%yj4U0AukO`%lsXX5qM5`eH!~F`Wr%KQW3`gTv49%jBpG6V1(8Jw^8# zS`@`p+%nyCn{8&{S#dnAdkUIRU7^q1lrCw zJAC08%9RSeJzFDBPD`6qp)_skPAkdhSC#n|{*~p}&Grb`$i%EfJnMKge>-=n$t|Ss zmeCUv68SWaC}h7_BgSXjl+#OoRFIR~U+oZ1+wy&TT5Ggf>SYUkvHrdxw01UrCp;FS z;6t7UTHiO1B@{rI}6Rkuej=(eA3w176~ zuadXy_9XRfG~76^a`64I+b4rk6@*Kk(LceO&tg??@tmzuma0VaFy?+mg z|6N<2lJz9Zlu=8_!9V9k+~!{V2$ouUVe#zy0#Ic%Q!zjoXC+)C;c_nz$q1*5b@YX1 zHwm;K<})8!yraa$!OhMOm#3mnx5q15b)F4-**Q;OTt#Hq(7Bg(r9$H zOa-SZrqkd@fz?0KgAvimkS!bpoH$|V1Ub2{sIr&#_y$1R zIMCYj^{u4~P9JZWwb(4D)1~MYG9-n-?=&gbUrML-fL;f}G`%sB^?Qd>*9qtE&dcp= zePAd?Q)f%?o_Vzq$?Jl7wyB8;(8~2(X=_pr8~|FD1k|?*`;C5}#HLAP<=!nuBIdI# z`&8(d8vvi#MnL)p)Ajwz>;f%LgB0eCX)?OJ#A6G1iW;jcuGryAt*1~g#b`Xb<_J?j zfAIL0^*i<``8=FU+*K4t`6WN64667Yxo#_Rqc;@8y=$?gQf{-VPj$9{`Z9dFZP?`_u$`Qm`T%rGQ)#?E?KgT;9LV4KkXY?9SG5Q1XZt)d3NjaY z1_%L?B=T2-ff4$29$Pj9>UA`jeYbfpLT>cY&Fj~1o_to??3|STtwQ<*lnQzdG)iYz zgQg#D7R0FV7cAL6Xu8tGXb+}PWA!Vstfp6ng_9guXCLR3ZEi<7kt&`Ff6NkcS{T(} z%K$ws>9@H;2RLjEg6RW}|1o4$^ZsGLg=YQI04X6&~KxTmvwFKtGx#-~i(?=>O)lH`#*(zr1(y`sA!m&bgVO8ea=rLEW_%ikP{KpZFYR1{mG9~0 zu7=~OFD@?bu?}hUqBnb<0IT{UEk71AIkJceU4QH8VikzTZ@UVhJjVg>$lcw#W~EZ6 zCPBY7(zcNUEq<};(&o3+-Ma^T(NIZZp-LNzH>-_6s@+`FXc8RFp=O^F!gA*fyqDF)E7@QzSku&rWQwI)+`VE{ya}Z z`S?*mp?xPwcb&?)+YeeQ1eH;%av9(T_>6nutb3twtc#86OaUBW-nz%(??07-If`N$ zXtN#1l2|nxxRpY|AUwDn#Qwp-?mVyF6mT#$7P)8n)GT4$WX4SU1H3%0WKIUaQ3}`s zZoj}n02t{g}1niiQ32d=CjBb2z}y9?sWLi?snB zkX`$Q%suwWatk-zA|(`?mOq%oA1Vdhv6Yydb+*>@_s2n<*wbSh@6p`YvT&MQc76%C z+EEm&whPp$0fY|N#IDgbiBXIk1_4wxA5sz$F1ukd#IHip!$=R)ZCgtLJ%wT#(Dl5< zWg=5}1JR6&U~jjjxRzb}2%()r9ZkpPD)f)`Yqq`@Pl!0(>J4mPri3O`eKz>0UX|+E zkOfM};{|PGWS^38m_bpp8n%40$R?OPNjaJXX~#f4u9r0&nT@BRd>MQab}iAOhwsp6 z=9M@KuMK`z3xjv-JW=r@&?A zW68*eH8(qK&^RLhG%3T`XXeFg-MHqS7;VOK2Ae@aZ3J0Ye>_-w)xQ4IwDxYc&}j($ zWMnVpN=ld3om!pp#)+kNLQnsN$E>1pA?ZZ;Y4U?!&2L4`&Dy0;v#tkk$HM8qv1MQG zf4fo33=9ljo^rSu$#mO+!~+F=UnQ;e6clo`bA8`izUOJ|e_KEKWf%NMY1<+-yj}H^ zURZhg{e837;);jbZ@T%^bIp%DZscPHjK7N~(w@YN(XFK|YMccHWx7dW;8F0Vz1drz zSiE~3#=W+RCbTC45kuO}TB$k2OoPpzdSwyx)Bt6pn)v@<$T#fRyXyq(iu!>XVi`TM zf1(FDoPAwC?@$B1fo7`Uj69T}5a2D$!}`lj6vnxF#kgTO<+!^MKZQoJ|u44*t_3DiRy| zKLbM;=6p=PadR*b7N(V{ti^YA; zIIk%jfq#yaxS~#kGTcR{({y+`$~fT5h(WqX+Yrl{c2n!j`E3^+kDFa%6W6j*B;kLm zSE|=S!uR*Rea2>|2#upXraE8Jo2)xL|LE%azDp*_cl-R=`v$;n=;?ab60@D|^j(wL z-x|EmR`uI%yD?JE952jAf;p5!xc&g^Di63 z-Zs5gKu*fQ0Ig-ZW%0N?4s&j8e*Qd`$b3Y795nKJ^EdI^6Gk_z?Ufn->zZ%Yz_*0g z@f8e@Oi{1Xmycp^P6w9=H%G|&ix0V|yLH16-KJuQ=%gWEQueY>*Di~Sd`|6l<=J9A zp8Lb016M^HE=>lIcQ#}f@OAFrDH61qm>EE{dT*TGzP%nK@$+8k5=wr@W;bZ%*^j2o z6n3#t@5)-sah}{iZgBz#4;-u6$SKia`B@lM1aea$@9mUXQp>k*8>zkFivx!ekL9{u z7r)uNN)0*=<)_|Z7idgJb~ryo0BEky^J*OCx?4WWy5Y@AteDNOk`3+`^1Ik6@1FuQ z27F6TcjWyNr|zM;gMK3CAi?`T(1r4edi5rj)oy~xmtU4`J-=X&Tfo4SYoyLqX_?P! zWwYtDJ_A5%4_gjedZT(-O2*030+XNhccuK0o35uz>}8rd<2v87bhSlmwe)yWrQ95# zOXjlR5i=QL@IH&g3-|iuxHGwvts<@Ynj#sTFq+gOiD8(uivsq_rjsjhtw`IVQ@9o@ z?JkOQ*kuvPMQB77n;^Zz1(d+&rgwNxJow4|8?Z1({#=sOy0+2`0eSn>%Na)A>jA`u z>wgJ?0@)XooL_YGySAmxKKSZCjCmDH`7qiEm^lJWNv_{{j;Yha)@8B~V*@(5U<57W z8xHO#F7#RuBMN)?QBW@A2Ts8XMIhqeV?y#}1&lYsqyDj1CXG9~347 zru^;^$v^WHASZ0sI=_*b!G(MXi)~Bsa2T6AA(OedAL%Ayafov7vtPhBqHw0XoT9w0BlIFl^t=uBMZf`P6DDku~D1h^EwZqHM zeIppKKgN(_%|excl0gNv3{MktWYh6H+0aKPz9EbB7*OX~Hum191}EN#FqXskKI|lu zf4h$V92620SP7l$o>jhQ`1!_qB8@i=rIO-SB{}vd z7M~&s5GINLS=}2KbdFViQfi5aK{|MXIJY_!5PVg!q((gZ^E)PaRWiM(C~3q9Ku_A+ z@Kol`@48Qex{tK0yPwdA`Euy}-uYw@@;3zBpiqJdpOYVs*AEZj)5 z@Ot1Mk7w*s397hW;3S#M6h_>qkgd0Wj-K`778^vw?9USQvS9QD%!^_0 zrQ~;U2u+ML5JFDBKOv#_+O#AmZKW(!WsK zmVV&eW_DWRmAa>AKEh3~8wLeCkN!85QQEJIE|i0)FiJrz?;f_Xb7F6yjiX2e9e)+P z2as7wY-rPeCv3iKtU!NTPl~j1{B+h8%B2mQu)qZX)_DUwBKPc+lrW6x z(&-+Go{{hL)Dq9wv<26h#GEk=0r-HA>MZqLX66mOwlV#IhnJdOa_b*a{Ljql7x#$7 zXAhyUV1VgKWH%)9gEF89DH8L(w-1t(|7Y8@!Xn_>6G{SoO9VeblF!?V+cnNQhBP#z zRDc}G7XlMFN~KgT)A?%SVVLtt0I3CP$T&tWX(4St$s)C4CF$?t|9F3hj0FwFSHDRjt9TW)TVT8WdkLgt4iYTb8C)w3r&jCJ3stc+C~h{ z1;B;xcCv^ID~5Bzrr!#*jvD}l3cJ2fGfkZnouQ zIHLOp`?Ax;6fgf=FqWt=QXyyYGz$WX&lJIW;yIXeaghmp=*+Ee06+jLc25VnLXG6kos~8ZOhSieW=cM5HEcWhm0$pWi>( z*JAxt-%Y-5Xae^F6Y;K9h=7n3!%4^@ACs0Q(ilTnkiGMs>XIu!r%~bH@K2cpF974bUVO0dIG{Z-48|Kkh>l{PFV!S`AFn z?@arD?K*DoWLVSoa5!%LDsniDSR~#_dT`82M3QiRh6^DniRNGP<_;tHj-c>s zW^5Wt1~6t@;ut8w3i#qZVPTd20TK$+7o5(y-NU3i*1wC;3arZ+JK1$=;f~?h4BEFT z(fwaoK)aJrpg^*cYKxgXfT@<0?OjImeFwPH1h||s#x#&M;9P98Ydhf^=gM(5a=t;( z#5}sx_*#vl+VEi~PVfswhA^xQ6g)h%DDpN)Qxeh7(Lec~xLdLU3RaSTs2=4j&OWg+ zrXOFy`FS6P6tq8}?1iiboW-MqaMq=M_m>lLGg^4;|Jrw%CcnS}aEyHyOzade9C8hn z;k3ou>kG7SEdr|~tpr}m2Ps5}cSB(oB2i)JU2okz+~$*j2<2bU4tC4gB-)!SVL^aG zmG)}_8hVR80y&O6qjhKt0wwWCBk^g>VRSF{ZbXyWMjPFABve3rjkM%NAZGX`R}2Uc ze)vu%s&v%~QFeg&9u0{j>JIgezt41AMJhEeLKTG-q9|HT0p}BnlK?qE-_3CPZa?LD z6+Rx=@%{_ZPXB!sIq%z9wU0Bk?2d@(zscD;iezre`g{psb*0O*AJJ`o?oP&A1SmHv z8-LNcAj|Eos{z`#ezf0G2pBjrJo+aMM|{s*rk~D+S~VC9C!ML1AkXJ@>{a?5b6s}q z8T3swJ%03;JN$hBSG$n=k~v8$;Ur5gwFc|qKQj$Iz05k(@S?o7hR>QR^hFa01q1BN zxs+>?Ip_6Dtb7u~>dQvuB;yxn=y?BLpVtR-RBm8<9s2NdM~4{;a+;znwFFi8DUm&; z07(8iA1s6GEi}>woolCVZ%&Puf8@uykYf%uI|~Firu@UX&PVS{0=>2Pg$wj(IKeR< z<-GhgQXP7uP++Oz?v&UV#17)0ay^n^C2~6-aE&d!p6|nzKRpC%qTir-t|{`bzQnS2Oi;m(v3c(B%@d-FF8nmYVSKPblCE zL^>y(eb-<{M}_0Ds{C@(>Np%1v^xsp4y%T$Mo?G@HuzHS5K49s*_avzI`aL7XOZDmg z8-`c}Y|Cradfc5D5vOPQ+ZCEW381(>_t&wvs>2c6?uQG2*b7+N;b!7dY^Y%%fjBrn zWKa#zd6Q&Axne7p;@0A7Nce?mN*0tf6;@lkVo8zvOwTc)Ny0?zyQA-5F}`#83}#pm zwfa(R5<&Fz{lJ5QtR>MLa992L{_wQxH0|OPhO6Gb`mbY=aOx^Es75DIS7U4weX5kp~Yz*eUY2m_nokCC@G-av%uZ zFP~H((LdOi#HW`LJ(7EUI{?l9U*;H=X7a;k2~~_Y+*r+;QQ$)DrP-48p8Eptrr{N2 zECPEQY2#TWhI~DTp%oha5ZO)<^^(c)!YKqwusDG88Q;*`|Kbv0r31erq;&4G;Jz+$ z!|`Y)IHhu(XIm_OV}ote#czS>5+i-POdiCAkE90UuPa-6s1f*_*+dN6^_98nFie@@ZMxE^CEcK z!eg{vhgq`jr)%4?u+#1oQ%0CT-uG#JBKHiRC9Z04 zZPj(-7tE<+?qwf8Do4SmtvHN+f3*$F!WdY+$@R0EFGHI=WF0}jM*x_MERDjWCO

q{Gr@}rt z3X3ct2t9E-cLsao?n!DvTCXvZ5#uj1νuagO=lEhWKPc?59quGKsAb&9q<-5iDL zVC|!lLDm(ZFoU-6Y*bJ&7@{Yoo?x`jTzZ#}c z;~*uwca|KD17!%z5R146vaBN^zpAebLsfn~M_Hpe*G5F7x7UDaf^l~}T}@9=9osiw za^wkMmZ-PC#}MvG}`~dv{WI{^nI_5*~_DQYe~; zuX})5lJB=Q25BrO&Ow}234A>GOU%@*VrGl=4$~a=dzv+LawHyR4Y`2dcKZ$jY{;s= z3*qBL7wmC-c8&EurtNC2`Pf}YZoTiHenp5_(mnIm{wZ^#bl2EaP=qX)mmhBjIj(QN z9A(ZQeATNh_-RNYKZ9>Y2NU`>7|tUKq!vC->H@S9nn zF|dzTNbmhV@eu8X+|GvlM^$V(QymQ{PD-Z|PX;a+)Je3ihh1V8KO;wku2AqUI!y@G zePbE6Q(od`ce~|!w||QH3s9bDt6V}v>$$v?#wtNZwNY(f*O`e=%Qr}L_XU?LO)=UW zIq5$!I$EGV2;>jmZUPc?gwzjBvu;axVi*E^2XZJj>fow3^XzZ8062)j7HZuS z(geR_s#FgJNv5d|kMiKzm7OQ@!xvX&d*s(&FZ2T z_h!wAIeYQBF|5#o!Rb+NFK52F`0nR|zF4&&qbsx`t>z7szm9~-w^EIZ&}9S;sZrDW z9lEV!w{;b|+(K`;Pg{6F2qO(a&{)w)6`hY{-CXu_(6Zeg*mUc`NG~t~QJA^uK>QlD zE;^R`Z|Ux&P;rRZ4?U@WDkU@=iSEPhwL=;+wa@}bFM0F5#9=Q~_TwQE^o%_&pc?|T zCD11gshb4t8poOaIuV(keFO+M&= z$R0q-jDo_X__!#^1UoZBjq<03?FlZDtY-(K7Q`R2$vgJH5PTb@Um%O*C`|alo^Di- zO!3XW=vmFYV$uJkUmAcn@rPtznE{`vjnar+X*`{s9SXTs(S~Jtccv1#4nM{9t5qW_ zM&;GSo`t)Q4>Cm7uyfpYej+D^%$zRG+~N37^w*Y51m zV5Q-$PUf{@8gYBkLvLuk(%9kIP=-r#N4oUf;&?N9+R>ct=nE8xhXu2>PL}*OtCs^! zefd=RY5X0Oc3Ds*(tL1x_aoR#YHk@H-ibaRP32wfTsa}lIA-~5yjlG%Qz=v}l!Qll z6uhIDINdCaTvBx_*KXKdVi&8_-Y}#lJZ*2fQWpxXZ7^LV|>x-?h56qn_*`1-l8 z)8`Kp?omMj(A}c*+bp$oS9c4!@1I{L{%ky(_c$rTC?<17=qa>@N5(7B>9Fd1bdgVF zIe7crr;Pr$zG|u3VJhq8e(4P!G)Qu_Gp5z?Y3CEK6>tl_nE|(Q(+p6bwT1b-KOaoc z9cbcb#gCRWDy1wwY;~K5%scfEqYT9qr<0>S8N#UUN9a7L(HPp#pE_kq{joU*ua8!! zeVyyiC%sx7_3JA5MCSXt11Y@Mcg67faaWDj)pR@$|IR6`rQ%h*qh3VfAWj^}rm`Oy zlVO00#PZ*4<&i2* za=EzFtR3C-E5#}P@CM!pa4Nb=iRVKEa}Ua@8UeOK3)+8U6DTD8ZXmY!|GhxqAY4Elj$1lI zi&G3I$neXw#==pI@$%joo?!@4SGge@jF+LuN;d-{RyPHHRT#x@43vvP5zx-QA_&_G6J^u56 zo2A=e;p$uL#TkkF=+}LmvfDYREOds-^gE_*UHBQcl;EiNJTfo`P|mBmZcb;u48iPc z_VaOV1fftv4`b#}c?j=apsD6Pgqd;BH?nuQhk}6bry$=blw1Fn&E=iZdXYIcM9S|Ue zmGQRQm6G!F&;&Bla`Y`vvx00AoZ*aN#cPLE2vlbD;&INwLcggQe>*EM!~n_`9+SFU z7*knnvz#V@Rpmtv9#qn4A-2`KLm&FDBC`_114;6=?ilvtCduq%p-X@%}87 zCR54UT|?|_9HPiVGEl6HOQ>{2<@QfOxkBO*m&2Q?n|LznIDZq5j`nQ@wz~hMnjGw51G!?WoIC02y4pFb1Xb- zuiudCzquU$7c&AT$aI75G5qDmA1tN2Q|MVEMl{oA$Dlz2pVF(zw86^T7M`N6V?7z*(mN z<^5o7{)wNDJE5VX4yU4GDylytG5fWdp`zM+2A#F`dxa4b&1WImS+HtSg;A?brTVO1 zRp%p91WF9X*KGo#D_H=;XBrx!RHh$U7V}XTp=S7 zmh#Tm#~4C1VUqr#z<_Weh`>L@^Goe;{B?iEsW3B>Br@qi_#la8$>gi>%ePWa&>g z8W&;4WV4970(#Hd)FUjdns;(9Y9WFrFen;6nO%+%e)2K@W?=zQcVPBY>7T==$z7sY zOs9?uP-Fy&3Qg*0l^;61=9zFla<6L)&%}wanp5I}7ehn@g}Rzqz4&j?LT7s`evVu( zx}@-moLb4H*RL~R@KJBxq2MS(BnV=Llylc+?ZatHd=L>8PGL1s8|JC2RlpL-=3Rxs z*bA45NHTy(S9Z`C^9tN>q^!<1OAT&$PKA=O$)S`37-t~p^W2=*Ab`kda$?MFKQgSx zvbP$a-&iI)`{(1Afj^fC!AzeIXw}`wrAt~m%Wa{Geyh_Qz5ni{%e4B%VW#9h``=J88dV<5PaX6IIXYGg;r9>Y<;bhXPv@XVS(CGzc z!Sq+-b&)c~R_NBsZiRxYVPWD;!i*;IWQ}?98y7UU8R`~X8}+akH{(tsk-n=pg&vwV z3OFmT1x++A;>p@9uTOCLq)r5lTZ3pFzSc#{lvfXc)6iP9eSlFH7(laBY0x2*08E5_ zMxJ0i_g`co%tdB)NF_x$lxq}_>|p#8(*Gxk#d?~t6N+Cl!8a= zIQyi3yC8<`nb!-IfG$6RH)**(Q94ESL)*sj{brh zNjqt1hcnN*rp@J)(4q5E=zME5SuJK!6pY4HtD}1hmpOsW6{07SDm9Vpq);k&UB`+K zu%KgR!T3gT#9A9c!JamRB+Q3J^A=t!BeQ1}^oXAc3!tjoAp2q8JcaFu@6w(_c%PN7 zSw(~)uh^!0+HzF2`9!ldxT|w=C8fINA_saJ zlkI;b{sN=dH!^b?F`SUenqjk}*_7ZFS;Ir{EsU{Kp_1&ijkQPtl}Z=3E4fn&wd`fK z#U=?##XM-KMRL?L#^CYCjwoJ3z*Mf7Y>v;%<3I%Nl?Pu2H1#?mP@QIN^cy~)`U56g z5}W>_`G`|*l=vj3kl5nt@d43@hZ> zKM|Di&X)|ox*H$;WNIB4;WG(L%-FZnmhcABIw<&T1-vXi1ki8P-5LA7<9$N1)-21e zOE%T-6%-X+pkajNfJu6-fMNFGLxepnQ@Q`v=o1Q1!T3^6JUmB3joS*j9t(J0TkJt6Vv_;cAk1C>bi-pkzam4aY<(jmq!*3J0Ew=p8v)E~Aaz&E{ z3meb7Kp=I7_#l;8PN)trMNaV)8>$caSKDqy&;5(~l7$op~ed26Lk>hsp=wj%wI zURNbjh*_w-3(wy(Os)CJ(iW=WAV>x$#`p?sEnV0h@|IQb1Yc%`%CcyVSQ(|T#^%Hd zEOPpZ-?FF?y3@@HEC5^sbNDQmn;kGjuQ6{k|51ZG$%c}c&V(W72%hZ#M&4#4QcL~5 zBp(2S1>|~CIOLNStbeB>8^1x8{Me}4uE-WP0yENt)T#!nj@>(3+7M?`ygo~0{*IWI z3p8BtC!tcyUlpfDTSeSifrVVvhRW#Q@U!K@#c@O|73{+j)keiqwJ!a@#pMA*m|BpQ zmp2b0g*>Um6uui*zs$&ze|tPAX79MhijSlRJ25fBJKIufz@93W5V^W|;f0LCRrs<* z7EawN1lrj^%EGESONi^}e>tz&NQ|s&EjVxr{rV|YGzFI96w7qj?d}N##w^UP3BOJw zTy^bD4hr9Lq*+R(eK|cQIwUx<_+Y#IEK!?6{A+oEHV+RngiBkeWCNGTaOk{{v@T9y zb=jo(c^v#(X2`Nv#G|L^t+EpqCR7HNUvyB2O8uR=SyY&bWeL82pfP5h%L%BIld9)* z-BrSageVBdNi{E8RVEF+LeVw3lnuNg-5dv^E7**97PX5IvNwABG4kCZ-m>`VbS#}e z8@9>1OMq%W;|%Oh|_rvf3;B8eo;c@RT>`_ z_+6pZ1U9L@l5Tu<{YT^L=9Vk`+k*#-vGg=t7UgaNL}sXC ze0o18^Y461#UHa7^#o?5GcX@Y)ryT=BRzV6M5u!@^i%l#z`K^Ve+|>_4gBSvZ@bo= zL98g3%VYTlwXEGjYvxDJ#R}sTy^k3pF1xjfKSNN-_RHx%d+P%BDqE~}t`%Cm<>`+>!<^ZDPX)8_R+fI6>5uxe`T-KoL! z|6Z&VO(N{z$wJV@=uU1jW+JDB*I|s5Y)m77 z7hkdM0U?AC4DwFnK7?8f$y!_{_3noXV1mbIQJ2vljN0PR(gA2afzu4JB!U5oCqR0$ z0SKC&;{_MK7qPUoRA$|H7a=&)OE&y|0S($+uhOu|)?4p*vvAD(O{nWb;fG*k+*x5F z9iq|Jg6ck1JLx#iywx^K6#4lHMpFghe9C-*(7XyT$@tRQys zD3^v}6)Ck&1rQ*6zrC%gt`JWZ0FpSgFo~r-PQan5Tam)i!6alFNs#_?{T+qvfl5{f z^W^d;H51ckkC+nblO+ZgZQP{c1@MS~yo|U>wuLn^FwTc-GLsX25UBzfG4>aj+G_Ur zQS5qqiQHZkMk_>kep*huVZ%#vfq>Gd2_`eyUrAlvFO zPbwv9BR?UJOBy~64t9hpV=L68AX$-e8ag_AxoqM z*v;o%bZSkI@W_D?WRz_OobB6v4QTXm)88WS7*(5GS;v3UDQ4D~F5Rlq$^%Cd_#!TE zR&THyfL0-fG*z8|$5tL`2tZnLROLGxAY4zK08U9(S#eO{eoKjyHb zucPmry~>i^Qa_D^KhlNMR;1r)v-rnjBdNlb@K+eLkqtqk;IiEJa=!q;Gb|#-e=}&A z|6&I}trZy}SgP%Q=7ve45GO5G3G_7deUxx?eBcl1?S4cd>^Gsn$9l_@pIJL%NE6RWXo2|t_8-q3c2?75277jl#$;V{DX=8OgTIv zBO~P%a* z-)hqACFJ}khd{vr!)F`Z&R&5jx(7R*bxmF@MNP^ARDd|b0W(LHQ##{I9?C7M;i2!lT4{W~)8Sfr(lm%pK z%s`d@qTt<{pt#(^v|D-@ngSrOS`9_gS0cs;HxY~6NVEW~@*)r>2%M6Am z^mtR}k)lOc8*Du`8Hy-yw?hvDaHo@gS)weC*#|)X4sw9qEc=(pgod`8y1l4igkwxH z`~A_p+l#HS{CRF1U_=uM$jOQE)Bienl&@W)`A1a~`CM!kZqX|lrH=T*j3L?E5Bn`C zM&c;-N6WM5eT+-zbGnVG&wP2jW{G>(1?H4n?t9Ws!4{QRn&3~xXYZxGTy!BOS>As4 zhqyB2YVYr<7eli8?W)(}MlC-I6E?zoBSN2-!HH&#htFno?N#%2gX{e;`^$^i-|qC? zez2%yq^hto?m~zgwi$t$W)CjVB>h?nl@#qWy|Sjkj-DHm|9Iwj-v`I{D`cKIkc&;R zQRW{BE57UNOJ}~Q#w+s$VA~K?&mbhDj=IWcm!0E>t89NUv0P)UQYI^6V& zax!P$=kFWJ5|>tH_>^@&hN6>ai8-+q{puh;GNaZz{`4a{y$O|=({rCV5E+lf=r7l+ zS|zY@kO?_%1D0m=lX1$6JaISHr-AU-^nG|t7DIEipKf}h1Pqdp69D_vVJk%yioH{k z@QzMo{WTTQO{K4lNwppk8ChY_0N{jLZl|kXP_89AFjXW!E}t4&*^)ohpulKb0xkkO z-As;j+mG-uV^WwnyNa|wzL{Oqtu#`6wD4}ZlMzRV*I_E!b|H@bq=Z{icN_qY+y5BW zV1q0Gj2E+dygmTBM5jSM00m9%0XFR!8&{6u(P4q~toV&v`PXhHIEn@U`f>uob3m?w zyC_^a41)`BftiKklT-P+YDOmYz3$Ibh-8GC=nGh=gy8=kfNN-TR8I z=xn3Vl`;UT>-J9qo0_yZ5TX&( z)G)nu1RNRwdYr_fHz#vG|IwUW{KH!gd-GAa3L>5+9FeCw&t5S91V&}o`JACjHs3xlo1kSdwo?U@ zVz&<98u4N`I~d}3Q@~D&i=$^{b-cSe1_^kB%b-KSBToH!&T4Qr^#bD=Ay+7cq~_tL zQ*_HHQ&+Y?25`_3NF}%hyerhr$T)Ok+5B}jlXwAzNSx*6DDwW8ni3mlJ@FK!((y z&5jaVZDjHJ@5ei@VqB|yOxDw|9| z?d-es{N=R`Ws-j@)d`M$1NcZgVYxkareN)W@uUPxrres$oFWX)*IgkgHjMIxY1^Ru(bsxnYy72FRl{={PS8 zeNfHXfRh9no@Uj-6cQ1bBs4nJFFDZ+2s$*iv{rfQ)C;8mOiACsMuK6aYd5#-4HZDeOjVFmWLs zh$c@U#-Ac)0yC#z>+w7;_VoAHu7~8&-n9PoSOoKmr0+@}sD%#Fq0}NkBN1|oP-V90 z2qc7&M%zO0DWa9&1h?)-6L;w(h!rWJ6KV`QVY`UfGR16PL)gi?ukR)*TjSC9m!A8P;=YAlrgv;dZUji&Aqf>A^gmeU5Nn3umKM4vtZ9JzRv z`9F18CElB25NmvTSs!=Vgi6(Pud8jNifdhY+s_&PH4ASXqrtHS%4V96LRQDFaZaqc znyj(f*9VOZ2Xr}JQEVB_oNSI-WD^h@QOm_S0H)cWNLVs5i7RiBPd#%^QPl|BYut6E z(?UP+peHwEhOF4C5USCn1l+yw(0x9}^bNjTHNV+)TnJ=t$WP&Lve%~M?CDyI-NUer zT0aWa$uyS9M0aEg_kJh_wj|z2`l+3lL$<(K-{a>~$@yF1g_h6;AT05D3)n|B27H4* z>bJwgQq3*QMdIhZ$J(6U(imxyzJt@X;XEd%Xc~>8N z&Um_%xTbK5I8dnn8H2Orf z+#L~wo63X?+`(m+Q_IGN1#k;6KkFzv^b0Da_viNA=zhG7zf`yuN62aHcBd9R35`bg zKiPPXj8pzi^}UzcN8x)@>&Fe-V=`1o5x;rkFqu+Z<gv&Z?Uk-xlWtLKDs;Zrpi#zQGwF2OuWWrVxYN*` z=(UG;XtUaI&-G0O9CmdHn{H6HI08VBfhPOjB@@ctqX)tuHz@k5P;&lYtRM!iF-nXk z!M6>yPc0nV$Do0d9;6jnT!eIJxtVY*4&FoC^+6D(lY|kkEX4*UP#T1prZ6^0@LPrP zpp1>*a-Ca+SHQ&WPd-SnS2d!Pr5M5rv|QG(7xpz9rUE`=NNTr1$^W{MGGqd0MH`1)M4-tgGSHGZjY=aZe-KABco_W+ z0Wz>tC>1b-3mJ53asE1jBFd?&@6ALW5-a{cm2#-Cf{A}u%3Y;|B}*G+()hmm!r=3U zouZBBuggeFcOMwY+d@lnP-tsswkEj5r7RnJO~Ttsf$1)xPO*|!C$ieX&^8_ZLf@#! z*4KHOs=DLY9&IF9_-XBA2%HKV4Y)QC<@k}y)b5H5ii{-P3WUuVrYR7P$&^0uKVcIG zzrO_Gq@q+?R|Dt+?}0`~{0QHE2R2^Of+x@|8n%O8?E^r_3HGozPM~IUD%i^)lCJzO zae}zOYzT|CXpogaz6G+!FyxvrC_%dW7Q(Xo@#zp@>#<~YLSHiTU%N)b{A{MOWCiy8 z&#nWqf}1~bx*~VfZ#CAFz*!}Rs4(BJaUskLLMW;`H`8kcf7~s#*lQ zYg%L$8Pm1|eE~{JA)$Ozt|nu;xaPON=$mql_S6gH_ZW*^y6Hpfd3FSynGxf$WS
qYYDH%?w!K;r1a)b4YhyD0VgSdJH}(`yflMZQ*e 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

  • 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 F{{gUsbC>`C -- 2.34.1 From eca97045c7ceba581a7c589dd7719aaaedbda20f Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Fri, 27 Dec 2024 00:00:12 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E5=88=86=E6=9E=90=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...签开源代码的质量分析报告.docx | Bin 13911 -> 25107 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/小米便签开源代码的质量分析报告.docx b/doc/小米便签开源代码的质量分析报告.docx index 45f911a6901431655901987f1bdf9f2d53d75f8f..43b5ce26039406156874d74d4777c075eb5825c7 100644 GIT binary patch literal 25107 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 -- 2.34.1 From 87a6c5b5ed8b0851f06b3a7d0be24cce6f1c5632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=94=E5=AD=90=E5=BA=B7?= <1824747710@qq.com> Date: Sat, 28 Dec 2024 19:55:31 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E9=87=8D=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vs/ProjectSettings.json | 3 +++ .vs/slnx.sqlite | Bin 0 -> 139264 bytes ...报告.docx => 202201012010孔子康.docx} | Bin 3 files changed, 3 insertions(+) create mode 100644 .vs/ProjectSettings.json create mode 100644 .vs/slnx.sqlite rename doc/{小米便签开源代码的质量分析报告.docx => 202201012010孔子康.docx} (100%) diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 0000000..f8b4888 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..75f36c59bfd01c00185aaae5a82078e66da8bb09 GIT binary patch literal 139264 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 kS!^oKVQnqdtCK1?Wh Date: Sat, 28 Dec 2024 19:59:51 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=E9=87=8D=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vs/VSWorkspaceState.json | 6 ++++++ .vs/pk2/v16/.suo | Bin 0 -> 13824 bytes .vs/slnx.sqlite | Bin 139264 -> 139264 bytes 3 files changed, 6 insertions(+) create mode 100644 .vs/VSWorkspaceState.json create mode 100644 .vs/pk2/v16/.suo diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..6b61141 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,6 @@ +{ + "ExpandedNodes": [ + "" + ], + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/pk2/v16/.suo b/.vs/pk2/v16/.suo new file mode 100644 index 0000000000000000000000000000000000000000..f787e768741453eb18a8e56de66070d226b9cfc3 GIT binary patch literal 13824 zcmeI2U2_vv7{^bFsQ3aRii#+;Rz#(>38l0!GDwRrGgg%HqE5$|ytE-SZ8AwH;FULC zdgC|n6QJL~Yj5!08Aq@D5RTOUZ+B0+*<`nyqzPrrlR4S5XV32cJkNRF&e{F#-u^#- z{Q1ydCO~J+E^~ilk9pMBp77PpGsZltbK352Y;16ex~cg$lj2e)jB8bQS@sVqnqX_Y0fU)W*P3?=;29oEm^eq1L%cnufLO`#M` zLHtwlKIA#IRQxx#sDuBA+J{u)@;|A*)~*kz{;!__)nD+QKSw1wIKe($<{&K{lH`rx?pCBWQ<6?B4Y=m9aimH%h{$peWa=m`EF zzfTMyHsJpcsl2RmSmhO!S5=Ov46D4RGNSUj%Bad4D*o}j6*wQ$`FNns-#=L2`7`if zN4TJjr|9G$1=ZHftnV2mh1)OeU-?&^_qh1z zsD0j?#r<0VHbXhJw0)BF?5 z@l|p8Kc>DnyhjG{SqEe_g0fHkUe}g~qaE=7rk=!jp;q{=1>`h}oUmCaRj zjJxi&qc|*l_I+&LXWOHWCv*`Tgf0uWD%?+`cU9Tkf;uSxS*8HdT;y_=k2D6vZ? z$TTkhDfPAdWAE5oMKLEWT|@uyu+{p?WJx2bg+_(b!YAh;&xwoQsDJqXo07j9_53s4 z90>#xpb6o#CMQ3Oi=R|qyZ%`ade-B0UA*IUD$1~edBM15h?%MxG&C(~5hIK_*Ma^| z=|1B1b`gAA+OB7|^BjxKv7P(2t8X{>LI30GAM@<)guSmwSXO>UZb(i-tDlHJ;FI%n z^j{0Bv!-Ov3R;l9;d#&|P(UW%A?}a`elFWUBgxIGbuZ!G%fIrUw)fxs_UE^^c}K6) zUM4gyt6)cNO~^U<1+*S3ZS+Xu-{BkHq0Nzn*WjI;6^g>Y>Tq6jd}OB1J9p$Hnd{C- zWPb_PRW5oweEg-&zZeJpyHo1+e*923`0&B~o6jhp(`jt#cKoq{LOO1aJETL5X1kX0 zckQOl|D3pW<}sr(XHJ_%vo76CX^odOGdgpl%A!_SvpTyhD<`X~sy;9KcUzNYQc)+R z{^RC^bLR;ixtB=83YEyTCjT=VWC4$rzVTO8c8mObGiOqGe?<*nuloBVE~k8?aBevsRG(O-rUowAKea z_Z`@ZIA!haYG*HOJ;)~+N3m~Qu4@*V&5AG!@|Z|>y${j_w<0e}PE~O77?69RySz0m z3Sn7@n?Asv_Y>0{Nq_ikj#CnE3`+Piml)lfy4XCr=|ao~@{g}oW}UcwMRrHFjrC#s zN%;UGII9F!>E!=qouf&t0dGi`SQYZ#8V{A%oU?OCZBac}6w#4?ICY!}an$4o)7Z&tSWUHyAT;BmP<~@B4#H!&; z(*Vi)4mhon4-#^(KcH|41g*#Q09$|e zWL0{w|NC(7|3>XUUGrl3Ma2SQI{RQjN_DXR#oh#Q1B$|*RTLsSFDaY+P*I6DQf2dL z!#>-QFvT*unP=!@f5$Q9O0>{*Uj!to);%9rv1* zJ;%8BY-hi2@PqsRo9|y^zyEbU<$Hkde;xbdyP~-ACnvD-5Ay$id;c5cfAIZB+uwh* t{r+hueg837|5;Da-Lcd4SGVgw;u!lEc(hK|pWUt>yOY}B?guS_{{R_cL?8eF literal 0 HcmV?d00001 diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite index 75f36c59bfd01c00185aaae5a82078e66da8bb09..31e29e7878de68f4d967583faad17d5c3126d6f0 100644 GIT binary patch delta 331 zcmZoTz|nAkV}djz`$QRMM)r*fOZWxYc)A(*U-0keU(RdEE5_5kSx}&yr@oPcjg`T< zF*cwmKPxr4#5q4NEi=8eD6u3nKQB17s4O!%wVIbpK|u+sz^ODdCj~`Sv67WSbgT|o zQE*9OQAue5RIOWPPAamjb7@ggYF-JlBK_nNp!U4doE%=RT18eC24_K+)TGk%ki??& z)RN-R;?$zx)RL0Sy!2x4)XM1C&6cvqghbi67#JAX_%j&zZ}E5VAK_oYpTYlrv!X%> kzl%JxGb0|quz}?9*uW~yY|S{?QC7V9u>JPK_Kda<02(1|YXATM delta 83 zcmV-Z0IdIjzzBfA2#^~A1(6&>0R^#Oqz?=P4LJZ0=nuaSrwuu?As{IYvpF5c5)lLn p0000452yeS+7HAJs1NV65rCKv1RDc80h2);7lXq;x5GaHJ>bb47#08k -- 2.34.1 From fb1e409f46e90b0e75c534b8b9c6827cf6ffe2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=94=E5=AD=90=E5=BA=B7?= <1824747710@qq.com> Date: Sat, 28 Dec 2024 23:48:58 +0800 Subject: [PATCH 8/9] =?UTF-8?q?ui=E5=A4=A7=E9=83=A8=E5=88=86=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=92=8Cwidget=E5=8C=85=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../micode/notes/ui/AlarmAlertActivity.java | 309 ++++++------ .../micode/notes/ui/AlarmInitReceiver.java | 116 +++-- src/net/micode/notes/ui/AlarmReceiver.java | 31 +- .../micode/notes/ui/DateTimePickerDialog.java | 177 ++++--- src/net/micode/notes/ui/DropdownMenu.java | 107 ++-- .../micode/notes/ui/FoldersListAdapter.java | 145 +++--- src/net/micode/notes/ui/NoteEditText.java | 465 ++++++++++-------- src/net/micode/notes/ui/NoteItemData.java | 425 ++++++++++------ src/net/micode/notes/ui/NotesListItem.java | 271 ++++++---- .../notes/widget/NoteWidgetProvider.java | 292 ++++++----- .../notes/widget/NoteWidgetProvider_2x.java | 74 +-- .../notes/widget/NoteWidgetProvider_4x.java | 75 +-- 12 files changed, 1467 insertions(+), 1020 deletions(-) diff --git a/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/net/micode/notes/ui/AlarmAlertActivity.java index 85723be..0ba30ee 100644 --- a/src/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -14,145 +14,170 @@ * limitations under the License. */ -package net.micode.notes.ui; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface.OnDismissListener; -import android.content.Intent; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.PowerManager; -import android.provider.Settings; -import android.view.Window; -import android.view.WindowManager; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.tool.DataUtils; - -import java.io.IOException; - - -public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { - private long mNoteId; - private String mSnippet; - private static final int SNIPPET_PREW_MAX_LEN = 60; - MediaPlayer mPlayer; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); - - final Window win = getWindow(); - win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - - if (!isScreenOn()) { - win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON - | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); - } - - Intent intent = getIntent(); - - try { - mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); - mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); - mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, - SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) - : mSnippet; - } catch (IllegalArgumentException e) { - e.printStackTrace(); - return; - } - - mPlayer = new MediaPlayer(); - if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { - showActionDialog(); - playAlarmSound(); - } else { - finish(); - } - } - - private boolean isScreenOn() { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - return pm.isScreenOn(); - } - - private void playAlarmSound() { - Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); - - int silentModeStreams = Settings.System.getInt(getContentResolver(), - Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); - - if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { - mPlayer.setAudioStreamType(silentModeStreams); - } else { - mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); - } - try { - mPlayer.setDataSource(this, url); - mPlayer.prepare(); - mPlayer.setLooping(true); - mPlayer.start(); - } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (SecurityException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalStateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - private void showActionDialog() { - AlertDialog.Builder dialog = new AlertDialog.Builder(this); - dialog.setTitle(R.string.app_name); - dialog.setMessage(mSnippet); - dialog.setPositiveButton(R.string.notealert_ok, this); - if (isScreenOn()) { - dialog.setNegativeButton(R.string.notealert_enter, this); - } - dialog.show().setOnDismissListener(this); - } - - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case DialogInterface.BUTTON_NEGATIVE: - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, mNoteId); - startActivity(intent); - break; - default: - break; - } - } - - public void onDismiss(DialogInterface dialog) { - stopAlarmSound(); - finish(); - } - - private void stopAlarmSound() { - if (mPlayer != null) { - mPlayer.stop(); - mPlayer.release(); - mPlayer = null; - } - } -} + package net.micode.notes.ui; + + import android.app.Activity; + import android.app.AlertDialog; + import android.content.Context; + import android.content.DialogInterface; + import android.content.DialogInterface.OnClickListener; + import android.content.DialogInterface.OnDismissListener; + import android.content.Intent; + import android.media.AudioManager; + import android.media.MediaPlayer; + import android.media.RingtoneManager; + import android.net.Uri; + import android.os.Bundle; + import android.os.PowerManager; + import android.provider.Settings; + import android.view.Window; + import android.view.WindowManager; + + import net.micode.notes.R; + import net.micode.notes.data.Notes; + import net.micode.notes.tool.DataUtils; + + import java.io.IOException; + + // AlarmAlertActivity类继承自Activity,同时实现了OnClickListener和OnDismissListener接口, + // 主要用于处理闹钟提醒相关的功能,例如展示提醒界面、播放提醒声音以及响应用户在提醒对话框中的操作等 + public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + private long mNoteId; // 用于存储对应笔记的唯一标识符(ID) + private String mSnippet; // 用于存储笔记内容的摘要信息,方便展示给用户查看 + private static final int SNIPPET_PREW_MAX_LEN = 60; // 定义笔记摘要信息展示的最大长度,超过该长度会进行截断处理 + MediaPlayer mPlayer; // 用于播放闹钟提醒声音的MediaPlayer对象实例 + + // onCreate方法是Android系统中Activity的生命周期方法,在Activity创建时被调用,在此方法中完成Activity的初始化工作,例如设置窗口属性、获取传递过来的意图数据、准备播放声音以及展示提醒对话框等操作 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); // 请求去除Activity的标题栏,使界面更加简洁,专注于闹钟提醒内容展示 + + final Window win = getWindow(); + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); // 设置窗口的标志位,使得窗口在屏幕锁定的情况下也能够显示出来,确保用户能及时看到提醒 + + if (!isScreenOn()) { + // 如果屏幕当前处于关闭状态,添加以下多个标志位,用于控制屏幕相关的行为,比如保持屏幕常亮、点亮屏幕、允许在屏幕锁定时进行操作等 + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + } + + Intent intent = getIntent(); // 获取启动该Activity的Intent对象,用于获取传递过来的相关数据 + + try { + // 从Intent传递的数据中获取笔记的ID,这里假设数据的格式是特定的路径形式,从中提取出对应位置的元素并转换为长整型 + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 通过DataUtils工具类的方法,根据笔记ID从内容解析器中获取笔记的摘要内容 + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果摘要内容的长度超过了预设的最大长度,进行截断处理,并添加特定的提示字符串,否则直接使用原摘要内容 + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); // 如果在获取数据过程中出现参数异常,打印异常堆栈信息方便调试 + return; // 直接返回,不再继续执行后续可能依赖正确数据的操作 + } + + mPlayer = new MediaPlayer(); // 创建一个新的MediaPlayer对象,用于后续播放闹钟提醒声音 + // 通过DataUtils工具类判断该笔记在数据库中是否可见(满足特定的类型条件等),如果可见则展示操作对话框并播放闹钟声音,否则直接结束该Activity + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + playAlarmSound(); + } else { + finish(); + } + } + + // isScreenOn方法用于判断当前设备的屏幕是否处于开启状态, + // 通过获取系统的PowerManager服务,并调用其isScreenOn方法来进行判断,返回一个布尔值表示屏幕的开启情况 + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + // playAlarmSound方法用于播放闹钟提醒的声音,主要涉及获取铃声资源、根据系统设置配置音频流类型、准备并启动声音播放等操作, + // 如果在过程中出现各种异常情况,会打印相应的异常堆栈信息方便排查问题 + private void playAlarmSound() { + // 通过RingtoneManager获取系统中设置的默认闹钟铃声的Uri,作为播放声音的数据源 + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + // 从系统设置中获取与静音模式相关的音频流设置值,用于后续判断如何配置MediaPlayer的音频流类型 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + // 根据静音模式相关设置判断是否使用特定的音频流类型,如果满足条件则设置MediaPlayer的音频流类型为获取到的设置值,否则设置为常规的闹钟音频流类型 + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM))!= 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + // 为MediaPlayer设置声音数据源,即之前获取到的闹钟铃声的Uri + mPlayer.setDataSource(this, url); + mPlayer.prepare(); // 准备MediaPlayer进行播放,例如加载音频数据等操作 + mPlayer.setLooping(true); // 设置声音循环播放,以持续提醒用户 + mPlayer.start(); // 启动MediaPlayer开始播放闹钟提醒声音 + } catch (IllegalArgumentException e) { + // 如果在设置数据源等操作中出现参数非法异常,打印异常堆栈信息 + e.printStackTrace(); + } catch (SecurityException e) { + // 如果出现安全相关异常,例如没有权限访问某些资源,打印异常堆栈信息 + e.printStackTrace(); + } catch (IllegalStateException e) { + // 如果MediaPlayer处于不正确的状态而导致操作异常,例如在未准备好的情况下尝试播放,打印异常堆栈信息 + e.printStackTrace(); + } catch (IOException e) { + // 如果在读取数据源等I/O操作中出现异常,例如文件不存在或无法读取,打印异常堆栈信息 + e.printStackTrace(); + } + } + // showActionDialog方法用于创建并显示一个包含操作按钮的对话框,用于向用户展示笔记摘要信息以及提供相应的操作选项, + // 例如确认提醒或者进入笔记详情等操作,同时设置对话框关闭的监听器为当前类(因为实现了OnDismissListener接口) + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); // 创建一个AlertDialog的构建器,传入当前Activity上下文 + dialog.setTitle(R.string.app_name); // 设置对话框的标题为应用名称,通常用于标识该提醒所属的应用 + dialog.setMessage(mSnippet); // 设置对话框显示的消息内容为之前获取到的笔记摘要信息 + dialog.setPositiveButton(R.string.notealert_ok, this); // 设置对话框的确定按钮,点击按钮的响应逻辑由当前类实现的OnClickListener接口处理 + if (isScreenOn()) { + // 如果屏幕处于开启状态,添加一个进入按钮,点击该按钮可进入笔记编辑等相关界面,同样点击响应逻辑由当前类处理 + dialog.setNegativeButton(R.string.notealert_enter, this); + } + dialog.show().setOnDismissListener(this); // 显示对话框,并设置对话框关闭的监听器为当前类实例,以便在对话框关闭时执行相应逻辑 + } + + // onClick方法是实现OnClickListener接口的方法,用于处理对话框中按钮点击的事件, + // 根据点击的按钮不同执行相应的操作,例如点击进入按钮时会跳转到笔记编辑页面等 + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + // 当点击Negative按钮(通常是进入相关界面的按钮)时,创建一个Intent用于启动NoteEditActivity, + // 设置相应的动作和传递笔记的ID作为额外数据,然后启动该Activity实现界面跳转 + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + startActivity(intent); + break; + default: + break; + } + } + + // onDismiss方法是实现OnDismissListener接口的方法,用于处理对话框关闭的事件, + // 在对话框关闭时,会调用stopAlarmSound方法停止正在播放的闹钟声音,并结束当前Activity + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); + } + + // stopAlarmSound方法用于停止正在播放的闹钟声音, + // 首先判断MediaPlayer对象是否为空,如果不为空则执行停止播放、释放资源的操作,并将MediaPlayer对象置为null,释放相关内存资源 + private void stopAlarmSound() { + if (mPlayer!= null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + } + } + } \ No newline at end of file diff --git a/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/net/micode/notes/ui/AlarmInitReceiver.java index f221202..de98dc0 100644 --- a/src/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/net/micode/notes/ui/AlarmInitReceiver.java @@ -14,52 +14,74 @@ * limitations under the License. */ -package net.micode.notes.ui; + package net.micode.notes.ui; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; - - -public class AlarmInitReceiver extends BroadcastReceiver { - - private static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.ALERTED_DATE - }; - - private static final int COLUMN_ID = 0; - private static final int COLUMN_ALERTED_DATE = 1; - - @Override - public void onReceive(Context context, Intent intent) { - long currentDate = System.currentTimeMillis(); - Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, - PROJECTION, - NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, - new String[] { String.valueOf(currentDate) }, - null); - - if (c != null) { - if (c.moveToFirst()) { - do { - long alertDate = c.getLong(COLUMN_ALERTED_DATE); - Intent sender = new Intent(context, AlarmReceiver.class); - sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); - AlarmManager alermManager = (AlarmManager) context + import android.app.AlarmManager; + import android.app.PendingIntent; + import android.content.BroadcastReceiver; + import android.content.ContentUris; + import android.content.Context; + import android.content.Intent; + import android.database.Cursor; + + import net.micode.notes.data.Notes; + import net.micode.notes.data.Notes.NoteColumns; + + // AlarmInitReceiver类继承自BroadcastReceiver,用于接收系统广播消息, + // 其主要功能是根据笔记的提醒日期等条件,初始化设置闹钟提醒相关的PendingIntent和AlarmManager, + // 以便在指定时间触发相应的提醒操作。 + public class AlarmInitReceiver extends BroadcastReceiver { + + // 定义一个字符串数组用于指定查询数据库时需要获取的列信息,这里只获取笔记的ID和提醒日期这两列数据。 + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + + // 定义常量表示查询结果中笔记ID列对应的索引位置,方便后续从Cursor中获取相应数据,初始化为0,表示在结果集中的第一列(从0开始计数)。 + private static final int COLUMN_ID = 0; + // 定义常量表示查询结果中提醒日期列对应的索引位置,方便后续从Cursor中获取相应数据,初始化为1,表示在结果集中的第二列(从0开始计数)。 + private static final int COLUMN_ALERTED_DATE = 1; + + // onReceive方法是BroadcastReceiver类中必须实现的抽象方法,当该广播接收器接收到相应广播时会被调用, + // 在此方法中会查询数据库中符合条件的笔记记录,并为每条记录设置对应的闹钟提醒。 + @Override + public void onReceive(Context context, Intent intent) { + // 获取当前系统的时间戳(以毫秒为单位),用于后续作为条件筛选出提醒日期大于当前时间的笔记记录,即还未到提醒时间但需要设置提醒的笔记。 + long currentDate = System.currentTimeMillis(); + // 通过内容解析器查询数据库,从指定的笔记内容Uri(Notes.CONTENT_NOTE_URI)中获取数据, + // 使用之前定义的PROJECTION指定要获取的列,通过条件筛选出提醒日期大于当前时间且类型为常规笔记(Notes.TYPE_NOTE)的记录, + // 条件中的占位符通过后面的字符串数组进行替换,最后一个参数null表示不进行排序等额外操作。 + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + null); + + // 判断游标是否成功获取到结果,如果不为空,表示查询到了符合条件的笔记记录。 + if (c!= null) { + // 将游标移动到查询结果的第一条记录位置,如果有记录则返回true,可以开始遍历结果集。 + if (c.moveToFirst()) { + do { + // 从游标中获取提醒日期这一列的数据(以长整型表示),对应之前定义的COLUMN_ALERTED_DATE索引位置, + // 该日期就是这条笔记设置的提醒时间点(以毫秒为单位的时间戳)。 + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + // 创建一个Intent对象,用于指定当闹钟触发时要启动的广播接收器,这里指定为AlarmReceiver类, + // 并且通过ContentUris.withAppendedId方法将当前笔记的ID附加到对应的内容Uri上,以便后续能识别是哪个笔记的提醒触发了。 + Intent sender = new Intent(context, AlarmReceiver.class); + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 创建一个PendingIntent对象,用于包装之前创建的Intent,使得该Intent可以在合适的时机(闹钟触发时)被系统安全地调用, + // 参数中的0表示请求码,这里暂设为0,最后的0表示默认的标志位,也可根据具体需求设置不同的标志位来控制其行为。 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 获取系统的AlarmManager服务,用于设置闹钟相关的操作,如指定触发时间、关联PendingIntent等。 + AlarmManager alermManager = (AlarmManager) context .getSystemService(Context.ALARM_SERVICE); - alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); - } while (c.moveToNext()); - } - c.close(); - } - } -} + // 使用AlarmManager的set方法设置一个一次性的闹钟提醒,指定闹钟类型为RTC_WAKEUP(在指定的绝对时间唤醒设备并触发提醒,即使设备处于睡眠状态), + // 传入之前获取到的提醒日期作为触发时间,以及关联对应的PendingIntent,当到达提醒时间时,系统会通过该PendingIntent触发相应的广播操作。 + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); // 将游标移动到下一条记录位置,如果还有下一条记录则继续循环执行上述设置闹钟的操作。 + } + c.close(); // 关闭游标,释放相关资源,避免内存泄漏等问题,因为查询操作完成后不再需要游标了。 + } + } + } \ No newline at end of file diff --git a/src/net/micode/notes/ui/AlarmReceiver.java b/src/net/micode/notes/ui/AlarmReceiver.java index 54e503b..97b5064 100644 --- a/src/net/micode/notes/ui/AlarmReceiver.java +++ b/src/net/micode/notes/ui/AlarmReceiver.java @@ -14,17 +14,22 @@ * limitations under the License. */ -package net.micode.notes.ui; + package net.micode.notes.ui; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class AlarmReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - intent.setClass(context, AlarmAlertActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } -} + import android.content.BroadcastReceiver; + import android.content.Context; + import android.content.Intent; + + // 定义一个广播接收器类,继承自BroadcastReceiver,用于接收系统广播等相关消息 + public class AlarmReceiver extends BroadcastReceiver { + // 重写onReceive方法,当接收到广播时会执行此方法 + @Override + public void onReceive(Context context, Intent intent) { + // 设置intent要启动的目标Activity为AlarmAlertActivity.class,这样后续启动时就会启动对应的Activity + intent.setClass(context, AlarmAlertActivity.class); + // 给intent添加一个标志,表明启动的Activity是一个新任务,常用于从非Activity的上下文中启动Activity的场景 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 通过传入的上下文对象启动对应的Activity,这里利用了之前设置好的intent相关信息 + context.startActivity(intent); + } + } \ No newline at end of file diff --git a/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/net/micode/notes/ui/DateTimePickerDialog.java index 2c47ba4..4d3933a 100644 --- a/src/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -14,77 +14,108 @@ * limitations under the License. */ -package net.micode.notes.ui; + package net.micode.notes.ui; -import java.util.Calendar; - -import net.micode.notes.R; -import net.micode.notes.ui.DateTimePicker; -import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.text.format.DateFormat; -import android.text.format.DateUtils; - -public class DateTimePickerDialog extends AlertDialog implements OnClickListener { - - private Calendar mDate = Calendar.getInstance(); - private boolean mIs24HourView; - private OnDateTimeSetListener mOnDateTimeSetListener; - private DateTimePicker mDateTimePicker; - - public interface OnDateTimeSetListener { - void OnDateTimeSet(AlertDialog dialog, long date); - } - - public DateTimePickerDialog(Context context, long date) { - super(context); - mDateTimePicker = new DateTimePicker(context); - setView(mDateTimePicker); - mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { - public void onDateTimeChanged(DateTimePicker view, int year, int month, - int dayOfMonth, int hourOfDay, int minute) { - mDate.set(Calendar.YEAR, year); - mDate.set(Calendar.MONTH, month); - mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); - mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); - mDate.set(Calendar.MINUTE, minute); - updateTitle(mDate.getTimeInMillis()); - } - }); - mDate.setTimeInMillis(date); - mDate.set(Calendar.SECOND, 0); - mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); - setButton(context.getString(R.string.datetime_dialog_ok), this); - setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); - set24HourView(DateFormat.is24HourFormat(this.getContext())); - updateTitle(mDate.getTimeInMillis()); - } - - public void set24HourView(boolean is24HourView) { - mIs24HourView = is24HourView; - } - - public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { - mOnDateTimeSetListener = callBack; - } - - private void updateTitle(long date) { - int flag = - DateUtils.FORMAT_SHOW_YEAR | - DateUtils.FORMAT_SHOW_DATE | - DateUtils.FORMAT_SHOW_TIME; - flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; - setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); - } - - public void onClick(DialogInterface arg0, int arg1) { - if (mOnDateTimeSetListener != null) { - mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); - } - } - -} \ No newline at end of file + import java.util.Calendar; + + import net.micode.notes.R; + import net.micode.notes.ui.DateTimePicker; + import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + + import android.app.AlertDialog; + import android.content.Context; + import android.content.DialogInterface; + import android.content.DialogInterface.OnClickListener; + import android.text.format.DateFormat; + import android.text.format.DateUtils; + + // 自定义的日期时间选择对话框类,继承自AlertDialog,用于以对话框的形式展示日期时间选择界面,并处理相关交互逻辑 + public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // 用于存储当前选择的日期时间信息的Calendar对象,初始化为当前系统时间对应的实例,后续会根据用户选择进行更新 + private Calendar mDate = Calendar.getInstance(); + // 用于标记是否处于24小时制视图,初始会根据系统设置进行判断赋值,后续也可通过方法进行设置改变 + private boolean mIs24HourView; + // 定义一个接口类型的成员变量,用于监听用户完成日期时间选择并点击确定按钮后的操作,外部类可实现此接口来响应最终的选择结果 + private OnDateTimeSetListener mOnDateTimeSetListener; + // 包含日期时间选择具体交互控件的DateTimePicker实例,用于在对话框中展示可操作的日期时间选择界面 + private DateTimePicker mDateTimePicker; + + // 定义一个接口,外部类需要实现此接口,当用户在对话框中完成日期时间选择并点击确定按钮后,会回调此接口中的方法,传入对话框本身以及最终选择的日期时间(以毫秒为单位的时间戳) + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + // 构造函数,创建一个DateTimePickerDialog实例,传入上下文以及初始的日期时间(以毫秒为单位的时间戳),完成对话框的初始化设置 + public DateTimePickerDialog(Context context, long date) { + super(context); + // 创建一个DateTimePicker实例,用于在对话框中展示日期时间选择的具体交互界面 + mDateTimePicker = new DateTimePicker(context); + // 将DateTimePicker实例设置为对话框的视图内容,这样对话框中就会显示日期时间选择相关的UI组件 + setView(mDateTimePicker); + + // 为DateTimePicker设置日期时间改变监听器,当用户在DateTimePicker中操作改变了日期、时间等内容时,会触发此监听器中的方法 + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + // 根据用户选择的新日期时间信息,更新内部存储的日期时间(mDate)对应的各个字段,如年、月、日、小时、分钟等 + mDate.set(Calendar.YEAR, year); + mDate.set(Calendar.MONTH, month); + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + mDate.set(Calendar.MINUTE, minute); + // 根据更新后的日期时间,调用updateTitle方法更新对话框标题显示,使其展示当前选择的日期时间信息 + updateTitle(mDate.getTimeInMillis()); + } + }); + + // 设置对话框初始显示的日期时间,先将传入的时间戳设置到内部的Calendar对象(mDate)中,同时将秒数设置为0,保证初始时间更规整 + mDate.setTimeInMillis(date); + mDate.set(Calendar.SECOND, 0); + // 将DateTimePicker的当前日期时间设置为和内部存储的初始日期时间一致,保证界面显示和内部数据的同步 + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + + // 设置对话框的确定按钮文本以及点击监听器,点击确定按钮时会触发当前类(实现了OnClickListener接口)的onClick方法 + setButton(context.getString(R.string.datetime_dialog_ok), this); + // 设置对话框的取消按钮文本以及点击监听器为null,表示点击取消按钮直接关闭对话框,不做额外处理 + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + + // 根据系统设置判断是否为24小时制视图,并设置到内部相应的变量以及对应的UI显示状态(例如是否显示AM/PM选择器等) + set24HourView(DateFormat.is24HourFormat(this.getContext())); + // 初始更新一次对话框标题,展示初始的日期时间信息 + updateTitle(mDate.getTimeInMillis()); + } + + // 设置是否为24小时制视图的方法,传入布尔值参数,更新内部的标记变量,并会影响DateTimePicker中相关UI组件的显示情况(如AM/PM选择器的可见性等) + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + // 设置日期时间选择完成后的监听器方法,外部类可通过传入实现了OnDateTimeSetListener接口的实例,来响应最终用户确定选择的操作 + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + // 根据传入的日期时间(以毫秒为单位的时间戳)更新对话框的标题显示内容,按照指定的格式展示日期、时间等信息 + private void updateTitle(long date) { + int flag = + DateUtils.FORMAT_SHOW_YEAR | // 显示年份 + DateUtils.FORMAT_SHOW_DATE | // 显示日期(月、日等) + DateUtils.FORMAT_SHOW_TIME; // 显示时间(小时、分钟等) + + // 根据是否为24小时制视图,添加对应的时间格式标志,用于控制最终显示的时间格式(24小时制还是12小时制带AM/PM) + flag |= mIs24HourView? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; + + // 使用DateUtils工具类的formatDateTime方法,按照设置好的格式标志,将传入的日期时间转换为字符串,并设置为对话框的标题内容 + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + // 实现OnClickListener接口的onClick方法,当用户点击对话框中的按钮(确定按钮,因为在构造函数中设置了当前类为确定按钮的点击监听器)时会触发此方法 + // 如果设置了日期时间选择完成后的监听器(mOnDateTimeSetListener),则调用其OnDateTimeSet方法,传入对话框本身以及当前选择的日期时间(以毫秒为单位的时间戳),通知外部类用户已完成选择操作 + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener!= null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } + + } \ No newline at end of file diff --git a/src/net/micode/notes/ui/DropdownMenu.java b/src/net/micode/notes/ui/DropdownMenu.java index 613dc74..5ef1027 100644 --- a/src/net/micode/notes/ui/DropdownMenu.java +++ b/src/net/micode/notes/ui/DropdownMenu.java @@ -14,48 +14,67 @@ * limitations under the License. */ -package net.micode.notes.ui; + package net.micode.notes.ui; -import android.content.Context; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.PopupMenu; -import android.widget.PopupMenu.OnMenuItemClickListener; - -import net.micode.notes.R; - -public class DropdownMenu { - private Button mButton; - private PopupMenu mPopupMenu; - private Menu mMenu; - - public DropdownMenu(Context context, Button button, int menuId) { - mButton = button; - mButton.setBackgroundResource(R.drawable.dropdown_icon); - mPopupMenu = new PopupMenu(context, mButton); - mMenu = mPopupMenu.getMenu(); - mPopupMenu.getMenuInflater().inflate(menuId, mMenu); - mButton.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - mPopupMenu.show(); - } - }); - } - - public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { - if (mPopupMenu != null) { - mPopupMenu.setOnMenuItemClickListener(listener); - } - } - - public MenuItem findItem(int id) { - return mMenu.findItem(id); - } - - public void setTitle(CharSequence title) { - mButton.setText(title); - } -} + import android.content.Context; + import android.view.Menu; + import android.view.MenuItem; + import android.view.View; + import android.view.View.OnClickListener; + import android.widget.Button; + import android.widget.PopupMenu; + import android.widget.PopupMenu.OnMenuItemClickListener; + + import net.micode.notes.R; + + // 自定义的下拉菜单类,用于创建和管理一个基于按钮点击弹出的菜单,方便在安卓界面中实现下拉式的功能菜单交互 + public class DropdownMenu { + + // 用于显示下拉菜单的按钮控件,点击此按钮会弹出对应的菜单,外部传入并在类内部进行相关操作 + private Button mButton; + // 实际的PopupMenu实例,它管理着弹出菜单的显示、隐藏以及菜单项相关操作等逻辑 + private PopupMenu mPopupMenu; + // 对应的Menu实例,用于获取、操作具体的菜单项,通过PopupMenu获取并进行后续的查找、设置等操作 + private Menu mMenu; + + // 构造函数,用于创建一个DropdownMenu实例,传入上下文、用于触发弹出菜单的按钮以及菜单资源ID(用于定义菜单项等信息) + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; + // 设置按钮的背景资源为指定的下拉图标资源(通常用于提示用户此按钮可点击弹出菜单),这里从R.drawable.dropdown_icon获取对应的图标资源 + mButton.setBackgroundResource(R.drawable.dropdown_icon); + + // 创建一个PopupMenu实例,关联传入的上下文以及触发弹出的按钮,这样弹出菜单会基于此按钮的位置等属性进行显示 + mPopupMenu = new PopupMenu(context, mButton); + + // 获取PopupMenu对应的Menu实例,后续可以通过这个实例来操作具体的菜单项,例如添加、查找、设置菜单项属性等 + mMenu = mPopupMenu.getMenu(); + + // 使用菜单填充器(从PopupMenu获取),根据传入的菜单资源ID(通常在XML资源文件中定义了菜单项的文本、图标等信息),将菜单项填充到Menu实例中,完成菜单的初始化构建 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + + // 为按钮设置点击监听器,当按钮被点击时,触发内部的onClick方法,在该方法中调用PopupMenu的show方法来显示弹出菜单 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + // 设置下拉菜单中菜单项的点击监听器方法,传入实现了OnMenuItemClickListener接口的实例,用于响应各个菜单项被点击后的操作逻辑 + // 如果PopupMenu实例不为空,则将传入的监听器设置给PopupMenu,这样当用户点击菜单项时,对应的回调方法就会被触发 + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu!= null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + // 根据传入的菜单项ID,在已构建的Menu实例中查找对应的MenuItem实例,方便外部类获取菜单项进行进一步的操作,例如获取菜单项的文本、图标、设置是否可用等 + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + // 设置按钮显示的文本内容,可用于给下拉菜单按钮设置一个标题之类的提示性文字,方便用户了解此下拉菜单的大致功能等信息 + public void setTitle(CharSequence title) { + mButton.setText(title); + } + } \ No newline at end of file diff --git a/src/net/micode/notes/ui/FoldersListAdapter.java b/src/net/micode/notes/ui/FoldersListAdapter.java index 96b77da..8559cde 100644 --- a/src/net/micode/notes/ui/FoldersListAdapter.java +++ b/src/net/micode/notes/ui/FoldersListAdapter.java @@ -14,67 +14,90 @@ * limitations under the License. */ -package net.micode.notes.ui; + package net.micode.notes.ui; -import android.content.Context; -import android.database.Cursor; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CursorAdapter; -import android.widget.LinearLayout; -import android.widget.TextView; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; - - -public class FoldersListAdapter extends CursorAdapter { - public static final String [] PROJECTION = { - NoteColumns.ID, - NoteColumns.SNIPPET - }; - - public static final int ID_COLUMN = 0; - public static final int NAME_COLUMN = 1; - - public FoldersListAdapter(Context context, Cursor c) { - super(context, c); - // TODO Auto-generated constructor stub - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return new FolderListItem(context); - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - if (view instanceof FolderListItem) { - String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + import android.content.Context; + import android.database.Cursor; + import android.view.View; + import android.view.ViewGroup; + import android.widget.CursorAdapter; + import android.widget.LinearLayout; + import android.widget.TextView; + + import net.micode.notes.R; + import net.micode.notes.data.Notes; + import net.micode.notes.data.Notes.NoteColumns; + + // FoldersListAdapter类继承自CursorAdapter,用于将数据库游标(Cursor)中的数据适配到特定的视图上, + // 在这里主要是为了展示文件夹相关信息列表,比如文件夹名称等,方便在列表视图等UI组件中显示。 + public class FoldersListAdapter extends CursorAdapter { + + // 定义一个字符串数组,用于指定从数据库查询时需要获取的列名,这里只获取了"ID"和"SNIPPET"两列数据, + // 通常会对应数据库中存储文件夹相关信息的列,后续用于填充视图展示给用户。 + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + // 定义一个常量,表示在查询结果游标(Cursor)中"ID"列所在的索引位置,方便后续通过游标获取对应列的数据。 + public static final int ID_COLUMN = 0; + // 定义一个常量,表示在查询结果游标(Cursor)中"NAME"(此处实际对应SNIPPET列,可能用于表示文件夹名称相关内容)列所在的索引位置。 + public static final int NAME_COLUMN = 1; + + // 构造函数,接收上下文(Context)和数据库游标(Cursor)作为参数,调用父类(CursorAdapter)的构造函数进行初始化操作。 + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub,这里可以添加一些自定义的初始化逻辑,如果有需要的话,当前为空。 + } + + // 重写CursorAdapter的newView方法,此方法用于创建一个新的视图(View)对象,该视图将用于显示游标中对应的数据项。 + // 在这里返回一个自定义的FolderListItem实例,它继承自LinearLayout,是每个列表项对应的视图布局。 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + // 重写CursorAdapter的bindView方法,此方法用于将游标(Cursor)中当前位置的数据绑定到指定的视图(View)上, + // 也就是填充视图中的各个控件(如TextView等),使其显示对应的数据内容。 + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + // 根据游标中获取的文件夹ID判断是否是根文件夹(通过和Notes.ID_ROOT_FOLDER比较), + // 如果是根文件夹,则获取对应的字符串资源(通常是用于显示"上级文件夹"之类表示根文件夹含义的文本)作为文件夹名称, + // 如果不是根文件夹,则从游标中获取对应列(NAME_COLUMN)的数据作为文件夹名称。 + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); - ((FolderListItem) view).bind(folderName); - } - } - - public String getFolderName(Context context, int position) { - Cursor cursor = (Cursor) getItem(position); - return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + // 调用FolderListItem的bind方法,将获取到的文件夹名称设置到对应的TextView控件中,完成数据绑定显示操作。 + ((FolderListItem) view).bind(folderName); + } + } + + // 定义一个方法,用于根据给定的位置(position)获取对应的文件夹名称。 + // 首先通过getItem方法(继承自CursorAdapter)获取该位置对应的游标(Cursor)对象, + // 然后同样根据游标中文件夹ID判断是否是根文件夹来确定返回的文件夹名称内容。 + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); - } - - private class FolderListItem extends LinearLayout { - private TextView mName; - - public FolderListItem(Context context) { - super(context); - inflate(context, R.layout.folder_list_item, this); - mName = (TextView) findViewById(R.id.tv_folder_name); - } - - public void bind(String name) { - mName.setText(name); - } - } - -} + } + + // 定义一个内部类FolderListItem,继承自LinearLayout,代表每个文件夹列表项的具体视图布局, + // 内部包含了用于显示文件夹名称的TextView控件,并提供了相应的方法来设置显示的文本内容。 + private class FolderListItem extends LinearLayout { + private TextView mName; + + // 构造函数,接收上下文(Context)作为参数,调用父类(LinearLayout)的构造函数进行初始化, + // 并通过inflate方法加载对应的布局文件(R.layout.folder_list_item)到当前视图中, + // 然后获取布局中用于显示文件夹名称的TextView控件实例。 + public FolderListItem(Context context) { + super(context); + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + // 定义一个方法,用于将传入的文件夹名称(name)设置到内部的TextView控件(mName)中,实现文本显示更新。 + public void bind(String name) { + mName.setText(name); + } + } + } \ No newline at end of file diff --git a/src/net/micode/notes/ui/NoteEditText.java b/src/net/micode/notes/ui/NoteEditText.java index 2afe2a8..dc4b7ed 100644 --- a/src/net/micode/notes/ui/NoteEditText.java +++ b/src/net/micode/notes/ui/NoteEditText.java @@ -14,204 +14,269 @@ * limitations under the License. */ -package net.micode.notes.ui; + package net.micode.notes.ui; -import android.content.Context; -import android.graphics.Rect; -import android.text.Layout; -import android.text.Selection; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.style.URLSpan; -import android.util.AttributeSet; -import android.util.Log; -import android.view.ContextMenu; -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; -import android.view.MotionEvent; -import android.widget.EditText; - -import net.micode.notes.R; - -import java.util.HashMap; -import java.util.Map; - -public class NoteEditText extends EditText { - private static final String TAG = "NoteEditText"; - private int mIndex; - private int mSelectionStartBeforeDelete; - - private static final String SCHEME_TEL = "tel:" ; - private static final String SCHEME_HTTP = "http:" ; - private static final String SCHEME_EMAIL = "mailto:" ; - - private static final Map sSchemaActionResMap = new HashMap(); - static { - sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); - sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); - sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); - } - - /** - * Call by the {@link NoteEditActivity} to delete or add edit text - */ - public interface OnTextViewChangeListener { - /** - * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens - * and the text is null - */ - void onEditTextDelete(int index, String text); - - /** - * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} - * happen - */ - void onEditTextEnter(int index, String text); - - /** - * Hide or show item option when text change - */ - void onTextChange(int index, boolean hasText); - } - - private OnTextViewChangeListener mOnTextViewChangeListener; - - public NoteEditText(Context context) { - super(context, null); - mIndex = 0; - } - - public void setIndex(int index) { - mIndex = index; - } - - public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { - mOnTextViewChangeListener = listener; - } - - public NoteEditText(Context context, AttributeSet attrs) { - super(context, attrs, android.R.attr.editTextStyle); - } - - public NoteEditText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - // TODO Auto-generated constructor stub - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - - int x = (int) event.getX(); - int y = (int) event.getY(); - x -= getTotalPaddingLeft(); - y -= getTotalPaddingTop(); - x += getScrollX(); - y += getScrollY(); - - Layout layout = getLayout(); - int line = layout.getLineForVertical(y); - int off = layout.getOffsetForHorizontal(line, x); - Selection.setSelection(getText(), off); - break; - } - - return super.onTouchEvent(event); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_ENTER: - if (mOnTextViewChangeListener != null) { - return false; - } - break; - case KeyEvent.KEYCODE_DEL: - mSelectionStartBeforeDelete = getSelectionStart(); - break; - default: - break; - } - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - switch(keyCode) { - case KeyEvent.KEYCODE_DEL: - if (mOnTextViewChangeListener != null) { - if (0 == mSelectionStartBeforeDelete && mIndex != 0) { - mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); - return true; - } - } else { - Log.d(TAG, "OnTextViewChangeListener was not seted"); - } - break; - case KeyEvent.KEYCODE_ENTER: - if (mOnTextViewChangeListener != null) { - int selectionStart = getSelectionStart(); - String text = getText().subSequence(selectionStart, length()).toString(); - setText(getText().subSequence(0, selectionStart)); - mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); - } else { - Log.d(TAG, "OnTextViewChangeListener was not seted"); - } - break; - default: - break; - } - return super.onKeyUp(keyCode, event); - } - - @Override - protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - if (mOnTextViewChangeListener != null) { - if (!focused && TextUtils.isEmpty(getText())) { - mOnTextViewChangeListener.onTextChange(mIndex, false); - } else { - mOnTextViewChangeListener.onTextChange(mIndex, true); - } - } - super.onFocusChanged(focused, direction, previouslyFocusedRect); - } - - @Override - protected void onCreateContextMenu(ContextMenu menu) { - if (getText() instanceof Spanned) { - int selStart = getSelectionStart(); - int selEnd = getSelectionEnd(); - - int min = Math.min(selStart, selEnd); - int max = Math.max(selStart, selEnd); - - final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); - if (urls.length == 1) { - int defaultResId = 0; - for(String schema: sSchemaActionResMap.keySet()) { - if(urls[0].getURL().indexOf(schema) >= 0) { - defaultResId = sSchemaActionResMap.get(schema); - break; - } - } - - if (defaultResId == 0) { - defaultResId = R.string.note_link_other; - } - - menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( - new OnMenuItemClickListener() { - public boolean onMenuItemClick(MenuItem item) { - // goto a new intent - urls[0].onClick(NoteEditText.this); - return true; - } - }); - } - } - super.onCreateContextMenu(menu); - } -} + import android.content.Context; + import android.graphics.Rect; + import android.text.Layout; + import android.text.Selection; + import android.text.Spanned; + import android.text.TextUtils; + import android.text.style.URLSpan; + import android.util.AttributeSet; + import android.util.Log; + import android.view.ContextMenu; + import android.view.KeyEvent; + import android.view.MenuItem; + import android.view.MenuItem.OnMenuItemClickListener; + import android.view.MotionEvent; + import android.widget.EditText; + + import net.micode.notes.R; + + import java.util.HashMap; + import java.util.Map; + + // NoteEditText类继承自EditText,是一个自定义的文本编辑框控件,在原生EditText的基础上添加了一些特定的功能和交互逻辑, + // 例如处理特定按键事件、触摸事件以及上下文菜单相关操作等,用于笔记编辑等相关场景。 + public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; + // 用于记录当前文本编辑框的索引位置,可能在多个编辑框组成的列表等场景下用于区分不同的编辑框个体。 + private int mIndex; + // 用于记录在删除操作前文本选择的起始位置,方便后续判断删除相关逻辑,例如是否删除整个编辑框内容等情况。 + private int mSelectionStartBeforeDelete; + + // 定义表示电话号码链接的协议头(scheme)字符串,用于识别文本中是否包含电话号码链接。 + private static final String SCHEME_TEL = "tel:" ; + // 定义表示网页链接(HTTP协议)的协议头字符串,用于识别文本中的网页链接。 + private static final String SCHEME_HTTP = "http:" ; + // 定义表示电子邮件链接的协议头字符串,用于识别文本中的邮件链接。 + private static final String SCHEME_EMAIL = "mailto:" ; + + // 创建一个HashMap,用于存储不同协议头(scheme)与对应的字符串资源ID的映射关系, + // 这些字符串资源ID通常用于在界面上显示对应链接类型的操作提示文本等内容。 + private static final Map sSchemaActionResMap = new HashMap(); + static { + // 将不同协议头与对应的字符串资源ID添加到映射表中,方便后续根据链接类型查找对应的显示文本资源。 + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + } + + // 定义一个接口,用于与外部类进行交互,外部类实现此接口可以监听该文本编辑框内文本相关的变化事件, + // 例如文本删除、回车键按下添加新编辑框以及文本有无变化等情况,并做出相应处理。 + public interface OnTextViewChangeListener { + /** + * 当按下删除键({@link KeyEvent#KEYCODE_DEL})且文本内容为空时,删除当前编辑文本框, + * 外部类实现此方法可处理对应删除逻辑,比如从列表中移除该编辑框等操作。 + */ + void onEditTextDelete(int index, String text); + + /** + * 当按下回车键({@link KeyEvent#KEYCODE_ENTER})时,在当前编辑文本框之后添加新的编辑文本框, + * 外部类实现此方法可进行创建新编辑框并添加到合适位置等相关操作。 + */ + void onEditTextEnter(int index, String text); + + /** + * 当文本内容发生变化时,隐藏或显示相关的菜单项选项等,外部类可根据文本有无来控制对应UI元素的显示隐藏状态。 + */ + void onTextChange(int index, boolean hasText); + } + + // 用于存储实现了OnTextViewChangeListener接口的实例,以便在文本编辑框相关事件发生时通知外部类进行相应处理。 + private OnTextViewChangeListener mOnTextViewChangeListener; + + // 构造函数,只传入上下文(Context)参数,调用父类构造函数创建实例,并初始化索引位置为0, + // 这种构造方式常用于通过代码动态创建该编辑框实例的场景。 + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + // 设置当前文本编辑框的索引位置的方法,外部可调用此方法来更新索引值,方便区分不同编辑框个体。 + public void setIndex(int index) { + mIndex = index; + } + + // 设置文本变化监听器的方法,外部类通过传入实现了OnTextViewChangeListener接口的实例, + // 来注册监听该编辑框相关文本变化事件,以便做出相应处理。 + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + // 构造函数,传入上下文(Context)和属性集(AttributeSet)参数,按照安卓系统默认的文本编辑框样式(通过android.R.attr.editTextStyle指定)创建实例, + // 通常用于在XML布局文件中定义该编辑框并解析相关属性进行初始化的场景。 + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + // 构造函数,传入上下文(Context)、属性集(AttributeSet)和默认样式(defStyle)参数,创建编辑框实例, + // 可用于更灵活地自定义编辑框的样式和属性,当前构造函数中没有添加额外的初始化逻辑(TODO处可按需添加)。 + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub + } + + // 重写父类的onTouchEvent方法,用于处理触摸事件,在这里主要是处理按下(ACTION_DOWN)动作, + // 目的是根据触摸的坐标位置来设置文本的选择位置,方便用户后续进行复制、粘贴等操作与选中位置相关的操作。 + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // 获取触摸点的原始X坐标 + int x = (int) event.getX(); + // 获取触摸点的原始Y坐标 + int y = (int) event.getY(); + // 减去左边的总内边距,将坐标转换为相对于文本内容区域的坐标 + x -= getTotalPaddingLeft(); + // 减去上边的总内边距,同样将坐标转换为相对于文本内容区域的坐标 + y -= getTotalPaddingTop(); + // 加上滚动的X偏移量,考虑文本滚动情况,获取准确的相对于文本内容的坐标 + x += getScrollX(); + // 加上滚动的Y偏移量,获取准确的相对于文本内容的坐标 + y += getScrollY(); + + // 获取文本的布局信息对象,用于后续根据坐标获取对应文本位置等操作 + Layout layout = getLayout(); + // 根据触摸点的垂直坐标(Y坐标)获取对应的文本行数 + int line = layout.getLineForVertical(y); + // 根据触摸点的水平坐标(X坐标)以及所在行数,获取对应的文本字符偏移量(即文本中的位置索引) + int off = layout.getOffsetForHorizontal(line, x); + // 根据获取到的字符偏移量设置文本的选择范围,这里只设置了一个位置,相当于将光标定位到该位置 + Selection.setSelection(getText(), off); + break; + } + + // 调用父类的onTouchEvent方法继续处理其他触摸事件相关逻辑,保证原生的触摸行为也能正常执行 + return super.onTouchEvent(event); + } + + // 重写父类的onKeyDown方法,用于处理按键按下事件,在这里主要是针对回车键(KEYCODE_ENTER)和删除键(KEYCODE_DEL)按下时做一些前期记录等准备工作, + // 具体的业务逻辑处理在按键抬起(onKeyUp)方法中进行。 + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + // 如果设置了文本变化监听器(mOnTextViewChangeListener不为空),表示外部类关注回车键按下事件, + // 这里返回false,让后续的按键抬起(onKeyUp)事件能继续处理相关逻辑,例如添加新编辑框等操作。 + if (mOnTextViewChangeListener!= null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + // 当按下删除键时,记录当前文本选择的起始位置,方便后续判断是否删除整个编辑框内容等情况。 + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + // 调用父类的onKeyDown方法继续处理其他按键按下相关逻辑,保证原生的按键按下行为也能正常执行 + return super.onKeyDown(keyCode, event); + } + + // 重写父类的onKeyUp方法,用于处理按键抬起事件,在这里针对回车键(KEYCODE_ENTER)和删除键(KEYCODE_DEL)抬起时, + // 根据不同情况执行相应的业务逻辑,例如删除编辑框、添加新编辑框等操作,前提是设置了文本变化监听器(mOnTextViewChangeListener不为空)。 + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch(keyCode) { + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener!= null) { + // 判断如果文本选择的起始位置为0(通常表示光标在文本开头)且当前编辑框索引不为0(说明不是第一个编辑框), + // 则调用文本变化监听器的onEditTextDelete方法通知外部类删除当前编辑框,然后返回true,表示已经处理了该按键事件。 + if (0 == mSelectionStartBeforeDelete && mIndex!= 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + // 如果没有设置文本变化监听器,则打印日志提示信息,表示监听器未设置,无法处理删除相关逻辑。 + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener!= null) { + // 获取当前文本选择的起始位置,用于后续截取文本操作。 + int selectionStart = getSelectionStart(); + // 获取从文本选择起始位置到文本末尾的子字符串,可能是要添加到新编辑框的内容(如果有需要的话)。 + String text = getText().subSequence(selectionStart, length()).toString(); + // 将当前编辑框的文本内容截取为从开头到选择起始位置的部分,相当于清空了选择位置之后的文本(可能是要添加到新编辑框的部分)。 + setText(getText().subSequence(0, selectionStart)); + // 调用文本变化监听器的onEditTextEnter方法通知外部类在当前编辑框之后添加新编辑框,并传入新编辑框索引和可能要添加的文本内容。 + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + // 如果没有设置文本变化监听器,则打印日志提示信息,表示监听器未设置,无法处理回车键按下添加新编辑框相关逻辑。 + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + // 调用父类的onKeyUp方法继续处理其他按键抬起相关逻辑,保证原生的按键抬起行为也能正常执行 + return super.onKeyUp(keyCode, event); + } + + // 重写父类的onFocusChanged方法,用于处理焦点变化事件,在这里根据焦点状态(是否获得焦点)以及文本内容是否为空, + // 通过文本变化监听器(mOnTextViewChangeListener)通知外部类文本有无情况,以便外部类进行相应的UI元素显示隐藏等操作。 + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener!= null) { + if (!focused && TextUtils.isEmpty(getText())) { + // 如果失去焦点且文本内容为空,调用文本变化监听器的onTextChange方法通知外部类文本为空的情况,参数hasText为false。 + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + // 如果获得焦点或者文本内容不为空,调用文本变化监听器的onTextChange方法通知外部类文本有内容的情况,参数hasText为true。 + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + // 重写父类的onCreateContextMenu方法,用于创建上下文菜单(长按文本等操作弹出的菜单), + // 在这里主要是检查当前选中的文本中是否包含URL链接(通过URLSpan判断),如果包含且只有一个链接, + // 则根据链接的协议头(scheme)查找对应的字符串资源ID,添加一个菜单项用于执行链接相关操作(如打开网页、拨打电话等)。 + @Override + protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + // 获取当前文本选择的起始位置 + int selStart = getSelectionStart(); + // 获取当前文本选择的结束位置 + int selEnd = getSelectionEnd(); + + // 获取选择起始位置和结束位置中的较小值,用于后续范围判断等操作 + int min = Math.min(selStart, selEnd); + // 获取选择起始位置和结束位置中的较大值,用于后续范围判断等操作 + int max = Math.max(selStart, selEnd); + + // 获取在选择文本范围内的所有URLSpan对象(即包含的链接信息),返回一个数组,可能包含多个链接(但这里后续只处理长度为1的情况)。 + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + for(String schema: sSchemaActionResMap.keySet()) { + // 遍历存储协议头与字符串资源ID映射关系的集合,检查链接的协议头是否匹配已定义的协议头(如tel、http、mailto等) + if(urls[0].getURL().indexOf(schema) >= 0) { + // 如果匹配,获取对应的字符串资源ID,用于设置菜单项显示的文本内容。 + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + // 如果没有匹配到已知的协议头,设置一个默认的字符串资源ID,表示其他类型链接。 + defaultResId = R.string.note_link_other; + } + + // 添加一个菜单项到上下文菜单中,菜单项的ID、顺序、分组等参数暂设为0(可根据实际需求调整),显示的文本内容根据获取到的字符串资源ID获取, + // 并设置菜单项的点击监听器,当点击该菜单项时,通过URLSpan的onClick方法执行链接相关操作(如打开网页、拨打电话等)。 + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // goto a new intent + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); + } + } \ No newline at end of file diff --git a/src/net/micode/notes/ui/NoteItemData.java b/src/net/micode/notes/ui/NoteItemData.java index 0f5a878..f6c282a 100644 --- a/src/net/micode/notes/ui/NoteItemData.java +++ b/src/net/micode/notes/ui/NoteItemData.java @@ -14,211 +14,320 @@ * limitations under the License. */ -package net.micode.notes.ui; - -import android.content.Context; -import android.database.Cursor; -import android.text.TextUtils; - -import net.micode.notes.data.Contact; -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.tool.DataUtils; - - -public class NoteItemData { - static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.ALERTED_DATE, - NoteColumns.BG_COLOR_ID, - NoteColumns.CREATED_DATE, - NoteColumns.HAS_ATTACHMENT, - NoteColumns.MODIFIED_DATE, - NoteColumns.NOTES_COUNT, - NoteColumns.PARENT_ID, - NoteColumns.SNIPPET, - NoteColumns.TYPE, - NoteColumns.WIDGET_ID, - NoteColumns.WIDGET_TYPE, - }; - - private static final int ID_COLUMN = 0; - private static final int ALERTED_DATE_COLUMN = 1; - private static final int BG_COLOR_ID_COLUMN = 2; - private static final int CREATED_DATE_COLUMN = 3; - private static final int HAS_ATTACHMENT_COLUMN = 4; - private static final int MODIFIED_DATE_COLUMN = 5; - private static final int NOTES_COUNT_COLUMN = 6; - private static final int PARENT_ID_COLUMN = 7; - private static final int SNIPPET_COLUMN = 8; - private static final int TYPE_COLUMN = 9; - private static final int WIDGET_ID_COLUMN = 10; - private static final int WIDGET_TYPE_COLUMN = 11; - - private long mId; - private long mAlertDate; - private int mBgColorId; - private long mCreatedDate; - private boolean mHasAttachment; - private long mModifiedDate; - private int mNotesCount; - private long mParentId; - private String mSnippet; - private int mType; - private int mWidgetId; - private int mWidgetType; - private String mName; - private String mPhoneNumber; - - private boolean mIsLastItem; - private boolean mIsFirstItem; - private boolean mIsOnlyOneItem; - private boolean mIsOneNoteFollowingFolder; - private boolean mIsMultiNotesFollowingFolder; - - public NoteItemData(Context context, Cursor cursor) { - mId = cursor.getLong(ID_COLUMN); - mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); - mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); - mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); - mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; - mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); - mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); - mParentId = cursor.getLong(PARENT_ID_COLUMN); - mSnippet = cursor.getString(SNIPPET_COLUMN); - mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( - NoteEditActivity.TAG_UNCHECKED, ""); - mType = cursor.getInt(TYPE_COLUMN); - mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); - mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); - - mPhoneNumber = ""; - if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { - mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); - if (!TextUtils.isEmpty(mPhoneNumber)) { - mName = Contact.getContact(context, mPhoneNumber); - if (mName == null) { - mName = mPhoneNumber; - } - } - } - - if (mName == null) { - mName = ""; - } - checkPostion(cursor); - } - - private void checkPostion(Cursor cursor) { - mIsLastItem = cursor.isLast() ? true : false; - mIsFirstItem = cursor.isFirst() ? true : false; - mIsOnlyOneItem = (cursor.getCount() == 1); - mIsMultiNotesFollowingFolder = false; - mIsOneNoteFollowingFolder = false; - - if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { - int position = cursor.getPosition(); - if (cursor.moveToPrevious()) { - if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER - || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { - if (cursor.getCount() > (position + 1)) { - mIsMultiNotesFollowingFolder = true; - } else { - mIsOneNoteFollowingFolder = true; - } - } - if (!cursor.moveToNext()) { - throw new IllegalStateException("cursor move to previous but can't move back"); - } - } - } - } - - public boolean isOneFollowingFolder() { - return mIsOneNoteFollowingFolder; - } - - public boolean isMultiFollowingFolder() { - return mIsMultiNotesFollowingFolder; - } - - public boolean isLast() { - return mIsLastItem; - } - - public String getCallName() { - return mName; - } - - public boolean isFirst() { - return mIsFirstItem; - } - - public boolean isSingle() { - return mIsOnlyOneItem; - } - - public long getId() { - return mId; - } - - public long getAlertDate() { - return mAlertDate; - } - - public long getCreatedDate() { - return mCreatedDate; - } - + package net.micode.notes.ui; + + import android.content.Context; + import android.database.Cursor; + import android.text.TextUtils; + + import net.micode.notes.data.Contact; + import net.micode.notes.data.Notes; + import net.micode.notes.data.Notes.NoteColumns; + import net.micode.notes.tool.DataUtils; + + // NoteItemData类用于从数据库游标(Cursor)中提取笔记相关的数据信息,并进行一些必要的处理和状态判断, + // 方便在应用中以对象的形式使用和传递笔记的各项属性数据,例如笔记的ID、创建日期、是否有附件等信息。 + public class NoteItemData { + + // 定义一个字符串数组,用于指定从数据库查询笔记相关信息时需要获取的列名,涵盖了笔记的多个属性字段, + // 后续可通过游标(Cursor)根据这些列名获取对应的数据来填充当前类的各个成员变量。 + static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, + NoteColumns.HAS_ATTACHMENT, + NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, + NoteColumns.PARENT_ID, + NoteColumns.SNIPPET, + NoteColumns.TYPE, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + }; + + // 定义一个常量,表示在查询结果游标(Cursor)中"ID"列所在的索引位置,方便后续通过游标获取对应列的数据。 + private static final int ID_COLUMN = 0; + // 定义一个常量,表示在查询结果游标(Cursor)中"ALERTED_DATE"列所在的索引位置,用于获取笔记的提醒日期数据。 + private static final int ALERTED_DATE_COLUMN = 1; + // 定义一个常量,表示在查询结果游标(Cursor)中"BG_COLOR_ID"列所在的索引位置,用于获取笔记的背景颜色ID数据。 + private static final int BG_COLOR_ID_COLUMN = 2; + // 定义一个常量,表示在查询结果游标(Cursor)中"CREATED_DATE"列所在的索引位置,用于获取笔记的创建日期数据。 + private static final int CREATED_DATE_COLUMN = 3; + // 定义一个常量,表示在查询结果游标(Cursor)中"HAS_ATTACHMENT"列所在的索引位置,用于判断笔记是否有附件。 + private static final int HAS_ATTACHMENT_COLUMN = 4; + // 定义一个常量,表示在查询结果游标(Cursor)中"MODIFIED_DATE"列所在的索引位置,用于获取笔记的修改日期数据。 + private static final int MODIFIED_DATE_COLUMN = 5; + // 定义一个常量,表示在查询结果游标(Cursor)中"NOTES_COUNT"列所在的索引位置,可能用于获取与笔记相关的数量信息(具体含义取决于业务逻辑)。 + private static final int NOTES_COUNT_COLUMN = 6; + // 定义一个常量,表示在查询结果游标(Cursor)中"PARENT_ID"列所在的索引位置,用于获取笔记所属父级的ID(例如所属文件夹的ID等)。 + private static final int PARENT_ID_COLUMN = 7; + // 定义一个常量,表示在查询结果游标(Cursor)中"SNIPPET"列所在的索引位置,通常用于获取笔记的摘要、简短描述等文本内容。 + private static final int SNIPPET_COLUMN = 8; + // 定义一个常量,表示在查询结果游标(Cursor)中"TYPE"列所在的索引位置,用于获取笔记的类型信息(例如普通笔记、系统笔记等不同类型的区分)。 + private static final int TYPE_COLUMN = 9; + // 定义一个常量,表示在查询结果游标(Cursor)中"WIDGET_ID"列所在的索引位置,可能与笔记在桌面小部件等相关功能中的标识ID有关。 + private static final int WIDGET_ID_COLUMN = 10; + // 定义一个常量,表示在查询结果游标(Cursor)中"WIDGET_TYPE"列所在的索引位置,可能用于区分不同类型的桌面小部件相关信息(如果有涉及的话)。 + private static final int WIDGET_TYPE_COLUMN = 11; + + // 用于存储笔记的ID,从数据库游标中获取对应列的数据进行赋值,代表该笔记在系统中的唯一标识。 + private long mId; + // 用于存储笔记的提醒日期,从数据库游标获取对应数据,可用于判断笔记是否设置了提醒以及具体的提醒时间点。 + private long mAlertDate; + // 用于存储笔记的背景颜色ID,根据游标数据赋值,可能用于在界面上显示笔记时设置对应的背景颜色。 + private int mBgColorId; + // 用于存储笔记的创建日期,通过游标获取,记录笔记最初被创建的时间信息。 + private long mCreatedDate; + // 用于标记笔记是否有附件,根据游标中对应列的数据转换为布尔值进行存储(大于0表示有附件,赋值为true,否则为false)。 + private boolean mHasAttachment; + // 用于存储笔记的修改日期,从游标获取相应数据,可用于跟踪笔记最后一次被修改的时间。 + private long mModifiedDate; + // 用于存储与笔记相关的数量信息(具体含义取决于业务逻辑,可能是关联的子笔记数量等情况),从游标获取对应整数值进行赋值。 + private int mNotesCount; + // 用于存储笔记所属父级的ID(比如所属文件夹的ID),从游标获取相应长整数值,方便判断笔记的归属关系等。 + private long mParentId; + // 用于存储笔记的摘要、简短描述等文本内容,从游标获取字符串数据,并进行一些特定字符串替换操作后赋值,去除可能存在的特定标记字符。 + private String mSnippet; + // 用于存储笔记的类型信息,从游标获取对应整数来表示不同类型(例如普通笔记、系统笔记等不同分类),便于后续根据类型进行不同的业务逻辑处理。 + private int mType; + // 用于存储可能与笔记在桌面小部件等相关功能中的标识ID,从游标获取对应整数值进行赋值(具体用途取决于相关小部件功能实现)。 + private int mWidgetId; + // 用于存储可能用于区分不同类型桌面小部件相关信息的类型值(如果有涉及桌面小部件相关业务逻辑的话),从游标获取对应整数进行赋值。 + private int mWidgetType; + // 用于存储与笔记相关的联系人姓名信息,如果笔记属于通话记录文件夹等相关情况,会尝试获取对应的联系人姓名,初始化为空字符串。 + private String mName; + // 用于存储与笔记相关的电话号码信息,如果笔记属于通话记录文件夹,会尝试获取对应的电话号码,初始化为空字符串。 + private String mPhoneNumber; + + // 用于标记当前笔记数据对应的笔记是否是列表中的最后一项,根据游标是否处于最后一条记录来判断赋值。 + private boolean mIsLastItem; + // 用于标记当前笔记数据对应的笔记是否是列表中的第一项,根据游标是否处于第一条记录来判断赋值。 + private boolean mIsFirstItem; + // 用于标记当前笔记数据对应的笔记是否是列表中唯一的一项,通过判断游标获取的总记录数是否为1来赋值。 + private boolean mIsOnlyOneItem; + // 用于标记当前笔记数据对应的笔记是否是某文件夹下仅跟随的一个笔记(具体判断逻辑在后续方法中实现),初始化为false。 + private boolean mIsOneNoteFollowingFolder; + // 用于标记当前笔记数据对应的笔记是否是某文件夹下跟随的多个笔记之一(具体判断逻辑在后续方法中实现),初始化为false。 + private boolean mIsMultiNotesFollowingFolder; + + // 构造函数,接收上下文(Context)和数据库游标(Cursor)作为参数,用于从游标中提取笔记相关的各项数据信息并进行初始化操作, + // 同时还会进行一些与笔记位置、关联情况相关的状态判断。 + public NoteItemData(Context context, Cursor cursor) { + // 从游标中获取笔记的ID数据,赋值给对应的成员变量mId。 + mId = cursor.getLong(ID_COLUMN); + // 从游标中获取笔记的提醒日期数据,赋值给mAlertDate成员变量。 + mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); + // 从游标中获取笔记的背景颜色ID数据,赋值给mBgColorId成员变量。 + mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); + // 从游标中获取笔记的创建日期数据,赋值给mCreatedDate成员变量。 + mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + // 根据游标中获取的表示是否有附件的数据(整数值),转换为布尔值后赋值给mHasAttachment成员变量,大于0表示有附件,赋值为true,否则为false。 + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0)? true : false; + // 从游标中获取笔记的修改日期数据,赋值给mModifiedDate成员变量。 + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + // 从游标中获取与笔记相关的数量信息(具体含义取决于业务逻辑),赋值给mNotesCount成员变量。 + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + // 从游标中获取笔记所属父级的ID(例如所属文件夹的ID等),赋值给mParentId成员变量。 + mParentId = cursor.getLong(PARENT_ID_COLUMN); + // 从游标中获取笔记的摘要、简短描述等文本内容,赋值给mSnippet成员变量,并进行特定字符串替换操作,去除可能存在的特定标记字符(如NoteEditActivity.TAG_CHECKED和NoteEditActivity.TAG_UNCHECKED)。 + mSnippet = cursor.getString(SNIPPET_COLUMN); + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + // 从游标中获取笔记的类型信息(整数表示不同类型),赋值给mType成员变量,便于后续根据类型进行不同业务逻辑处理。 + mType = cursor.getInt(TYPE_COLUMN); + // 从游标中获取可能与笔记在桌面小部件等相关功能中的标识ID(具体用途取决于相关小部件功能实现),赋值给mWidgetId成员变量。 + mWidgetId = cursor.getInt(WIDGET_TYPE_COLUMN); + // 从游标中获取可能用于区分不同类型桌面小部件相关信息的类型值(如果有涉及桌面小部件相关业务逻辑的话),赋值给mWidgetType成员变量。 + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + // 初始化电话号码成员变量为空字符串,后续根据笔记所属情况等可能会重新赋值。 + mPhoneNumber = ""; + // 如果笔记所属父级ID等于通话记录文件夹的特定ID(Notes.ID_CALL_RECORD_FOLDER),表示该笔记与通话记录相关,尝试获取对应的电话号码。 + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + // 通过DataUtils工具类的方法,根据笔记ID从内容解析器(context.getContentResolver())中获取对应的电话号码信息,并赋值给mPhoneNumber成员变量。 + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + // 如果获取到的电话号码不为空字符串,说明找到了对应的电话号码,尝试获取对应的联系人姓名。 + if (!TextUtils.isEmpty(mPhoneNumber)) { + // 通过Contact类的静态方法,根据上下文(context)和电话号码(mPhoneNumber)获取对应的联系人姓名,赋值给mName成员变量。 + mName = Contact.getContact(context, mPhoneNumber); + // 如果获取联系人姓名失败(返回null),则将电话号码本身作为联系人姓名赋值给mName成员变量。 + if (mName == null) { + mName = mPhoneNumber; + } + } + } + + // 如果最终联系人姓名仍为null(可能之前获取过程都失败了),则将其设置为空字符串,保证成员变量有合理的初始值。 + if (mName == null) { + mName = ""; + } + + // 调用checkPostion方法,根据游标情况进行一些与笔记位置、在文件夹下关联情况等相关的状态判断,更新对应的成员变量值。 + checkPostion(cursor); + } + + // 私有方法,用于根据游标情况进行一些与笔记位置、在文件夹下关联情况等相关的状态判断,更新对应的成员变量值, + // 例如判断是否是某文件夹下唯一跟随的笔记、是否是多个笔记跟随某文件夹等情况。 + private void checkPostion(Cursor cursor) { + // 根据游标是否处于最后一条记录,判断当前笔记是否是列表中的最后一项,赋值给mIsLastItem成员变量。 + mIsLastItem = cursor.isLast()? true : false; + // 根据游标是否处于第一条记录,判断当前笔记是否是列表中的第一项,赋值给mIsFirstItem成员变量。 + mIsFirstItem = cursor.isFirst()? true : false; + // 通过判断游标获取的总记录数是否为1,确定当前笔记是否是列表中唯一的一项,赋值给mIsOnlyOneItem成员变量。 + mIsOnlyOneItem = (cursor.getCount() == 1); + // 初始化是否是某文件夹下跟随的多个笔记的标记为false,后续根据具体判断逻辑可能会更新。 + mIsMultiNotesFollowingFolder = false; + // 初始化是否是某文件夹下仅跟随的一个笔记的标记为false,后续根据具体判断逻辑可能会更新。 + mIsOneNoteFollowingFolder = false; + + // 如果笔记类型是普通笔记(Notes.TYPE_NOTE)且不是列表中的第一项(即前面还有其他记录),进行以下判断逻辑。 + if (mType == Notes.TYPE_NOTE &&!mIsFirstItem) { + // 获取当前游标所在的位置索引,用于后续判断操作。 + int position = cursor.getPosition(); + // 将游标移动到前一条记录,以便查看前一条记录对应的笔记类型等信息。 + if (cursor.moveToPrevious()) { + // 判断前一条记录对应的笔记类型是否是文件夹类型(Notes.TYPE_FOLDER)或者系统类型(Notes.TYPE_SYSTEM), + // 如果是,则说明当前笔记可能是跟随在某个文件夹或系统记录之后的笔记,继续进行后续判断。 + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + // 判断游标获取的总记录数是否大于当前位置索引加1(即当前笔记后面是否还有其他记录), + // 如果是,则说明当前笔记是某文件夹下跟随的多个笔记之一,将对应标记变量设置为true。 + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true; + } else { + // 如果后面没有其他记录了,则说明当前笔记是某文件夹下仅跟随的一个笔记,将对应标记变量设置为true。 + mIsOneNoteFollowingFolder = true; + } + } + // 将游标再移回原来的位置(即当前笔记对应的记录位置),保证游标位置的正确性,避免影响后续其他操作对游标的使用。 + if (!cursor.moveToNext()) { + throw new IllegalStateException("cursor move to previous but can't move back"); + } + } + } + } + + // 判断当前笔记是否是某文件夹下仅跟随的一个笔记,返回对应的标记变量值,外部可调用此方法获取该状态信息。 + public boolean isOneFollowingFolder() { + return mIsOneNoteFollowingFolder; + } + + // 判断当前笔记是否是某文件夹下跟随的多个笔记之一,返回对应的标记变量值,外部可调用此方法获取该状态信息。 + public boolean isMultiFollowingFolder() { + return mIsMultiNotesFollowingFolder; + } + + // 判断当前笔记是否是列表中的最后一项,返回对应的标记变量值,外部可调用此方法获取该状态信息。 + public boolean isLast() { + return mIsLastItem; + } + + // 获取与笔记相关的联系人姓名信息(如果有获取到的话),返回对应的成员变量值,外部可调用此方法获取该姓名信息。 + public String getCallName() { + return mName; + } + + // 判断当前笔记是否是列表中的第一项,返回对应的标记变量值,外部可调用此方法获取该状态信息。 + public boolean isFirst() { + return mIsFirstItem; + } + + // 判断当前笔记是否是列表中唯一的一项,返回对应的标记变量值,外部可调用此方法获取该状态信息。 + public boolean isSingle() { + return mIsOnlyOneItem; + } + + // 获取笔记的ID,返回对应的成员变量值,外部可调用此方法获取笔记的唯一标识信息。 + public long getId() { + return mId; + } + + // 获取笔记的提醒日期,返回对应的成员变量值,外部可调用此方法获取笔记设置的提醒时间信息。 + public long getAlertDate() { + return mAlertDate; + } + + // 获取笔记的创建日期,返回对应的成员变量值,外部可调用此方法获取笔记最初被创建的时间信息。 + public long getCreatedDate() { + return mCreatedDate; + } + + // 判断笔记是否有附件,返回对应的标记变量值(mHasAttachment),外部可调用此方法获取笔记是否包含附件的状态信息, + // 如果返回true,表示笔记存在附件,否则表示没有附件。 public boolean hasAttachment() { return mHasAttachment; } + // 获取笔记的修改日期,返回对应的成员变量值(mModifiedDate),外部可调用此方法获取笔记最后一次被修改的时间信息, + // 常用于记录笔记的修改历史、版本管理等相关业务逻辑场景中。 public long getModifiedDate() { return mModifiedDate; } + // 获取笔记的背景颜色ID,返回对应的成员变量值(mBgColorId),外部可调用此方法获取用于设置笔记显示背景颜色的标识信息, + // 根据该ID可以在应用的颜色资源等相关配置中找到对应的具体颜色来应用到笔记展示界面上。 public int getBgColorId() { return mBgColorId; } + // 获取笔记所属父级的ID(例如所属文件夹的ID等),返回对应的成员变量值(mParentId),外部可调用此方法获取笔记的归属关系信息, + // 便于判断笔记位于哪个文件夹下或者隶属于哪个上级模块等情况,方便进行分类管理、层级展示等操作。 public long getParentId() { return mParentId; } + // 获取与笔记相关的数量信息(具体含义取决于业务逻辑,可能是关联的子笔记数量等情况),返回对应的成员变量值(mNotesCount), + // 外部可调用此方法获取该数量值,在涉及笔记的统计、分组等业务场景中可能会用到。 public int getNotesCount() { return mNotesCount; } + // 获取笔记所属文件夹的ID,这里直接返回笔记的父级ID(mParentId),因为通常情况下父级就是所属的文件夹,外部可调用此方法明确获取到笔记所属文件夹的标识, + // 方便进行文件夹相关的操作,比如在文件夹内查找笔记、统计文件夹内笔记数量等操作。 public long getFolderId () { return mParentId; } + // 获取笔记的类型信息,返回对应的成员变量值(mType),外部可调用此方法获取笔记所属的类型(例如普通笔记、系统笔记等不同分类), + // 根据不同类型可以在应用中进行差异化的展示、操作等业务逻辑处理,比如不同类型笔记有不同的编辑权限、显示样式等。 public int getType() { return mType; } + // 获取可能用于区分不同类型桌面小部件相关信息的类型值(如果有涉及桌面小部件相关业务逻辑的话),返回对应的成员变量值(mWidgetType), + // 外部可调用此方法获取该类型值,以便针对不同小部件类型进行相应的配置、显示控制等操作,例如不同类型小部件展示笔记的不同部分内容或者交互方式不同等。 public int getWidgetType() { return mWidgetType; } + // 获取可能与笔记在桌面小部件等相关功能中的标识ID(具体用途取决于相关小部件功能实现),返回对应的成员变量值(mWidgetId), + // 外部可调用此方法获取该标识ID,用于在小部件相关功能中准确识别和操作对应的笔记,比如在小部件中更新特定笔记的显示内容等情况。 public int getWidgetId() { return mWidgetId; } + // 获取笔记的摘要、简短描述等文本内容,返回对应的成员变量值(mSnippet),外部可调用此方法获取笔记的简要信息, + // 常用于在列表展示等场景中先显示笔记的简短描述,用户点击后再查看详细内容,起到一个预览的作用。 public String getSnippet() { return mSnippet; } + // 判断笔记是否设置了提醒,通过检查提醒日期(mAlertDate)是否大于0来确定,如果大于0则表示设置了提醒,返回true,否则返回false, + // 外部可调用此方法快速知晓笔记是否有提醒相关的设置,以便在应用中进行相应的提醒业务逻辑处理,比如定时提醒用户查看笔记等操作。 public boolean hasAlert() { return (mAlertDate > 0); } + // 判断笔记是否属于通话记录类型,通过检查笔记所属父级ID是否等于通话记录文件夹的特定ID(Notes.ID_CALL_RECORD_FOLDER)并且电话号码(mPhoneNumber)不为空字符串来确定, + // 如果满足条件则返回true,表示是通话记录相关笔记,否则返回false,外部可调用此方法区分通话记录笔记和其他类型笔记,进行针对性的业务操作,例如通话记录笔记可能有特殊的展示格式或关联操作等。 public boolean isCallRecord() { - return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); + return (mParentId == Notes.ID_CALL_RECORD_FOLDER &&!TextUtils.isEmpty(mPhoneNumber)); } + // 静态方法,接收数据库游标(Cursor)作为参数,从游标中获取笔记的类型信息(通过TYPE_COLUMN指定的列索引获取整数值)并返回, + // 外部可直接通过类名调用此方法,方便在不需要创建NoteItemData类实例的情况下,仅根据游标快速获取笔记的类型信息,常用于一些仅需判断类型的简单业务场景中。 public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } -} +} \ No newline at end of file diff --git a/src/net/micode/notes/ui/NotesListItem.java b/src/net/micode/notes/ui/NotesListItem.java index 1221e80..9fa79a6 100644 --- a/src/net/micode/notes/ui/NotesListItem.java +++ b/src/net/micode/notes/ui/NotesListItem.java @@ -14,109 +14,170 @@ * limitations under the License. */ -package net.micode.notes.ui; + package net.micode.notes.ui; -import android.content.Context; -import android.text.format.DateUtils; -import android.view.View; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.tool.DataUtils; -import net.micode.notes.tool.ResourceParser.NoteItemBgResources; - - -public class NotesListItem extends LinearLayout { - private ImageView mAlert; - private TextView mTitle; - private TextView mTime; - private TextView mCallName; - private NoteItemData mItemData; - private CheckBox mCheckBox; - - public NotesListItem(Context context) { - super(context); - inflate(context, R.layout.note_item, this); - mAlert = (ImageView) findViewById(R.id.iv_alert_icon); - mTitle = (TextView) findViewById(R.id.tv_title); - mTime = (TextView) findViewById(R.id.tv_time); - mCallName = (TextView) findViewById(R.id.tv_name); - mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); - } - - public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { - if (choiceMode && data.getType() == Notes.TYPE_NOTE) { - mCheckBox.setVisibility(View.VISIBLE); - mCheckBox.setChecked(checked); - } else { - mCheckBox.setVisibility(View.GONE); - } - - mItemData = data; - if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { - mCallName.setVisibility(View.GONE); - mAlert.setVisibility(View.VISIBLE); - mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); - mTitle.setText(context.getString(R.string.call_record_folder_name) - + context.getString(R.string.format_folder_files_count, data.getNotesCount())); - mAlert.setImageResource(R.drawable.call_record); - } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { - mCallName.setVisibility(View.VISIBLE); - mCallName.setText(data.getCallName()); - mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); - if (data.hasAlert()) { - mAlert.setImageResource(R.drawable.clock); - mAlert.setVisibility(View.VISIBLE); - } else { - mAlert.setVisibility(View.GONE); - } - } else { - mCallName.setVisibility(View.GONE); - mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); - - if (data.getType() == Notes.TYPE_FOLDER) { - mTitle.setText(data.getSnippet() - + context.getString(R.string.format_folder_files_count, - data.getNotesCount())); - mAlert.setVisibility(View.GONE); - } else { - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); - if (data.hasAlert()) { - mAlert.setImageResource(R.drawable.clock); - mAlert.setVisibility(View.VISIBLE); - } else { - mAlert.setVisibility(View.GONE); - } - } - } - mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); - - setBackground(data); - } - - private void setBackground(NoteItemData data) { - int id = data.getBgColorId(); - if (data.getType() == Notes.TYPE_NOTE) { - if (data.isSingle() || data.isOneFollowingFolder()) { - setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); - } else if (data.isLast()) { - setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); - } else if (data.isFirst() || data.isMultiFollowingFolder()) { - setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); - } else { - setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); - } - } else { - setBackgroundResource(NoteItemBgResources.getFolderBgRes()); - } - } - - public NoteItemData getItemData() { - return mItemData; - } -} + import android.content.Context; + import android.text.format.DateUtils; + import android.view.View; + import android.widget.CheckBox; + import android.widget.ImageView; + import android.widget.LinearLayout; + import android.widget.TextView; + + import net.micode.notes.R; + import net.micode.notes.data.Notes; + import net.micode.notes.tool.DataUtils; + import net.micode.notes.tool.ResourceParser.NoteItemBgResources; + + // NotesListItem类继承自LinearLayout,代表了笔记列表中每一项的视图布局,用于展示笔记相关的各项信息, + // 如标题、时间、是否有提醒等,并根据笔记的不同类型、状态等来设置对应的显示样式和背景等。 + public class NotesListItem extends LinearLayout { + + // 用于显示提醒相关图标的ImageView控件,例如有提醒时显示闹钟图标等,方便用户直观看到笔记是否设置了提醒功能。 + private ImageView mAlert; + // 用于显示笔记标题的TextView控件,会根据笔记的类型、具体内容等情况设置相应的文本内容进行展示。 + private TextView mTitle; + // 用于显示笔记时间相关信息的TextView控件,通常会展示笔记的修改时间等,并以相对时间的格式(如“几分钟前”“昨天”等)进行显示。 + private TextView mTime; + // 用于显示与笔记相关的联系人姓名(如果笔记属于通话记录等相关情况)的TextView控件,若不涉及则可能隐藏该控件。 + private TextView mCallName; + // 用于存储当前列表项对应的笔记数据对象(NoteItemData类型),包含了笔记的各种详细信息,方便在视图设置等操作中获取对应的数据进行展示和判断。 + private NoteItemData mItemData; + // 用于在选择模式下(例如多选操作时)显示选择状态的CheckBox控件,根据是否处于选择模式以及笔记类型等情况决定其可见性和选中状态。 + private CheckBox mCheckBox; + + // 构造函数,接收上下文(Context)作为参数,调用父类(LinearLayout)的构造函数进行初始化, + // 并通过inflate方法加载对应的布局文件(R.layout.note_item)到当前视图中,然后获取布局中各个相关的子控件实例。 + public NotesListItem(Context context) { + super(context); + inflate(context, R.layout.note_item, this); + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); + mTitle = (TextView) findViewById(R.id.tv_title); + mTime = (TextView) findViewById(R.id.tv_time); + mCallName = (TextView) findViewById(R.id.tv_name); + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + } + + // 用于将笔记相关的数据绑定到当前列表项视图上,根据笔记数据的不同情况(如类型、是否有提醒等)以及传入的一些显示控制参数(如是否处于选择模式、是否已选中), + // 来设置各个子控件的显示内容、可见性以及背景等样式,完成数据与视图的关联展示。 + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 如果处于选择模式(choiceMode为true)并且笔记类型是普通笔记(Notes.TYPE_NOTE),则显示CheckBox控件, + // 并根据传入的是否已选中参数(checked)设置其选中状态,用于在多选等操作场景下展示选择情况。 + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setChecked(checked); + } else { + // 如果不满足上述条件(非选择模式或者不是普通笔记类型),则隐藏CheckBox控件,不展示选择相关操作界面。 + mCheckBox.setVisibility(View.GONE); + } + + // 将传入的笔记数据对象赋值给成员变量mItemData,方便后续在其他方法中获取该笔记的各项详细信息。 + mItemData = data; + + // 如果笔记的ID等于通话记录文件夹的特定ID(Notes.ID_CALL_RECORD_FOLDER),说明当前列表项对应的是通话记录文件夹,进行以下设置: + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 隐藏显示联系人姓名的TextView控件(mCallName),因为通话记录文件夹本身不需要显示联系人姓名。 + mCallName.setVisibility(View.GONE); + // 显示提醒图标相关的ImageView控件(mAlert),可能用于表示该文件夹有特殊含义或者相关提醒功能(具体取决于业务逻辑)。 + mAlert.setVisibility(View.VISIBLE); + // 设置标题(mTitle)的文本外观样式为主要项的样式(通过R.style.TextAppearancePrimaryItem资源样式设置),使其在界面上以相应的样式展示。 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + // 设置标题的文本内容,由通话记录文件夹的名称(通过字符串资源获取)和该文件夹内文件数量(通过格式化字符串资源结合笔记数据中的文件数量信息获取)组成,展示给用户文件夹相关信息。 + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + // 设置提醒图标(mAlert)显示的图片资源为通话记录相关的特定图标(R.drawable.call_record),以特定图标表示该文件夹的性质。 + mAlert.setImageResource(R.drawable.call_record); + } + // 如果笔记所属父级的ID等于通话记录文件夹的特定ID(Notes.ID_CALL_RECORD_FOLDER),说明当前笔记属于通话记录文件夹下的具体笔记,进行如下设置: + else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 显示联系人姓名的TextView控件(mCallName),并设置其文本内容为笔记相关的联系人姓名(通过笔记数据中的联系人姓名信息获取),展示给用户对应的联系人信息。 + mCallName.setVisibility(View.VISIBLE); + mCallName.setText(data.getCallName()); + // 设置标题(mTitle)的文本外观样式为次要项的样式(通过R.style.TextAppearanceSecondaryItem资源样式设置),使其与文件夹等其他项在样式上有所区分展示。 + mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); + // 设置标题的文本内容为经过格式化后的笔记摘要信息(通过DataUtils工具类的方法对笔记数据中的摘要内容进行格式化处理获取),展示笔记的简要内容。 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 如果笔记设置了提醒(通过笔记数据中的提醒日期判断,hasAlert方法返回true),则进行以下提醒图标相关设置: + if (data.hasAlert()) { + // 设置提醒图标(mAlert)显示的图片资源为闹钟图标(R.drawable.clock),直观提示用户该笔记设置了提醒功能。 + mAlert.setImageResource(R.drawable.clock); + // 显示提醒图标相关的ImageView控件(mAlert),使其在界面上可见。 + mAlert.setVisibility(View.VISIBLE); + } else { + // 如果笔记没有设置提醒,则隐藏提醒图标相关的ImageView控件(mAlert),不在界面上展示该图标。 + mAlert.setVisibility(View.GONE); + } + } + // 如果笔记不属于上述通话记录文件夹相关的情况,则进行以下通用设置: + else { + // 隐藏联系人姓名的TextView控件(mCallName),因为该笔记与通话记录无关,不需要展示联系人姓名。 + mCallName.setVisibility(View.GONE); + // 设置标题(mTitle)的文本外观样式为主要项的样式(通过R.style.TextAppearancePrimaryItem资源样式设置),以相应的样式展示标题内容。 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + + // 如果笔记类型是文件夹类型(Notes.TYPE_FOLDER),进行以下文件夹相关的标题设置: + if (data.getType() == Notes.TYPE_FOLDER) { + // 设置标题的文本内容为笔记的摘要信息(通过笔记数据中的摘要内容获取)加上该文件夹内文件数量(通过格式化字符串资源结合笔记数据中的文件数量信息获取),展示文件夹及其包含文件数量的相关信息给用户。 + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); + // 隐藏提醒图标相关的ImageView控件(mAlert),文件夹通常不需要展示提醒图标(除非有特殊业务逻辑要求)。 + mAlert.setVisibility(View.GONE); + } else { + // 如果笔记不是文件夹类型(即普通笔记等其他类型),设置标题的文本内容为经过格式化后的笔记摘要信息(通过DataUtils工具类的方法对笔记数据中的摘要内容进行格式化处理获取),展示笔记的简要内容。 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 如果笔记设置了提醒(通过笔记数据中的提醒日期判断,hasAlert方法返回true),则进行以下提醒图标相关设置: + if (data.hasAlert()) { + // 设置提醒图标(mAlert)显示的图片资源为闹钟图标(R.drawable.clock),直观提示用户该笔记设置了提醒功能。 + mAlert.setImageResource(R.drawable.clock); + // 显示提醒图标相关的ImageView控件(mAlert),使其在界面上可见。 + mAlert.setVisibility(View.VISIBLE); + } else { + // 如果笔记没有设置提醒,则隐藏提醒图标相关的ImageView控件(mAlert),不在界面上展示该图标。 + mAlert.setVisibility(View.GONE); + } + } + } + + // 设置显示时间的TextView控件(mTime)的文本内容,通过DateUtils工具类的方法,将笔记的修改日期(通过笔记数据中的修改日期信息获取)转换为相对时间格式的字符串(如“几分钟前”“昨天”等)进行展示,方便用户直观了解笔记的修改时间情况。 + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 调用setBackground方法,根据笔记数据中的背景颜色ID以及笔记的类型、位置等状态信息,设置当前列表项的背景样式,使其在列表中以合适的背景展示。 + setBackground(data); + } + + // 私有方法,根据笔记数据(NoteItemData类型)中的背景颜色ID以及笔记的类型、位置等状态信息, + // 通过NoteItemBgResources工具类获取对应的背景资源,并设置为当前列表项的背景,实现不同情况下的差异化背景展示。 + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); + // 如果笔记类型是普通笔记(Notes.TYPE_NOTE),根据笔记在列表中的不同位置、数量等情况设置不同的背景资源: + if (data.getType() == Notes.TYPE_NOTE) { + // 如果笔记是列表中唯一的一项或者是某文件夹下仅跟随的一个笔记(通过笔记数据中的相关判断方法确定), + // 通过NoteItemBgResources工具类获取对应的单个笔记背景资源(根据背景颜色ID获取具体资源ID),并设置为当前列表项的背景。 + if (data.isSingle() || data.isOneFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } + // 如果笔记是列表中的最后一项(通过笔记数据中的相关判断方法确定),通过NoteItemBgResources工具类获取对应的最后一个笔记背景资源(根据背景颜色ID获取具体资源ID),并设置为当前列表项的背景。 + else if (data.isLast()) { + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } + // 如果笔记是列表中的第一项或者是某文件夹下跟随的多个笔记之一(通过笔记数据中的相关判断方法确定), + // 通过NoteItemBgResources工具类获取对应的第一个笔记或多个笔记中某个的背景资源(根据背景颜色ID获取具体资源ID),并设置为当前列表项的背景。 + else if (data.isFirst() || data.isMultiFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } else { + // 如果笔记不属于上述特殊位置等情况,则通过NoteItemBgResources工具类获取对应的普通笔记背景资源(根据背景颜色ID获取具体资源ID),并设置为当前列表项的背景。 + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + } else { + // 如果笔记不是普通笔记类型(例如是文件夹类型等),通过NoteItemBgResources工具类获取对应的文件夹背景资源,并设置为当前列表项的背景,以区别于普通笔记的背景展示。 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + // 对外提供获取当前列表项对应的笔记数据对象的方法,外部可调用此方法获取笔记的详细信息,例如在点击列表项等操作后, + // 通过获取笔记数据进一步进行查看详情、编辑等相关业务逻辑处理。 + public NoteItemData getItemData() { + return mItemData; + } + } \ No newline at end of file diff --git a/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/net/micode/notes/widget/NoteWidgetProvider.java index ec6f819..7a30d01 100644 --- a/src/net/micode/notes/widget/NoteWidgetProvider.java +++ b/src/net/micode/notes/widget/NoteWidgetProvider.java @@ -14,119 +14,179 @@ * limitations under the License. */ -package net.micode.notes.widget; -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.util.Log; -import android.widget.RemoteViews; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.tool.ResourceParser; -import net.micode.notes.ui.NoteEditActivity; -import net.micode.notes.ui.NotesListActivity; - -public abstract class NoteWidgetProvider extends AppWidgetProvider { - public static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.BG_COLOR_ID, - NoteColumns.SNIPPET - }; - - public static final int COLUMN_ID = 0; - public static final int COLUMN_BG_COLOR_ID = 1; - public static final int COLUMN_SNIPPET = 2; - - private static final String TAG = "NoteWidgetProvider"; - - @Override - public void onDeleted(Context context, int[] appWidgetIds) { - ContentValues values = new ContentValues(); - values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - for (int i = 0; i < appWidgetIds.length; i++) { - context.getContentResolver().update(Notes.CONTENT_NOTE_URI, - values, - NoteColumns.WIDGET_ID + "=?", - new String[] { String.valueOf(appWidgetIds[i])}); - } - } - - private Cursor getNoteWidgetInfo(Context context, int widgetId) { - return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, - PROJECTION, - NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", - new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, - null); - } - - protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - update(context, appWidgetManager, appWidgetIds, false); - } - - private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, - boolean privacyMode) { - for (int i = 0; i < appWidgetIds.length; i++) { - if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { - int bgId = ResourceParser.getDefaultBgId(context); - String snippet = ""; - Intent intent = new Intent(context, NoteEditActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); - intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); - - Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); - if (c != null && c.moveToFirst()) { - if (c.getCount() > 1) { - Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); - c.close(); - return; - } - snippet = c.getString(COLUMN_SNIPPET); - bgId = c.getInt(COLUMN_BG_COLOR_ID); - intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); - intent.setAction(Intent.ACTION_VIEW); - } else { - snippet = context.getResources().getString(R.string.widget_havenot_content); - intent.setAction(Intent.ACTION_INSERT_OR_EDIT); - } - - if (c != null) { - c.close(); - } - - RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); - rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); - intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); - /** - * Generate the pending intent to start host for the widget - */ - PendingIntent pendingIntent = null; - if (privacyMode) { - rv.setTextViewText(R.id.widget_text, - context.getString(R.string.widget_under_visit_mode)); - pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( - context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); - } else { - rv.setTextViewText(R.id.widget_text, snippet); - pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - - rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); - appWidgetManager.updateAppWidget(appWidgetIds[i], rv); - } - } - } - - protected abstract int getBgResourceId(int bgId); - - protected abstract int getLayoutId(); - - protected abstract int getWidgetType(); -} + package net.micode.notes.widget; + + import android.app.PendingIntent; + import android.appwidget.AppWidgetManager; + import android.appwidget.AppWidgetProvider; + import android.content.ContentValues; + import android.content.Context; + import android.content.Intent; + import android.database.Cursor; + import android.util.Log; + import android.widget.RemoteViews; + + import net.micode.notes.R; + import net.micode.notes.data.Notes; + import net.micode.notes.data.Notes.NoteColumns; + import net.micode.notes.tool.ResourceParser; + import net.micode.notes.ui.NoteEditActivity; + import net.micode.notes.ui.NotesListActivity; + + // NoteWidgetProvider类是一个抽象类,继承自AppWidgetProvider,用于为桌面小部件提供通用的基础功能和抽象方法定义, + // 子类可以继承它并实现特定的抽象方法来定制不同类型桌面小部件的具体行为,例如设置背景资源、布局以及小部件类型等,同时它也处理了一些小部件相关的通用逻辑,如数据更新、删除等操作。 + public abstract class NoteWidgetProvider extends AppWidgetProvider { + + // 定义一个字符串数组,用于指定从数据库查询小部件相关笔记信息时需要获取的列名,包括笔记的ID、背景颜色ID以及摘要信息等, + // 后续通过数据库游标(Cursor)可根据这些列名获取对应的数据来进行小部件的内容展示等操作。 + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.BG_COLOR_ID, + NoteColumns.SNIPPET + }; + + // 定义一个常量,表示在查询结果游标(Cursor)中"ID"列所在的索引位置,方便后续通过游标获取对应列的数据。 + public static final int COLUMN_ID = 0; + // 定义一个常量,表示在查询结果游标(Cursor)中"BG_COLOR_ID"列所在的索引位置,用于获取笔记的背景颜色ID数据来设置小部件的背景样式。 + public static final int COLUMN_BG_COLOR_ID = 1; + // 定义一个常量,表示在查询结果游标(Cursor)中"SNIPPET"列所在的索引位置,用于获取笔记的摘要信息,可展示在小部件上作为简要内容。 + public static final int COLUMN_SNIPPET = 2; + + private static final String TAG = "NoteWidgetProvider"; + + // 重写父类(AppWidgetProvider)的onDeleted方法,该方法在桌面小部件被删除时会被调用, + // 这里主要用于处理与小部件相关的数据清理工作,例如将数据库中与被删除小部件关联的记录进行更新,清除对应的小部件ID关联等信息。 + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // 创建一个ContentValues对象,用于存储要更新到数据库中的键值对数据,这里主要是要更新笔记数据表中与小部件ID相关的字段。 + ContentValues values = new ContentValues(); + // 将NoteColumns.WIDGET_ID字段的值设置为无效的小部件ID(AppWidgetManager.INVALID_APPWIDGET_ID),表示该笔记不再与任何小部件关联。 + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + + // 遍历传入的被删除小部件的ID数组,对每个小部件ID执行以下数据库更新操作。 + for (int i = 0; i < appWidgetIds.length; i++) { + // 使用上下文的内容解析器(context.getContentResolver())来更新数据库中的数据, + // 更新的表是Notes.CONTENT_NOTE_URI所指向的表(通常是存储笔记相关信息的表), + // 使用values中设置的数据进行更新,更新的条件是NoteColumns.WIDGET_ID字段等于当前遍历到的小部件ID, + // 并且传入一个字符串数组作为条件参数的值,将小部件ID转换为字符串形式用于条件匹配。 + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + + // 私有方法,用于根据给定的小部件ID(widgetId)从数据库中查询获取与该小部件相关的笔记信息,返回一个数据库游标(Cursor)对象, + // 通过设置查询条件,筛选出特定小部件关联且不属于回收站文件夹(Notes.ID_TRASH_FOLER)的笔记信息,方便后续获取具体的数据进行小部件内容展示等操作。 + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + } + + // 对外公开的update方法,用于触发小部件的更新操作,它实际上是调用了另一个重载的私有update方法,并传入默认的隐私模式参数(false), + // 方便外部类(如子类或者其他调用者)简单地调用该方法来启动小部件更新流程,而无需关心隐私模式相关的细节(可在重载方法中处理)。 + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); + } + + // 私有重载的update方法,是真正执行小部件更新具体逻辑的地方,它会遍历传入的小部件ID数组,对每个有效的小部件ID进行一系列操作, + // 包括从数据库获取相关笔记信息、设置小部件的视图内容(如文本、背景图片等)、创建点击小部件时的意图(PendingIntent)等,最终完成小部件的更新展示。 + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + // 遍历传入的小部件ID数组,对每个小部件ID进行更新操作。 + for (int i = 0; i < appWidgetIds.length; i++) { + // 检查当前小部件ID是否是有效的小部件ID(不等于AppWidgetManager.INVALID_APPWIDGET_ID),如果有效则进行后续更新操作。 + if (appWidgetIds[i]!= AppWidgetManager.INVALID_APPWIDGET_ID) { + // 获取默认的背景颜色ID,通过ResourceParser工具类的方法获取,用于在没有获取到具体笔记的背景颜色ID时作为默认背景设置,初始设置小部件的背景样式。 + int bgId = ResourceParser.getDefaultBgId(context); + // 初始化用于存储笔记摘要信息的字符串为空,后续会根据从数据库获取的情况进行更新,该摘要信息会展示在小部件上作为简要内容。 + String snippet = ""; + // 创建一个意图(Intent)对象,用于指定点击小部件时要启动的活动(Activity),这里初始设置为启动NoteEditActivity,后续会根据情况调整。 + Intent intent = new Intent(context, NoteEditActivity.class); + // 设置意图的标志位,使得如果对应的活动已经在栈顶(处于运行状态),则不会重新创建新的实例,而是复用已有的活动实例,优化性能并保证界面状态的连贯性。 + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + // 将当前小部件的ID作为额外的数据添加到意图中,方便在目标活动(NoteEditActivity)中获取并知道是哪个小部件触发的操作,用于后续相关逻辑处理。 + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + // 将当前小部件的类型(通过抽象方法getWidgetType获取,由具体子类实现返回不同类型值)作为额外的数据添加到意图中,同样方便在目标活动中区分不同类型小部件的相关操作。 + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + // 调用getNoteWidgetInfo方法,根据当前小部件ID从数据库中获取与该小部件相关的笔记信息,返回一个游标对象,用于后续获取具体数据。 + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c!= null && c.moveToFirst()) { + // 如果游标不为空且能够移动到第一条记录(表示查询到了相关数据),进行以下操作: + if (c.getCount() > 1) { + // 如果查询到的记录数大于1,说明存在多个笔记与同一个小部件ID关联,这可能是不符合预期的情况,记录错误日志并关闭游标,直接返回,不进行后续更新操作。 + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + // 获取游标中对应列(COLUMN_SNIPPET)的笔记摘要信息,并赋值给snippet变量,用于后续展示在小部件上。 + snippet = c.getString(COLUMN_SNIPPET); + // 获取游标中对应列(COLUMN_BG_COLOR_ID)的笔记背景颜色ID,并赋值给bgId变量,用于设置小部件的背景样式,覆盖之前的默认背景颜色ID。 + bgId = c.getInt(COLUMN_BG_COLOR_ID); + // 将游标中对应列(COLUMN_ID)的笔记ID作为额外的数据添加到意图中,方便在目标活动中明确具体是哪个笔记相关的操作,例如编辑该笔记等。 + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + // 设置意图的动作(Action)为Intent.ACTION_VIEW,通常表示查看相关内容的操作,意味着点击小部件后可能是查看对应的笔记详情等行为(具体取决于目标活动的实现)。 + intent.setAction(Intent.ACTION_VIEW); + } else { + // 如果游标为空或者无法移动到第一条记录(表示没有查询到与该小部件相关的有效笔记信息),进行以下操作: + snippet = context.getResources().getString(R.string.widget_havenot_content); + // 设置意图的动作(Action)为Intent.ACTION_INSERT_OR_EDIT,意味着点击小部件后可能是进行插入新笔记或者编辑相关内容的操作(同样具体取决于目标活动的实现),因为没有关联的已有笔记,所以提供创建或编辑的入口。 + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + // 如果游标对象不为空,关闭游标,释放相关资源,避免内存泄漏等问题,因为已经获取完需要的数据了。 + if (c!= null) { + c.close(); + } + + // 创建一个RemoteViews对象,用于设置小部件的远程视图内容,通过传入应用的包名和小部件的布局资源ID(由抽象方法getLayoutId获取,具体子类实现返回不同布局资源)来初始化, + // 后续可通过该对象设置小部件上各个子视图(如文本视图、图片视图等)的显示内容、属性等。 + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + // 设置小部件上背景图片视图(通过R.id.widget_bg_image指定)的图片资源,调用抽象方法getBgResourceId并传入获取到的背景颜色ID(bgId)来获取对应的背景图片资源,进行背景设置。 + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + // 将背景颜色ID作为额外的数据添加到意图中,方便在目标活动中获取并可能用于保持界面显示等相关一致性操作(例如设置相同的背景颜色等)。 + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + + // 定义一个PendingIntent对象,用于包装意图(intent),使其可以在合适的时候被触发执行(例如点击小部件时),初始化为null,后续根据隐私模式情况进行创建。 + PendingIntent pendingIntent = null; + if (privacyMode) { + // 如果处于隐私模式(privacyMode为true),进行以下操作: + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + // 创建一个PendingIntent,用于启动NotesListActivity,当点击小部件时会跳转到该活动, + // 并设置标志位为PendingIntent.FLAG_UPDATE_CURRENT,保证如果已经存在相同的PendingIntent,会更新其携带的意图数据,保持最新状态。 + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + // 如果不处于隐私模式(privacyMode为false),进行以下操作: + rv.setTextViewText(R.id.widget_text, snippet); + // 创建一个PendingIntent,包装之前创建的intent(根据情况设置了不同动作、携带不同数据等,用于执行与笔记相关的操作,如查看、编辑等), + // 同样设置标志位为PendingIntent.FLAG_UPDATE_CURRENT,保证意图数据的更新,使得点击小部件时能正确执行相应操作。 + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + // 设置小部件上文本视图(通过R.id.widget_text指定)的点击事件PendingIntent,使得点击该文本区域时会触发对应的操作(如跳转到相关活动等),完成小部件交互逻辑的设置。 + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + // 通过AppWidgetManager对象,使用小部件的ID,更新小部件的远程视图内容,将之前设置好的RemoteViews对象应用到小部件上,完成小部件的更新展示。 + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + // 抽象方法,用于获取给定背景颜色ID(bgId)对应的小部件背景资源ID,由具体的子类实现,根据不同类型小部件或者不同的业务逻辑返回相应的背景资源ID, + // 以便在小部件更新时设置合适的背景样式。 + protected abstract int getBgResourceId(int bgId); + + // 抽象方法,用于获取小部件对应的布局资源ID,由具体的子类实现,不同类型的小部件(如不同尺寸、不同样式等)可以返回各自对应的布局资源, + // 用于在小部件更新时创建RemoteViews对象来设置小部件的整体布局和显示内容。 + protected abstract int getLayoutId(); + + // 抽象方法,用于获取小部件的类型标识,由具体的子类实现返回不同的类型值(例如不同尺寸类型的小部件返回不同的类型常量等), + // 方便在应用中对不同类型小部件进行区分和针对性的管理、操作以及相关业务逻辑处理,比如根据类型设置不同的显示规则、数据加载方式等。 + protected abstract int getWidgetType(); + } \ No newline at end of file diff --git a/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/net/micode/notes/widget/NoteWidgetProvider_2x.java index adcb2f7..6bda022 100644 --- a/src/net/micode/notes/widget/NoteWidgetProvider_2x.java +++ b/src/net/micode/notes/widget/NoteWidgetProvider_2x.java @@ -14,34 +14,46 @@ * limitations under the License. */ -package net.micode.notes.widget; - -import android.appwidget.AppWidgetManager; -import android.content.Context; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.tool.ResourceParser; - - -public class NoteWidgetProvider_2x extends NoteWidgetProvider { - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - super.update(context, appWidgetManager, appWidgetIds); - } - - @Override - protected int getLayoutId() { - return R.layout.widget_2x; - } - - @Override - protected int getBgResourceId(int bgId) { - return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); - } - - @Override - protected int getWidgetType() { - return Notes.TYPE_WIDGET_2X; - } -} + package net.micode.notes.widget; + + import android.appwidget.AppWidgetManager; + import android.content.Context; + + import net.micode.notes.R; + import net.micode.notes.data.Notes; + import net.micode.notes.tool.ResourceParser; + + // NoteWidgetProvider_2x类继承自NoteWidgetProvider,是用于实现特定尺寸(这里可能是2倍尺寸相关,从类名推测)桌面小部件的提供类, + // 它重写了父类的一些方法来定制该尺寸小部件的相关属性和更新逻辑等内容。 + public class NoteWidgetProvider_2x extends NoteWidgetProvider { + + // 重写父类的onUpdate方法,该方法会在桌面小部件需要更新时被调用(例如定时更新或者有相关数据变化触发更新的情况)。 + // 在这里直接调用了父类的update方法,将更新的具体操作委托给父类来处理,自身没有添加额外的更新逻辑(当然也可以根据需要添加特定于2倍尺寸小部件的更新操作)。 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); + } + + // 重写父类的getLayoutId方法,用于获取该尺寸桌面小部件对应的布局资源ID。 + // 返回的是R.layout.widget_2x,意味着这个2倍尺寸的小部件会使用名为"widget_2x"的布局文件来进行界面展示, + // 该布局文件中定义了小部件在界面上显示的各种控件及其布局方式等内容。 + @Override + protected int getLayoutId() { + return R.layout.widget_2x; + } + + // 重写父类的getBgResourceId方法,该方法的作用是根据传入的背景资源ID(bgId)获取适用于该尺寸桌面小部件的背景资源ID。 + // 通过调用ResourceParser.WidgetBgResources工具类中的getWidget2xBgResource方法,传入bgId来获取对应2倍尺寸小部件的背景资源, + // 这样可以针对不同的背景设置需求,为该尺寸小部件准确配置合适的背景样式。 + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + } + + // 重写父类的getWidgetType方法,用于明确该桌面小部件的类型。 + // 返回的是Notes.TYPE_WIDGET_2X,以此标识该小部件属于特定的2倍尺寸类型,方便在应用中对不同类型的小部件进行区分和针对性的管理、操作等处理。 + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_2X; + } + } \ No newline at end of file diff --git a/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/net/micode/notes/widget/NoteWidgetProvider_4x.java index c12a02e..4d5fdf3 100644 --- a/src/net/micode/notes/widget/NoteWidgetProvider_4x.java +++ b/src/net/micode/notes/widget/NoteWidgetProvider_4x.java @@ -14,33 +14,48 @@ * limitations under the License. */ -package net.micode.notes.widget; - -import android.appwidget.AppWidgetManager; -import android.content.Context; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.tool.ResourceParser; - - -public class NoteWidgetProvider_4x extends NoteWidgetProvider { - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - super.update(context, appWidgetManager, appWidgetIds); - } - - protected int getLayoutId() { - return R.layout.widget_4x; - } - - @Override - protected int getBgResourceId(int bgId) { - return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); - } - - @Override - protected int getWidgetType() { - return Notes.TYPE_WIDGET_4X; - } -} + package net.micode.notes.widget; + + import android.appwidget.AppWidgetManager; + import android.content.Context; + + import net.micode.notes.R; + import net.micode.notes.data.Notes; + import net.micode.notes.tool.ResourceParser; + + // NoteWidgetProvider_4x类继承自NoteWidgetProvider,它主要用于处理特定的4倍尺寸桌面小部件相关的逻辑, + // 通过重写父类的部分方法来定制该4倍尺寸小部件在布局、背景资源以及类型标识等方面的特性。 + + public class NoteWidgetProvider_4x extends NoteWidgetProvider { + + // 重写父类的onUpdate方法,此方法会在桌面小部件需要更新时被调用,例如系统定时触发小部件更新、相关数据变化导致小部件内容需刷新等情况。 + // 在这里它直接调用了父类的update方法,将具体的更新操作交给父类去执行,自身暂时没有添加额外的、针对4倍尺寸小部件特有的更新逻辑, + // 不过后续可以根据具体需求在此方法中添加相应内容,比如根据4倍尺寸特点来更新小部件显示的内容等操作。 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); + } + + // 重写父类的getLayoutId方法,其目的是获取该4倍尺寸桌面小部件所对应的布局资源ID。 + // 这里返回R.layout.widget_4x,表示这个4倍尺寸的小部件将会使用名为“widget_4x”的布局文件来进行界面展示, + // 该布局文件中定义了适合4倍尺寸小部件的各种控件摆放位置、大小以及样式等布局相关内容,以此来呈现出符合该尺寸特点的视觉效果。 + protected int getLayoutId() { + return R.layout.widget_4x; + } + + // 重写父类的getBgResourceId方法,该方法负责根据传入的背景资源ID(bgId)来获取适用于这个4倍尺寸桌面小部件的背景资源ID。 + // 通过调用ResourceParser.WidgetBgResources工具类中的getWidget4xBgResource方法,并传入bgId参数,就能得到对应4倍尺寸小部件的特定背景资源, + // 这样可以依据不同的背景设置需求,为该尺寸小部件准确地配置合适的背景样式,使其在外观上更符合整体设计要求。 + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + // 重写父类的getWidgetType方法,用于明确标识该桌面小部件的类型为4倍尺寸类型。 + // 返回的是Notes.TYPE_WIDGET_4X,借助这个返回值,在整个应用中就能方便地区分不同尺寸类型的小部件,进而对其进行有针对性的管理、操作以及相关业务逻辑处理, + // 比如针对不同类型小部件采用不同的数据加载方式、显示规则等。 + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_4X; + } + } \ No newline at end of file -- 2.34.1 From dd48656d9244aa3dec08a5d4126b203390a4f421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=94=E5=AD=90=E5=BA=B7?= <1824747710@qq.com> Date: Mon, 30 Dec 2024 21:34:25 +0800 Subject: [PATCH 9/9] =?UTF-8?q?=E6=B3=9B=E8=AF=BB=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...米便签开源代码的泛读报告.docx | Bin 229030 -> 236474 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/小米便签开源代码的泛读报告.docx b/doc/小米便签开源代码的泛读报告.docx index 14a9a9d0fd33256d7dc9a07aac7f0d3103cdfaf3..c2291d5f8f5111be7b3565a0816240478a8e0faf 100644 GIT binary patch delta 24012 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