From 138a04170d964da9cdec228e95b976875ae52481 Mon Sep 17 00:00:00 2001 From: Karl Rosaen <> Date: Thu, 23 Apr 2009 19:00:21 -0700 Subject: [PATCH] AI 147564: Merge back from search branch to donut. Notes: - all public apis and framework changes have been reviewed by relevant folks in our branch (e.g romainguy) - all new public apis are @hidden; they will still get reviewed by api council once we're in git - other than that, it's mostly GlobalSearch and search dialog stuff, a new apps provider, and some tweaks to the contacts provider that was reviewed by jham Automated import of CL 147564 --- res/drawable/search_button_bg.9.png | Bin 0 -> 676 bytes res/drawable/search_button_voice.png | Bin 0 -> 965 bytes res/drawable/search_floater.9.png | Bin 0 -> 777 bytes res/drawable/textfield_searchwidget.xml | 31 + .../textfield_searchwidget_default.9.png | Bin 0 -> 1742 bytes .../textfield_searchwidget_pressed.9.png | Bin 0 -> 2032 bytes .../textfield_searchwidget_selected.9.png | Bin 0 -> 1886 bytes res/layout/widget_search.xml | 79 +- res/xml/default_workspace.xml | 2 +- src/com/android/launcher/Launcher.java | 218 +++-- src/com/android/launcher/Search.java | 879 +++++------------- src/com/android/launcher/Workspace.java | 102 +- 12 files changed, 459 insertions(+), 852 deletions(-) create mode 100644 res/drawable/search_button_bg.9.png create mode 100644 res/drawable/search_button_voice.png create mode 100644 res/drawable/search_floater.9.png create mode 100644 res/drawable/textfield_searchwidget.xml create mode 100644 res/drawable/textfield_searchwidget_default.9.png create mode 100644 res/drawable/textfield_searchwidget_pressed.9.png create mode 100644 res/drawable/textfield_searchwidget_selected.9.png diff --git a/res/drawable/search_button_bg.9.png b/res/drawable/search_button_bg.9.png new file mode 100644 index 0000000000000000000000000000000000000000..b140b24b68a46503bae7a3e155cdcf5ad594d4d0 GIT binary patch literal 676 zcmV;V0$crwP)8cDt1I88`(e;Fvk#zB3v_V@X3=)2UGDra|+E!{NP7r?ZRe z9k2n`K!Ewc=+|pJW`{JSr5MJz4@y@o7Pn%tn3hVV&bb_y;2+J2`7e2f;@m)^luD&w zuh-kc^>5qK4TnQD9*dPgM# z0mPt^{*)~S65TMG0~2LxoURkb=WfnMs5mrnq_N6Kb7^ACOgMUuo}yW0^w=g|m62CE z&&V@+tw!`0{wzi&?`PgU@{Bwq&*-ffi5U$owRT2svlAwIsnI;+G&5nMo8DE52S(X! zcBRp1sJx3?FcVr#>~1`1DTXwoWHLFAy=Z`U;>%!(H(A7@0lj~T$NET1F{DO`L}JwM z_dnHYwRPV5`x!D`;p6nV{<}&V(ozg3A2haGt=9KSr8162qlf8qdQ>PBE=+cS_xGh< zudB!;2$Rinx$Mv7azD_2;N;86x4~c#gIH-co9FFzdt$c|GMS7opU=O?q?AY`(nHt2 zn@=>`;4AnHK1x>I5%>ds88dnIZ$U4*KYC+E^k!@Fb}MbS0t^77Zyj?d3(chf0000< KMNUMnLSTZQ$~w~k literal 0 HcmV?d00001 diff --git a/res/drawable/search_button_voice.png b/res/drawable/search_button_voice.png new file mode 100644 index 0000000000000000000000000000000000000000..4c3d683e70e842119e91fcadd74d18a8b035665d GIT binary patch literal 965 zcmV;$13LVPP)I zT)6A`gNz>_$x1}Q^io}n0TC1-&c>Z|f_uko1XpH~wK#t;!L?v70-~sC-)}SLG-+@o z?If|oG!$LkRdr9DTc_^5N+2H@ z4GjX!tuX>09UY15>+6AyjSa(9S2s2`2AZ3jWp8hVr#`$&{uL`hjycj^$7~6m!0aumrN3M2-@#yHN4h5?)mPn;in{VB02M#C3 zp-?D*eiUmAtksIy_Z0g%K0dA@I?dJB*9Q4E44FE)nq%0U$v#6Wqa1DA+}xCxBo-DH@*N!=;`sPjJU%`S zfQ5;$c-Yp~CZC_5|C-B$!OdJ;TnuEhSrePh_4oH%k#1>eNh#UY1+gTV2#{;V<>h5~ zZEej2>qt{mlV#A3V*DmEu+^5AmkrZ2=SYT(F`jXUERZZ1r81l#L3Btd3$@SrPJx8 zLyK?$Q0;5)cxzwj>sw;P3P#Z)+jxj`Pm z(9jTFZ(_dosR~5bbrpizgJ2|uW*CO0RC^uInd)aTzy<-D(8br;+1bpeOP}@ib%}G+ zF*l6z#;UWfuC4^tO>yx|+2GzJjw3QXJ^jz}r>tTR^^~731^5(z8P(`k11%2LNlnv? zQf(=f)YZ5)D^P8e%EeiJU%^xLx|+}a{=Q(*$FqgtDk|tpW;`-7qJ!ZamCX8!bu}Xv zDofVXRzrk~?e6YITxE<9s1^id1qZ&72rMN(JUlR_3q-9jrn(D^B0fP7-c_>ZHL}wq nBOF8P>FH^rs^W6v`6IvpQJ(caHC-!a00000NkvXXu0mjfPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXJ< z6&e)e93Jcd00N0gL_t(&-tC&fN*hrWhQB-2icT;Mfz(w&+eN#WSy|`{2&J1oMYqAF zP!Yrwe22bADY$AkwoBUxmKZ6`s+A!bG0A8$bGmS}lc|n@HpvX!1DAwJnECF>fB$ni zHwr*R08mO@y}vG$Qa6A24+l%%3(R#bx+4cS4JP1TaIeb4jZ?`+U_4?7eZQF~08D$#ak6p-Z zRVvF}&)W|~WD=w6dHY+H$}*6<4Om&0wbb>zgGs?gpbsp|S^`uOaIRLXZAeO@R;z6Q zxhQblCCr6|g_mh@gpkFIF1I-FoN1a?T7W~Axg;N+QA$mx1vq4xNdh*~Ufhh(u&*YS zLSUu$5*kD?3k3`0WR0uitdMkiT3vp@+Id0t?`Cao*gE zJaeF!skB$Nc<6?<2F^P0P`k2=Kc|d7hB1I)NycR*xAOc3)&lInn^mIf00000NkvXX Hu0mjf!meeD literal 0 HcmV?d00001 diff --git a/res/drawable/textfield_searchwidget.xml b/res/drawable/textfield_searchwidget.xml new file mode 100644 index 0000000000..80f3dca34a --- /dev/null +++ b/res/drawable/textfield_searchwidget.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/res/drawable/textfield_searchwidget_default.9.png b/res/drawable/textfield_searchwidget_default.9.png new file mode 100644 index 0000000000000000000000000000000000000000..247abca85727123b3699cb4376be95cba16e1889 GIT binary patch literal 1742 zcmV;<1~K`GP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXJ< z77G(A=N1M400vG;L_t(&-tC&tZWGBChrjNw{@=E2#{uME2Ur9)2`|8k#XP`Bb62xs z$Cc*3K-Mho>Mmx%jur1Q8%AP>6eLyzMM_9wL!3Bq+irJv)m=cBoDhiPFoUmjk5r12 zs52&siU!FX9@lVK6 zBZR;-%~KFR3aC({_$;L)2m-<|#Phrx-EQ}1;1fuxRC?HIwWwCBIF5s5Szmz6iz-q| z@;t}$JetiWX_`I+5P&L|%S2Ix5CY4x&Q)ZdKmddgL{UV!T)tm`5JeHb?_=9Gwry)g z=5*K6FWC2eq9_7T0x%rMQKo5fcFbZ22M6r$@6+ve8I4Bz=bq2k;TPDR#sMUUH8f$ z#S0MtmSySXGz-Yy-X7cA+ayUsxm?Ecyi*%Sp63KXz<4|+j$=BV4sjfF|Nec#Fq|2r z@LPjSIzI_CtrEMtyC|htSXe*^fomb>b#Yv>80wrNkzU3NS^0dmPMMT1VMmp+f&VR z90%8R84ib-rb&_{jK^bq-@h8jWVwEs(!F>{p65(1(P^wuQV2nw=VVzn)!eW7d8J^t zEQ>tPwIh>o7uj$)B+D`^%OVH@y%EeB?B=%=xoG&luM<+I(_t_eOf|1ajD-_LZkg2& z4KgcCxm?x>u-ol&baZs#vj8?XH%XF2D^NUVR){ZRB~W3bt*Z ziix*x-?F#2hiRG!AxP7d+qZA);_@;eUk&zIKn%kuPH8BmFbso!zt6jO@7USd(dV4O zV4#zr=Xuz+O_pWEam>NN0ZU6u)3<%T1#%S=LeOfp*xlX5vMe0OAxRRFB%#;qadL8^ z&kaQ(X;~IomXT!{TU%SS+ijvK;@-V`RIAn5{P-THHqY~DG#W%v#Npu~!{Jb$t^0Ap}AQJkP^% z9HJ;9&vV-CHm>XHK0&9`VPj*1)z#HY6_MW+qwst9)zqG^PI1XVL;yBK^S-a6_u&}Uz>$+O8Fbp-!;o;$YfLvrz6wzoj zs8lK%#&unyD5BkN^Xk6?Y)83Pu0WrjY1W6sA>;8_D>83)h=n4bb4)J?g5P1{R(E%I_gCAt|C1z1 zJ&xl^>E`BEw@3}2?+;J%JpX5-(Rh9e1fbjPKJb12H`6qK0xD-K+aC>5^aZ7qe-4Mk k^;)g=V&-Qur~kt7U+RJ}_2DS>(f|Me07*qoM6N<$g0Ou{5&!@I literal 0 HcmV?d00001 diff --git a/res/drawable/textfield_searchwidget_pressed.9.png b/res/drawable/textfield_searchwidget_pressed.9.png new file mode 100644 index 0000000000000000000000000000000000000000..d628cbca1ff2779af10b8d588fb010676a855f1b GIT binary patch literal 2032 zcmVPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXJ< z77ZDlXjq;A00(eML_t(&-tC#oZ(LU$$3MSw&V9_wW5ij69p>KD$v3L2{s@V zi5*L}RYe74#SW=|04qRh)jcaVEVJka5E89W9tEkS&7;uRjbl3=duHOfb06o&;?DRH zC(bx2wUPKqS2JVJo%{Qo@9%LQuzyqjZ~r}$?H?{)`1+e?Pfovea&~;i8ab2>7hbvb zd8>8v_R23``}L)_cY*xr=X39T^W4N6QDEhNL4kf&=m*zY?|$buw|=q-^2ax3-u%|N z#&4s*n7`fZ@ZRNRVy6T~Fb3ohpm`5zq4aa%d*`S5#_0x0=JkVXou7X9-KDo3z}!@N zA=0+Fd?V%kPkNj==BUR)-~=lV#NPp!0);}BE8RqS|C3dW_Bekcl)0(w0#JfXN68%h zgiE*K)bWUCCN1^Y;v5(w2d;=Jc(3F+bd!S0YQd%XKIf12nU0tPZ~(C+jvV4@o$1Md z=}AYmVg_M#SW-qQ3aBQQoSaq<$UDSRGB`lhmtSo(9L;9HSkn?l1~Y&-3@oaks<=>T z=E7LpGxQr!2Z$DBldDuMQPmPv9YJ882w=HguyC(nsa=qyTfMUy2}kOd=Vu(_wI};+ zRls_po)T9rT5SE#l_jD4V1rT#8(56n-;=sM&$We&wfi0g>Q&2;Mwwq(q4fI7opsM* zJ7sp<^5Th*IN0Aa1h5EU^m`M;4?(OTPB4MMJ{scuy_|pD%}G*atZt};25TsD z3@AKQDoQO=?kCFUYX$9opI6RA)MK+R7$YD~uukv~DzX`CPCy18j0n={c|Ke56be)0 z4rd{7LbGZJ1HmA8Ede?`PvAV(8P>W5S3b=+e>$SJFBmO-6xp65J3+(yqH=HCW)N0CDsw}p0upW%C+V#x0Jg1+rTi<YZr79rCv3N5#rczbkb3;>>gno2ytBg_MO!n52OYBdnIRT+%eT0O%)Lk zTl%Vx%8@dxWb5@Yw$#oru7vOiQ{w^kn%(oBX2a0Spr1n}7P>u8J5i=4%;=?S;G=(} zA~MJ(Gg`8*Z+OHA1yHY;5@Lk=CMO&#odW9w7YIoXDkIJybeQq%5nB5M$p?8?j;_H4 z21Xdzzz8ldh*c1wmnliER3h_qAb!}ljM;XLFg&JwaGrV87{rVQ0${ActH(J*=7qEn zD#nZ$TBVO(_fkYgSHu{h@Q`^(bHz*fw~-oXU&(x#{MFbH#iNBKI?VWog_drWgt7#W zR<$azrI&gT;clzo*tF8DKYmn`RJot{K?zmDsO&kTJX5r66Nqg2y-N{fKr1876vrBt zY+VTh!%Dm0UdJ<5cYEHd3O5$|^fL$p!3&Jl9M+8(lf$S%_R102SF+Deg{*ZwQ3O${ zT)ol5IpM@?_{e+itYpluWCT_)PKceMR<)x~iFC`)-8rJ$X}4XEv0BA&WZZIZ9qL|b zcMGoG*r2^paQ0+OxNYLu>wB)>?(_L_h7oYCT=C5vk7zdR=*!S;{;BSfBRj%tgf9Es z(TFtfm!l&>ccb9Oe9C7_8TE>#8VmP(o-|XkT!}*=2n>0ybbAFWYdObf!hP=hJR(P) zX3H&b!l~Jig_Vp}J12BPKlSu8Wxboz*$|AB`Ov6Y!a%SFawp`4^68zF#nqg8#qibV zD@=^pC)z69smK#r92t&J1=K5+)pkzO_Y__!{FZWNP?~G8VP+zr*Z0gXrvPjuo>n_2 z&6JnURyZ=|Mqg;{P~;1+I5I?`p&ASQRLKk73@7KxnQ$CRsZ7-}<_&jOGonzi#^9W= z)XMnqI(+Sg3X{$Kx%P#D3`f3EwKS^3O*ADRfCN_9-ni{x%HQjIR3Q#Q)pGY<#)k?o z??V|Fc|!4I76n4BVu*sB5JNjQ9WdE+M4{PKtX?UVy83aC)pl_>Kptm}s^!_4kcoyZ zVVn?!hQq4!8TO)QkA{plEOBTE1~B!iVY!v_*S~k4AV+lD>z8;6Qi}{H zWIIVkv$QFqA2cyV#WxjGUdU)4+&_{U?WRUWCuJ~N> zIaoOO^Mr>?Nqi1IFZ`N4%`WLs~^X&um;O(9BQ-W z)je`HO{y^S`td~jAMZr}{C!>We&-}~;@%P1;@g0k@1J04n_g-_KC4cTLeDv1L%Rl_fQ{~-ZROvq(6f{-cPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXJ< z77aT{2pM_+00!PkL_t(&-tC%SXk1wtz<=l7bN|lG>||zA$~2SGf=LCF`lsN-7GDGv zeUN<;Tw(D+#6{UR7g-j;7g=A0T?#9{=!-&b1h);qA{6tCr)+%4S`jbEYAP2ee z);C`Lf_?v&hKQ8EyLJAmehsrQg_IIiRVTv|aZD6N_+FPU#ed>+_5=J>OW*e{{P4xs z|M0d1IIli@%?NjtmEcqSW9DTpR4Q1Og{rCuA@cS-Bu&%2#c@op*JF9NjJo|Rmh^v% z^QwFe7=ToxhZjkE+?sxg+1XjjGgx!yTQ^1lZez_=iro0~j*_z>UsDHe;ErkPKO5P~?4v2B|m2yk7OX0wUwx?H|| znL?p3Hb|yl@+3$)1TwBgT-Rk|V}mqJsZ=URDbX~Isi`S+T}KE(k|cPZhwHlNx{ju4 zbUGbw-@eV_;v%+fk8Ke}QHFiwFh~|J#sOJdTO$lZN~IEtqEILlD3{B*RfG_vX^P`G z_`XlG*~~4~>2zo`8k|3W9x3Hm4ljgwI!HFas37fjn{Ky@<2d;hoUI>zE2PYSe7+1$gw_>q=%ho)Y-|Dj^p^y0HZ3C zrYTa&JhL68@34MJ`iz#As;b0soGUU6carpaJ)$T=RaI=;&NqV7E-ht{j1Ah87CEV9 zsXjon*~Ig_k@{tUF|$AxTgDwf)B7<E9b#)cr_j3zm z-x+7@C-pw$BTp)ElAJwzmi_&GOw+{oeO6XhkWx~w*Pq&FYiolg8+0uF@AVLW0^*Uh~=0#7p+vWcK`>e08<2VkMW#M^V9t5+ZEs7#s*JXEimviUN zjh&tx3-V!(+@zGu&CRi~v4N_p=(>*Y`}n?3r_L(T?K~$1#mY1H&-# zim%yhva+(m;^N|1N#rvH$!6Yh93001$U!tsJCs~z!PK^G`u#p>nqrzJX`1FROG`^* z!63T8&Djf?*iB#R`Q&4%2S8Cj;apaU6$QtwyO-%3%z{z;PTJ zjRwoh%XyHUXpk%x&CbqJDwXoBIfL2W-sa}bn;*ZAoK|exre3cfIvzC)16|iihO4JZ z2gwcx=jP@}l7u8lP!t8jFt~W};`kuh`D!*RAGesM$=uxB;M&0Vsn_dy3Ur)~M?u77 zfMk|gSXelu_|qnP_?k)rY3A}#6y=4|q#a-nm)ChA_js!k5)g<%*5Y{{{eGVy2=bsc zX%ImW(C_#0Jnx7_K-W?cheF%$S!(zc6N|=-b1L6s#*%+0L1<5 zE6Xpey!-hp53cdP0{^-6O>Emf1oAv@tcF26&qMy}_gr~!4K(O1y|N7N4vcg6?%k`? zH{SezcbMQsqq5A7k}LM zKJ(r0)oQih3~jsvX|-BcEzA0mq9|VhN>66C&(lAKeLhK&8@*odjdHns{nX!NLjS_? YUyH0Pu - + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="top"> - - - + + - - - + + + diff --git a/res/xml/default_workspace.xml b/res/xml/default_workspace.xml index ae2e2a6e09..2280b6126a 100644 --- a/res/xml/default_workspace.xml +++ b/res/xml/default_workspace.xml @@ -19,7 +19,7 @@ + launcher:y="1" /> 0) { - // something usable has been typed - dispatch it now. - final String str = mDefaultKeySsb.toString(); - - boolean isDialable = true; - final int count = str.length(); - for (int i = 0; i < count; i++) { - if (!PhoneNumberUtils.isReallyDialable(str.charAt(i))) { - isDialable = false; - break; - } - } - Intent intent; - if (isDialable) { - intent = new Intent(Intent.ACTION_DIAL, Uri.fromParts("tel", str, null)); - } else { - intent = new Intent(Contacts.Intents.UI.FILTER_CONTACTS_ACTION); - intent.putExtra(Contacts.Intents.UI.FILTER_TEXT_EXTRA_KEY, str); - } - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - - try { - startActivity(intent); - } catch (android.content.ActivityNotFoundException ex) { - // Oh well... no one knows how to filter/dial. Life goes on. - } - - mDefaultKeySsb.clear(); - mDefaultKeySsb.clearSpans(); - Selection.setSelection(mDefaultKeySsb, 0); - - return true; + // something usable has been typed - start a search + // the typed text will be retrieved and cleared by + // showSearchDialog() + // If there are multiple keystrokes before the search dialog takes focus, + // onSearchRequested() will be called for every keystroke, + // but it is idempotent, so it's fine. + return onSearchRequested(); } } return handled; } + private String getTypedText() { + return mDefaultKeySsb.toString(); + } + + private void clearTypedText() { + mDefaultKeySsb.clear(); + mDefaultKeySsb.clearSpans(); + Selection.setSelection(mDefaultKeySsb, 0); + } + /** * Restores the previous state, if it exists. * @@ -495,7 +480,7 @@ public final class Launcher extends Activity implements View.OnClickListener, On drawer.setOnDrawerCloseListener(drawerManager); drawer.setOnDrawerScrollListener(drawerManager); - grid.setTextFilterEnabled(true); + grid.setTextFilterEnabled(false); grid.setDragger(dragLayer); grid.setLauncher(this); @@ -827,7 +812,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On sModel.abortLoaders(); getContentResolver().unregisterContentObserver(mObserver); - getContentResolver().unregisterContentObserver(mAppWidgetResetObserver); unregisterReceiver(mApplicationsReceiver); } @@ -840,13 +824,76 @@ public final class Launcher extends Activity implements View.OnClickListener, On @Override public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) { + + closeDrawer(false); + + // Slide the search widget to the top, if it's on the current screen, + // otherwise show the search dialog immediately. + Search searchWidget = mWorkspace.findSearchWidgetOnCurrentScreen(); + if (searchWidget == null) { + showSearchDialog(initialQuery, selectInitialQuery, appSearchData, globalSearch); + } else { + searchWidget.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); + // show the currently typed text in the search widget while sliding + searchWidget.setQuery(getTypedText()); + } + } + + /** + * Show the search dialog immediately, without changing the search widget. + * See {@link Activity.startSearch()} for the arguments. + */ + public void showSearchDialog(String initialQuery, boolean selectInitialQuery, + Bundle appSearchData, boolean globalSearch) { + + if (initialQuery == null) { + // Use any text typed in the launcher as the initial query + initialQuery = getTypedText(); + clearTypedText(); + } if (appSearchData == null) { appSearchData = new Bundle(); appSearchData.putString(SearchManager.SOURCE, "launcher-search"); } - super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); + + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + + final Search searchWidget = mWorkspace.findSearchWidgetOnCurrentScreen(); + if (searchWidget != null) { + // This gets called when the user leaves the search dialog to go back to + // the Launcher. + searchManager.setOnCancelListener(new SearchManager.OnCancelListener() { + public void onCancel() { + searchManager.setOnCancelListener(null); + stopSearch(true); + } + }); + } + + searchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(), + appSearchData, globalSearch); } + /** + * Cancel search dialog if it is open. + * + * @param animate Whether to animate the search gadget (if any) when restoring it + * to its original position. + */ + public void stopSearch(boolean animate) { + // Close search dialog + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + if (searchManager.isVisible()) { + searchManager.stopSearch(); + } + // Restore search widget to its normal position + Search searchWidget = mWorkspace.findSearchWidgetOnCurrentScreen(); + if (searchWidget != null) { + searchWidget.stopSearch(false); + } + } + @Override public boolean onCreateOptionsMenu(Menu menu) { if (mDesktopLocked) return false; @@ -905,15 +952,16 @@ public final class Launcher extends Activity implements View.OnClickListener, On return super.onOptionsItemSelected(item); } + + /** + * Indicates that we want global search for this activity by setting the globalSearch + * argument for {@link #startSearch} to true. + */ @Override public boolean onSearchRequested() { - if (mWorkspace.snapToSearch()) { - closeDrawer(true); // search widget: get drawer out of the way - return true; - } else { - return super.onSearchRequested(); // no search widget: use system search UI - } + startSearch(null, false, null, true); + return true; } private void addItems() { @@ -975,6 +1023,8 @@ public final class Launcher extends Activity implements View.OnClickListener, On final View view = mInflater.inflate(info.layoutResource, null); view.setTag(info); + Search search = (Search) view.findViewById(R.id.widget_search); + search.setLauncher(this); mWorkspace.addInCurrentScreen(view, xy[0], xy[1], info.spanX, spanY); } @@ -1158,7 +1208,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On private void registerContentObservers() { ContentResolver resolver = getContentResolver(); resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true, mObserver); - resolver.registerContentObserver(LauncherProvider.CONTENT_APPWIDGET_RESET_URI, true, mAppWidgetResetObserver); } @Override @@ -1224,16 +1273,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On sModel.loadUserItems(false, this, false, false); } - /** - * When reset, we handle by calling {@link AppWidgetHost#startListening()} - * to make sure our callbacks are set correctly. - */ - private void onAppWidgetReset() { - if (mAppWidgetHost != null) { - mAppWidgetHost.startListening(); - } - } - void onDesktopItemsLoaded() { if (mDestroyed) return; bindDesktopItems(); @@ -1250,8 +1289,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On return; } - mAllAppsGrid.setAdapter(drawerAdapter); - final Workspace workspace = mWorkspace; int count = workspace.getChildCount(); for (int i = 0; i < count; i++) { @@ -1275,7 +1312,7 @@ public final class Launcher extends Activity implements View.OnClickListener, On mBinder.mTerminate = true; } - mBinder = new DesktopBinder(this, shortcuts, appWidgets); + mBinder = new DesktopBinder(this, shortcuts, appWidgets, drawerAdapter); mBinder.startBindingItems(); } @@ -1317,6 +1354,9 @@ public final class Launcher extends Activity implements View.OnClickListener, On final View view = mInflater.inflate(R.layout.widget_search, (ViewGroup) workspace.getChildAt(screen), false); + Search search = (Search) view.findViewById(R.id.widget_search); + search.setLauncher(this); + final Widget widget = (Widget) item; view.setTag(widget); @@ -1329,7 +1369,7 @@ public final class Launcher extends Activity implements View.OnClickListener, On if (end >= count) { finishBindDesktopItems(); - binder.startBindingAppWidgetsWhenIdle(); + binder.startBindingDrawer(); } else { binder.obtainMessage(DesktopBinder.MESSAGE_BIND_ITEMS, i, count).sendToTarget(); } @@ -1376,6 +1416,12 @@ public final class Launcher extends Activity implements View.OnClickListener, On mDrawer.unlock(); } + private void bindDrawer(Launcher.DesktopBinder binder, + ApplicationsAdapter drawerAdapter) { + mAllAppsGrid.setAdapter(drawerAdapter); + binder.startBindingAppWidgetsWhenIdle(); + } + private void bindAppWidgets(Launcher.DesktopBinder binder, LinkedList appWidgets) { @@ -1386,14 +1432,10 @@ public final class Launcher extends Activity implements View.OnClickListener, On final LauncherAppWidgetInfo item = appWidgets.removeFirst(); final int appWidgetId = item.appWidgetId; - final AppWidgetProviderInfo appWidgetInfo = - mAppWidgetManager.getAppWidgetInfo(appWidgetId); + final AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); - if (LOGD) { - d(LOG_TAG, String.format("about to setAppWidget for id=%d, info=%s", - appWidgetId, appWidgetInfo)); - } + if (LOGD) d(LOG_TAG, String.format("about to setAppWidget for id=%d, info=%s", appWidgetId, appWidgetInfo)); item.hostView.setAppWidget(appWidgetId, appWidgetInfo); item.hostView.setTag(item); @@ -1890,21 +1932,6 @@ public final class Launcher extends Activity implements View.OnClickListener, On } } - /** - * Receives notifications when the {@link AppWidgetHost} has been reset, - * usually only when the {@link LauncherProvider} database is first created. - */ - private class AppWidgetResetObserver extends ContentObserver { - public AppWidgetResetObserver() { - super(new Handler()); - } - - @Override - public void onChange(boolean selfChange) { - onAppWidgetReset(); - } - } - /** * Receives intents from other applications to change the wallpaper. */ @@ -1996,21 +2023,25 @@ public final class Launcher extends Activity implements View.OnClickListener, On private static class DesktopBinder extends Handler implements MessageQueue.IdleHandler { static final int MESSAGE_BIND_ITEMS = 0x1; static final int MESSAGE_BIND_APPWIDGETS = 0x2; + static final int MESSAGE_BIND_DRAWER = 0x3; // Number of items to bind in every pass static final int ITEMS_COUNT = 6; private final ArrayList mShortcuts; private final LinkedList mAppWidgets; + private final ApplicationsAdapter mDrawerAdapter; private final WeakReference mLauncher; - public volatile boolean mTerminate = false; + public boolean mTerminate = false; DesktopBinder(Launcher launcher, ArrayList shortcuts, - ArrayList appWidgets) { + ArrayList appWidgets, + ApplicationsAdapter drawerAdapter) { mLauncher = new WeakReference(launcher); mShortcuts = shortcuts; + mDrawerAdapter = drawerAdapter; // Sort widgets so active workspace is bound first final int currentScreen = launcher.mWorkspace.getCurrentScreen(); @@ -2030,6 +2061,10 @@ public final class Launcher extends Activity implements View.OnClickListener, On public void startBindingItems() { obtainMessage(MESSAGE_BIND_ITEMS, 0, mShortcuts.size()).sendToTarget(); } + + public void startBindingDrawer() { + obtainMessage(MESSAGE_BIND_DRAWER).sendToTarget(); + } public void startBindingAppWidgetsWhenIdle() { // Ask for notification when message queue becomes idle @@ -2059,6 +2094,10 @@ public final class Launcher extends Activity implements View.OnClickListener, On launcher.bindItems(this, mShortcuts, msg.arg1, msg.arg2); break; } + case MESSAGE_BIND_DRAWER: { + launcher.bindDrawer(this, mDrawerAdapter); + break; + } case MESSAGE_BIND_APPWIDGETS: { launcher.bindAppWidgets(this, mAppWidgets); break; @@ -2067,3 +2106,4 @@ public final class Launcher extends Activity implements View.OnClickListener, On } } } + diff --git a/src/com/android/launcher/Search.java b/src/com/android/launcher/Search.java index 71ab7ef448..3d0947987c 100644 --- a/src/com/android/launcher/Search.java +++ b/src/com/android/launcher/Search.java @@ -16,71 +16,59 @@ package com.android.launcher; -import android.app.ISearchManager; -import android.app.SearchManager; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.content.res.Resources.NotFoundException; -import android.database.Cursor; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.Uri; +import android.content.res.Configuration; import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.server.search.SearchableInfo; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.view.View.OnClickListener; import android.view.View.OnKeyListener; import android.view.View.OnLongClickListener; -import android.widget.AdapterView; -import android.widget.AutoCompleteTextView; -import android.widget.CursorAdapter; -import android.widget.Filter; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.Interpolator; +import android.view.animation.Transformation; +import android.view.inputmethod.InputMethodManager; import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.SimpleCursorAdapter; import android.widget.TextView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemSelectedListener; -public class Search extends LinearLayout implements OnClickListener, OnKeyListener, - OnLongClickListener, TextWatcher, OnItemClickListener, OnItemSelectedListener { +public class Search extends LinearLayout + implements OnClickListener, OnKeyListener, OnLongClickListener { + + // Speed at which the widget slides up/down, in pixels/ms. + private static final float ANIMATION_VELOCITY = 1.0f; private final String TAG = "SearchWidget"; - private AutoCompleteTextView mSearchText; - private ImageButton mGoButton; + private Launcher mLauncher; + + private TextView mSearchText; private ImageButton mVoiceButton; - private OnLongClickListener mLongClickListener; - - // Support for suggestions - private SuggestionsAdapter mSuggestionsAdapter; - private SearchableInfo mSearchable; - private String mSuggestionAction = null; - private Uri mSuggestionData = null; - private String mSuggestionQuery = null; - private int mItemSelected = -1; - + + /** The animation that morphs the search widget to the search dialog. */ + private Animation mMorphAnimation; + + /** The animation that morphs the search widget back to its normal position. */ + private Animation mUnmorphAnimation; + + // These four are passed to Launcher.startSearch() when the search widget + // has finished morphing. They are instance variables to make it possible to update + // them while the widget is morphing. + private String mInitialQuery; + private boolean mSelectInitialQuery; + private Bundle mAppSearchData; + private boolean mGlobalSearch; + // For voice searching private Intent mVoiceSearchIntent; - private Rect mTempRect = new Rect(); - private boolean mRestoreFocus = false; - /** * Used to inflate the Workspace from XML. * @@ -89,293 +77,235 @@ public class Search extends LinearLayout implements OnClickListener, OnKeyListen */ public Search(Context context, AttributeSet attrs) { super(context, attrs); + + Interpolator interpolator = new AccelerateDecelerateInterpolator(); + + mMorphAnimation = new ToParentOriginAnimation(); + // no need to apply transformation before the animation starts, + // since the gadget is already in its normal place. + mMorphAnimation.setFillBefore(false); + // stay in the top position after the animation finishes + mMorphAnimation.setFillAfter(true); + mMorphAnimation.setInterpolator(interpolator); + mMorphAnimation.setAnimationListener(new Animation.AnimationListener() { + // The amount of time before the animation ends to show the search dialog. + private static final long TIME_BEFORE_ANIMATION_END = 80; + + // The runnable which we'll pass to our handler to show the search dialog. + private final Runnable mShowSearchDialogRunnable = new Runnable() { + public void run() { + showSearchDialog(); + } + }; + + public void onAnimationEnd(Animation animation) { } + public void onAnimationRepeat(Animation animation) { } + public void onAnimationStart(Animation animation) { + // Make the search dialog show up ideally *just* as the animation reaches + // the top, to aid the illusion that the widget becomes the search dialog. + // Otherwise, there is a short delay when the widget reaches the top before + // the search dialog shows. We do this roughly 80ms before the animation ends. + getHandler().postDelayed( + mShowSearchDialogRunnable, + Math.max(mMorphAnimation.getDuration() - TIME_BEFORE_ANIMATION_END, 0)); + } + }); + + mUnmorphAnimation = new FromParentOriginAnimation(); + // stay in the top position until the animation starts + mUnmorphAnimation.setFillBefore(true); + // no need to apply transformation after the animation finishes, + // since the gadget is now back in its normal place. + mUnmorphAnimation.setFillAfter(false); + mUnmorphAnimation.setInterpolator(interpolator); + mUnmorphAnimation.setAnimationListener(new Animation.AnimationListener(){ + public void onAnimationEnd(Animation animation) { + clearAnimation(); + } + public void onAnimationRepeat(Animation animation) { } + public void onAnimationStart(Animation animation) { } + }); mVoiceSearchIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH); mVoiceSearchIntent.putExtra(android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL, android.speech.RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); } - + /** - * Implements OnClickListener (for button) + * Implements OnClickListener. */ public void onClick(View v) { - if (v == mGoButton) { - query(); - } else if (v == mVoiceButton) { - try { - getContext().startActivity(mVoiceSearchIntent); - } catch (ActivityNotFoundException ex) { - // Should not happen, since we check the availability of - // voice search before showing the button. But just in case... - Log.w(TAG, "Could not find voice search activity"); + if (v == mVoiceButton) { + startVoiceSearch(); + } else { + mLauncher.onSearchRequested(); + } + } + + private void startVoiceSearch() { + try { + getContext().startActivity(mVoiceSearchIntent); + } catch (ActivityNotFoundException ex) { + // Should not happen, since we check the availability of + // voice search before showing the button. But just in case... + Log.w(TAG, "Could not find voice search activity"); + } + } + + /** + * Sets the query text. The query field is not editable, instead we forward + * the key events to the launcher, which keeps track of the text, + * calls setQuery() to show it, and gives it to the search dialog. + */ + public void setQuery(String query) { + mSearchText.setText(query, TextView.BufferType.NORMAL); + } + + /** + * Morph the search gadget to the search dialog. + * See {@link Activity.startSearch()} for the arguments. + */ + public void startSearch(String initialQuery, boolean selectInitialQuery, + Bundle appSearchData, boolean globalSearch) { + mInitialQuery = initialQuery; + mSelectInitialQuery = selectInitialQuery; + mAppSearchData = appSearchData; + mGlobalSearch = globalSearch; + + // Call up the keyboard before we actually call the search dialog so that it + // (hopefully) animates in at about the same time as the widget animation, and + // so that it becomes available as soon as possible. Only do this if a hard + // keyboard is not currently available. + if (getContext().getResources().getConfiguration().hardKeyboardHidden == + Configuration.HARDKEYBOARDHIDDEN_YES) { + // Make sure the text field is not focusable, so it's not responsible for + // causing the whole view to shift up to accommodate the keyboard. + mSearchText.setFocusable(false); + + InputMethodManager inputManager = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.showSoftInputUnchecked(0, null); + } + + if (isAtTop()) { + showSearchDialog(); + } else { + // Start the animation, unless it has already started. + if (getAnimation() != mMorphAnimation) { + mMorphAnimation.setDuration(getAnimationDuration()); + startAnimation(mMorphAnimation); } } } - private void query() { - String query = mSearchText.getText().toString(); - if (TextUtils.getTrimmedLength(mSearchText.getText()) == 0) { - return; + /** + * Shows the system search dialog immediately, without any animation. + */ + private void showSearchDialog() { + mLauncher.showSearchDialog( + mInitialQuery, mSelectInitialQuery, mAppSearchData, mGlobalSearch); + } + + /** + * Restore the search gadget to its normal position. + * + * @param animate Whether to animate the movement of the gadget. + */ + public void stopSearch(boolean animate) { + setQuery(""); + + // Set the search field back to focusable after making it unfocusable in + // startSearch, so that the home screen doesn't try to shift around when the + // keyboard comes up. + mSearchText.setFocusable(true); + // Only restore if we are not already restored. + if (getAnimation() == mMorphAnimation) { + if (animate && !isAtTop()) { + mUnmorphAnimation.setDuration(getAnimationDuration()); + startAnimation(mUnmorphAnimation); + } else { + clearAnimation(); + } + } + } + + private boolean isAtTop() { + return getTop() == 0; + } + + private int getAnimationDuration() { + return (int) (getTop() / ANIMATION_VELOCITY); + } + + /** + * Modify clearAnimation() to invalidate the parent. This works around + * an issue where the region where the end of the animation placed the view + * was not redrawn after clearing the animation. + */ + @Override + public void clearAnimation() { + Animation animation = getAnimation(); + if (animation != null) { + super.clearAnimation(); + if (animation.hasEnded() + && animation.getFillAfter() + && animation.willChangeBounds()) { + ((View) getParent()).invalidate(); + } else { + invalidate(); + } } - Bundle appData = new Bundle(); - appData.putString(SearchManager.SOURCE, "launcher-widget"); - sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, 0, null, mSearchable); - clearQuery(); } - /** - * Assemble a search intent and send it. - * - * This is copied from SearchDialog. - * - * @param action The intent to send, typically Intent.ACTION_SEARCH - * @param data The data for the intent - * @param query The user text entered (so far) - * @param appData The app data bundle (if supplied) - * @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will - * be sent here. Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code. - * @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the - * corresponding tag message will be sent here. Pass null for no actionKey message. - * @param si Reference to the current SearchableInfo. Passed here so it can be used even after - * we've called dismiss(), which attempts to null mSearchable. - */ - private void sendLaunchIntent(final String action, final Uri data, final String query, - final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) { - Intent launcher = new Intent(action); - launcher.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - if (query != null) { - launcher.putExtra(SearchManager.QUERY, query); - } - - if (data != null) { - launcher.setData(data); - } - - if (appData != null) { - launcher.putExtra(SearchManager.APP_DATA, appData); - } - - // add launch info (action key, etc.) - if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { - launcher.putExtra(SearchManager.ACTION_KEY, actionKey); - launcher.putExtra(SearchManager.ACTION_MSG, actionMsg); - } - - // attempt to enforce security requirement (no 3rd-party intents) - if (si != null) { - launcher.setComponent(si.mSearchActivity); - } - - getContext().startActivity(launcher); - } - - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - if (!hasWindowFocus && hasFocus()) { - mRestoreFocus = true; - } - - super.onWindowFocusChanged(hasWindowFocus); - - if (hasWindowFocus && mRestoreFocus) { - if (isInTouchMode()) { - final AutoCompleteTextView searchText = mSearchText; - searchText.setSelectAllOnFocus(false); - searchText.requestFocusFromTouch(); - searchText.setSelectAllOnFocus(true); + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (!event.isSystem() && + (keyCode != KeyEvent.KEYCODE_DPAD_UP) && + (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) && + (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) && + (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) && + (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) { + // Forward key events to Launcher, which will forward text + // to search dialog + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + return mLauncher.onKeyDown(keyCode, event); + case KeyEvent.ACTION_MULTIPLE: + return mLauncher.onKeyMultiple(keyCode, event.getRepeatCount(), event); + case KeyEvent.ACTION_UP: + return mLauncher.onKeyUp(keyCode, event); } - mRestoreFocus = false; } - } - - /** - * Implements TextWatcher (for EditText) - */ - public void beforeTextChanged(CharSequence s, int start, int before, int after) { - } - - /** - * Implements TextWatcher (for EditText) - */ - public void onTextChanged(CharSequence s, int start, int before, int after) { - // enable the button if we have one or more non-space characters - boolean enabled = TextUtils.getTrimmedLength(mSearchText.getText()) != 0; - mGoButton.setEnabled(enabled); - mGoButton.setFocusable(enabled); - } - - /** - * Implements TextWatcher (for EditText) - */ - public void afterTextChanged(Editable s) { - } - - /** - * Implements OnKeyListener (for EditText and for button) - * - * This plays some games with state in order to "soften" the strength of suggestions - * presented. Suggestions should not be used unless the user specifically navigates to them - * (or clicks them, in which case it's obvious). This is not the way that AutoCompleteTextBox - * normally works. - */ - public final boolean onKey(View v, int keyCode, KeyEvent event) { - if (v == mSearchText) { - boolean searchTrigger = (keyCode == KeyEvent.KEYCODE_ENTER || - keyCode == KeyEvent.KEYCODE_SEARCH || - keyCode == KeyEvent.KEYCODE_DPAD_CENTER); - if (event.getAction() == KeyEvent.ACTION_UP) { -// Log.d(TAG, "onKey() ACTION_UP isPopupShowing:" + mSearchText.isPopupShowing()); - if (!mSearchText.isPopupShowing()) { - if (searchTrigger) { - query(); - return true; - } - } - } else { -// Log.d(TAG, "onKey() ACTION_DOWN isPopupShowing:" + mSearchText.isPopupShowing() + -// " mItemSelected="+ mItemSelected); - if (searchTrigger && mItemSelected < 0) { - query(); - return true; - } - } - } else if (v == mGoButton || v == mVoiceButton) { - boolean handled = false; - if (!event.isSystem() && - (keyCode != KeyEvent.KEYCODE_DPAD_UP) && - (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) && - (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) && - (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) && - (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) { - if (mSearchText.requestFocus()) { - handled = mSearchText.dispatchKeyEvent(event); - } - } - return handled; - } - return false; } - - @Override - public void setOnLongClickListener(OnLongClickListener l) { - super.setOnLongClickListener(l); - mLongClickListener = l; - } - + /** - * Implements OnLongClickListener (for button) + * Implements OnLongClickListener to pass long clicks on child views + * to the widget. This makes it possible to pick up the widget by long + * clicking on the text field or a button. */ public boolean onLongClick(View v) { - // Pretend that a long press on a child view is a long press on the search widget - if (mLongClickListener != null) { - return mLongClickListener.onLongClick(this); - } - return false; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - // Request focus unless the user tapped on the voice search button - final int x = (int) ev.getX(); - final int y = (int) ev.getY(); - final Rect frame = mTempRect; - mVoiceButton.getHitRect(frame); - if (!frame.contains(x, y)) { - requestFocusFromTouch(); - } - return super.onInterceptTouchEvent(ev); - } - - /** - * In order to keep things simple, the external trigger will clear the query just before - * focusing, so as to give you a fresh query. This way we eliminate any sources of - * accidental query launching. - */ - public void clearQuery() { - mSearchText.setText(null); + return performLongClick(); } @Override protected void onFinishInflate() { super.onFinishInflate(); - mSearchText = (AutoCompleteTextView) findViewById(R.id.input); - // TODO: This can be confusing when the user taps the text field to give the focus - // (it is not necessary but I ran into this issue several times myself) - // mTitleInput.setOnClickListener(this); - mSearchText.setOnKeyListener(this); - mSearchText.addTextChangedListener(this); - - mGoButton = (ImageButton) findViewById(R.id.search_go_btn); + mSearchText = (TextView) findViewById(R.id.search_src_text); mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn); - mGoButton.setOnClickListener(this); + + mSearchText.setOnKeyListener(this); + + mSearchText.setOnClickListener(this); mVoiceButton.setOnClickListener(this); - mGoButton.setOnKeyListener(this); - mVoiceButton.setOnKeyListener(this); - + setOnClickListener(this); + mSearchText.setOnLongClickListener(this); - mGoButton.setOnLongClickListener(this); mVoiceButton.setOnLongClickListener(this); - - // disable the button since we start out w/empty input - mGoButton.setEnabled(false); - mGoButton.setFocusable(false); - - configureSearchableInfo(); - configureSuggestions(); + configureVoiceSearchButton(); } - - /** - * Cache of popup padding value after read from {@link Resources}. - */ - private static float mPaddingInset = -1; - - /** - * When our size is changed, pass down adjusted width and offset values to - * correctly center the {@link AutoCompleteTextView} popup and include our - * padding. - */ - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (changed) { - if (mPaddingInset == -1) { - mPaddingInset = getResources().getDimension(R.dimen.search_widget_inset); - } - - // Fill entire width of widget, minus padding inset - float paddedWidth = getWidth() - (mPaddingInset * 2); - float paddedOffset = -(mSearchText.getLeft() - mPaddingInset); - - mSearchText.setDropDownWidth((int) paddedWidth); - mSearchText.setDropDownHorizontalOffset((int) paddedOffset); - } - } - - /** - * Read the searchable info from the search manager - */ - private void configureSearchableInfo() { - ISearchManager sms; - SearchableInfo searchable; - sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE)); - try { - // TODO null isn't the published use of this API, but it works when global=true - // TODO better implementation: defer all of this, let Home set it up - searchable = sms.getSearchableInfo(null, true); - } catch (RemoteException e) { - searchable = null; - } - if (searchable == null) { - // no suggestions so just get out (no need to continue) - return; - } - mSearchable = searchable; - } - + /** * If appropriate & available, configure voice search * @@ -384,346 +314,45 @@ public class Search extends LinearLayout implements OnClickListener, OnKeyListen * voice search. */ private void configureVoiceSearchButton() { - boolean voiceSearchVisible = false; - if (mSearchable.getVoiceSearchEnabled() && mSearchable.getVoiceSearchLaunchWebSearch()) { - // Enable the voice search button if there is an activity that can handle it - PackageManager pm = getContext().getPackageManager(); - ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent, - PackageManager.MATCH_DEFAULT_ONLY); - voiceSearchVisible = ri != null; - } - + // Enable the voice search button if there is an activity that can handle it + PackageManager pm = getContext().getPackageManager(); + ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent, + PackageManager.MATCH_DEFAULT_ONLY); + boolean voiceSearchVisible = ri != null; + // finally, set visible state of voice search button, as appropriate mVoiceButton.setVisibility(voiceSearchVisible ? View.VISIBLE : View.GONE); } - - /** The rest of the class deals with providing search suggestions */ - + /** - * Set up the suggestions provider mechanism + * Sets the {@link Launcher} that this gadget will call on to display the search dialog. */ - private void configureSuggestions() { - // get SearchableInfo - - mSearchText.setOnItemClickListener(this); - mSearchText.setOnItemSelectedListener(this); - - // attach the suggestions adapter - mSuggestionsAdapter = new SuggestionsAdapter(mContext, - com.android.internal.R.layout.search_dropdown_item_2line, null, - SuggestionsAdapter.TWO_LINE_FROM, SuggestionsAdapter.TWO_LINE_TO, mSearchable); - mSearchText.setAdapter(mSuggestionsAdapter); + public void setLauncher(Launcher launcher) { + mLauncher = launcher; } - - /** - * Remove internal cursor references when detaching from window which - * prevents {@link Context} leaks. - */ - @Override - public void onDetachedFromWindow() { - if (mSuggestionsAdapter != null) { - mSuggestionsAdapter.changeCursor(null); - mSuggestionsAdapter = null; - } - } - - /** - * Implements OnItemClickListener - */ - public void onItemClick(AdapterView parent, View view, int position, long id) { -// Log.d(TAG, "onItemClick() position " + position); - launchSuggestion(mSuggestionsAdapter, position); - } - + /** - * Implements OnItemSelectedListener + * Moves the view to the top left corner of its parent. */ - public void onItemSelected(AdapterView parent, View view, int position, long id) { -// Log.d(TAG, "onItemSelected() position " + position); - mItemSelected = position; - } - - /** - * Implements OnItemSelectedListener - */ - public void onNothingSelected(AdapterView parent) { -// Log.d(TAG, "onNothingSelected()"); - mItemSelected = -1; - } - - /** - * Code to launch a suggestion query. - * - * This is copied from SearchDialog. - * - * @param ca The CursorAdapter containing the suggestions - * @param position The suggestion we'll be launching from - * - * @return Returns true if a successful launch, false if could not (e.g. bad position) - */ - private boolean launchSuggestion(CursorAdapter ca, int position) { - if (ca != null) { - Cursor c = ca.getCursor(); - if ((c != null) && c.moveToPosition(position)) { - setupSuggestionIntent(c, mSearchable); - - SearchableInfo si = mSearchable; - String suggestionAction = mSuggestionAction; - Uri suggestionData = mSuggestionData; - String suggestionQuery = mSuggestionQuery; - sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, null, - KeyEvent.KEYCODE_UNKNOWN, null, si); - clearQuery(); - return true; - } - } - return false; - } - - /** - * When a particular suggestion has been selected, perform the various lookups required - * to use the suggestion. This includes checking the cursor for suggestion-specific data, - * and/or falling back to the XML for defaults; It also creates REST style Uri data when - * the suggestion includes a data id. - * - * NOTE: Return values are in member variables mSuggestionAction, mSuggestionData and - * mSuggestionQuery. - * - * This is copied from SearchDialog. - * - * @param c The suggestions cursor, moved to the row of the user's selection - * @param si The searchable activity's info record - */ - void setupSuggestionIntent(Cursor c, SearchableInfo si) { - try { - // use specific action if supplied, or default action if supplied, or fixed default - mSuggestionAction = null; - int column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION); - if (column >= 0) { - final String action = c.getString(column); - if (action != null) { - mSuggestionAction = action; - } - } - if (mSuggestionAction == null) { - mSuggestionAction = si.getSuggestIntentAction(); - } - if (mSuggestionAction == null) { - mSuggestionAction = Intent.ACTION_SEARCH; - } - - // use specific data if supplied, or default data if supplied - String data = null; - column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA); - if (column >= 0) { - final String rowData = c.getString(column); - if (rowData != null) { - data = rowData; - } - } - if (data == null) { - data = si.getSuggestIntentData(); - } - - // then, if an ID was provided, append it. - if (data != null) { - column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); - if (column >= 0) { - final String id = c.getString(column); - if (id != null) { - data = data + "/" + Uri.encode(id); - } - } - } - mSuggestionData = (data == null) ? null : Uri.parse(data); - - mSuggestionQuery = null; - column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY); - if (column >= 0) { - final String query = c.getString(column); - if (query != null) { - mSuggestionQuery = query; - } - } - } catch (RuntimeException e ) { - int rowNum; - try { // be really paranoid now - rowNum = c.getPosition(); - } catch (RuntimeException e2 ) { - rowNum = -1; - } - Log.w(TAG, "Search Suggestions cursor at row " + rowNum + - " returned exception" + e.toString()); - } - } - - SearchAutoCompleteTextView getSearchInputField() { - return (SearchAutoCompleteTextView) mSearchText; - } - - /** - * This class provides the filtering-based interface to suggestions providers. - * It is hardwired in a couple of places to support GoogleSearch - for example, it supports - * two-line suggestions, but it does not support icons. - */ - private static class SuggestionsAdapter extends SimpleCursorAdapter { - public final static String[] TWO_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1, - SearchManager.SUGGEST_COLUMN_TEXT_2 }; - public final static int[] TWO_LINE_TO = {com.android.internal.R.id.text1, - com.android.internal.R.id.text2}; - - private final String TAG = "SuggestionsAdapter"; - - Filter mFilter; - SearchableInfo mSearchable; - private Resources mProviderResources; - String[] mFromStrings; - - public SuggestionsAdapter(Context context, int layout, Cursor c, - String[] from, int[] to, SearchableInfo searchable) { - super(context, layout, c, from, to); - mFromStrings = from; - mSearchable = searchable; - - // set up provider resources (gives us icons, etc.) - Context activityContext = mSearchable.getActivityContext(mContext); - Context providerContext = mSearchable.getProviderContext(mContext, activityContext); - mProviderResources = providerContext.getResources(); - } - - /** - * Use the search suggestions provider to obtain a live cursor. This will be called - * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions). - * The results will be processed in the UI thread and changeCursor() will be called. - */ + private class ToParentOriginAnimation extends Animation { @Override - public Cursor runQueryOnBackgroundThread(CharSequence constraint) { - String query = (constraint == null) ? "" : constraint.toString(); - return getSuggestions(mSearchable, query); + protected void applyTransformation(float interpolatedTime, Transformation t) { + float dx = -getLeft() * interpolatedTime; + float dy = -getTop() * interpolatedTime; + t.getMatrix().setTranslate(dx, dy); } - - /** - * Overriding this allows us to write the selected query back into the box. - * NOTE: This is a vastly simplified version of SearchDialog.jamQuery() and does - * not universally support the search API. But it is sufficient for Google Search. - */ - @Override - public CharSequence convertToString(Cursor cursor) { - CharSequence result = null; - if (cursor != null) { - int column = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY); - if (column >= 0) { - final String query = cursor.getString(column); - if (query != null) { - result = query; - } - } - } - return result; - } - - /** - * Get the query cursor for the search suggestions. - * - * TODO this is functionally identical to the version in SearchDialog.java. Perhaps it - * could be hoisted into SearchableInfo or some other shared spot. - * - * @param query The search text entered (so far) - * @return Returns a cursor with suggestions, or null if no suggestions - */ - private Cursor getSuggestions(final SearchableInfo searchable, final String query) { - Cursor cursor = null; - if (searchable.getSuggestAuthority() != null) { - try { - StringBuilder uriStr = new StringBuilder("content://"); - uriStr.append(searchable.getSuggestAuthority()); - - // if content path provided, insert it now - final String contentPath = searchable.getSuggestPath(); - if (contentPath != null) { - uriStr.append('/'); - uriStr.append(contentPath); - } - - // append standard suggestion query path - uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY); - - // inject query, either as selection args or inline - String[] selArgs = null; - if (searchable.getSuggestSelection() != null) { // use selection if provided - selArgs = new String[] {query}; - } else { - uriStr.append('/'); // no sel, use REST pattern - uriStr.append(Uri.encode(query)); - } - - // finally, make the query - cursor = mContext.getContentResolver().query( - Uri.parse(uriStr.toString()), null, - searchable.getSuggestSelection(), selArgs, - null); - } catch (RuntimeException e) { - Log.w(TAG, "Search Suggestions query returned exception " + e.toString()); - cursor = null; - } - } - - return cursor; - } - - /** - * Overriding this allows us to affect the way that an icon is loaded. Specifically, - * we can be more controlling about the resource path (and allow icons to come from other - * packages). - * - * TODO: This is 100% identical to the version in SearchDialog.java - * - * @param v ImageView to receive an image - * @param value the value retrieved from the cursor - */ - @Override - public void setViewImage(ImageView v, String value) { - int resID; - Drawable img = null; - - try { - resID = Integer.parseInt(value); - if (resID != 0) { - img = mProviderResources.getDrawable(resID); - } - } catch (NumberFormatException nfe) { - // img = null; - } catch (NotFoundException e2) { - // img = null; - } - - // finally, set the image to whatever we've gotten - v.setImageDrawable(img); - } - - /** - * This method is overridden purely to provide a bit of protection against - * flaky content providers. - * - * TODO: This is 100% identical to the version in SearchDialog.java - * - * @see android.widget.ListAdapter#getView(int, View, ViewGroup) - */ - @Override - public View getView(int position, View convertView, ViewGroup parent) { - try { - return super.getView(position, convertView, parent); - } catch (RuntimeException e) { - Log.w(TAG, "Search Suggestions cursor returned exception " + e.toString()); - // what can I return here? - View v = newView(mContext, mCursor, parent); - if (v != null) { - TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1); - tv.setText(e.toString()); - } - return v; - } - } - } + + /** + * Moves the view from the top left corner of its parent. + */ + private class FromParentOriginAnimation extends Animation { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + float dx = -getLeft() * (1.0f - interpolatedTime); + float dy = -getTop() * (1.0f - interpolatedTime); + t.getMatrix().setTranslate(dx, dy); + } + } + } diff --git a/src/com/android/launcher/Workspace.java b/src/com/android/launcher/Workspace.java index 359767aea7..c76fb7ea97 100644 --- a/src/com/android/launcher/Workspace.java +++ b/src/com/android/launcher/Workspace.java @@ -1027,7 +1027,7 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag } return result; } - + /** * Find a search widget on the given screen */ @@ -1041,102 +1041,14 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag } return null; } - + /** - * Focuses on the search widget on the specified screen, - * if there is one. Also clears the current search selection so we don't + * Gets the first search widget on the current screen, if there is one. + * Returns null otherwise. */ - private boolean focusOnSearch(int screen) { - CellLayout currentScreen = (CellLayout) getChildAt(screen); - final Search searchWidget = findSearchWidget(currentScreen); - if (searchWidget != null) { - // This is necessary when focus on search is requested from the menu - // If the workspace was not in touch mode before the menu is invoked - // and the user clicks "Search" by touching the menu item, the following - // happens: - // - // - We request focus from touch on the search widget - // - The search widget gains focus - // - The window focus comes back to Home's window - // - The touch mode change is propagated to Home's window - // - The search widget is not focusable in touch mode and ViewRoot - // clears its focus - // - // Forcing focusable in touch mode ensures the search widget will - // keep the focus no matter what happens. - // - // Note: the search input field disables focusable in touch mode - // after the window gets the focus back, see SearchAutoCompleteTextView - final SearchAutoCompleteTextView input = searchWidget.getSearchInputField(); - input.setFocusableInTouchMode(true); - input.showKeyboardOnNextFocus(); - - if (isInTouchMode()) { - searchWidget.requestFocusFromTouch(); - } else { - searchWidget.requestFocus(); - } - searchWidget.clearQuery(); - return true; - } - return false; - } - - /** - * Snap to the nearest screen with a search widget and give it focus - * - * @return True if a search widget was found - */ - public boolean snapToSearch() { - // The screen we are searching - int current = mCurrentScreen; - - // first position scanned so far - int first = current; - - // last position scanned so far - int last = current; - - // True if we should move down on the next iteration - boolean next = false; - - // True when we have looked at the first item in the data - boolean hitFirst; - - // True when we have looked at the last item in the data - boolean hitLast; - - final int count = getChildCount(); - - while (true) { - if (focusOnSearch(current)) { - return true; - } - - hitLast = last == count - 1; - hitFirst = first == 0; - - if (hitLast && hitFirst) { - // Looked at everything - break; - } - - if (hitFirst || (next && !hitLast)) { - // Either we hit the top, or we are trying to move down - last++; - current = last; - // Try going up next time - next = false; - } else { - // Either we hit the bottom, or we are trying to move up - first--; - current = first; - // Try going down next time - next = true; - } - - } - return false; + public Search findSearchWidgetOnCurrentScreen() { + CellLayout currentScreen = (CellLayout)getChildAt(mCurrentScreen); + return findSearchWidget(currentScreen); } public Folder getFolderForTag(Object tag) {