From 62524510c409cf8f7f3a2baa8c63ed4c6f6472a2 Mon Sep 17 00:00:00 2001 From: Laurent Cremmer Date: Wed, 23 Sep 2015 16:54:52 +0100 Subject: [PATCH] Add In-Car Listening possible through MirrorLink using the MediaBrowserService --- AndroidManifest.xml | 10 + orig/repeat_inactive_service.svgz | Bin 0 -> 1905 bytes orig/shuffle_inactive_service.svgz | Bin 0 -> 2711 bytes res/drawable-hdpi/repeat_inactive_service.png | Bin 0 -> 2077 bytes .../shuffle_inactive_service.png | Bin 0 -> 2090 bytes res/drawable-mdpi/repeat_inactive_service.png | Bin 0 -> 1367 bytes .../shuffle_inactive_service.png | Bin 0 -> 1253 bytes .../repeat_inactive_service.png | Bin 0 -> 2843 bytes .../shuffle_inactive_service.png | Bin 0 -> 2671 bytes .../repeat_inactive_service.png | Bin 0 -> 4414 bytes .../shuffle_inactive_service.png | Bin 0 -> 4944 bytes .../MirrorLinkMediaBrowserService.java | 814 ++++++++++++++++++ .../android/vanilla/PlaybackService.java | 133 ++- 13 files changed, 925 insertions(+), 32 deletions(-) create mode 100644 orig/repeat_inactive_service.svgz create mode 100644 orig/shuffle_inactive_service.svgz create mode 100644 res/drawable-hdpi/repeat_inactive_service.png create mode 100644 res/drawable-hdpi/shuffle_inactive_service.png create mode 100644 res/drawable-mdpi/repeat_inactive_service.png create mode 100644 res/drawable-mdpi/shuffle_inactive_service.png create mode 100644 res/drawable-xhdpi/repeat_inactive_service.png create mode 100644 res/drawable-xhdpi/shuffle_inactive_service.png create mode 100644 res/drawable-xxhdpi/repeat_inactive_service.png create mode 100644 res/drawable-xxhdpi/shuffle_inactive_service.png create mode 100644 src/ch/blinkenlights/android/vanilla/MirrorLinkMediaBrowserService.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 37858054..0ae84b6e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -34,6 +34,7 @@ THE SOFTWARE. + @@ -132,6 +133,15 @@ THE SOFTWARE. + + + + + + + 4egpUA7_Tc;c!NqA5LYVy+vDa}ZgZ}tEF z`oV~Gt>LK3P(dra)hnw1@bs|x&M>t13?q&+?MV1R`&4~xQdDE@@xXb#wyfjvXp)|2 zQkGk9HN$v%c-S;A`Gcluptow)S-REx33bZ~3CvOp7r4X~Z!E`jEPaBf6Fe2#$qPiZI&*S`O5D^a#f>%4<@_Vb_+hL2 zu^b&R$q#%Sl5DGky-^(aay;l?k8xp}p4<p(@LdP0w^Rmc(%=L9=k03Cw6JEZpi0 z*RXxRlM1C`6HQS6I#)njUbp;reB!w3q`~@>#Y(oAXU-Nn&c~;kGH&cifjgDfftJ`R zP))5*Da5jsS3ocb{5JRj;?45hljm9x?YwQQ5@!S%b%D+WX}HlDY}Qr2sOFT_u(^r7 z$UG0sC*W=}yl(`m^Sc;<<3q^-Zg_qd2x3chWUgLxFa<7%?ONwACSa?BwF( zj)1cSIQINA3Fza+F{dd*&}*cqCOyn!*J6UT!fSO@9LVRSoWcyG`rLziH_~rt|DeHhz5moL-<*Q zbQrmQ5C%~Y#;zB;PH>ZXz2zfm4uD(|-7BSZCpq>CoN-+ENFvAU>~L2rpTm%qkbAWmiWLhx}q z3_H}|UT}?a+PZ&5w%o4Xbpo|{3x_ildrL^4Y@g+GuiO>ri&r7^3o>-4-+8(gOOD1I1 zR$P}xTM-UdQhmCb-ZCZtkj*f-EsuP6P%+*N&lcgX~w_`_BT!^B44FiEhCnvzQRM@5KvUEFMS&RUf$qocVc zP%w?%j-LKXW5-wIPS=}^Fr!BNa7#tM$`YMONnUX#!mK}9;~|n}3`dbN0-N0D+yjhW zDGZD8sLHb~nXws$t{ucv_7a|x!Xhtgv~x>giM$sH9rp_xQ>5PFM>?ij_c2Wi>y z?1c~r6UUzv%}&3H{Rra^jFvw$3^DTsa}EtwGm7J9QPiK#Vw$elmy=3d3S%A~i03&` zRQw>XyFae!=S%u-yf8^b&KFA&#aZ zV4y%%oDBI68nyK=&mYDbCQaABQTA1-cL0EP&=hR-u{agAcDjbvszm&WlyGZ_uIzhg zrJx)c88fihhe>EB7D^~t_$)T6Sy~b?$@;=cQG60glv=A3Dmzumo)p1i0YByteJZgx zf$#!9eTFtgdyfiKLhH>ikrsC>Rg85lMwe?s|8kadlfOBXSq#>D5#wJ2fne=TF~BB*a+mz%$J z(Gl;=fhYy1sr3u48O_>MBoh77_unG;8M;zNcI~2IhW}^@%=lN5|3f)A!5fec)&P*csAcKD&mLVjgZ1XjmGN+8~i42$0hwG~X*5zT$u2nt^x`5U&E; zOajq~znyLielDYN4JIq`1nS7FY$JIA&^h5R!osU3&ioG z5!fquCat61K?__Z@BYGui#G>(mWRJHFxK$)-D7MoZp9{$hvQ6YVo4rY|MEnl4LbF$is80pbUJX!fKmnwK1uz@@ z;)`jvkP7#8oY-+Bq&h1Y0z7d%IdG(0LlHc{Lu~*GR1b9>DYAnkVgSz|&sjN_R*tep zt$;B)pwS!Zb#NmB>%{{k*G#-9&^r4Sr}$>lMVvAE2D}$suVM7g0CWV*=)s0u#_M(1 z$WW)544pgqJ#{e> r$3~l06~ql?2DIFe@mhvO+cv!u$-688|2Co;d3yL0Nm8jQf)@Y)4$7b9 literal 0 HcmV?d00001 diff --git a/orig/shuffle_inactive_service.svgz b/orig/shuffle_inactive_service.svgz new file mode 100644 index 0000000000000000000000000000000000000000..f4106bd3a967ed8e8293448318f3b5a651b73a36 GIT binary patch literal 2711 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 MO9u;EEa^?Tk6{&K zN-iU!OjGi*+-mN(hW76L3*PfQ=X{>eIiKg8^V=uK5o0SU4iN_c0Fr1sq_Y6+{!KAa zL3QIc*$M!JN1$EB1d%8flr7)~qwGBK0D!`qe^W>?$^jv09wwkX2rjq~LY#k0Fd!~2 zPCqm<93SW(6|9eoxlCF*4FLcSRHBh^*Z4==k_fB|>u?W$2w!(3%axFKby!bW1bHt$ z=N`h@DucD2R% zqi$c?%sBCGBPN)-Xsn%Q7QJLXWF^zeK+`BQpuDB!++Z@EJoklfX?22+M~ANz#{+>p68 z^I`fGC)Tb0P*D>DC$g3>;kVu0t-3dbjhL0ziXirH+lH;TRgE&+eBivhrSDp_Sc;wD z(DMur(Ow|n6PS@r+tS4wGsZMTRqxu{aAhbC?rv?6Ju@M>21duLR?WS$cM$B*ZmzilsQlq zfBc69=Aydat7&$8{Wx#;?Cl3<$nQ4~4+b<@=5nh}B=nier9LJ@cSW)py%*tY?Pb45 zO0YTjZ-0-!DdAiYg~6!Y(jKPwNWMX9pv*(sP{k7*R-1m>t)>y^ge#jKO;r&8z?;mx zD?j?Z)=Wu%#b;GC&t}rWQgo1@1gxtY7n2d*zvreJEaUk>6!JnVn~$ zNa)VhxWLeQm$K_@n2CLuv}`|W-XUPm35-h+sc<*!9;8Y2-~W{LM7puuH>cCW)XPz2 zlJ>KU8awesI&EG|Z0^ts!8b6p$90n)&*91lL5+AGX{S$owmAwmGJ~`ZKC>I{ZdWq3!3s{-&BqTF@0ad<-i(8&-|IX;Rbtws~@&@npNYvDw&8Beu8{ zZ_EtUs1e$#EGn3e1^CGk2%3~k+l`_)`eCy|t|=yH^17q?`~|Ur*C1WeM{w64!ulDi zI!B7pH7}J8(4v!Nh*Ri&r%tlyu>x*%km*8tN_vUOkFgn_+%to zdNT2urA~Fd8p_P7*Wo#ZA_Y@N11*iH4ZWJ5%4SMB; z1S?Q8o!+#fhNNRnSs4FH4M`)O4oFdEA34P8XIC;wMwy?n_PaSXNhW`SCe{tDO;QgX zZ1`fGJTAZ0afi(`)KGqwWRt#ut;|`xH6KE0*UHcaFk0JI)0&a-`3#F{VT%0clE&LO z?frJSS7bO+T~B9g8+ZPcjScCO{kG6dkJW3C>xq-~P5BOA;rxzwxj$Ic9tg5QBH&up z9|1#*imL;>@vI$`nks3lcHv#I(dcBRB=P(A|4T`kaI^|E`E?JEs#L9}pXGEh{2%ureCK!A>~5gR?@;FCGEQAJjhRaOixx5B)eW=n8QJ@6+{<-Z7#50uOHwOeKvXbF5BfJR}EbqK$-e*n(1?9TuI literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/repeat_inactive_service.png b/res/drawable-mdpi/repeat_inactive_service.png new file mode 100644 index 0000000000000000000000000000000000000000..15eab58cb479350b028f2539596297aa09c65c5d GIT binary patch literal 1367 zcmV-d1*rOoP)`C(1mp&EiR<= zLnto#L05tcsS6b;hBPKqwNNM&EQW$HR+BuPOpIdD2$o9KWJu=CyZ3aVBQedJWSWVC zF#p}XoO{my_qcQKxkJp1=UF1(24<=PK)zJ~$hQgr`BniS-+Jx=jOhl3TCc1YhFBv3 zOAt+aT{hCpt^#7()m`@1+6&WJLsRua)g8SRr7*uUh?XhvS*`gB18zq)ewtxeuE!ykk3;pmwA zO5e87U9Z@T0FBYuj}mm{eA9~0XT|9hS{v>EQh{r`o2<=VF`j!l7`3grFzOSo-ZFkZ z9JMWWg+G@@(;`DB`2bK#t||+dmFG4s zugtCWr}g@&_^oiOUDdZWbR7T~+)FINUh$syfB^XbJo)-$u&IDVPTfu-1tiE|bKI2k z*vq8Kc#%!qCK|K<{G3>0s;!#UP5dmL&)L>^?puihK)#h1fZFz++NzeT@8=>{RscjZ zFSzd1!0ttXIoPH#^D z4U9;jBt1@Q|8NO&`K!_E3;qi50}MVLKe*_U%O2fo{mK3fa}68?;rPAV!_DPON_R`5 zs`Y2j25vr%Sqi}Ur}9Mzlqa=+IK7sD5(SK84d4*%dm8|^>iG8R9)cc&6TTlH4R-BG6F4g)tGZ~{v_uamCLbm`Q zeA3=fB#rHo0L70QH~|8P1A?fYnJXQIuP$r*8?S?5+5l-a9=>QM=lLztA_NH^0LaH5 zRzc&zD7yaKQFHY_1pumVkL~qI+RIve)YTkpU4Le9k12WMm{KPa>QTq3paBJkg|_|o zlCka&8dB5oHEn>Is;(ops@M=GicNXm6g=oC%==|y^`4pLeP*phPqX#MNFdoT>MAFt z=!=Atn>XpK{6XcVo((G#0Ct8?_WzvHswNjXpV=0gEfw17e5(MEZxsOYtpY&4RRGAh Z{slF9*ys|Id3)PSRAgI@Zc41;t(xlV)pLyE0*! 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 literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/shuffle_inactive_service.png b/res/drawable-xhdpi/shuffle_inactive_service.png new file mode 100644 index 0000000000000000000000000000000000000000..920a27817472f72476577ad89103287339ebec00 GIT binary patch literal 2671 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 It6PWtA711y;{X5v literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/shuffle_inactive_service.png b/res/drawable-xxhdpi/shuffle_inactive_service.png new file mode 100644 index 0000000000000000000000000000000000000000..59b9e5bb03516a59741012a5bd0353dfbcb0c521 GIT binary patch literal 4944 zcmc&&hf~u{6aFOx2oWSy#h`QpLPP;UIwFt&QVhKasFYBo*B2=wy-5|MMym9P6e*#p zNRbYq2?Elk2~s4KFa8zZ&Ft;mGq<~UGq-!sY?Q7xngMnN1^@tqIz|Oc(Fy+qN<*1f z)D}Kc6vRVWT^~vr&!N_llr^0z#>4{vgu?#`hhECuB7R5kY0bFuaGdF*Zj`1ttT zw0Cy&c>37Y=BA6gUFMeD6#!uFQ&+jC@0+!m9pGwU6Zp0D$4XYH6kAbU%a_#$=O>xTnyayvj`m6BR^EJ zy2T%(Q4b17Ao#fN3&sZ1zr6((3+;e*!D0UoKv3&DS?zTFx69~&M*Y>tx4{}Le5rSM zdh0H#)k#&@7^J2;`zL*Y3TjQjF27BYSN`3gtUYU{%SJ@bND91U(#_~HgqcKl)kQjK z_Fc945NNBl*W)?uMZ63ujB-MBVO3u9I#F8-#ee9o5O*}S$o|l@zw9|ZZW$Xq9Lhj# z#w1CX$(k8j5h)UOXVa8Rbe0LFULSYbP6;STZ441nHI6>VB~}^JLkh1?)XC_Y&oq0h zhWilWDCS^1W*ch$y~%etxhE-t&O#T$YGoEZkp2F`0lS?m?esq%g>9i8+ox_@-W6HO z%CktYn_H#vyIhNrl*Fq}e0YiEbfndJ-pRlL*Q;-|1oqJT;Al?tL$jFDOv^ZHW7^CT zjF;>Aued#Z$>qtNsX*!_p?cZB4oZjl4|r;DIV|%gsNIIbnE?cs(2;Uu;B}$5t~#I6 z52`+FRe$b#k4nuCl@5+I+{uxgWg@&pbooU;A3QH{-_BD}6Y34ad1bS|FuQAcs8)K^?!Yi6!F2uHiq zLMpc}uKiH^WO#+jf`bd3g&o6|3Td)h)4hm1uK@;!zJi(?Y4-QyqiocdS|WH6O=Niw z@A}#2eW^hpYX$(f?XrlA>ro#*Iys1j`4HA8xG0gkrx8+98qWwPkA+VapX@}W%vnC5 ze?-k@K8~2(pEa2BcT>4vTGcg{rfu}?$box3skOqd(Pph!(EP!I_i@Kg?eJ3Xy{h}1 zhk;*fUl3XKubEE~gTb~Wi7Ngh-r)D(-&<@O#jn4v-`G_|L^cZC*MR6B8G7ayer&%q zpn!N6cgM1Eq+Rj&99d)YTamd&!j+a<8gN%ygt|JO?6=eMxA~#oasK`#EJ_NnQ}Y+| zO2>|*)1u{zBgLw&WUEpQ3M>Y(?Pw;n^1HH+w%!{wKMW2J_txJ;_wv;SOx_|k{+$(B z@B*!TUWYc!DJyz8O)g3$mlzpSZl0&46ipaBtVfyH8mMl&qW0nVG>|#BlJ`!|xrA(q zYRfo5F32dTSEStGBcmZnEm}_P7A}=iqgq%Oidi?@ogIGYozv=xGC0LGR_B%W9E+6A z(ejVqw{rCL@=%eqtQ#9aF)jtlPe^`FW(M`Wi+8ShCpj^@XtH#6ZHoM&z_S-w|1p0m z4QE9aOs8bdBIs+h#`J2`Aa=XfM7Y%1FJv$ih`-oBBy5Wi`8!VcOpB|aj8SZ5a~X?r zxS-PM`1@Ebel^91pm&|ytb~DG4~Y*BDa9RNGy~F7YJpBrpk+*?sW&odBp4t#IoABG+7lHOGN-^?W$E_k8=odj0h6}*CU)jffGR@i z_>TJBbWW3urhw)C)na&U=I2drHRcIZ(~)ZE+*9t9AElOWyTl{XmQKwww6+rQbW{Y> z!P{v5kRCn5KYk|Cp&5h+J)7jA;S;{8p${s9{9qiDCY2-Ph3{gDp`f(+>-Gh|rUrc~ zB&6C@)2U}cB3OEeEjcJX`iwDw7i7SYqQ?7nZBV>8^gE662<;r=Zg+MIQztssKwvO zho{mIt~vef`zYwfIreuxzt6_P+fs~dz zZoCiQ?uFvSSfcP!iK~iU!!n8KNeT)1cYr0u?IZm4YXw`Asi1dOG|~rwybyp#>5^T! zkWoxoW+uiS_*h)Y$*Z4tr92mFQc%lK|l0PrfSdm|--v7z2)FTifWlYk1oa*CYnEE7j^)?|cJ0 zx*2izD=w(~Ne9!8f(Yn}D&Y)gV$s(&iubqGmr7%fa}g3uT~DY%J=It zDN$Vv?ul|f&Mp!11FuPr%1q80b^5bY_e%2*36Q=>y^dV>O%%xR(-$*n`~9bNwFP!|K2MaoP!8Sm^|o;aUn4O zSa)fChm4+e$U>WLhg_bVH~M)I=YRS>I%>$e=Ycsw;xKzv-Xmtzv2rJKCt}5PF+?{< zCZ{Og-G#p4&FW9hpu>)#-4i7Bt`xJQ$q{;_NM$@i$mhSlZsp;rrB@`N-G%#j=;>18 zQg;;EXYCj2=#a#wF})8C6i@dYOE~p>m|pqN(2`ynM9nGOd;T7Jrr@povBhe-yTiKY zM!EAxGsiLZdq(x$cdL*Z|=cY_x9&y(dIx7u>VwnV?q zO33?`%gkEFI$5}HW|sPJ6!l+!05{#w2*^DrE(uPE0v|sGWd$thqE0@_9qM16=422Q zu*gLm2_MaDPgK(z8IIXt&<#@I8!S-3nyQ8-Ck}xW5iNJV5y- zPh#oi^QY;!iKf~#U5Iyo`>2;MB2DF*V#2-r@|MfHU@#Er1ZwV%_D5}#=pXbK7G zohaXouFJV9m^@XZ*2Hcp_g|}b!A`!7AIIdwt+>ZL__DMy{~cO6h{lD|JgNN^1#A_) zZGw!RHIP7Y!C6a2xPSRy1NA=4p0({8Py{psVgEv^oyD(f4v^5@Y0tD>2^XG&8Z z4mRzMR3;XUm)Ui=f#8mYIf1a&nU$ra0G;pbIeKeTV%cl-2vAg)!TA!K4f#Vi5wH4$ z#{l~LHZFW-j-C%R^xHB%q;u6mlm;J~4Ag*vi~jume5I0N!^FHd81qh)+-Pwj{?m_1 zjrqU#BsRlsC!kPk^w-HG528680Kd=*wI!%qJB1QF#hoB3+;!dY3umQOzou{QvI3U? zmAhWYTcjqM;T-Z0C-ZEw+NertE7OkRi8%o8T8hb6`?J>TV8J3S`q_V068GLS(?UK$ zauUX}AtG%|APW7YvOnk~z!RoP^DToNRK@Jt&7@qi%CB)y(C?}7T^wk;QUZ>4=c`Gai!xE8R0FwP&jwYEH)QJQ2`DLn+ol{NIJ zlny-jmv^)sX3aNoP1n*w>-7y=xMX>J>u`3SQ%Vy{=;Bwh?&hx-NJ)>bV*@%}&zo{% zfpRXdZ-QlME{!j)8Hf6y#~0p0nV(+1mNY>AhgVjl61nT1B_=Q=MNk)}p`YEHz1U1(QA zeV31Vx{A9~j4W^08YEueai;??v&l=Q=wbsY%fsCXpb$QdwB;SMgNu8kk%0!T(*qbk z8Fo6uo%N{dleCT${~D2PI7E|s=efgYW9nw<0Cca}NSQ)+>lw2_F{ZXHj-%u({D<+~ z)@st(jq@#aLX}q7yD*3m=!sw1EdZEy{##)w|K4Y|xs1p@s`IV0qrX5NO9XR+L-fH9 z!KS=<5Q1PHa04p>Vgtn}3xx7o7x3;ux@d3ZWd`hmcedAicn26g1D=>=QolmBtawcj zEBb6uw;P|MMr`nE$+8xL?6>PKfXyBsRc@5HYUYtl2)*Sl#@T z$|~?x`W%eoO3AG3xU?x+6bCM*cH$nwzc{~JC$c-gKOwbWQM4`O9+Lli#u|Lv6SfT& z0!hf&&wZ6@}MiK(T5K zj**&jLERSzOc2Wv$XWeEH=7g`^)24mYoe2zZqEcJ7LUCKoe?;{BU%eG#7!BH#V# z@t`0x>1xH7jtxGD`{`Nt9dMBLT+%MD!4hufY^XdNrBg6IoFh%RyPW_oVtU?J&jCRK zcN1EP_9#7xm-35zV@NR%JIw|`BID<50GKxbmt91~orJdW0+)!w(pN&#>_|1urj+mW z!Li5|2TO@U7^_h>hiqBn5}H*X#JTzENejFy!5I> z(WL1L7bbWAjiv^5N<3!&i=uRffjLV6%|Y$RmECdEPzyO+)UdO#eAcbSD&gGGybm{1 zaYI!UY6)hF!i~!qH|PJ|T)Wf#R+>n#+TbSK|<@fe*WWHSI(OFTW(PFzovPF&aH;|Aa_j%4yY4;Tex z^qL|C+R>yL|FgG`sJ5TNM*`7Y{jxN3I&`9&^%oR3>pcrjy}xLZ@T&n#{L5rb40+RU ztKDL1|DxRulBe&YIC9g6a>NTVqARQXX$pCxKITRDZlE$&RD2C%o;%wg?scMJEf@n< z1`vcTkrDJ9I!6xkztUL~knNRLIKw%OmFE5Cruxbd2)!|JWppSjagTM4;w#XTiL1oo zYm51rd;HQHrr^DBOI`1LF@5$FhD<>3O>~sL3kFuCd@Zs z&OO^dUhG_B^;1pBK2OWh_q5WB$;I%fn z7sH(fn>lq7wz+;aJDAef7W6c>*QemHkx&lh#}I317*hfdtC~`{?q$&b1vpXs`Z6Gw Ud*gJ4()|L|Rkc+rm90Yl2bGR2y#N3J literal 0 HcmV?d00001 diff --git a/src/ch/blinkenlights/android/vanilla/MirrorLinkMediaBrowserService.java b/src/ch/blinkenlights/android/vanilla/MirrorLinkMediaBrowserService.java new file mode 100644 index 00000000..188a57ca --- /dev/null +++ b/src/ch/blinkenlights/android/vanilla/MirrorLinkMediaBrowserService.java @@ -0,0 +1,814 @@ +/* + * Copyright (C) 2013 - 2015 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.vanilla; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.provider.MediaStore; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.browse.MediaBrowser; +import android.media.browse.MediaBrowser.MediaItem; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.service.media.MediaBrowserService; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Message; +import android.os.Looper; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.os.SystemClock; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Handles Music Playback through MirrorLink(tm) by implementing a MediaBrowserService. + */ +public class MirrorLinkMediaBrowserService extends MediaBrowserService implements Handler.Callback { + + private static final String TAG = "MirrorLinkMediaBrowserService"; + // Action to change the repeat mode + private static final String CUSTOM_ACTION_REPEAT = "ch.blinkenlights.android.vanilla.REPEAT"; + // Action to change the repeat mode + private static final String CUSTOM_ACTION_SHUFFLE = "ch.blinkenlights.android.vanilla.SHUFFLE"; + + // Media managers + private MediaAdapter mArtistAdapter; + private MediaAdapter mAlbumAdapter; + private MediaAdapter mSongAdapter; + private MediaAdapter mPlaylistAdapter; + private MediaAdapter mGenreAdapter; + private MediaAdapter[] mMediaAdapters = new MediaAdapter[MediaUtils.TYPE_GENRE + 1]; + private List mAlbums = new ArrayList(); + private List mArtists = new ArrayList(); + private List mSongs = new ArrayList(); + private List mPlaylists = new ArrayList(); + private List mGenres = new ArrayList(); + private List mFiltered = new ArrayList(); + private boolean mCatalogReady = false; + + private final List mMediaRoot = new ArrayList(); + + // Media Session + private MediaSession mSession; + private Bundle mSessionExtras; + + // Indicates whether the service was started. + private boolean mServiceStarted; + + private Looper mLooper; + private Handler mHandler; + + @Override + public void onCreate() { + Log.d("VanillaMusic", "MediaBrowserService#onCreate"); + super.onCreate(); + + HandlerThread thread = new HandlerThread("MediaBrowserService", Process.THREAD_PRIORITY_DEFAULT); + thread.start(); + + // Prep the Media Adapters (caches the top categories) + mArtistAdapter = new MediaAdapter(this, MediaUtils.TYPE_ARTIST, null ,null, null); + mAlbumAdapter = new MediaAdapter(this, MediaUtils.TYPE_ALBUM, null, null, null); + mSongAdapter = new MediaAdapter(this, MediaUtils.TYPE_SONG, null, null, null); + mPlaylistAdapter = new MediaAdapter(this, MediaUtils.TYPE_PLAYLIST, null, null, null); + mGenreAdapter = new MediaAdapter(this, MediaUtils.TYPE_GENRE, null, null, null); + mMediaAdapters[MediaUtils.TYPE_ARTIST] = mArtistAdapter; + mMediaAdapters[MediaUtils.TYPE_ALBUM] = mAlbumAdapter; + mMediaAdapters[MediaUtils.TYPE_SONG] = mSongAdapter; + mMediaAdapters[MediaUtils.TYPE_PLAYLIST] = mPlaylistAdapter; + mMediaAdapters[MediaUtils.TYPE_GENRE] = mGenreAdapter; + + // Fill and cache the top queries + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_ARTIST)) + .setTitle(getString(R.string.artists)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.artists)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_ALBUM)) + .setTitle(getString(R.string.albums)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.albums)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_SONG)) + .setTitle(getString(R.string.songs)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.songs)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_GENRE)) + .setTitle(getString(R.string.genres)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.genres)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_PLAYLIST)) + .setTitle(getString(R.string.playlists)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.playlists)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + + // Start a new MediaSession + mSession = new MediaSession(this, "VanillaMediaBrowserService"); + setSessionToken(mSession.getSessionToken()); + mSession.setCallback(new MediaSessionCallback()); + mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mSessionExtras = new Bundle(); + mSession.setExtras(mSessionExtras); + + // Register with the PlaybackService + PlaybackService.registerService(this); + + // Make sure the PlaybackService is running + if(!PlaybackService.hasInstance()) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + PlaybackService.get(MirrorLinkMediaBrowserService.this); + } + }); + t.start(); + } + + mLooper = thread.getLooper(); + mHandler = new Handler(mLooper, this); + + updatePlaybackState(null); + } + + @Override + public int onStartCommand(Intent startIntent, int flags, int startId) { + Log.d("VanillaMusic", "MediaBrowserService#onStartCommand"); + return START_STICKY; + } + + @Override + public void onDestroy() { + Log.d("VanillaMusic", "MediaBrowserService#onDestroy"); + mServiceStarted = false; + PlaybackService.unregisterService(); + mSession.release(); + } + + /** + * Helper class to encode/decode item references + * derived from queries in a string + */ + private static class MediaID { + // Separators used to build MediaIDs for the MediaBrowserService + public static final String ID_TYPE_ROOT = Integer.toString(MediaUtils.TYPE_INVALID); + public static final String MEDIATYPE_SEPARATOR = "/"; + public static final String FILTER_SEPARATOR = "#"; + + final int mType; + final long mId; + final String mLabel; + + public MediaID(int type, long id, String label) { + mType = type; + mId = id; + mLabel = label; + } + + public MediaID(String mediaId) { + int type = MediaUtils.TYPE_INVALID; + long id = -1; + String label = null; + if(mediaId != null) { + String[] items = mediaId.split(MEDIATYPE_SEPARATOR); + type = items.length > 0 ? Integer.parseInt(items[0]) : MediaUtils.TYPE_INVALID; + if(items.length > 1) { + items = items[1].split(FILTER_SEPARATOR); + if(items.length >= 2) { + label = items[1]; + id = Long.parseLong(items[0]); + } + } + } + mType = type; + mId = id; + mLabel = label; + } + + public boolean isTopAdapter() { + return mId == -1; + } + + public boolean isInvalid() { + return mType == MediaUtils.TYPE_INVALID; + } + + @Override + public String toString() { + return toString(mType, mId, mLabel); + } + + public static boolean isTopAdapter(String mediaId) { + return mediaId.indexOf(MEDIATYPE_SEPARATOR) == -1; + } + + public static String toString(int type, long id, String label) { + return Integer.toString(type) + + (id == -1 ? "" : ( + MEDIATYPE_SEPARATOR + + id + + (label == null ? "" : + FILTER_SEPARATOR + + label + ) + ) + ); + } + } + + private static Limiter buildLimiterFromMediaID(MediaID parent) { + Limiter limiter = null; + String[] fields; + Object data; + if(!parent.isInvalid() && !parent.isTopAdapter()) { + switch(parent.mType) { + case MediaUtils.TYPE_ARTIST: + // expand using a album query limited by artist + fields = new String[] { parent.mLabel }; + data = String.format("%s=%d", MediaStore.Audio.Media.ARTIST_ID, parent.mId); + limiter = new Limiter(MediaUtils.TYPE_ARTIST, fields, data); + break; + case MediaUtils.TYPE_ALBUM: + // expand using a song query limited by album + fields = new String[] { parent.mLabel }; + data = String.format("%s=%d", MediaStore.Audio.Media.ALBUM_ID, parent.mId); + limiter = new Limiter(MediaUtils.TYPE_SONG, fields, data); + break; + case MediaUtils.TYPE_GENRE: + // expand using an artist limiter by genere + fields = new String[] { parent.mLabel }; + data = parent.mId; + limiter = new Limiter(MediaUtils.TYPE_GENRE, fields, data); + break; + case MediaUtils.TYPE_PLAYLIST: + // don't build much, a a playlist is playable but not expandable + case MediaUtils.TYPE_SONG: + // don't build much, a song is playable but not expandable + case MediaUtils.TYPE_INVALID: + break; + } + } + return limiter; + } + + private QueryTask buildQueryFromMediaID(MediaID parent, boolean empty, boolean all) + { + String[] projection; + + if (parent.mType == MediaUtils.TYPE_PLAYLIST) { + projection = empty ? Song.EMPTY_PLAYLIST_PROJECTION : Song.FILLED_PLAYLIST_PROJECTION; + } else { + projection = empty ? Song.EMPTY_PROJECTION : Song.FILLED_PROJECTION; + } + + QueryTask query; + if (all && (parent.mType != MediaUtils.TYPE_PLAYLIST)) { + query = (mMediaAdapters[parent.mType]).buildSongQuery(projection); + query.data = parent.mId; + query.mode = SongTimeline.MODE_PLAY_ID_FIRST; + } else { + query = MediaUtils.buildQuery(parent.mType, parent.mId, projection, null); + query.mode = SongTimeline.MODE_PLAY; + } + + return query; + } + + private void loadChildrenAsync( final MediaID parent, + final Result> result) { + + // Asynchronously load the music catalog in a separate thread + final Limiter limiter = buildLimiterFromMediaID(parent); + new AsyncTask() { + private static final int ASYNCTASK_SUCCEEDED = 1; + private static final int ASYNCTASK_FAILED = 0; + + @Override + protected Integer doInBackground(Void... params) { + int result = ASYNCTASK_FAILED; + try { + if(!mCatalogReady) { + runQuery(mArtists, MediaUtils.TYPE_ARTIST , mArtistAdapter); + runQuery(mAlbums, MediaUtils.TYPE_ALBUM, mAlbumAdapter); + runQuery(mSongs, MediaUtils.TYPE_SONG, mSongAdapter); + runQuery(mGenres, MediaUtils.TYPE_GENRE, mGenreAdapter); + runQuery(mPlaylists, MediaUtils.TYPE_PLAYLIST, mPlaylistAdapter); + mCatalogReady = true; + } + if(limiter != null) { + mFiltered.clear(); + switch(limiter.type) { + case MediaUtils.TYPE_ALBUM: + mSongAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_SONG, mSongAdapter); + break; + case MediaUtils.TYPE_ARTIST: + mAlbumAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_ALBUM, mAlbumAdapter); + break; + case MediaUtils.TYPE_SONG: + mSongAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_SONG, mSongAdapter); + break; + case MediaUtils.TYPE_PLAYLIST: + mPlaylistAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_PLAYLIST, mPlaylistAdapter); + break; + case MediaUtils.TYPE_GENRE: + mSongAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_SONG, mSongAdapter); + break; + } + } + result = ASYNCTASK_SUCCEEDED; + } catch (Exception e) { + Log.d("VanillaMusic","Failed retrieving Media"); + } + return Integer.valueOf(result); + } + + @Override + protected void onPostExecute(Integer current) { + List items = null; + if (result != null) { + if(parent.isTopAdapter()) { + switch(parent.mType) { + case MediaUtils.TYPE_ALBUM: + items = mAlbums; + mAlbumAdapter.setLimiter(null); + break; + case MediaUtils.TYPE_ARTIST: + items = mArtists; + mArtistAdapter.setLimiter(null); + break; + case MediaUtils.TYPE_SONG: + items = mSongs; + mSongAdapter.setLimiter(null); + break; + case MediaUtils.TYPE_PLAYLIST: + items = mPlaylists; + mPlaylistAdapter.setLimiter(null); + break; + case MediaUtils.TYPE_GENRE: + items = mGenres; + mGenreAdapter.setLimiter(null); + break; + } + } else { + items = mFiltered; + } + if (current == ASYNCTASK_SUCCEEDED) { + result.sendResult(items); + } else { + result.sendResult(Collections.emptyList()); + } + } + } + }.execute(); + } + + private Uri getArtUri(int mediaType, String id) { + switch(mediaType) { + case MediaUtils.TYPE_SONG: + return Uri.parse("content://media/external/audio/media/" + id + "/albumart"); + case MediaUtils.TYPE_ALBUM: + return Uri.parse("content://media/external/audio/albumart/" + id); + } + return Uri.parse("android.resource://ch.blinkenlights.android.vanilla/drawable/fallback_cover"); + } + + private String subtitleForMediaType(int mediaType) { + switch(mediaType) { + case MediaUtils.TYPE_ARTIST: + return getString(R.string.artists); + case MediaUtils.TYPE_SONG: + return getString(R.string.songs); + case MediaUtils.TYPE_PLAYLIST: + return getString(R.string.playlists); + case MediaUtils.TYPE_GENRE: + return getString(R.string.genres); + case MediaUtils.TYPE_ALBUM: + return getString(R.string.albums); + } + return ""; + } + + private void runQuery(List populateMe, int mediaType, MediaAdapter adapter) { + populateMe.clear(); + try { + Cursor cursor = adapter.query(); + + if (cursor == null) { + return; + } + + final int flags = (mediaType != MediaUtils.TYPE_SONG) + && (mediaType != MediaUtils.TYPE_PLAYLIST) ? + MediaBrowser.MediaItem.FLAG_BROWSABLE : MediaBrowser.MediaItem.FLAG_PLAYABLE; + + final int count = cursor.getCount(); + + for (int j = 0; j != count; ++j) { + cursor.moveToPosition(j); + final String id = cursor.getString(0); + final String label = cursor.getString(2); + MediaBrowser.MediaItem item = new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(MediaID.toString(mediaType, Long.parseLong(id), label)) + .setTitle(label) + .setSubtitle(subtitleForMediaType(mediaType)) + .setIconUri(getArtUri(mediaType, id)) + .build(), + flags); + populateMe.add(item); + } + + cursor.close(); + } catch (Exception e) { + Log.d("VanillaMusic","Failed retrieving Media"); + } + } + + /* + ** MediaBrowserService APIs + */ + + @Override + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + return new BrowserRoot(MediaID.ID_TYPE_ROOT, null); + } + + @Override + public void onLoadChildren(final String parentMediaId, final Result> result) { + // Use result.detach to allow calling result.sendResult from another thread: + result.detach(); + if (!MediaID.ID_TYPE_ROOT.equals(parentMediaId)) { + loadChildrenAsync(new MediaID(parentMediaId), result); + } else { + result.sendResult(mMediaRoot); + } + } + + private void setSessionActive() { + if (!mServiceStarted) { + // The MirrorLinkMediaBrowserService needs to keep running even after the calling MediaBrowser + // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer + // need to play media. + startService(new Intent(getApplicationContext(), MirrorLinkMediaBrowserService.class)); + mServiceStarted = true; + } + + if (!mSession.isActive()) { + mSession.setActive(true); + } + } + + private void setSessionInactive() { + if(mServiceStarted) { + // service is no longer necessary. Will be started again if needed. + MirrorLinkMediaBrowserService.this.stopSelf(); + mServiceStarted = false; + } + + if(mSession.isActive()) { + mSession.setActive(false); + } + } + + private static final int MSG_PLAY = 1; + private static final int MSG_PLAY_QUERY = 2; + private static final int MSG_PAUSE = 3; + private static final int MSG_STOP = 4; + private static final int MSG_SEEKTO = 5; + private static final int MSG_NEXTSONG = 6; + private static final int MSG_PREVSONG = 7; + private static final int MSG_SEEKFW = 8; + private static final int MSG_SEEKBW = 9; + private static final int MSG_REPEAT = 10; + private static final int MSG_SHUFFLE = 11; + private static final int MSG_UPDATE_STATE = 12; + + @Override + public boolean handleMessage(Message message) + { + switch (message.what) { + case MSG_PLAY: + setSessionActive(); + + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).play(); + } + break; + case MSG_PLAY_QUERY: + setSessionActive(); + if(PlaybackService.hasInstance()) { + QueryTask query = buildQueryFromMediaID(new MediaID((String)message.obj), false, true); + PlaybackService.get(MirrorLinkMediaBrowserService.this).addSongs(query); + } + break; + case MSG_PAUSE: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).pause(); + } + break; + case MSG_STOP: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).pause(); + } + setSessionInactive(); + break; + case MSG_SEEKTO: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).seekToProgress(message.arg1); + } + break; + case MSG_NEXTSONG: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.NextSong, null); + } + break; + case MSG_PREVSONG: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.SeekForward, null); + } + break; + case MSG_SEEKFW: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.SeekBackward, null); + } + break; + case MSG_SEEKBW: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.Repeat, null); + } + break; + case MSG_REPEAT: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.Shuffle, null); + } + break; + case MSG_SHUFFLE: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.NextSong, null); + } + break; + case MSG_UPDATE_STATE: + updatePlaybackState((String)message.obj); + break; + default: + return false; + } + + return true; + } + /* + ** MediaSession.Callback + */ + private final class MediaSessionCallback extends MediaSession.Callback { + + @Override + public void onPlay() { + mHandler.sendEmptyMessage(MSG_PLAY); + } + + @Override + public void onSeekTo(long position) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_SEEKTO, (int) position ,0)); + } + + @Override + public void onPlayFromMediaId(final String mediaId, Bundle extras) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_PLAY_QUERY, mediaId)); + } + + @Override + public void onPause() { + mHandler.sendEmptyMessage(MSG_PAUSE); + } + + @Override + public void onStop() { + mHandler.sendEmptyMessage(MSG_STOP); + } + + @Override + public void onSkipToNext() { + mHandler.sendEmptyMessage(MSG_NEXTSONG); + } + + @Override + public void onSkipToPrevious() { + mHandler.sendEmptyMessage(MSG_PREVSONG); + } + + @Override + public void onFastForward() { + mHandler.sendEmptyMessage(MSG_SEEKFW); + } + + @Override + public void onRewind() { + mHandler.sendEmptyMessage(MSG_SEEKBW); + } + + @Override + public void onCustomAction(String action, Bundle extras) { + if (CUSTOM_ACTION_REPEAT.equals(action)) { + mHandler.sendEmptyMessage(MSG_REPEAT); + } else if (CUSTOM_ACTION_SHUFFLE.equals(action)) { + mHandler.sendEmptyMessage(MSG_SHUFFLE); + } + } + } + + /** + * Update the current media player state, optionally showing an error message. + * + * @param error if not null, error message to present to the user. + */ + private void updatePlaybackState(String error) { + long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN; + int state = PlaybackState.STATE_PAUSED; + + if(PlaybackService.hasInstance()) { + if (PlaybackService.get(this).isPlaying()) { + state = PlaybackState.STATE_PLAYING; + } + position = PlaybackService.get(this).getPosition(); + } + + PlaybackState.Builder stateBuilder = new PlaybackState.Builder() + .setActions(getAvailableActions()); + + setCustomAction(stateBuilder); + + // If there is an error message, send it to the playback state: + if (error != null) { + // Error states are really only supposed to be used for errors that cause playback to + // stop unexpectedly and persist until the user takes action to fix it. + stateBuilder.setErrorMessage(error); + state = PlaybackState.STATE_ERROR; + } + stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime()); + mSession.setPlaybackState(stateBuilder.build()); + + } + // 'DriveSafe' icons need to meet contrast requirement, and as such are usually + // monochrome in nature, hence the new repeat_inactive_service and shuffle_inactive_service + // artwork + + private static final int[] FINISH_ICONS = + { R.drawable.repeat_inactive_service + , R.drawable.repeat_active + , R.drawable.repeat_current_active + , R.drawable.stop_current_active + , R.drawable.random_active }; + + private static final int[] SHUFFLE_ICONS = + { R.drawable.shuffle_inactive_service + , R.drawable.shuffle_active + , R.drawable.shuffle_active + , R.drawable.shuffle_album_active }; + + private void setCustomAction(PlaybackState.Builder stateBuilder) { + if(PlaybackService.hasInstance()) { + Bundle customActionExtras = new Bundle(); + final int finishMode = PlaybackService.finishAction(PlaybackService.get(this).getState()); + final int shuffleMode = PlaybackService.shuffleMode(PlaybackService.get(this).getState()); + + stateBuilder.addCustomAction(new PlaybackState.CustomAction.Builder( + CUSTOM_ACTION_REPEAT, getString(R.string.cycle_repeat_mode), FINISH_ICONS[finishMode]) + .setExtras(customActionExtras) + .build()); + + stateBuilder.addCustomAction(new PlaybackState.CustomAction.Builder( + CUSTOM_ACTION_SHUFFLE, getString(R.string.cycle_shuffle_mode), SHUFFLE_ICONS[shuffleMode]) + .setExtras(customActionExtras) + .build()); + } + } + + private long getAvailableActions() { + long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackState.ACTION_SKIP_TO_PREVIOUS | PlaybackState.ACTION_SKIP_TO_NEXT; + + if(PlaybackService.hasInstance()) { + if (PlaybackService.get(this).isPlaying()) { + actions |= PlaybackState.ACTION_PAUSE; + actions |= PlaybackState.ACTION_FAST_FORWARD; + actions |= PlaybackState.ACTION_REWIND; + } + } + return actions; + } + + /** + * Implementation of the PlaybackService callbacks + */ + public void onTimelineChanged() { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null)); + // updatePlaybackState(null); + } + + public void setState(long uptime, int state) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null)); + // updatePlaybackState(null); + } + + public void setSong(long uptime, Song song) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null)); +// updatePlaybackState(null); + if(song == null) { + if(PlaybackService.hasInstance()) { + song = PlaybackService.get(this).getSong(0); + } + } + + if(song != null) { + MediaMetadata metadata = new MediaMetadata.Builder() + .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(song.id)) + .putString(MediaMetadata.METADATA_KEY_ALBUM, song.album) + .putString(MediaMetadata.METADATA_KEY_ARTIST, song.artist) + .putLong(MediaMetadata.METADATA_KEY_DURATION, song.duration) + .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, "content://media/external/audio/media/" + Long.toString(song.id) + "/albumart") + .putString(MediaMetadata.METADATA_KEY_TITLE, song.title) + .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, song.trackNumber) + .build(); + mSession.setMetadata(metadata); + } + } + + public void onPositionInfoChanged() { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null)); + // updatePlaybackState(null); + } + + public void onError(String error) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, error)); + // updatePlaybackState(error); + } + + public void onMediaChanged() { + if(PlaybackService.hasInstance()) { + setSong(0,PlaybackService.get(this).getSong(0)); + } + + } +} diff --git a/src/ch/blinkenlights/android/vanilla/PlaybackService.java b/src/ch/blinkenlights/android/vanilla/PlaybackService.java index b62e5f7d..e9391218 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaybackService.java +++ b/src/ch/blinkenlights/android/vanilla/PlaybackService.java @@ -72,12 +72,12 @@ import java.util.ArrayList; */ public final class PlaybackService extends Service implements Handler.Callback - , MediaPlayer.OnCompletionListener - , MediaPlayer.OnErrorListener - , SharedPreferences.OnSharedPreferenceChangeListener - , SongTimeline.Callback - , SensorEventListener - , AudioManager.OnAudioFocusChangeListener + , MediaPlayer.OnCompletionListener + , MediaPlayer.OnErrorListener + , SharedPreferences.OnSharedPreferenceChangeListener + , SongTimeline.Callback + , SensorEventListener + , AudioManager.OnAudioFocusChangeListener { /** * Name of the state file. @@ -99,7 +99,7 @@ public final class PlaybackService extends Service * Rewind song if we already played more than 2.5 sec */ private static final int REWIND_AFTER_PLAYED_MS = 2500; - + /** * Action for startService: toggle playback on/off. */ @@ -255,12 +255,12 @@ public final class PlaybackService extends Service * g: {@link PlaybackService#FLAG_DUCKING} */ int mState; - + /** * How many broken songs we did already skip */ int mSkipBroken; - + /** * Object used for state-related locking. */ @@ -277,6 +277,10 @@ public final class PlaybackService extends Service * Static referenced-array to PlaybackActivities, used for callbacks */ private static final ArrayList sActivities = new ArrayList(5); + /** + * Static reference to MirrorLinkMediaBrowserService, used for callbacks + */ + private static MirrorLinkMediaBrowserService sMirrorLinkMediaBrowserService = null; /** * Cached app-wide SharedPreferences instance. */ @@ -432,7 +436,7 @@ public final class PlaybackService extends Service mBastpUtil = new BastpUtil(); mReadahead = new ReadaheadThread(); mReadahead.start(); - + mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE); @@ -618,14 +622,14 @@ public final class PlaybackService extends Service mp.setOnErrorListener(this); return mp; } - + public void prepareMediaPlayer(VanillaMediaPlayer mp, String path) throws IOException{ mp.setDataSource(path); mp.prepare(); applyReplayGain(mp); } - - + + /** * Make sure that the current ReplayGain volume matches * the (maybe just changed) user settings @@ -646,20 +650,20 @@ public final class PlaybackService extends Service * and adjusts the volume */ private void applyReplayGain(VanillaMediaPlayer mp) { - + float[] rg = getReplayGainValues(mp.getDataSource()); /* track, album */ float adjust = 0f; - + if(mReplayGainAlbumEnabled) { adjust = (rg[0] != 0 ? rg[0] : adjust); /* do we have track adjustment ? */ adjust = (rg[1] != 0 ? rg[1] : adjust); /* ..or, even better, album adj? */ } - + if(mReplayGainTrackEnabled || (mReplayGainAlbumEnabled && adjust == 0)) { adjust = (rg[1] != 0 ? rg[1] : adjust); /* do we have album adjustment ? */ adjust = (rg[0] != 0 ? rg[0] : adjust); /* ..or, even better, track adj? */ } - + if(adjust == 0) { /* No RG value found: decrease volume for untagged song if requested by user */ adjust = (mReplayGainUntaggedDeBump-150)/10f; @@ -669,12 +673,12 @@ public final class PlaybackService extends Service ** But we want -15 <-> +15, so 75 shall be zero */ adjust += 2*(mReplayGainBump-75)/10f; /* 2* -> we want +-15, not +-7.5 */ } - + if(mReplayGainAlbumEnabled == false && mReplayGainTrackEnabled == false) { /* Feature is disabled: Make sure that we are going to 100% volume */ adjust = 0f; } - + float rg_result = ((float)Math.pow(10, (adjust/20) ))*mFadeOut; if(rg_result > 1.0f) { rg_result = 1.0f; /* android would IGNORE the change if this is > 1 and we would end up with the wrong volume */ @@ -685,7 +689,7 @@ public final class PlaybackService extends Service } /** - * Returns the (hopefully cached) replaygain + * Returns the (hopefully cached) replaygain * values of given file */ public float[] getReplayGainValues(String path) { @@ -718,7 +722,7 @@ public final class PlaybackService extends Service private void triggerGaplessUpdate() { if(mMediaPlayerInitialized != true) return; - + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return; /* setNextMediaPlayer is supported since JB */ @@ -1017,6 +1021,11 @@ public final class PlaybackService extends Service ArrayList list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).setState(uptime, state); + + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.setState(uptime, state); + } } if (song != null) { @@ -1025,6 +1034,11 @@ public final class PlaybackService extends Service list.get(i).setSong(uptime, song); } + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.setSong(uptime, song); + } + updateWidgets(); if (mReadaheadEnabled) @@ -1105,6 +1119,23 @@ public final class PlaybackService extends Service mNotificationManager.cancel(NOTIFICATION_ID); } + /** + * When playing through MirrorLink(tm) don't interact + * with the User directly as this is considered distracting + * while driving + */ + private void showMirrorLinkSafeToast(int resId, int duration) { + if(sMirrorLinkMediaBrowserService == null) { + Toast.makeText(this, resId, duration).show(); + } + } + + private void showMirrorLinkSafeToast(CharSequence text, int duration) { + if(sMirrorLinkMediaBrowserService == null) { + Toast.makeText(this, text, duration).show(); + } + } + /** * Start playing if currently paused. * @@ -1116,7 +1147,7 @@ public final class PlaybackService extends Service if ((mState & FLAG_EMPTY_QUEUE) != 0) { setFinishAction(SongTimeline.FINISH_RANDOM); setCurrentSong(0); - Toast.makeText(this, R.string.random_enabling, Toast.LENGTH_SHORT).show(); + showMirrorLinkSafeToast(R.string.random_enabling, Toast.LENGTH_SHORT); } int state = updateState(mState | FLAG_PLAYING); @@ -1270,7 +1301,7 @@ public final class PlaybackService extends Service { /* Save our 'current' state as the try block may set the ERROR flag (which clears the PLAYING flag */ boolean playing = (mState & FLAG_PLAYING) != 0; - + try { mMediaPlayerInitialized = false; mMediaPlayer.reset(); @@ -1287,7 +1318,7 @@ public final class PlaybackService extends Service else if(song.path != null) { prepareMediaPlayer(mMediaPlayer, song.path); } - + mMediaPlayerInitialized = true; // Cancel any pending gapless updates and re-send them @@ -1310,16 +1341,16 @@ public final class PlaybackService extends Service } catch (IOException e) { mErrorMessage = getResources().getString(R.string.song_load_failed, song.path); updateState(mState | FLAG_ERROR); - Toast.makeText(this, mErrorMessage, Toast.LENGTH_LONG).show(); + showMirrorLinkSafeToast(mErrorMessage, Toast.LENGTH_LONG); Log.e("VanillaMusic", "IOException", e); - + /* Automatically advance to next song IF we are currently playing or already did skip something * This will stop after skipping 10 songs to avoid endless loops (queue full of broken stuff */ if(mTimeline.isEndOfQueue() == false && getSong(1) != null && (playing || (mSkipBroken > 0 && mSkipBroken < 10))) { mSkipBroken++; mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SKIP_BROKEN_SONG, getTimelinePosition(), 0), 1000); } - + } updateNotification(); @@ -1351,6 +1382,10 @@ public final class PlaybackService extends Service public boolean onError(MediaPlayer player, int what, int extra) { Log.e("VanillaMusic", "MediaPlayer error: " + what + ' ' + extra); + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.onError("MediaPlayer Error"); + } return true; } @@ -1689,8 +1724,7 @@ public final class PlaybackService extends Service default: throw new IllegalArgumentException("Invalid add mode: " + query.mode); } - - Toast.makeText(this, getResources().getQuantityString(text, count, count), Toast.LENGTH_SHORT).show(); + showMirrorLinkSafeToast(getResources().getQuantityString(text, count, count), Toast.LENGTH_SHORT); } /** @@ -1781,6 +1815,11 @@ public final class PlaybackService extends Service ArrayList list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).onTimelineChanged(); + + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.onTimelineChanged(); + } } @Override @@ -1789,6 +1828,11 @@ public final class PlaybackService extends Service ArrayList list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).onPositionInfoChanged(); + + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.onPositionInfoChanged(); + } } private final ContentObserver mObserver = new ContentObserver(null) { @@ -1849,6 +1893,24 @@ public final class PlaybackService extends Service sActivities.remove(activity); } + /** + * Register a MirrorLinkMediaBrowserService instance + * + * @param service the Service to be registered + */ + public static void registerService(MirrorLinkMediaBrowserService service) { + sMirrorLinkMediaBrowserService = service; + } + + /** + * Deregister a MirrorLinkMediaBrowserService instance + * + * @param service the Service to be deregistered + */ + public static void unregisterService() { + sMirrorLinkMediaBrowserService = null; + } + /** * Initializes the service state, loading songs saved from the disk into the * song timeline. @@ -2171,7 +2233,7 @@ public final class PlaybackService extends Service break; case ClearQueue: clearQueue(); - Toast.makeText(this, R.string.queue_cleared, Toast.LENGTH_SHORT).show(); + showMirrorLinkSafeToast(R.string.queue_cleared, Toast.LENGTH_SHORT); break; case ShowQueue: Intent intentShowQueue = new Intent(this, ShowQueueActivity.class); @@ -2203,6 +2265,13 @@ public final class PlaybackService extends Service } } + /** + * Returns the playing status of the current song + */ + public boolean isPlaying() { + return (mState & FLAG_PLAYING) != 0; + } + /** * Returns the position of the current song in the song timeline. */ @@ -2218,14 +2287,14 @@ public final class PlaybackService extends Service { return mTimeline.getLength(); } - + /** * Returns 'Song' with given id from timeline */ public Song getSongByQueuePosition(int id) { return mTimeline.getSongByQueuePosition(id); } - + /** * Do a 'hard' jump to given queue position */