From 1b9337ea4f41c12cb108cbe67e0077169b1f0b8c Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Thu, 23 Sep 2010 09:19:44 -0700 Subject: [PATCH] Wireframe email widget * Formatting and assets are preliminary * Functionality correct * Needs cleanup, etc. Change-Id: I75051df93d233ef529a616c7a9efae403d320bd2 --- AndroidManifest.xml | 248 +++++--- res/drawable-hdpi/widget_bg.9.png | Bin 0 -> 2838 bytes res/drawable-hdpi/widget_bg_focus.9.png | Bin 0 -> 3116 bytes res/drawable-hdpi/widget_bg_press.9.png | Bin 0 -> 3098 bytes res/drawable-hdpi/widget_bg_top.9.png | Bin 0 -> 2603 bytes res/drawable-mdpi/widget_bg.9.png | Bin 0 -> 1694 bytes res/drawable-mdpi/widget_bg_focus.9.png | Bin 0 -> 1907 bytes res/drawable-mdpi/widget_bg_press.9.png | Bin 0 -> 1908 bytes res/drawable-mdpi/widget_bg_top.9.png | Bin 0 -> 1877 bytes res/drawable/widget_background.xml | 22 + res/layout/widget.xml | 68 +++ res/layout/widget_list_item.xml | 56 ++ res/layout/widget_loading.xml | 33 + res/values/strings.xml | 13 + res/xml/widget_info.xml | 22 + .../android/email/activity/MessageView.java | 6 +- .../android/email/provider/EmailContent.java | 9 +- .../android/email/provider/EmailProvider.java | 26 +- .../email/provider/WidgetProvider.java | 577 ++++++++++++++++++ 19 files changed, 991 insertions(+), 89 deletions(-) create mode 100644 res/drawable-hdpi/widget_bg.9.png create mode 100644 res/drawable-hdpi/widget_bg_focus.9.png create mode 100644 res/drawable-hdpi/widget_bg_press.9.png create mode 100644 res/drawable-hdpi/widget_bg_top.9.png create mode 100644 res/drawable-mdpi/widget_bg.9.png create mode 100644 res/drawable-mdpi/widget_bg_focus.9.png create mode 100644 res/drawable-mdpi/widget_bg_press.9.png create mode 100644 res/drawable-mdpi/widget_bg_top.9.png create mode 100644 res/drawable/widget_background.xml create mode 100644 res/layout/widget.xml create mode 100644 res/layout/widget_list_item.xml create mode 100644 res/layout/widget_loading.xml create mode 100644 res/xml/widget_info.xml create mode 100644 src/com/android/email/provider/WidgetProvider.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 651aaa01c..1cd69bcdc 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -14,47 +14,70 @@ limitations under the License. --> - - + - - - - - - - - - - - - + + + + + + + + + + + + - - - + + + - - + + - - + + - - + + - - - + + + - - + + - - + android:label="@string/account_shortcut_picker_name"> + + @@ -186,7 +215,8 @@ > - + @@ -197,11 +227,16 @@ - - - - - + + + + + - - - - - + + + + + - - - - + + + + - - - - + + + + + + + - - + + - - + - - - + + + - + - + - + - + @@ -290,7 +355,8 @@ android:enabled="true" > - + - + @@ -315,7 +382,8 @@ android:name="com.android.exchange.EmailSyncAdapterService" android:exported="true"> - + @@ -326,7 +394,8 @@ android:name="com.android.exchange.ContactsSyncAdapterService" android:exported="true"> - + @@ -337,7 +406,8 @@ android:name="com.android.exchange.CalendarSyncAdapterService" android:exported="true"> - + @@ -357,7 +427,8 @@ android:enabled="true" > - + - + - + + + + + + + + + diff --git a/res/drawable-hdpi/widget_bg.9.png b/res/drawable-hdpi/widget_bg.9.png new file mode 100644 index 0000000000000000000000000000000000000000..d9af8fbada29a0b74ccfcd79b752afdf7ba942b1 GIT binary patch literal 2838 zcmV+x3+eQUP)tuesH#_THFFVqc*Sl-4J>PuqeednqlcZ_N@@`3z9O zNOh}0szIt-4N~1|kZO?XR)bWx8l)Pey44`nAQ9hyH`^i0cGX&H*?zY(VOm zw!n5%1FGsEK~Q?~aSv?wG{8tewE_TA9p98TCf{f5#*G`_ojZ5#tLf?KPkEbYG#c+s zaPRl~Y-wp}Yk7HjX>oD!_qn;bANcNF3D&Lz>VR>gKQTZJZu5xqdrp6aXU3XKR@^C z)vF#KNofo#LyPrYR#sMed;rgiev$+F5KtJ9P%%lmIt7xjwqO%?YbWmAyZ0A&apTc) zf~5Gc6*G6dfB*hJmo8oUoNvD6w#{t^#w(x86Yj2o1SrwiGyv(^wQKV+0jgrVaN)wo z*RNmyUWSMJgTI|WfBtI;(h*GsqufS|01^}q;$q`x z&Yby3sG1O0Wjoi!{+O;X#HCSQ1JC73cS?fP1&XgBB{_@5BnI-I19#Wybec6!`~zxY zVnPh;j)6%mHj^5FNdq*)m-||VnkJeZSY?-Jv8fb=xe>Z)*)YVS1OpaDIOuPh2;rb4ct{|(i3k(3 ze1gJsJd{BWHUym``^a>QCmNG*mVVT3NreG9R0C=Ge9=jH0k97K13;nz}0|lc*zhR3~cY{kfws2{W>7)drKY zb1^E0;>*ICEc*%pNf!=OSEPW$xIdRWwSg515OtsLSgG7y=!l`dE|%+zIod}`I8dZF zhEXn8hha{ri=!|I`&fG6P(?{U5=??I2{OU-7=u|pVUdMJF&P6NW5lGPxjX$FQ{B!5 zu+R0`Dic{;92E@Tu|hx^mW#v2yQ;dw!fJOINv^+dh^vdiBI-FKTX86|Dv<}nVkx)6 zDh^oD;1UY9<2LbtiAfZp5W*VYc4=Z#R+pD26J27$#f&^kJ%z+13Ufl@lTEf$XA5PC zN1@xLkl@&gfGbDBiUq2Gz`8x3tJAkB$0}KIh#?M4of8u5%TV9T%p~aU;Apq8DrABR zrV@c+jVr!nb!(>Qu@qn{=BN*yA%szMK6htwc^D@7avT>Z*nNdE2@7Dz#KsWC82gq- zS?2OZRd*QS;OcOSo3pWp>h4@Z0qfFyj4F+~(?P%jUc85)J zO)&{80z&0A2VC!_k4-4k7Xve=&OQmO8xa7rqU5>T1K7=g{JB z9y%Ezg%t-MV_dlsHiyz#Rb%$Y;FBkGDJIN%t)?cnnN=+A|j#f-ez)rRgRd+rS zVPhe+(&6aCTtL#7>KrWn)9dxf74cZ=jOr3&C$pF)`bYLrYU;z7!XY)haVl0Io0^(x z@^(m0>n=?H*(N5b)#sJl+?(sy8}XUN8gPyYBHPUCzQVGxJv}-$G5h&*wd#^U$g(Ol9^K2M0Iy?4b|Sl!or^$ z8ygG)r3Xx1ikgo5%KxGFbwb|@^PaZv1D^|k*4Eb8g9i`(kN_Q8$~1ONQg19SG+37N z)~#E=K7Raom($rAo3hAg-+iFGV_}v;ujr4})m5>k-}=p)H-BOK3tZ_yMRj*NK*G8^ zJ3E^ksGlD_dXzqY{+w-ZZKZyQVTwF|V-@g4L+h1%^ ztwPq*Jd%H{S>Fhg3j+#eFj#$z+fi zb*eKtIhhF6Crp8*o(mJt_5EU7H?<#vs+6D!NMCLQ%QP^KC=+f(i*ip&2Hq%xo zX4Pv@@$Siv7YunL!C03-DF$sy7VUr-Ofs;k1>#djGDlg5y;}tmGO1Bvkotg0T{(c?N>j{v!va@q zrh=reklB`CD6q!ay9IC6@utDuB^3dO84~<1Fa^ps0OydxiwzP3tk74Ii@05hh!xkklUWMWevu&Drsg9C4SY*EahaFlh)tK_F>B zF^yRa0K_!*R23wqN$I`;IGALUJ3PsH<0~=OV-m}DRan^YJ{=MB7A-$`%i!Y0N6m-w9`nvyZ`_I07*qoM6N<$g27cS`v3p{ literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/widget_bg_focus.9.png b/res/drawable-hdpi/widget_bg_focus.9.png new file mode 100644 index 0000000000000000000000000000000000000000..ee098af160ab1d1fad9b9df9a8cfcaa8cec56e9c GIT binary patch literal 3116 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 literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/widget_bg_top.9.png b/res/drawable-hdpi/widget_bg_top.9.png new file mode 100644 index 0000000000000000000000000000000000000000..c55f8083372ee0ffb26c20b6fd76a0cec1ca5f82 GIT binary patch literal 2603 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 literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/widget_bg_focus.9.png b/res/drawable-mdpi/widget_bg_focus.9.png new file mode 100644 index 0000000000000000000000000000000000000000..f4bbb08eb8be15a4faaa731279eeb503fdcbee1b GIT binary patch literal 1907 zcmV-(2aNcMP)cNX1ywsy0h!<^h>7l&{RS^&B!FmuBsc3VPVv*WH zHYpVw3~gBRXS17bW@miA-Ffll_5Jz1`Lk~74m@V&?PNN?&wPJ>-kX_vQ4~Si#q+%M zC&_SOHhHOS$^y*Yxb(sF!SuoO!SuoO!E8Y=tXA=UJPyT)aX4P|V1jiW@I1JumyKhl z{l!)nTMy!q`z*tbxF zBYWP4pGAg_oX&?XA;rH97;){LdfGMq6}gUpupb*>aiLHGsAmY`22( z>!;u(p0NJqu}TmGQ5c3!Ew9D(I`G;XUmUHpZ8-%Wpl#e^RA34wH)E~nW1wZnE*_ZN zTPPH^Y?i@m|NG$N9<;^pBoAX4@GW3`c9O^NWJ6>3mKk~OgQmD7k94ia5Jt6i;IMl!bt+f*2lNA%t}gEq6Sb` zI67CNLeT-)^jltv;RaE%5e1HI*pLQ_+}9PwPzyX89GffYVCf*3`n#I0vzQVv)k!YZ zaCg&ns1Lp_1)g}kF3wP0DXC)Gt`z3zN@B~xrKpNijqq)4BO5tZzPY7q7dTYK*y4w+ z?;O0HnUe?W$!1C`Zdk|aX_9k|@;7#$S38g&iAssb?S zm`JW&a<60fyatD(+cw1<*C&rj3OW&pyr0{&g9t%Z4CS$D)?g^rZW31_r)%BpnQGrO z$wc{kQ@U3bcuvJ-p6OatjI0`6rVZ4go@O4p3SAy>**(sn<>l)K*A@Ycx3P%@X zx)OY41*rh3;831T*_2`mxH=%tV5}cjM&{y6o{X}+7^Iv3rI!1qxJ4R{WTcpgpC|}| z&RdUyI!rDlr*C=2&-VDm)s?iuA_PY72K%JU$3#nd30^dFt%}#&P7ZQ^;OH# z(h}6>W>;`~HGnHN#S|>jrBz*nUvTTt<+C>z78YP}aZ!6$R4I~Dw%gLNsgk8A2BB1| zRXB9z>@BpV+F&+yUX_oMfI;7A9sCMTtggfL{f&#`yMF)rTBTBf8#iu1yWMb_0_I;!Nwrzz{sRY=3-`{9_pT(qh6SuKpyuqe=4lc(gRRF~` z-xk~Y`ue8*=Gs~~Sf5{dVR@=L9$adF$o1(oy!PsO_?~Spv$n?mt(L;}$OHz}RAOxh zYdcRLhNCZ!!#kq`uphSIvebk>Kc9j3-@E`nu-!!rHLNwYU%45;biO43*cQVX9WKG{ zbI0KI2g>l&z9ASN@?oTxVX(xpJYR>IKWcF4#qZ#wm33G^MlqDz=uSz&cyUTcINMn( z$8AUKW&>dPisL_8!Z?OB(OqH?wYW86C;@&b=Nw}ca7r;C-Uj0SLXU#M7Q}FF3vE5_ zH-1S={=69Y_e&iZzMvEi#p@9$kp!m|g9wC7llK4k}0RT|z1%@YU8rJ{-002ovPDHLkV1frrpvV9K literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/widget_bg_press.9.png b/res/drawable-mdpi/widget_bg_press.9.png new file mode 100644 index 0000000000000000000000000000000000000000..d060b77556bb6930c035116e65dbe6ae3fbed24e GIT binary patch literal 1908 zcmV-)2aEWLP)EJ?a zauVCr2`~q7$@CHQ!SuoO!SuoO!Su}HTGT4e_a8uZ?ijq9&A=S#y3cXord~FVR3d+p zj|-j;i;GJz|Io|e1%~yBwvIs{__28o9vm5h_sP~>`%wYJ%u8E!cxCK3EVOGtwc$!I z6w38);P|0Q_zwFTgHNoKC*PbJoxF2=bZjEq%P^{&6|Y(@Y)qElDCM`mx&!-)WqA1T zqi{|IC#>Z0J5j^$F$O*T2n=~Y!lmFm1Zx-HT?zK-vi1u(cVYvvi!OxcdAnByKSfs^`}iv- z+K=v|Q;tdnqVIHlXaoGanNIdKWhK^Fk>RSi zql2PbSM_nO)EUL_F-8|7M1XW~;y{so8h}YS?GON@5oeN2!Y7)*H?+q}F*L^5Tyb`P z{_k|hF;NKLr&5xQVys=s#C=RYO#-s1+ejgF0}QIgM#6F7#DSvgqwAf_N*G)>R zTuC;%juUicP*PJ2o32Gb*ghL&$l{D?&^2)-c0JXx5(1zgWU4WEyu>>2>RgEe5JBm@ zVtlz`=%-3aNvA(e6(dp(|DDZ{z-ka_Gnh+K5O!j4Djk5PG3QIEi(EcPOBX zIjO}M5d@Qo>bP-621XV~L~$imFzgTTP-LJwb(~nS29l5ve`4ywrY~e zK$|84znQ+7~MWb@9bdM(G|5lFk2`miwx>Wi%Y=kYWOQpjNBZ ze(I4?hsve+^v%!s>2AN+x)QGzz8}vWZD7CziCl>svzl>Wv{u{lv)ODWpU?jXgv_O6 zb2TPPGI1px47N3!O{mvvWM|0u0o0PF>!8+z0P9$o7)jBZ>ca}m`|ebG{> zRG_-PvWe?k+wiMSF&RyC@k&+r3D;(~&a4-UMJSic(yJm$ktSukE*+~XSrf$|lv1e# zvyC&?(U#Dr_M`Ga95Cn`-u-XEsjUWFJ({~PGrj)Bl|rEatE;O}tJU^AEpD1wY-i`< zs1p}FH$2B{*RH|x@-j@XpS^NR_UD;u16FQ-4NgU1q|;W8V$vF)oW_7M{>h8**mJY+ zG4@}s-@EcMxUJu=}A3}$k<9AI-@cel}P5|hHlu4BV^gH3cFoR3YS0E(YYOypkv=qL`_301r!t)=&*JO2rvk~t86!zg#sl)G|pNF?keg@x?%`%1>(wfq@+!SEi?-BqEMsOy^@^I+vNqFhD zX*hOd9A<_GV7!-Mki@aEwgH!a`wNzyIt?FgHlT=%A}HJFPI19FQA$TRL!^yHZ6tED z0nog~@joqo6hoTmE)j@o)antG7QQHFUB`s4q_q9M#r#++I`BpW#f_l!A{ctU%?Jc- zJ5hi9w`e)#WFmFAQOj~roF0J^aB#c`M2pEp$zCrO0HP_Lh1JW8&&oR?1k literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/widget_bg_top.9.png b/res/drawable-mdpi/widget_bg_top.9.png new file mode 100644 index 0000000000000000000000000000000000000000..af0f466c124dab03ae9c1bd2f00ec4d883850b61 GIT binary patch literal 1877 zcmV-b2demqP)I;|u=f-#lI%j#mow#Wz2{cKyVY@9y^(FYR{wXK=oC!XQ-L zyM0j4spGVdZwjLc16p=^Z`#Cd+xFev9e+6<4?p~I|M$<{e*M|+^5hqRKL7gHPcE;Y ze(%98z|8^RNO2s~1hB)npX5HZt$?23StvQ6@FUU#E^VsEr7ik`^t&Cv)dj%g^LBgx zw@=@B227Zn}xgR?erhiBjLy^k(tVg9_0gGrMx+!BDxptI8E{wW<((ln{`1lyj!bqg8=mopG_ z@8j_NMWBawyBFZV%j}>W-Dr=Z4Wcjn8C?T*RIH!f+dAjN?YD;@!~Vr05HVi;MfLq7 zM8F`!1DTfwcI0@3Xoa!Qf?XAXcvecD6%ug+GHQ7NsD=GSQ2l z;qd>IEgN_|rjzg2Te%-%o6Pfj8`yn8XafOE8`L{rm&sr{7oaoMu?Unfi^Cv?y3{^< zNMR;}Sb_ytC^gwk0_>_-tD+L20NIQ)PXTmny?5DTQVl^)*b-G9e4HXwQ9`6{F82_a zNI*spO7+0qK&YlrITn9Uhgr619{to{G*UO-sCL|WuN-}0xp|waDr5}|eM=jC#1`!o zGd+ojKr)=Np+THR4g%Cc#zjlpkWlO#(5flHXe=!3LDD>8OWU4v?ZV94sod>1Di`Q- z>P5m5Ef<|x&6tJSX_kWyLS0+2n$Wc)uHyqk3i%0Zp}m-lLKKjUoQx1Pmcv9ItS6ei zck~H>nseGGYL-hU!4(MAV8VDj6q86m(re)HT_YK9YOyV#KDkE}qLY=^m^RnWnTvZQU<*B>x% zh!A*ijvPM(gMbtijD6wy?D zfSnkQ&HGM4h&4iy7;q?191xblfbc_^$`@OjqXuNI^#l`~R)S$C4B9G0b*j+Nc0dQ~ z#ULXC%WjDfBw8wFL#*tw63fMjTC<(D4Q_wOvD#Vm`a-rlO%bgy%Tq}gVhChPuSz1{ zX|f4*cpxx0f)f2hfdF-XQ8yG(L<)H@+mN(f>rc`E$q|AaG>CK`^bz7^*3lD4&^4G4 zsMu0^Xi4mp4>yxy2!Kzu9ot6(gen1;%n(46L5c_SZg%ZMw}(DxuNxs;OBh_Cufb zB83yMBI*JivM$W67D;%e-Rt9(B1w`O%M7545Eyloen_dEC<6=z;&p`PGDp^ zet$)1iRDNM(h4N(KfKZ zZ%`8pzLW5}IRcmTj2+sBm8JZ%Hd3Wv*v4`!mQM`Rp`dQ$K^a&?++BtP=q-PMyEH;I z>1txR9#YJ9K)R1Ol7Vfr<^=ORF5exrKIb$W>Mp0K7k8HHRZ!sSmRt-&&>ypGh}#%C zc4CX79UD4j8NGF`<{UbVH!7XNfQ8Ojtm>;=k1S9x2}Mu#gz(Jv(omn4}V0U>0B z!WV)szxfr@-C@Zvmrui{c|SL%pX4r literal 0 HcmV?d00001 diff --git a/res/drawable/widget_background.xml b/res/drawable/widget_background.xml new file mode 100644 index 000000000..83f7a8b7b --- /dev/null +++ b/res/drawable/widget_background.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/res/layout/widget.xml b/res/layout/widget.xml new file mode 100644 index 000000000..2885ac079 --- /dev/null +++ b/res/layout/widget.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/widget_list_item.xml b/res/layout/widget_list_item.xml new file mode 100644 index 000000000..3332c1b6e --- /dev/null +++ b/res/layout/widget_list_item.xml @@ -0,0 +1,56 @@ + + + + + + + + \ No newline at end of file diff --git a/res/layout/widget_loading.xml b/res/layout/widget_loading.xml new file mode 100644 index 000000000..a25e32de0 --- /dev/null +++ b/res/layout/widget_loading.xml @@ -0,0 +1,33 @@ + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 0a3e88951..db5ef2b18 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1012,6 +1012,19 @@ save attachment. %1$d of %2$s + + + + Tap email icon for other views + + All Mail + + All Unread + + All Starred + + Loading\u2026 + + + + \ No newline at end of file diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java index 8ab1c6c2d..d485f0c25 100644 --- a/src/com/android/email/activity/MessageView.java +++ b/src/com/android/email/activity/MessageView.java @@ -62,13 +62,17 @@ public class MessageView extends MessageViewBase implements View.OnClickListener * @param mailboxId identifies the sequence of messages used for newer/older navigation. */ public static void actionView(Context context, long messageId, long mailboxId) { + context.startActivity(getActionViewIntent(context, messageId, mailboxId)); + } + + public static Intent getActionViewIntent(Context context, long messageId, long mailboxId) { if (messageId < 0) { throw new IllegalArgumentException("MessageView invalid messageId " + messageId); } Intent i = new Intent(context, MessageView.class); i.putExtra(EXTRA_MESSAGE_ID, messageId); i.putExtra(EXTRA_MAILBOX_ID, mailboxId); - context.startActivity(i); + return i; } @Override diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index 3ffbe822b..5e7ddf907 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -63,13 +63,16 @@ import java.util.UUID; */ public abstract class EmailContent { public static final String AUTHORITY = EmailProvider.EMAIL_AUTHORITY; + public static final String NOTIFIER_AUTHORITY = EmailProvider.EMAIL_NOTIFIER_AUTHORITY; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); public static final String PARAMETER_LIMIT = "limit"; + public static final Uri CONTENT_NOTIFIER_URI = Uri.parse("content://" + NOTIFIER_AUTHORITY); + // All classes share this public static final String RECORD_ID = "_id"; - private static final String[] COUNT_COLUMNS = new String[]{"count(*)"}; + public static final String[] COUNT_COLUMNS = new String[]{"count(*)"}; /** * This projection can be used with any of the EmailContent classes, when all you need @@ -448,7 +451,6 @@ public abstract class EmailContent { public static final String DELETED_TABLE_NAME = "Message_Deletes"; // To refer to a specific message, use ContentUris.withAppendedId(CONTENT_URI, id) - @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/message"); public static final Uri CONTENT_URI_LIMIT_1 = uriWithLimit(CONTENT_URI, 1); public static final Uri SYNCED_CONTENT_URI = @@ -457,6 +459,8 @@ public abstract class EmailContent { Uri.parse(EmailContent.CONTENT_URI + "/deletedMessage"); public static final Uri UPDATED_CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/updatedMessage"); + public static final Uri NOTIFIER_URI = + Uri.parse(EmailContent.CONTENT_NOTIFIER_URI + "/message"); public static final String KEY_TIMESTAMP_DESC = MessageColumns.TIMESTAMP + " desc"; @@ -925,7 +929,6 @@ public abstract class EmailContent { public static final class Account extends EmailContent implements AccountColumns, Parcelable { public static final String TABLE_NAME = "Account"; - @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/account"); public static final Uri ADD_TO_FIELD_URI = Uri.parse(EmailContent.CONTENT_URI + "/accountIdAddToField"); diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 79559b358..f19f26481 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -37,6 +37,7 @@ import android.accounts.AccountManager; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; +import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; @@ -116,6 +117,10 @@ public class EmailProvider extends ContentProvider { public static final int BODY_DATABASE_VERSION = 6; public static final String EMAIL_AUTHORITY = "com.android.email.provider"; + // The notifier authority is used to send notifications regarding changes to messages (insert, + // delete, or update) and is intended as an optimization for use by clients of message list + // cursors (initially, the email AppWidget). + public static final String EMAIL_NOTIFIER_AUTHORITY = "com.android.email.notifier"; private static final int ACCOUNT_BASE = 0; private static final int ACCOUNT = ACCOUNT_BASE; @@ -911,6 +916,7 @@ public class EmailProvider extends ContentProvider { int table = match >> BASE_SHIFT; String id = "0"; boolean messageDeletion = false; + ContentResolver resolver = context.getContentResolver(); if (Email.LOGD) { Log.v(TAG, "EmailProvider.delete: uri=" + uri + ", match is " + match); @@ -940,6 +946,7 @@ public class EmailProvider extends ContentProvider { // Bodies are auto-deleted here; Attachments are auto-deleted via trigger messageDeletion = true; db.beginTransaction(); + resolver.notifyChange(Message.NOTIFIER_URI, null); break; } switch (match) { @@ -1045,7 +1052,7 @@ public class EmailProvider extends ContentProvider { } // Notify all existing cursors. - getContext().getContentResolver().notifyChange(EmailContent.CONTENT_URI, null); + resolver.notifyChange(EmailContent.CONTENT_URI, null); return result; } @@ -1101,6 +1108,8 @@ public class EmailProvider extends ContentProvider { if (Email.DEBUG_THREAD_CHECK) Email.warnIfUiThread(); int match = sURIMatcher.match(uri); Context context = getContext(); + ContentResolver resolver = context.getContentResolver(); + // See the comment at delete(), above SQLiteDatabase db = getDatabase(context); int table = match >> BASE_SHIFT; @@ -1121,10 +1130,12 @@ public class EmailProvider extends ContentProvider { try { switch (match) { + case MESSAGE: + resolver.notifyChange(Message.NOTIFIER_URI, null); + //$FALL-THROUGH$ case UPDATED_MESSAGE: case DELETED_MESSAGE: case BODY: - case MESSAGE: case ATTACHMENT: case MAILBOX: case ACCOUNT: @@ -1175,7 +1186,7 @@ public class EmailProvider extends ContentProvider { } // Notify all existing cursors. - getContext().getContentResolver().notifyChange(EmailContent.CONTENT_URI, null); + resolver.notifyChange(EmailContent.CONTENT_URI, null); return resultUri; } @@ -1360,6 +1371,7 @@ public class EmailProvider extends ContentProvider { int match = sURIMatcher.match(uri); Context context = getContext(); + ContentResolver resolver = context.getContentResolver(); // See the comment at delete(), above SQLiteDatabase db = getDatabase(context); int table = match >> BASE_SHIFT; @@ -1411,10 +1423,12 @@ public class EmailProvider extends ContentProvider { db.setTransactionSuccessful(); db.endTransaction(); break; - case BODY_ID: - case MESSAGE_ID: case SYNCED_MESSAGE_ID: + resolver.notifyChange(Message.NOTIFIER_URI, null); + //$FALL-THROUGH$ case UPDATED_MESSAGE_ID: + case MESSAGE_ID: + case BODY_ID: case ATTACHMENT_ID: case MAILBOX_ID: case ACCOUNT_ID: @@ -1490,7 +1504,7 @@ public class EmailProvider extends ContentProvider { throw e; } - context.getContentResolver().notifyChange(notificationUri, null); + resolver.notifyChange(notificationUri, null); return result; } diff --git a/src/com/android/email/provider/WidgetProvider.java b/src/com/android/email/provider/WidgetProvider.java new file mode 100644 index 000000000..653edb23f --- /dev/null +++ b/src/com/android/email/provider/WidgetProvider.java @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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 com.android.email.provider; + +import com.android.email.Email; +import com.android.email.R; +import com.android.email.activity.MessageCompose; +import com.android.email.activity.MessageView; +import com.android.email.data.ThrottlingCursorLoader; +import com.android.email.provider.EmailContent.Message; +import com.android.email.provider.EmailContent.MessageColumns; + +import android.app.Activity; +import android.app.PendingIntent; +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.Loader; +import android.database.Cursor; +import android.graphics.Paint.Align; +import android.graphics.Typeface; +import android.net.Uri; +import android.net.Uri.Builder; +import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.TextUtils.TruncateAt; +import android.text.format.DateUtils; +import android.text.style.StyleSpan; +import android.util.Log; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import java.util.HashMap; +import java.util.List; + +public class WidgetProvider extends AppWidgetProvider { + private static final String TAG = "WidgetProvider"; + + /** + * When handling clicks in a widget ListView, a single PendingIntent template is provided to + * RemoteViews, and the individual "on click" actions are distinguished via a "fillInIntent" + * on each list element; when a click is received, this "fillInIntent" is merged with the + * PendingIntent using Intent.fillIn(). Since this mechanism does NOT preserve the Extras + * Bundle, we instead encode information about the action (e.g. view, reply, etc.) and its + * arguments (e.g. messageId, mailboxId, etc.) in an Uri which is added to the Intent via + * Intent.setDataAndType() + * + * The mime type MUST be set in the Intent, even though we do not use it; therefore, it's value + * is entirely arbitrary. + * + * Our "command" Uri is NOT used by the system in any manner, and is therefore constrained only + * in the requirement that it be syntactically valid. + * + * We use the following convention for our commands: + * widget://command//[/] + */ + private static final String WIDGET_DATA_MIME_TYPE = "com.android.email/widget_data"; + private static final Uri COMMAND_URI = Uri.parse("widget://command"); + + // Command names and Uri's built upon COMMAND_URI + private static final String COMMAND_NAME_SWITCH_LIST_VIEW = "switch_list_view"; + private static final Uri COMMAND_URI_SWITCH_LIST_VIEW = + COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_SWITCH_LIST_VIEW).build(); + private static final String COMMAND_NAME_VIEW_MESSAGE = "view_message"; + private static final Uri COMMAND_URI_VIEW_MESSAGE = + COMMAND_URI.buildUpon().appendPath(COMMAND_NAME_VIEW_MESSAGE).build(); + + private static final int TOTAL_COUNT_UNKNOWN = -1; + private static final int MAX_MESSAGE_LIST_COUNT = 25; + + private static final String SORT_DESCENDING = MessageColumns.TIMESTAMP + " DESC"; + + // Map holding our instantiated widgets, accessed by widget id + private static HashMap sWidgetMap = new HashMap(); + private static AppWidgetManager sWidgetManager; + private static Context sContext; + private static ContentResolver sResolver; + private static TextPaint sDatePaint = new TextPaint(); + + /** + * Types of views that we're prepared to show in the widget - all mail, unread mail, and starred + * mail; we rotate between them. Each ViewType is composed of a selection string and a title. + */ + public enum ViewType { + ALL_MAIL(null, R.string.widget_all_mail), + UNREAD(MessageColumns.FLAG_READ + "=0", R.string.widget_unread), + STARRED(MessageColumns.FLAG_FAVORITE + "=1", R.string.widget_starred); + + private final String selection; + private final int titleResource; + private String title; + + ViewType(String _selection, int _titleResource) { + selection = _selection; + titleResource = _titleResource; + } + + public String getTitle(Context context) { + if (title == null) { + title = context.getString(titleResource); + } + return title; + } + } + + static class EmailWidget implements RemoteViewsService.RemoteViewsFactory { + // The widget identifier + private final int mWidgetId; + + // The cursor underlying the message list for this widget; this must only be modified while + // holding mCursorLock + private volatile Cursor mCursor; + // A lock on our cursor, which is used in the UI thread while inflating views, and by + // our Loader in the background + private final Object mCursorLock = new Object(); + // Number of records in the cursor + private int mCursorCount = TOTAL_COUNT_UNKNOWN; + // The widget's loader (derived from ThrottlingCursorLoader) + private WidgetLoader mLoader; + + // The current view type (all mail, unread, or starred for now) + private ViewType mViewType = ViewType.ALL_MAIL; + + // The projection to be used by the WidgetLoader + public static final String[] WIDGET_PROJECTION = new String[] { + EmailContent.RECORD_ID, MessageColumns.DISPLAY_NAME, MessageColumns.TIMESTAMP, + MessageColumns.SUBJECT, MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, + MessageColumns.FLAG_ATTACHMENT, MessageColumns.MAILBOX_KEY, MessageColumns.SNIPPET, + MessageColumns.ACCOUNT_KEY + }; + public static final int WIDGET_COLUMN_ID = 0; + public static final int WIDGET_COLUMN_DISPLAY_NAME = 1; + public static final int WIDGET_COLUMN_TIMESTAMP = 2; + public static final int WIDGET_COLUMN_SUBJECT = 3; + public static final int WIDGET_COLUMN_FLAG_READ = 4; + public static final int WIDGET_COLUMN_FLAG_FAVORITE = 5; + public static final int WIDGET_COLUMN_FLAG_ATTACHMENT = 6; + public static final int WIDGET_COLUMN_MAILBOX_KEY = 7; + public static final int WIDGET_COLUMN_SNIPPET = 8; + public static final int WIDGET_COLUMN_ACCOUNT_KEY = 9; + + public EmailWidget(int _widgetId) { + super(); + if (Email.DEBUG) { + Log.d(TAG, "Creating EmailWidget with id = " + _widgetId); + } + mWidgetId = _widgetId; + mLoader = new WidgetLoader(); + if (sDatePaint == null) { + sDatePaint = new TextPaint(); + sDatePaint.setTypeface(Typeface.DEFAULT); + sDatePaint.setTextSize(14); + sDatePaint.setAntiAlias(true); + sDatePaint.setTextAlign(Align.RIGHT); + } + } + + /** + * The ThrottlingCursorLoader does all of the heavy lifting in managing the data loading + * task; all we need is to register a listener so that we're notified when the load is + * complete. + */ + final class WidgetLoader extends ThrottlingCursorLoader { + protected WidgetLoader() { + super(sContext, Message.CONTENT_URI, WIDGET_PROJECTION, mViewType.selection, null, + SORT_DESCENDING); + registerListener(0, new OnLoadCompleteListener() { + @Override + public void onLoadComplete(Loader loader, Cursor cursor) { + synchronized (mCursorLock) { + // Save away the cursor + mCursor = cursor; + // Reset the notification Uri to our Message table notifier URI + mCursor.setNotificationUri(sResolver, Message.NOTIFIER_URI); + // Save away the count (for display) + mCursorCount = mCursor.getCount(); + if (Email.DEBUG) { + Log.d(TAG, "onLoadComplete, count = " + cursor.getCount()); + } + } + RemoteViews views = + new RemoteViews(sContext.getPackageName(), R.layout.widget); + views.setTextViewText(R.id.widget_title, + mViewType.getTitle(sContext) + " (" + mCursorCount + ")"); + sWidgetManager.partiallyUpdateAppWidget(mWidgetId, views); + sWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list); + } + }); + startLoading(); + } + + /** + * Convenience method that stops existing loading (if any), sets a (possibly new) + * selection criterion, and starts loading + * + * @param selection a valid query selection argument + */ + void startLoadingWithSelection(String selection) { + stopLoading(); + setSelection(selection); + startLoading(); + } + } + + /** + * Switch to the next widget view (cycles all -> unread -> starred) + */ + public void switchToNextView() { + switch(mViewType) { + case ALL_MAIL: + mViewType = ViewType.UNREAD; + break; + case UNREAD: + mViewType = ViewType.STARRED; + break; + case STARRED: + mViewType = ViewType.ALL_MAIL; + break; + } + synchronized(mCursorLock) { + mCursorCount = TOTAL_COUNT_UNKNOWN; + invalidateCursorLocked(); + mLoader.startLoadingWithSelection(mViewType.selection); + } + } + + /** + * Invalidates the current cursor and tells the UI that the underlying data has changed. + * This method must be called while holding mCursorLock + */ + private void invalidateCursorLocked() { + mCursor = null; + sWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list); + } + + private void setStyleSpan(SpannableString str, int typeface) { + int length = str.length(); + str.setSpan(new StyleSpan(typeface), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private CharSequence formattedText(String str, int typeface) { + if (str == null) { + return ""; + } + SpannableString ss = new SpannableString(str); + setStyleSpan(ss, typeface); + return ss; + } + + private CharSequence formattedTextFromCursor(Cursor c, int column, int typeface) { + return formattedText(mCursor.getString(column), typeface); + } + + + /** + * Convenience method for creating an onClickPendingIntent that executes a command via + * our command Uri. Used for the "next view" command; appends the widget id to the command + * Uri. + * + * @param views The RemoteViews we're inflating + * @param buttonId the id of the button view + * @param data the command Uri + */ + private void setCommandIntent(RemoteViews views, int buttonId, Uri data) { + Intent intent = new Intent(sContext, WidgetService.class); + intent.setDataAndType(ContentUris.withAppendedId(data, mWidgetId), + WIDGET_DATA_MIME_TYPE); + PendingIntent pendingIntent = PendingIntent.getService(sContext, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + views.setOnClickPendingIntent(buttonId, pendingIntent); + } + + /** + * Convenience method for creating an onClickPendingIntent that launches another activity + * directly. Used for the "Compose" button + * + * @param views The RemoteViews we're inflating + * @param buttonId the id of the button view + * @param activityClass the class of the activity to be launched + */ + private void setActivityIntent(RemoteViews views, int buttonId, + Class activityClass) { + Intent intent = new Intent(sContext, activityClass); + PendingIntent pendingIntent = PendingIntent.getActivity(sContext, 0, intent, 0); + views.setOnClickPendingIntent(buttonId, pendingIntent); + } + + /** + * Convenience method for constructing a fillInIntent for a given list view element. + * Appends the command and any arguments to a base Uri. + * + * @param views the RemoteViews we are inflating + * @param viewId the id of the view + * @param baseUri the base uri for the command + * @param args any arguments to the command + */ + private void setFillInIntent(RemoteViews views, int viewId, Uri baseUri, String ... args) { + Intent intent = new Intent(); + Builder builder = baseUri.buildUpon(); + for (String arg: args) { + builder.appendPath(arg); + } + intent.setDataAndType(builder.build(), WIDGET_DATA_MIME_TYPE); + views.setOnClickFillInIntent(viewId, intent); + } + + /** + * Update the "header" of the widget (i.e. everything that doesn't include the scrolling + * message list) + */ + private void updateHeader() { + if (Email.DEBUG) { + Log.d(TAG, "updateWidget " + mWidgetId); + } + + // Get the widget layout + RemoteViews views = new RemoteViews(sContext.getPackageName(), R.layout.widget); + + // Set up the list with an adapter + Intent intent = new Intent(sContext, WidgetService.class); + intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId); + views.setRemoteAdapter(R.id.message_list, intent); + + // Set up the title (view type + count of messages) + views.setTextViewText(R.id.widget_title, + mViewType.getTitle(sContext) + " (" + mCursorCount + ")"); + + // Set up "new" button (compose new message) and "next view" button + setActivityIntent(views, R.id.widget_compose, MessageCompose.class); + setCommandIntent(views, R.id.widget_logo, COMMAND_URI_SWITCH_LIST_VIEW); + + // Use a bare intent for our template; we need to fill everything in + intent = new Intent(sContext, WidgetService.class); + PendingIntent pendingIntent = + PendingIntent.getService(sContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + views.setPendingIntentTemplate(R.id.message_list, pendingIntent); + + // And finally update the widget + sWidgetManager.updateAppWidget(mWidgetId, views); + } + + /* (non-Javadoc) + * @see android.widget.RemoteViewsService.RemoteViewsFactory#getViewAt(int) + */ + public RemoteViews getViewAt(int position) { + // Use the cursor to set up the widget + synchronized (mCursorLock) { + if (mCursor == null || !mCursor.moveToPosition(position)) { + return getLoadingView(); + } + RemoteViews views = + new RemoteViews(sContext.getPackageName(), R.layout.widget_list_item); + + // Typeface for from, subject, and date (normal/bold) depends on whether the message + // is read/unread + int typeface = (mCursor.getInt(WIDGET_COLUMN_FLAG_READ) == 0) ? Typeface.BOLD + : Typeface.NORMAL; + views.setTextViewText(R.id.widget_from, + formattedTextFromCursor(mCursor, WIDGET_COLUMN_DISPLAY_NAME, typeface)); + views.setTextViewText(R.id.widget_subject, + formattedTextFromCursor(mCursor, WIDGET_COLUMN_SUBJECT, typeface)); + + long timestamp = mCursor.getLong(WIDGET_COLUMN_TIMESTAMP); + // Get a nicely formatted date string (relative to today) + String date = DateUtils.getRelativeTimeSpanString(sContext, timestamp).toString(); + views.setTextViewText(R.id.widget_date, TextUtils.ellipsize(date, sDatePaint, 64, + TruncateAt.END)); + + // Set button intents for view, reply, and delete + String messageId = mCursor.getString(WIDGET_COLUMN_ID); + String mailboxId = mCursor.getString(WIDGET_COLUMN_MAILBOX_KEY); + setFillInIntent(views, R.id.widget_message, COMMAND_URI_VIEW_MESSAGE, messageId, + mailboxId); + + return views; + } + } + + @Override + public int getCount() { + if (mCursor == null) return 0; + return Math.min(mCursor.getCount(), MAX_MESSAGE_LIST_COUNT); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public RemoteViews getLoadingView() { + RemoteViews view = new RemoteViews(sContext.getPackageName(), R.layout.widget_loading); + view.setTextViewText(R.id.loading_text, sContext.getString(R.string.widget_loading)); + return view; + } + + @Override + public int getViewTypeCount() { + // Regular list view and the "loading" view + return 2; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public void onDataSetChanged() { + } + + @Override + public void onDestroy() { + if (mLoader != null) { + mLoader.stopLoading(); + } + sWidgetMap.remove(mWidgetId); + } + + @Override + public void onCreate() { + } + } + + private static synchronized void update(Context context, AppWidgetManager widgetManager, + int[] appWidgetIds) { + for (int widgetId: appWidgetIds) { + getOrCreateWidget(widgetId).updateHeader(); + } + } + + private static EmailWidget getOrCreateWidget(int widgetId) { + EmailWidget widget = sWidgetMap.get(widgetId); + if (widget == null) { + if (Email.DEBUG) { + Log.d(TAG, "Creating EmailWidget for id #" + widgetId); + } + widget = new EmailWidget(widgetId); + sWidgetMap.put(widgetId, widget); + } + return widget; + } + + @Override + public void onDisabled(Context context) { + super.onDisabled(context); + if (Email.DEBUG) { + Log.d(TAG, "onDisabled"); + } + context.stopService(new Intent(context, WidgetService.class)); + } + + @Override + public void onEnabled(final Context context) { + super.onEnabled(context); + if (Email.DEBUG) { + Log.d(TAG, "onEnabled"); + } + context.startService(new Intent(context, WidgetService.class)); + } + + @Override + public void onReceive(final Context context, Intent intent) { + String action = intent.getAction(); + if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) { + Bundle extras = intent.getExtras(); + if (extras != null) { + final int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS); + if (appWidgetIds != null && appWidgetIds.length > 0) { + if (sWidgetManager == null) { + sWidgetManager = AppWidgetManager.getInstance(context); + sContext = context.getApplicationContext(); + sResolver = sContext.getContentResolver(); + } + context.startService(new Intent(context, WidgetService.class)); + update(sContext, sWidgetManager, appWidgetIds); + } + } + } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) { + Bundle extras = intent.getExtras(); + if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) { + final int widgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID); + // Find the widget in the map + EmailWidget widget = sWidgetMap.get(widgetId); + if (widget != null) { + // Stop loading and remove the widget from the map + widget.onDestroy(); + } + } + } + } + + /** + * We use the WidgetService for two purposes: + * 1) To provide a widget factory for RemoteViews, and + * 2) To process our command Uri's (i.e. take actions on user clicks) + */ + public static class WidgetService extends RemoteViewsService { + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + // Which widget do we want (nice alliteration, huh?) + int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + if (widgetId == -1) return null; + // Find the existing widget or create it + EmailWidget widget = sWidgetMap.get(widgetId); + if (widget == null) { + throw new IllegalStateException("onGetViewFactory, widget does not exist"); + } + return widget; + } + + @Override + public void startActivity(Intent intent) { + // Since we're not calling startActivity from an Activity, we need the new task flag + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + super.startActivity(intent); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Uri data = intent.getData(); + if (Email.DEBUG) { + Log.d(TAG, "Executing: " + data); + } + if (data == null) return Service.START_NOT_STICKY; + List pathSegments = data.getPathSegments(); + // Our path segments are , [, ] + // First, a quick check of Uri validity + if (pathSegments.size() < 2) { + throw new IllegalArgumentException(); + } + String command = pathSegments.get(0); + // Ignore unknown action names + try { + long arg1 = Long.parseLong(pathSegments.get(1)); + if (COMMAND_NAME_VIEW_MESSAGE.equals(command)) { + // "view", , + Intent i = MessageView.getActionViewIntent(this, arg1, + Long.parseLong(pathSegments.get(2))); + startActivity(i); + } else if (COMMAND_NAME_SWITCH_LIST_VIEW.equals(command)) { + // "next_view", + EmailWidget widget = sWidgetMap.get((int)arg1); + if (widget != null) { + widget.switchToNextView(); + } + } + } catch (NumberFormatException e) { + // Shouldn't happen as we construct all of the Uri's + } + return Service.START_NOT_STICKY; + } + + } +}