From ee5207092573725c620e6cfe86582af02aeab0fc Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 11 Apr 2023 16:32:18 +0000 Subject: [PATCH 01/69] Update dependency org.robolectric:robolectric to v4.10 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 716a1706..ca7906c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ mockito = "5.2.0" mockitoKotlin = "4.1.0" kluent = "1.72" apacheCodecs = "1.15" -robolectric = "4.9.2" +robolectric = "4.10" timber = "5.0.1" fastScroll = "2.0.1" colorPicker = "2.2.4" From dbef8307ea8aea6ffd2097b7fdaebea36ac91997 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 12 Apr 2023 12:34:09 +0000 Subject: [PATCH 02/69] Update dependency gradle to v8.1 --- gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 ++++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 8979 zcmY*fV{{$d(moANW81db*tXT!Nn`UgX2ZtD$%&n`v2C-lt;YD?@2-14?EPcUv!0n* z`^Ws4HP4i8L%;4p*JkD-J9ja2aKi!sX@~#-MY5?EPBK~fXAl)Ti}^QGH@6h+V+|}F zv=1RqQxhWW9!hTvYE!)+*m%jEL^9caK;am9X8QP~a9X0N6(=WSX8KF#WpU-6TjyR3 zpKhscivP97d$DGc{KI(f#g07u{Jr0wn#+qNr}yW}2N3{Kx0lCq%p4LBKil*QDTEyR zg{{&=GAy_O0VJ(8ZbtS4tPeeeILKK(M?HtQY!6K^wt zxsPH>E%g%V@=!B;kWF54$xjC&4hO!ZEG0QFMHLqe!tgH;%vO62BQj||nokbX&2kxF zzg#N!2M|NxFL#YdwOL8}>iDLr%2=!LZvk_&`AMrm7Zm%#_{Ot_qw=HkdVg{f9hYHF zlRF*9kxo~FPfyBD!^d6MbD?BRZj(4u9j!5}HFUt+$#Jd48Fd~ahe@)R9Z2M1t%LHa z_IP|tDb0CDl(fsEbvIYawJLJ7hXfpVw)D-)R-mHdyn5uZYefN0rZ-#KDzb`gsow;v zGX>k|g5?D%Vn_}IJIgf%nAz{@j0FCIEVWffc1Z+lliA}L+WJY=MAf$GeI7xw5YD1) z;BJn$T;JI5vTbZ&4aYfmd-XPQd)YQ~d({>(^5u>Y^5rfxEUDci9I5?dXp6{zHG=Tc z6$rLd^C~60=K4ptlZ%Fl-%QLc-x{y=zU$%&4ZU}4&Yu?jF4eqB#kTHhty`Aq=kJE% zzq(5OS9o1t-)}S}`chh1Uu-Sl?ljxMDVIy5j`97Eqg7L~Ak9NSZ?!5M>5TRMXfD#} zFlMmFnr%?ra>vkvJQjmWa8oB{63qPo1L#LAht%FG|6CEe9KP2&VNe_HNb7M}pd*!t zpGL0vzCU02%iK@AKWxP^64fz-U#%u~D+FV?*KdPY9C_9{Ggn;Y;;iKE0b|}KmC&f(WIDcFtvRPDju z?Dc&_dP4*hh!%!6(nYB*TEJs<4zn*V0Nw1O4VzYaNZul>anE2Feb@T$XkI?)u6VK$bg* z22AY7|Ju!_jwc2@JX(;SUE>VDWRD|d56WYUGLAAwPYXU9K&NgY{t{dyMskUBgV%@p zMVcFn>W|hJA?3S?$k!M|1S2e1A&_~W2p$;O2Wpn`$|8W(@~w>RR4kxHdEr`+q|>m@ zTYp%Ut+g`T#HkyE5zw<5uhFvt2=k5fM3!8OxvGgMRS|t7RaJn7!2$r_-~a%C7@*Dq zGUp2g0N^HzLU=%bROVFi2J;#`7#WGTUI$r!(wmbJlbS`E#ZpNp7vOR#TwPQWNf$IW zoX>v@6S8n6+HhUZB7V^A`Y9t4ngdfUFZrDOayMVvg&=RY4@0Z~L|vW)DZTIvqA)%D zi!pa)8L7BipsVh5-LMH4bmwt2?t88YUfIRf!@8^gX$xpKTE^WpM!-=3?UVw^Cs`Y7 z2b<*~Q=1uqs79{h&H_8+X%><4qSbz_cSEa;Hkdmtq5uwGTY+|APD{i_zYhLXqT7HO zT^Am_tW?Cmn%N~MC0!9mYt-~WK;hj-SnayMwqAAHo#^ALwkg0>72&W}5^4%|Z|@T; zwwBQTg*&eXC}j8 zra77(XC^p&&o;KrZ$`_)C$@SDWT+p$3!;ZB#yhnK{CxQc&?R}ZQMcp`!!eXLLhiP8W zM=McHAMnUMlar8XLXk&jx#HBH3U0jbhJuqa~#l`aB)N6;WI(Im322o#{K&92l6(K z)(;=;-m!%9@j#WSA1uniU(^x(UTi+%idMd)x*!*Hub0Rg7DblI!cqo9QUZf29Y#?XN!K!|ovJ7~!^H}!zsaMl(57lpztQ7V zyo#`qJ4jv1zGAW2uIkU3o&7_=lYWz3=SR!sgfuYp{Um<*H%uW8MdUT2&o*QKjD3PEH zHz;H}qCN~`GFsJ_xz$9xga*@VzJTH7-3lggkBM&7xlz5#qWfkgi=#j%{&f-NMsaSv zeIZ60Jpw}QV+t`ovOJxVhYCXe8E7r*eLCJ{lP6sqc}BYrhjXlt(6e9nw=2Le1gOT0 zZX!q9r#DZ&8_cAhWPeq~CJkGvpRU&q8>rR@RBW4~@3j1X>RBum#U z1wjcEdB`|@sXAWxk2*TOj> zr(j{nr1;Mk3x^gvAtZsahY=ou{eAJi-d(XISF-?+Q6{Um4+lu?aA=S33@k=6^OT?F z8TE`ha;q@=ZQ-dlt!q49;Wjjl<&Yee^!h5MFkd)Oj=fsvxytK%!B z-P#YJ)8^dMi=wpKmt43|apX6v2dNXzZ-WHlLEh`JoKFNjCK7LhO^P5XW?Y~rjGcIpv$2v41rE}~0{aj9NVpDXGdD6W8{fyzioQdu&xkn8 zhT*^NY0zv>Om?h3XAku3p-4SHkK@fXrpi{T=@#bwY76TsD4$tAHAhXAStdb$odc z02~lZyb!fG_7qrU_F5 zoOG|pEwdyDhLXDwlU>T|;LF@ACJk(qZ*2h6GB@33mKk};HO^CQM(N7@Ml5|8IeHzt zdG4f$q}SNYA4P=?jV!mJ%3hRKwi&!wFptWZRq4bpV9^b7&L>nW%~Y|junw!jHj%85 z3Ck6%`Y=Abvrujnm{`OtE0uQkeX@3JPzj#iO#eNoAX6cDhM+cc2mLk8;^bG62mtjQ zj|kxI2W|4n{VqMqB?@YnA0y}@Mju)&j3UQ4tSdH=Eu?>i7A50b%i$pc{YJki7ubq7 zVTDqdkGjeAuZdF)KBwR6LZob}7`2935iKIU2-I;88&?t16c-~TNWIcQ8C_cE_F1tv z*>4<_kimwX^CQtFrlk)i!3-+2zD|=!D43Qqk-LtpPnX#QQt%eullxHat97k=00qR|b2|M}`q??yf+h~};_PJ2bLeEeteO3rh+H{9otNQDki^lu)(`a~_x(8NWLE*rb%T=Z~s?JC|G zXNnO~2SzW)H}p6Zn%WqAyadG=?$BXuS(x-2(T!E&sBcIz6`w=MdtxR<7M`s6-#!s+ znhpkcNMw{c#!F%#O!K*?(Hl(;Tgl9~WYBB(P@9KHb8ZkLN>|}+pQ)K#>ANpV1IM{Q z8qL^PiNEOrY*%!7Hj!CwRT2CN4r(ipJA%kCc&s;wOfrweu)H!YlFM z247pwv!nFWbTKq&zm4UVH^d?H2M276ny~@v5jR2>@ihAmcdZI-ah(&)7uLQM5COqg?hjX2<75QU4o5Q7 zZ5gG;6RMhxLa5NFTXgegSXb0a%aPdmLL4=`ox2smE)lDn^!;^PNftzTf~n{NH7uh_ zc9sKmx@q1InUh_BgI3C!f>`HnO~X`9#XTI^Yzaj1928gz8ClI!WIB&2!&;M18pf0T zsZ81LY3$-_O`@4$vrO`Cb&{apkvUwrA0Z49YfZYD)V4;c2&`JPJuwN_o~2vnyW_b! z%yUSS5K{a*t>;WJr&$A_&}bLTTXK23<;*EiNHHF-F<#hy8v2eegrqnE=^gt+|8R5o z_80IY4&-!2`uISX6lb0kCVmkQ{D}HMGUAkCe`I~t2~99(<#}{E;{+Y0!FU>leSP(M zuMoSOEfw3OC5kQ~Y2)EMlJceJlh}p?uw}!cq?h44=b2k@T1;6KviZGc_zbeTtTE$@EDwUcjxd#fpK=W*U@S#U|YKz{#qbb*|BpcaU!>6&Ir zhsA+ywgvk54%Nj>!!oH>MQ+L~36v1pV%^pOmvo7sT|N}$U!T6l^<3W2 z6}mT7Cl=IQo%Y~d%l=+;vdK)yW!C>Es-~b^E?IjUU4h6<86tun6rO#?!37B)M8>ph zJ@`~09W^@5=}sWg8`~ew=0>0*V^b9eG=rBIGbe3Ko$pj!0CBUTmF^Q}l7|kCeB(pX zi6UvbUJWfKcA&PDq?2HrMnJBTW#nm$(vPZE;%FRM#ge$S)i4!y$ShDwduz@EPp3H? z`+%=~-g6`Ibtrb=QsH3w-bKCX1_aGKo4Q7n-zYp->k~KE!(K@VZder&^^hIF6AhiG z;_ig2NDd_hpo!W1Un{GcB@e{O@P3zHnj;@SzYCxsImCHJS5I&^s-J6?cw92qeK8}W zk<_SvajS&d_tDP~>nhkJSoN>UZUHs?)bDY`{`;D^@wMW0@!H1I_BYphly0iqq^Jp; z_aD>eHbu@e6&PUQ4*q*ik0i*$Ru^_@`Mbyrscb&`8|c=RWZ>Ybs16Q?Cj1r6RQA5! zOeuxfzWm(fX!geO(anpBCOV|a&mu|$4cZ<*{pb1F{`-cm1)yB6AGm7b=GV@r*DataJ^I!>^lCvS_@AftZiwtpszHmq{UVl zKL9164tmF5g>uOZ({Jg~fH~QyHd#h#E;WzSYO~zt)_ZMhefdm5*H1K-#=_kw#o%ch zgX|C$K4l4IY8=PV6Q{T8dd`*6MG-TlsTEaA&W{EuwaoN+-BDdSL2>|lwiZ++4eR8h zNS1yJdbhAWjW4k`i1KL)l#G*Y=a0ouTbg8R1aUU`8X7p*AnO+uaNF9mwa+ooA)hlj zR26XBpQ-{6E9;PQAvq2<%!M1;@Q%r@xZ16YRyL&v}9F`Nnx#RLUc<78w$S zZElh==Rnr2u<*qKY|aUR9(A|{cURqP81O-1a@X)khheokEhC}BS-g~|zRbn-igmID z$Ww!O0-j!t(lx>-JH+0KW3*Bgafpm>%n=`(ZLa^TWd*-je!Xi7H*bZ8pz`HPFYeC? zk>`W)4Cj6*A3A8g$MEhp*<@qO&&>3<4YI%0YAMmQvD3 z${78Fa2mqiI>P7|gE)xs$cg3~^?UBb4y6B4Z#0Fzy zN8Gf!c+$uPS`VRB=wRV1f)>+PEHBYco<1?ceXET}Q-tKI=E`21<15xTe@%Bhk$v09 zVpoL_wNuw)@^O+C@VCeuWM}(%C(%lTJ}7n)JVV!^0H!3@)ydq#vEt;_*+xos$9i?{ zCw5^ZcNS&GzaeBmPg6IKrbT`OSuKg$wai+5K}$mTO-Z$s3Y+vb3G}x%WqlnQS1;|Z zlZ$L{onq1Ag#5JrM)%6~ToQ}NmM2A(7X5gy$nVI=tQFOm;7|Oeij{xb_KU{d@%)2z zsVqzTl@XPf(a95;P;oBm9Hlpo`9)D9>G>!Bj=ZmX{ces=aC~E^$rTO5hO$#X65jEA zMj1(p+HXdOh7FAV;(_)_RR#P>&NW?&4C7K1Y$C$i**g;KOdu|JI_Ep zV-N$wuDRkn6=k|tCDXU%d=YvT!M1nU?JY;Pl`dxQX5+660TX7~q@ukEKc!Iqy2y)KuG^Q-Y%$;SR&Mv{%=CjphG1_^dkUM=qI*3Ih^Bk621n`6;q(D;nB_y|~ zW*1ps&h|wcET!#~+Ptsiex~YVhDiIREiw1=uwlNpPyqDZ`qqv9GtKwvxnFE}ME93fD9(Iq zz=f&4ZpD~+qROW6Y2AjPj9pH*r_pS_f@tLl88dbkO9LG0+|4*Xq(Eo7fr5MVg{n<+p>H{LGr}UzToqfk_x6(2YB~-^7>%X z+331Ob|NyMST64u|1dK*#J>qEW@dKNj-u}3MG)ZQi~#GzJ_S4n5lb7vu&>;I-M49a z0Uc#GD-KjO`tQ5ftuSz<+`rT)cLio$OJDLtC`t)bE+Nu@Rok2;`#zv1=n z7_CZr&EhVy{jq(eJPS)XA>!7t<&ormWI~w0@Y#VKjK)`KAO~3|%+{ z$HKIF?86~jH*1p=`j#}8ON0{mvoiN7fS^N+TzF~;9G0_lQ?(OT8!b1F8a~epAH#uA zSN+goE<-psRqPXdG7}w=ddH=QAL|g}x5%l-`Kh69D4{M?jv!l))<@jxLL$Eg2vt@E zc6w`$?_z%awCE~ca)9nMvj($VH%2!?w3c(5Y4&ZC2q#yQ=r{H2O839eoBJ{rfMTs8 zn2aL6e6?;LY#&(BvX_gC6uFK`0yt zJbUATdyz5d3lRyV!rwbj0hVg#KHdK0^A7_3KA%gKi#F#-^K%1XQbeF49arI2LA|Bj z?=;VxKbZo(iQmHB5eAg=8IPRqyskQNR!&KEPrGv&kMr(8`4oe?vd?sIZJK+JY04kc zXWk)4N|~*|0$4sUV3U6W6g+Z3;nN<~n4H17QT*%MCLt_huVl@QkV`A`jyq<|q=&F_ zPEOotTu9?zGKaPJ#9P&ljgW!|Vxhe+l85%G5zpD5kAtn*ZC})qEy!v`_R}EcOn)&# z-+B52@Zle@$!^-N@<_=LKF}fqQkwf1rE(OQP&8!En}jqr-l0A0K>77K8{zT%wVpT~ zMgDx}RUG$jgaeqv*E~<#RT?Q)(RGi8bUm(1X?2OAG2!LbBR+u1r7$}s=lKqu&VjXP zUw3L9DH({yj)M%OqP%GC+$}o0iG|*hN-Ecv3bxS|Mxpmz*%x`w7~=o9BKfEVzr~K- zo&Fh`wZ{#1Jd5QFM4&!PabL!tf%TfJ4wi;45AqWe$x}8*c2cgqua`(6@ErE&P{K5M zQfwGQ4Qg&M3r4^^$B?_AdLzqtxn5nb#kItDY?BTW z#hShspeIDJ1FDmfq@dz1TT`OV;SS0ImUp`P6GzOqB3dPfzf?+w^40!Wn*4s!E;iHW zNzpDG+Vmtnh%CyfAX>X z{Y=vt;yb z;TBRZpw##Kh$l<8qq5|3LkrwX%MoxqWwclBS6|7LDM(I31>$_w=;{=HcyWlak3xM1 z_oaOa)a;AtV{*xSj6v|x%a42{h@X-cr%#HO5hWbuKRGTZS)o=^Id^>H5}0p_(BEXX zx3VnRUj6&1JjDI);c=#EYcsg;D5TFlhe)=nAycR1N)YSHQvO+P5hKe9T0ggZT{oF@ z#i3V4TpQlO1A8*TWn|e}UWZ(OU;Isd^ zb<#Vj`~W_-S_=lDR#223!xq8sRjAAVSY2MhRyUyHa-{ql=zyMz?~i_c&dS>eb>s>#q#$UI+!&6MftpQvxHA@f|k2(G9z zAQCx-lJ-AT;PnX%dY5}N$m6tFt5h6;Mf78TmFUN9#4*qBNg4it3-s22P+|Rw zG@X%R0sm*X07ZZEOJRbDkcjr}tvaVWlrwJ#7KYEw&X`2lDa@qb!0*SHa%+-FU!83q zY{R15$vfL56^Nj42#vGQlQ%coT4bLr2s5Y0zBFp8u&F(+*%k4xE1{s75Q?P(SL7kf zhG?3rfM9V*b?>dOpwr%uGH7Xfk1HZ!*k`@CNM77g_mGN=ucMG&QX19B!%y77w?g#b z%k3x6q_w_%ghL;9Zk_J#V{hxK%6j`?-`UN?^e%(L6R#t#97kZaOr1{&<8VGVs1O>} z6~!myW`ja01v%qy%WI=8WI!cf#YA8KNRoU>`_muCqpt_;F@rkVeDY}F7puI_wBPH9 zgRGre(X_z4PUO5!VDSyg)bea1x_a7M z4AJ?dd9rf{*P`AY+w?g_TyJlB5Nks~1$@PxdtpUGGG##7j<$g&BhKq0mXTva{;h5E ztcN!O17bquKEDC#;Yw2yE>*=|WdZT9+ycgUR^f?~+TY-E552AZlzYn{-2CLRV9mn8 z+zNoWLae^P{co`F?)r;f!C=nnl*1+DI)mZY!frp~f%6tX2g=?zQL^d-j^t1~+xYgK zv;np&js@X=_e7F&&ZUX|N6Q2P0L=fWoBuh*L7$3~$-A)sdy6EQ@Pd-)|7lDA@%ra2 z4jL@^w92&KC>H(=v2j!tVE_3w0KogtrNjgPBsTvW F{TFmrHLU;u delta 8469 zcmY*q~ZGqoW{=01$bgB@1Nex`%9%S2I04)5Jw9+UyLS&r+9O2bq{gY;dCa zHW3WY0%Dem?S7n5JZO%*yiT9fb!XGk9^Q`o-EO{a^j%&)ZsxsSN@2k2eFx1*psqn0e*crIbAO}Rd~_BifMu*q7SUn{>WD$=7n_$uiQ0wGc$?u1hM%gf??nL?m22h!8{ zYmFMLvx6fjz*nwF^tAqx1uv0yEW9-tcIV5Q{HNh`9PMsuqD8VE%oAs5FsWa0mLV$L zPAF5e^$tJ8_Kwp!$N1M<#Z154n!X6hFpk8)eMLu; zaXS71&`24 zV`x~}yAxBw##Oj@qo_@DcBqc+2TB&=bJyZWTeR55zG<{Z@T^hSbMdm~Ikkr?4{7WT zcjPyu>0sDjl7&?TL@ z)cW?lW@Pfwu#nm7E1%6*nBIzQrKhHl`t54$-m>j8f%0vVr?N0PTz`}VrYAl+8h^O~ zuWQj@aZSZmGPtcVjGq-EQ1V`)%x{HZ6pT-tZttJOQm?q-#KzchbH>>5-jEX*K~KDa z#oO&Qf4$@}ZGQ7gxn<;D$ziphThbi6zL^YC;J#t0GCbjY)NHdqF=M4e(@|DUPY_=F zLcX1HAJ+O-3VkU#LW`4;=6szwwo%^R4#UK}HdAXK` z{m!VZj5q9tVYL=^TqPH*6?>*yr>VxyYF4tY{~?qJ*eIoIU0}-TLepzga4g}}D7#Qu zn;6I;l!`xaL^8r*Tz*h`^(xJCnuVR_O@Gl*Q}y$lp%!kxD`%zN19WTIf`VX*M=cDp z*s4<9wP|ev;PARRV`g$R*QV@rr%Ku~z(2-s>nt{JI$357vnFAz9!ZsiiH#4wOt+!1 zM;h;EN__zBn)*-A^l!`b?b*VI-?)Sj6&Ov3!j9k$5+#w)M>`AExCm0!#XL+E{Bp)s;Hochs+-@@)7_XDMPby#p<9mLu+S{8e2Jn`1`1nrffBfy4u)p7FFQWzgYt zXC}GypRdkTUS+mP!jSH$K71PYI%QI-{m;DvlRb*|4GMPmvURv0uD2bvS%FOSe_$4zc--*>gfRMKN|D ztP^WFfGEkcm?sqXoyRmuCgb?bSG17#QSv4~XsbPH>BE%;bZQ_HQb?q%CjykL7CWDf z!rtrPk~46_!{V`V<;AjAza;w-F%t1^+b|r_um$#1cHZ1|WpVUS&1aq?Mnss|HVDRY z*sVYNB+4#TJAh4#rGbr}oSnxjD6_LIkanNvZ9_#bm?$HKKdDdg4%vxbm-t@ZcKr#x z6<$$VPNBpWM2S+bf5IBjY3-IY2-BwRfW_DonEaXa=h{xOH%oa~gPW6LTF26Y*M)$N z=9i`Y8};Qgr#zvU)_^yU5yB;9@yJjrMvc4T%}a|jCze826soW-d`V~eo%RTh)&#XR zRe<8$42S2oz|NVcB%rG(FP2U&X>3 z4M^}|K{v64>~rob;$GO55t;Nb&T+A3u(>P6;wtp6DBGWbX|3EZBDAM2DCo&4w|WGpi;~qUY?Ofg$pX&`zR~)lr)8}z^U3U38Nrtnmf~e7$i=l>+*R%hQgDrj%P7F zIjyBCj2$Td=Fp=0Dk{=8d6cIcW6zhK!$>k*uC^f}c6-NR$ zd<)oa+_fQDyY-}9DsPBvh@6EvLZ}c)C&O-+wY|}RYHbc2cdGuNcJ7#yE}9=!Vt-Q~ z4tOePK!0IJ0cW*jOkCO? zS-T!bE{5LD&u!I4tqy;dI*)#e^i)uIDxU?8wK1COP3Qk{$vM3Sm8(F2VwM?1A+dle z6`M6bbZye|kew%w9l`GS74yhLluJU5R=#!&zGwB7lmTt}&eCt0g(-a;Mom-{lL6u~ zFgjyUs1$K*0R51qQTW_165~#WRrMxiUx{0F#+tvgtcjV$U|Z}G*JWo6)8f!+(4o>O zuaAxLfUl;GHI}A}Kc>A8h^v6C-9bb}lw@rtA*4Q8)z>0oa6V1>N4GFyi&v69#x&CwK*^!w&$`dv zQKRMKcN$^=$?4to7X4I`?PKGi(=R}d8cv{74o|9FwS zvvTg0D~O%bQpbp@{r49;r~5`mcE^P<9;Zi$?4LP-^P^kuY#uBz$F!u1d{Ens6~$Od zf)dV+8-4!eURXZZ;lM4rJw{R3f1Ng<9nn2_RQUZDrOw5+DtdAIv*v@3ZBU9G)sC&y!vM28daSH7(SKNGcV z&5x#e#W2eY?XN@jyOQiSj$BlXkTG3uAL{D|PwoMp$}f3h5o7b4Y+X#P)0jlolgLn9xC%zr3jr$gl$8?II`DO6gIGm;O`R`bN{;DlXaY4b`>x6xH=Kl@ z!>mh~TLOo)#dTb~F;O z8hpjW9Ga?AX&&J+T#RM6u*9x{&%I8m?vk4eDWz^l2N_k(TbeBpIwcV4FhL(S$4l5p z@{n7|sax){t!3t4O!`o(dYCNh90+hl|p%V_q&cwBzT*?Nu*D0wZ)fPXv z@*;`TO7T0WKtFh8~mQx;49VG_`l`g|&VK}LysK%eU4})Cvvg3YN)%;zI?;_Nr z)5zuU1^r3h;Y+mJov*->dOOj>RV^u2*|RraaQWsY5N?Uu)fKJOCSL2^G=RB%(4K{* zx!^cB@I|kJR`b+5IK}(6)m=O{49P5E^)!XvD5zVuzJH{01^#$@Cn514w41BB;FAoS2SYl3SRrOBDLfl5MvgA3 zU6{T?BW}l~8vU;q@p9IOM(=;WdioeQmt?X|=L9kyM&ZsNc*-Knv8@U*O96T@4ZiJ$ zeFL2}pw_~Tm3d4#q!zZS0km@vYgym33C0h(6D)6|Y)*UXI^T`(QPQh$WF?&h(3QYh zqGw@?BTk@VA_VxK@z?a@UrMhY zUD16oqx4$$6J_k0HnXgARm}N#(^yA1MLdbwmEqHnX*JdHN>$5k2E|^_bL< zGf5Z+D!9dXR>^(5F&5gIew1%kJtFUwI5P1~I$4LL_6)3RPzw|@2vV;Q^MeQUKzc=KxSTTX`}u%z?h~;qI#%dE@OZwehZyDBsWTc&tOC1c%HS#AyTJ= zQixj=BNVaRS*G!;B$}cJljeiVQabC25O+xr4A+32HVb;@+%r}$^u4-R?^3yij)0xb z86i@aoVxa%?bfOE;Bgvm&8_8K(M-ZEj*u9ms_Hk#2eL`PSnD#At!0l{f!v`&Kg}M$n(&R)?AigC5Z?T7Jv^lrDL!yYS{4 zq_H}oezX-Svu>dp)wE@khE@aR5vY=;{C-8Hws++5LDpArYd)U47jc-;f~07_TPa^1 zO`0+uIq)@?^!%JXCDid+nt|c@NG1+ce@ijUX&@rV9UiT|m+t-nqVB7?&UX*|{yDBFw9x52&dTh@;CL)Q?6s1gL=CUQTX7#TJPs9cpw<4>GFMUKo|f{! z&(%2hP6ghr%UFVO-N^v9l|tKy>&e%8us}wT0N*l(tezoctVtLmNdGPOF6oaAGJI5R zZ*|k@z3H!~Mm9fXw{bbP6?lV-j#Rfgnjf++O7*|5vz2#XK;kk ztJbi%r0{U5@QwHYfwdjtqJ6?;X{Ul3?W0O0bZ$k*y z4jWsNedRoCb7_|>nazmq{T3Y_{<5IO&zQ?9&uS@iL+|K|eXy^F>-60HDoVvovHelY zy6p(}H^7b+$gu@7xLn_^oQryjVu#pRE5&-w5ZLCK&)WJ5jJF{B>y;-=)C;xbF#wig zNxN^>TwzZbV+{+M?}UfbFSe#(x$c)|d_9fRLLHH?Xbn!PoM{(+S5IEFRe4$aHg~hP zJYt`h&?WuNs4mVAmk$yeM;8?R6;YBMp8VilyM!RXWj<95=yp=4@y?`Ua8 znR^R?u&g%`$Wa~usp|pO$aMF-en!DrolPjD_g#{8X1f=#_7hH8i|WF+wMqmxUm*!G z*4p980g{sgR9?{}B+a0yiOdR()tWE8u)vMPxAdK)?$M+O_S+;nB34@o<%lGJbXbP` z5)<({mNpHp&45UvN`b&K5SD#W){}6Y_d4v~amZPGg|3GdlWDB;;?a=Z{dd zELTfXnjCqq{Dgbh9c%LjK!Epi1TGI{A7AP|eg2@TFQiUd4Bo!JsCqsS-8ml`j{gM& zEd7yU`djX!EX2I{WZq=qasFzdDWD`Z?ULFVIP!(KQP=fJh5QC9D|$JGV95jv)!sYWY?irpvh06rw&O?iIvMMj=X zr%`aa(|{Ad=Vr9%Q(61{PB-V_(3A%p&V#0zGKI1O(^;tkS{>Y<`Ql@_-b7IOT&@?l zavh?#FW?5otMIjq+Bp?Lq)w7S(0Vp0o!J*~O1>av;)Cdok@h&JKaoHDV6IVtJ?N#XY=lknPN+SN8@3Gb+D-X*y5pQ)wnIpQlRR!Rd)@0LdA85}1 zu7W6tJ*p26ovz+`YCPePT>-+p@T_QsW$uE`McLlXb;k}!wwWuh$YC4qHRd=RS!s>2 zo39VCB-#Ew?PAYOx`x!@0qa5lZKrE?PJEwVfkww#aB_$CLKlkzHSIi4p3#IeyA@u@ z`x^!`0HJxe>#V7+Grku^in>Ppz|TD*`Ca4X%R3Yo|J=!)l$vYks|KhG{1CEfyuzK( zLjCz{5l}9>$J=FC?59^85awK0$;^9t9UxwOU8kP7ReVCc*rPOr(9uMY*aCZi2=JBu z(D0svsJRB&a9nY;6|4kMr1Er5kUVOh1TuBwa3B2C<+rS|xJo&Lnx3K-*P83eXQCJ= z(htQSA3hgOMcs`#NdYB17#zP_1N_P0peHrNo1%NsYn=;PgLXTic6b#{Y0Z~x9Ffav z^3eO+diquPfo1AXW*>G(JcGn{yN?segqKL$Wc9po(Kex z#tw_};zd++we+MPhOOgaXSmguul67JOvBysmg?wRf=OUeh(XyRcyY@8RTV@xck_c~ zLFMWAWb4^7xwR)3iO1PIs1<}L3CMJ1L-}s=>_y!`!FvYf^pJO|&nII{!Dz+b?=bUd zPJUUn))z)-TcpqKF(1tr-x1;lS?SB@mT#O7skl0sER{a|d?&>EKKaw* zQ>D^m*pNgV`54BKv?knU-T5bcvBKnI@KZo^UYjKp{2hpCo?_6v(Sg77@nQa{tSKbn zUgMtF>A3hndGocRY+Snm#)Q4%`|Qq3YTOU^uG}BGlz!B=zb?vB16sN&6J`L(k1r+$ z5G6E9tJ~Iwd!d!NH7Q%Z@BR@0e{p6#XF2))?FLAVG`npIjih*I+0!f6;+DM zLOP-qDsm9=ZrI!lfSDn%XuF17$j~gZE@I}S(Ctw&Te75P5?Fj%FLT;p-tm33FaUQc z5cR;$SwV|N0xmjox3V~XL3sV?YN}U0kkfmygW@a5JOCGgce6JyzGmgN$?NM%4;wEhUMg0uTTB~L==1Fvc(6)KMLmU z(12l^#g&9OpF7+Ll30F6(q=~>NIY=-YUJJ}@&;!RYnq*xA9h!iMi`t;B2SUqbyNGn zye@*0#Uu`OQy%utS%IA%$M1f4B|bOH={!3K1=Tc7Ra|%qZgZ{mjAGKXb)}jUu1mQ_ zRW7<;tkHv(m7E0m>**8D;+2ddTL>EcH_1YqCaTTu_#6Djm z*64!w#=Hz<>Fi1n+P}l#-)0e0P4o+D8^^Mk& zhHeJoh2paKlO+8r?$tx`qEcm|PSt6|1$1q?r@VvvMd1!*zAy3<`X9j?ZI|;jE-F(H zIn1+sm(zAnoJArtytHC|0&F0`i*dy-PiwbD-+j`ezvd4C`%F1y^7t}2aww}ZlPk)t z=Y`tm#jNM$d`pG%F42Xmg_pZnEnvC%avz=xNs!=6b%%JSuc(WObezkCeZ#C|3PpXj zkR8hDPyTIUv~?<%*)6=8`WfPPyB9goi+p$1N2N<%!tS2wopT2x`2IZi?|_P{GA|I5 z?7DP*?Gi#2SJZ!x#W9Npm)T;=;~Swyeb*!P{I^s@o5m_3GS2Lg?VUeBdOeae7&s5$ zSL_VuTJih_fq7g8O8b0g+GbmE+xG}^Wx`g~{mWTyr@=h zKlAymoHeZa`DgR?Pj8Yc+I|MrSB>X*ts#wNFOJxs!3aGE)xeTHlF`fC5^g(DTacl$ zx!ezQJdwIyc$8RyNS~Wh{0pp>8NcW)*J=7AQYdT?(QhJuq4u`QniZ!%6l{KWp-0Xp z4ZC6(E(_&c$$U_cmGFslsyX6(62~m*z8Yx2p+F5xmD%6A7eOnx`1lJA-Mrc#&xZWJ zzXV{{OIgzYaq|D4k^j%z|8JB8GnRu3hw#8Z@({sSmsF(x>!w0Meg5y(zg!Z0S^0k# z5x^g1@L;toCK$NB|Fn Date: Sat, 15 Apr 2023 10:10:30 +0000 Subject: [PATCH 03/69] Translated using Weblate (Galician) Currently translated at 6.3% (27 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/ --- ultrasonic/src/main/res/values-gl/strings.xml | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/res/values-gl/strings.xml b/ultrasonic/src/main/res/values-gl/strings.xml index a6b3daec..86dcf5c1 100644 --- a/ultrasonic/src/main/res/values-gl/strings.xml +++ b/ultrasonic/src/main/res/values-gl/strings.xml @@ -1,2 +1,27 @@ - \ No newline at end of file + + Cargando… + Ocorreu un erro de rede. Por favor comproba a dirección do servidor ou téntao de novo mais tarde. + A API do servidor v%1$s non admite esta función. + Este programa require acceso á rede. Por favor acende a Wi-Fi ou a rede móbil. + Recurso non atopado. Por favor comproba a dirección do servidor. + Non se entende a resposta. Por favor comproba a dirección do servidor. + Erro do certificado HTTPS: %1$s. + Excepción de conexión SSL. Comprobe o certificado do servidor. + Por favor agarde… + Biblioteca + Chat + Reproducindo agora + Reproducir + Pausar + Repetir + Mesturar + Parar + Seguinte + Anterior + Podcast + Non hai canles de Podcasts rexistrados + Podcast + Buscar + Enviar unha mensaxe + \ No newline at end of file From 97556a36e5d15141e2f3765b427282e5c6225179 Mon Sep 17 00:00:00 2001 From: Kaiyang Wu Date: Sun, 16 Apr 2023 17:28:28 +0000 Subject: [PATCH 04/69] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (426 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hans/ --- .../src/main/res/values-zh-rCN/strings.xml | 104 ++++++++++-------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 82215162..ac3c0deb 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -3,11 +3,11 @@ 加载中… 发生网络错误。请检查服务器地址或稍后重试。 服务端 API v%1$s 不支持此功能。 - 此软件需要连接网络,请打开Wi-Fi或移动网络。 + 此软件需要连接网络,请打开 Wi-Fi 或移动网络。 未找到资源,请检查服务器地址。 - 未知回复内容,请检查服务器地址 - HTTPS 证书错误: %1$s. - SSL连接异常。请检查服务器证书。 + 未知回复内容,请检查服务器地址。 + HTTPS 证书错误:%1$s. + SSL 连接异常。请检查服务器证书。 请稍等… 书签 媒体库 @@ -50,20 +50,20 @@ 取消固定 您真的要删除当前选择吗? 群星 - 确定要删除 %1$s吗 + 确定要删除 %1$s 吗 书签已删除。 - 书签设置为 %s。 + 书签设置在 %s。 未下载任何内容 空的播放列表 - 不允许远程控制. 请在您的服务器上的 Users > Settings 打开点唱机模式。 - 关闭远程控制,音乐将在手机上播放 - 离线模式不支持远程控制 - 打开远程控制,音乐将在服务端播放。 - 远程控制不支持,请升级您的 Subsonic服务器。 + 不允许远程控制. 请在您 Subsonic 服务器上的 用户 > 设置 中打开点唱机模式。 + 已关闭远程控制,音乐将在手机上播放。 + 离线模式不支持远程控制。 + 已打开远程控制,音乐将在服务端播放。 + 远程控制不支持,请升级您的 Subsonic 服务器。 远程音量 均衡器 - 关闭 Jukebox - 开启 Jukebox + 关闭点唱机 + 开启点唱机 歌词 保存播放列表 关闭屏幕常亮 @@ -86,7 +86,7 @@ 均衡器 选择预设 错误 - 默认自动点唱机 + 默认使用点唱机 找不到歌词 系统默认 按艺术家排序 @@ -101,7 +101,7 @@ 艺术家 流派 离线 - %s - 已设置服务器 + %s - 设置服务器 随机 收藏夹 歌曲 @@ -110,13 +110,13 @@ \n \n➤ 如果您想试试此应用, 可以添加试用服务器。 \n -\n➤ 可在 设置中编辑服务器配置信息。 +\n➤ 可在 设置 中编辑服务器配置信息。 欢迎! 点击前往设置 关于 公共 已删除播放列表 %s - 播放列表删除失败%s + 播放列表删除失败 %s 下载 退出 设置 @@ -136,8 +136,9 @@ 搜索 找不到歌曲 已选择 %d 首曲目 - 警告:当前没有可用的网络.\n 如果您要使用移动数据,您需要在设置中允许使用流量进行下载 - 错误:没有SD卡 + 警告:当前没有可用的网络。 +\n 如果您要使用移动数据,您需要在设置中允许使用流量进行下载。 + 错误:没有SD卡。 播放全部 所有文件夹 选择文件夹 @@ -199,10 +200,10 @@ 展示比特率和文件后缀 在艺术家姓名后追加比特率和文件后缀 正在播放 - 隐藏来自其他应用的音乐 + 隐藏来自其他应用的音乐。 隐藏其他来源 在安卓系统下次扫描音乐时生效。 - 请填写有效的URL。 + 请填写有效的 URL。 最大专辑 最大艺术家 112 Kbps @@ -215,9 +216,9 @@ 64 Kbps 80 Kbps 96 Kbps - 最大比特率-移动网络 + 最大比特率 - 移动网络 不限制 - 最大比特率-WIFI + 最大比特率 - WIFI 最大歌曲 响应手机、耳机和蓝牙设备的媒体按钮 媒体按钮 @@ -249,7 +250,7 @@ 5 首歌 不限制 请记得在服务器上的 Scrobble 服务中设置您的用户名和密码 - Scrobble我的播放列表 + Scrobble 我的播放列表 1 10 100 @@ -278,8 +279,8 @@ 显示曲目编号 显示歌曲时包括曲目编号 测试连接 - Light - Dark + 亮色 + 暗色 Black 主题 允许自签名 HTTPS 证书 @@ -292,7 +293,7 @@ 如果可用,在艺术家列表中显示艺术家图片 视频 仅未计量的网络用于下载媒体 - 仅使用Wi-Fi进行下载 + 仅使用 Wi-Fi 进行下载 %d kbps 0 B 0.00 GB @@ -330,7 +331,7 @@ 保存为默认 评论 有效期 - %s已从播放列表中移除 + %s 已从播放列表中移除 分享播放列表 分享当前曲目 默认分享问候语 @@ -338,14 +339,14 @@ 分享歌曲通过 分享 显示艺术家 - Multiple Years + 数年 调试选项 将调试日志写入文件 日志文件可在 %1$s/%2$s 获取 %3$s 目录中有 %1$s 个日志文件占用了 ~%2$s MB 空间。您想保留这些吗? 保留文件 删除文件 - 删除日志文件 + 删除日志文件。 在后台下载媒体… 配置服务器 您确定要删除此服务器吗? @@ -373,45 +374,45 @@ %d 首曲目 - 已选择 %d 首歌曲进行固定。 + 已选择 %d 首歌曲进行固定 - 已选择要下载 %d 首歌曲。 + 已选择要下载 %d 首歌曲 - 已选择 %d 首歌曲取消固定。 + 已选择 %d 首歌曲取消固定 %d 首歌曲被删除 - 已将 %d 首歌曲添加到播放队列的末尾。 + 已将 %d 首歌曲添加到播放队列的末尾 - 在当前歌曲之后插入了 %d 首歌曲。 + 在当前歌曲之后插入了 %d 首歌曲 - 一般 API 错误: %1$s + 一般 API 错误:%1$s 服务器未发送任何信息 - LDAP用户不支持以token形式授权连接。 - 用户名或密码错误 - 授权失败,请在 Subsonic server 检查用户权限。 + LDAP 用户不支持以 token 形式授权连接。 + 用户名或密码错误。 + 授权失败,请在 Subsonic 服务器上检查用户权限。 缺少必需的参数。 未找到请求的数据。 - 试用期结束 + 试用期结束。 版本不兼容,请升级 Ultrasonic 应用。 - 不兼容的版本。请升级Subsonic 服务。 + 版本不兼容,请升级 Subsonic 服务器。 为歌曲使用五星评分 500 首歌 - 如果您启用此设置,它将只显示您使用 Ultrasonic 4.0 或更高版本下载的音乐。较早的下载没有下载必要的元数据。您可以在 Pin 和 Save 模式之间切换,以触发缺失元数据的下载。 + 如果您启用此设置,它将只显示您使用 Ultrasonic 4.0 或更高版本下载的音乐。较早的下载没有下载必要的元数据。您可以在固定和保存模式之间切换,以触发缺失元数据的下载。 如果媒体通知仍然存在,请按媒体通知中的播放按钮;否则请打开应用程序开始播放,并重新连接会话到控制器 无法恢复播放 头像图片 你真的想取消固定当前选择吗\? 简体中文(中国大陆) 繁体中文(中国台湾) - + 英语 按时间排序 在媒体库中开始播放后切换到正在播放页面 快进/快退间隔 @@ -421,7 +422,7 @@ 50 首歌 100 首歌 1000 首歌 - 这会强制应用程序始终以未加密的方式发送密码。如果 Subsonic 服务器不支持新的用户身份验证 API,则该选项非常有用。 + 这会强制应用程序始终以未加密的方式发送密码。如果此 Subsonic 服务器不支持新的用户身份验证 API,则该选项非常有用。 在正在播放中展示更多歌曲细节(流派,年份,比特率) 在正在播放中展示更多歌曲细节 离线时也使用 ID3 方法 @@ -431,8 +432,21 @@ 需要通知权限才能进行媒体播放。您可以随时在 Android 设置中授予权限。 对歌曲使用五星评级系统,而不是简单的星标/取消星标。 使用硬件回放(实验性) - 尝试使用手机上的媒体解码器芯片来播放媒体。这可以改善电池使用情况。 + 尝试使用手机上的媒体解码器芯片来播放媒体。这可以改善电池使用情况。部分用户报告启用该选项后播放会有问题! 列表 封面 已支持的功能 + 西班牙语 + 法语 + 意大利语 + 俄语 + 荷兰语 + 日与夜 + 匈牙利语 + 波兰语 + 点唱机 + 葡萄牙语 + 捷克语 + 德语 + 葡萄牙语(巴西) \ No newline at end of file From aede9be97cdebe22151139cc1f5163036429e662 Mon Sep 17 00:00:00 2001 From: Kaiyang Wu Date: Sun, 16 Apr 2023 18:34:05 +0000 Subject: [PATCH 05/69] Translated using Weblate (Chinese (Traditional)) Currently translated at 49.7% (212 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hant/ --- .../src/main/res/values-zh-rTW/strings.xml | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index b9c989d4..d36c4e57 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -1,6 +1,5 @@ - 載入中… 書籤 媒體庫 @@ -32,7 +31,7 @@ 全選 標題 各式歌手 - 您想刪除 %1$s 嗎? + 您想刪除 %1$s 嗎 書籤已移除。 無下載 播放清單是空的 @@ -130,4 +129,88 @@ 已停用 註記 刪除 - + 顯示專輯 + 簡體中文(中國) + 儲存播放清單 + 書籤設置在 %s。 + 不支援遠端控制,請升級您的 Subsonic 伺服器。 + 播客 + 您真的要取消固定目前選取的項目嗎? + Ultrasonic + SSL 連線異常。請檢查伺服器憑證。 + 固定 + 傳送 + 聊天 + 遠端音量 + 頭像 + 您真的要刪除目前選取的項目嗎? + 無法理解答覆,請檢查伺服器位址。 + 荷蘭語 + 已關閉遠端控制,音樂將在手機上播放。 + 德語 + 請稍候… + 取消固定 + 輸入播放清單名稱: + 依照時間排列 + 正在儲存播放清單 \"%s\"… + 系統預設 + HTTPS 憑證錯誤:%1$s. + 波蘭語 + 沒有已註冊的播客頻道 + 傳送訊息 + 打開熒幕常亮 + 義大利語 + 法語 + 此程式需要連結網路。請打開 Wi-Fi 或行動網路。 + 最高評分 + %s - 設定伺服器 + 俄語 + 西班牙語 + 播客 + 儲存播放清單失敗,請稍後再試。 + 關閉熒幕常亮 + 顯示更多 + 打開點唱機 + 選擇預設 + 歌曲 + 離線模式下無法使用遠端控制。 + 隨機播放 + 確定 + 關閉點唱機 + 未找到歌詞 + 葡萄牙語(巴西) + 全部循環播放 + 找不到資源,請檢查伺服器位址。 + 單曲循環播放 + 匈牙利語 + 前往設定 + 繁體中文(臺灣) + 關閉循環播放 + 預設使用點唱機 + 發生網路錯誤。請檢查伺服器位址或稍後重試。 + 已打開遠端控制,音樂將在伺服器上播放。 + 儲存播放清單 + 捷克語 + 英語 + 葡萄牙語 + 歌曲 + 歡迎! + 已刪除播放清單 %s + 要使用 Ultrasonic 播放您的音樂,需要您 自己的伺服器。 +\n +\n➤ 如果您想嘗試此應用程式, 可以添加一個演示伺服器。 +\n +\n➤ 此外,可在 設定 中配置您的伺服器。 + 播放清單刪除失敗 %s + 沒有匹配的結果,請重試 + 找不到媒體 + 已選取 %d 首曲目 + 伺服器 API v%1$s 不支援此功能。 + 已儲存播放清單。 + 更新了 %s 的播放清單資訊 + 更新 %s 的播放清單資訊失敗 + 刷新 + 離線媒體 + 更新資訊 + 不允許遠端控制。請在您在 Subsonic 伺服器上的 使用者 > 設定 中啟用點唱機模式。 + \ No newline at end of file From 732d44cb731131cdff44fef4addcb4d537681e61 Mon Sep 17 00:00:00 2001 From: Eryk Michalak Date: Tue, 18 Apr 2023 20:11:53 +0000 Subject: [PATCH 06/69] Translated using Weblate (Polish) Currently translated at 100.0% (426 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pl/ --- ultrasonic/src/main/res/values-pl/strings.xml | 186 +++++++++++++++--- 1 file changed, 163 insertions(+), 23 deletions(-) diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 1b84f3e0..c75f5e26 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -1,7 +1,6 @@ - - Ładowanie… + Ładowanie… Wystąpił błąd sieci. Proszę sprawdzić adres serwera i spróbować później. Server api v%1$s does not support this function. Ta aplikacja wymaga dostępu do sieci. Proszę włączyć wi-fi lub dane komórkowe. @@ -9,7 +8,7 @@ Brak prawidłowej odpowiedzi. Proszę sprawdzić adres serwera. Błąd certyfikatu HTTPS: %1$s. Błąd połączenia SSL. Proszę sprawdzić certyfikat serwera. - Proszę czekać… + Proszę czekać… Zakładki Biblioteka Czat @@ -39,7 +38,7 @@ Zapisz Odepnij Różni artyści - Czy chcesz usunąć %1$s? + Czy chcesz usunąć %1$s Zakładka usunięta. Zakładka ustawiona na %s. Playlista jest pusta @@ -62,7 +61,7 @@ Playlista została zapisana. Błąd zapisu playlisty. Proszę spróbować później. Wprowadź nazwę playlisty: - Trwa zapis playlisty \"%s\"… + Trwa zapis playlisty \"%s\"… Zapisz playlistę Powtarzaj wszystko Powtarzanie wyłączone @@ -96,7 +95,7 @@ Usunięcie playlisty %s nie powiodło się Zakończ Ustawienia - Refresh + Odśwież Biblioteka mediów Media offline Playlisty @@ -172,8 +171,8 @@ Dołącza bitrate i typ pliku do nazwy artysty Ukrywa pliki muzyczne przed innymi aplikacjami. Ukryj pliki - Efekt widoczny będzie po następnym skanowaniu muzyki przez system Android - Proszę wprowadzić prawidłowy URL + Efekt widoczny będzie po następnym skanowaniu muzyki przez system Android. + Proszę wprowadzić prawidłowy URL. Maksymalna ilość wyników - albumy Maksymalna ilość wyników - artyści 112 Kbps @@ -205,10 +204,10 @@ Sieć Inne ustawienia Ustawienia sterowania odtwarzaniem - Resume when a Bluetooth device is connected - Pause when a Bluetooth device is disconnected - All Bluetooth devices - Only audio (A2DP) devices + Wznów po podłączeniu urządzenia Bluetooth + Wstrzymaj, gdy urządzenie Bluetooth jest odłączone + Wszystkie urządzenia Bluetooth + Tylko urządzenia audio (A2DP) Wyłączone Wznawiaj po podłączeniu słuchawek Aplikacja wznowi zatrzymane odtwarzanie po podpięciu słuchawek. @@ -298,18 +297,18 @@ Udostępnianie Wyświetlaj artystę Z różnych lat - Configured servers - Are you sure you want to delete the server? - Editing server + Skonfigurowane serwery + Czy na pewno chcesz usunąć ten serwer\? + Edycja serwera Dodaj serwer - Are you sure you want to leave and lose your changes? - This field is required - Edit + Czy na pewno chcesz wyjść i utracić dokonane zmiany\? + To pole jest wymagane + Edytuj Usuń - Move up - Move down + Przesuń się w górę + Przesuń się w dół Authentication - Advanced settings + Ustawienia zaawansowane %d utwór %d utwory @@ -327,7 +326,148 @@ Okres próbny się zakończył. Brak zgodności wersji. Uaktualnij aplikację Ultrasonic na Androida. Brak zgodności wersji. Uaktualnij serwer Subsonic. - Użyj pięciu gwiazdek dla utworów - + Pokaż okno potwierdzające usunięcie lub odpięcie utworów + Angielski + Pamiętaj o ustawieniu nazwy użytkownika i hasła do usługi Scrobble na serwerze + Użyj metody ID3 także kiedy nie masz połączenia + Zatrzymaj pliki + Wyłączony tryb losowy + Zatrzymaj + Francuski + Czy na pewno chcesz odpiąć zaznaczone pozycje\? + Użyj niestandardowej lokacji pamięci podręcznej + Wybierz wszystko + Włączony tryb losowy + Następne + Chronologicznie + Otwórz ustawienia + Czeski + Wyślij + Zaznaczono %d utworów + Stwórz udostępnienie na serwerze + Niemiecki + Zgłoś błąd + Pobieranie w tle… + 1000 piosenek + Wspierane funkcje + Polski + Artysta + Holenderski + Węgierski + Zapisanych jest %1$s plików z logami, które zajmują ~%2$s MB miejsca w katalogu %3$s. Czy chcesz je zachować\? + Poprzednie + + Usunięto %d utwór + Usunięto %d utwory + Usunięto %d utworów + Usunięto %d utworów + + Powtarzaj + Nic nie jest pobierane + Rosyjski + Byforowanie… + %s - Ustaw serwer + 50 piosenek + Chiński (Chiny) + Nadpisz język + Odtwórz + Domyślne systemowe + Pobrane + Wyświetlaj bitrate i typ pliku + Portugalski (Brazylia) + Plik z logami jest dostępny w %1$s/%2$s + Usunięte pliki z logami. + Naciśnij przycisk odtwarzania na powiadomieniu o mediach, jeśli jest ono nadal obecne, w przeciwnym razie otwórz aplikację, aby rozpocząć odtwarzanie i ponownie podłącz sesję do kontrolera + Włoski + Portugalski + Kolor serwera + Pauza + Pokaż obraz wykonawcy na liście + Tytuł + Czy na pewno chcesz usunąć zaznaczone pozycje\? + Okładka albumu + Album + 500 piosenek + Udostępnianie spowoduje utworzenie go na serwerze i udostępnienie jego adresu URL. Jeśli ta opcja jest wyłączona, udostępniane są tylko szczegóły utworu + Pokaż Obecnie odtwarzane po kliknięciu przycisku Odtwarzaj + Hiszpański + Wymagane jest ponowne uruchomienie aplikacji po zmianie języka + Nie można wznowić odtwarzania + Awatar + Chiński (Tajwan) + Dzień i noc + Czarny + Zmusza to aplikację do wysyłania hasła w postaci niezaszyfrowanej. Przydatne, jeśli serwer Subsonic nie obsługuje nowego interfejsu API uwierzytelniania dla użytkowników. + Pokaż więcej informacji o utworze w sekcji Obecnie odtwarzane (gatunek, rok, przepustowość) + Pokaż szczegóły w sekcji Obecnie odtwarzane + Pobieraj tylko przez Wi-Fi + Zawsze pytaj o opis i czas wygaśnięcia podczas tworzenia udostępnienia na serwerze + Usuń pliki + + %d utwór zaznaczony do przypięcia + %d utwory zaznaczone do przypięcia + %d utworów zaznaczonych do przypięcia + %d utworów zaznaczonych do przypięcia + + + %d utworów zaznaczonych do pobrania + %d utwory zaznaczone do pobrania + %d utworów zaznaczonych do pobrania + %d utworów zaznaczonych do pobrania + + Odwiedź stronę internetową + + Odpięto %d utwór + Odpięto %d utwory + Odpięto %d utworów + Odpięto %d utworów + + Użyj odtwarzania sprzętowwego (eksperymentalne) + Jukebox + Uwaga: Brak dostępnych sieci do użycia. +\n Jeżeli używasz danych mobilnych, potrzebne może być włączenie płatnych połączeń w ustawieniach. + Przełącz na Obecnie odtwarzane po rozpoczęciu odtwarzania w widoku multimediów + Odstępy między wyszukaniami + Ilość równocześnie pobieranych piosenek + 100 piosenek + Scrobbluj moje odtworzenia + Jeśli włączysz to ustawienie, będzie ono wyświetlać tylko muzykę pobraną za pomocą Ultrasonic w wersji 4.0 lub nowszej. Wcześniejsze pobrane pliki nie zawierają wymaganych metadanych. Możesz przełączać się między trybami Przypinania i Zapisywania, aby wyzwolić pobieranie brakujących metadanych. + Wyświetla obraz wykonawcy na liście wykonawców, jeśli jest dostępny + Pobieraj tylko podczas połączeń niepłatnuch + Udostępnij obecnie odtwarzaną piosenkę + Pokaż okno potwierdzające + Opcje debugowania + Zapisz logi debugowania do pliku + Powiadomienia są wymagane do odtwarzania multimediów. Możesz przyznać uprawnienie do nich w dowolnym momencie w ustawieniach Androida. + Jedna lub więcej funkcji zostało wyłączonych ponieważ serwer ich nie obsługiwał. +\nMożesz uruchomić ten test ponownie kiedykolwiek. + Serwer demonstracyjny + Użyj systemu pięciu gwiazdek do oceniania utworów zamiast po prostu dodawać lub usuwać utwory z ulubionych. + Lista + Okładka + Spróbuj odtworzyć pliki multimedialne za pomocą układu dekodującego w telefonie. Może to zmniejszyć zużycie baterii! + + Dodano %d utwór na koniec kolejki odtwarzania + Dodano %d utwory na koniec kolejki odtwarzania + Dodano %d utworów na koniec kolejki odtwarzania + Dodano %d utworów na koniec kolejki odtwarzania + + + Wstawiono %d utwór po bieżącym utworze + Wstawiono %d utwory po bieżącym utworze + Wstawiono %d utworów po bieżącym utworze + Wstawiono %d utworów po bieżącym utworze + + Ultrasonic to darmowy i otwarty klient strumieniowego przesyłania muzyki dla serwerów kompatybilnych z API Subsonic (wersja 1.7.0 lub nowsza). +\n +\nDzięki Ultrasonic możesz łatwo przesyłać strumieniowo lub pobierać muzykę z komputera domowego na telefon za pomocą serwera multimediów kompatybilnego z Subsonic. Oprogramowanie serwera Subsonic wymaga oddzielnej konfiguracji od Ultrasonic. +\n +\nDomyślnie Ultrasonic nie jest skonfigurowane. Po skonfigurowaniu własnego serwera zmień ustawienia serwera, aby połączyć się z komputerem. + Aby używać Ultrasonic z własną muzyką, potrzebujesz własnego serwera. +\n +\n➤ Jeśli chcesz najpierw wypróbować aplikację, możesz teraz dodać serwer demonstracyjny. +\n +\n➤ W przeciwnym razie możesz skonfigurować serwer w ustawieniach. + \ No newline at end of file From 6694d6f60bb6e0ea2a36d54ccb49c1079a7fd23c Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Thu, 20 Apr 2023 11:24:28 +0000 Subject: [PATCH 07/69] Update Gradle plugin to v8 --- fastlane/metadata/android/en-US/full_description.txt | 4 ++-- gradle.properties | 11 ++++++++++- gradle/libs.versions.toml | 2 +- ultrasonic/build.gradle | 1 + ultrasonic/src/main/AndroidManifest.xml | 1 - .../main/kotlin/org/moire/ultrasonic/util/Storage.kt | 3 ++- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index fcf6a32d..18d5f968 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -3,12 +3,12 @@ Ultrasonic is a Subsonic (and compatible servers) client to Android. You can use Main features: * Thin * Fast -* Dark and light theme +* Material theme with dark and light variants * Multiple server support * Offline Mode * Bookmarks * Playlists on server -* Ramdom play +* Random play * Jukebox mode * Server chat * And much more!!! diff --git a/gradle.properties b/gradle.properties index a7c0d4f8..7ab3793a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,10 +4,19 @@ org.gradle.configureondemand=true org.gradle.caching=true org.gradle.jvmargs=-Xmx2g -XX:+UseParallelGC - kotlin.incremental=true kotlin.caching.enabled=true kotlin.incremental.usePreciseJavaTracking=true android.useAndroidX=true android.enableJetifier=false + +# This properties enables transitive Resource classes, which decreases build time, +# but could lead to problems referencing Resources. Set them to false if needed. +android.nonTransitiveRClass=true +android.nonFinalResIds=true + +# This config was suggested by Android Studio to reduce build time +# It can be removed if it makes problems +org.gradle.unsafe.configuration-cache=true + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca7906c4..0e8244f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ gradle = "7.6" navigation = "2.5.3" -gradlePlugin = "7.4.2" +gradlePlugin = "8.0.0" androidxcore = "1.10.0" ktlint = "0.43.2" ktlintGradle = "11.3.1" diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index d3d23252..bf38f5cf 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -56,6 +56,7 @@ android { buildFeatures { viewBinding true dataBinding true + buildConfig true } compileOptions { diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 8be19a15..a9b35248 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -7,7 +7,6 @@ - diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt index 73022b81..f6b5b9d2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt @@ -16,7 +16,8 @@ import timber.log.Timber /** * Provides filesystem access abstraction which works - * both on File based paths and Storage Access Framework Uris + * both on File based paths (when using the internal directory for storing media files) + * and Storage Access Framework Uris (when using a custom directory) */ object Storage { From 4f59c4d3adaaba0572d0d08eeec9af1976c5b2fe Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Thu, 20 Apr 2023 11:25:25 +0000 Subject: [PATCH 08/69] Fix shuffle --- ultrasonic/build.gradle | 2 +- .../ultrasonic/adapters/TrackViewBinder.kt | 4 - .../ultrasonic/adapters/TrackViewHolder.kt | 2 +- .../ultrasonic/fragment/NowPlayingFragment.kt | 4 +- .../ultrasonic/fragment/PlayerFragment.kt | 105 +++++++------- .../ultrasonic/playback/PlaybackService.kt | 42 ++++++ .../service/MediaPlayerController.kt | 137 +++++++++++++++--- .../service/MediaPlayerLifecycleSupport.kt | 8 +- .../org/moire/ultrasonic/service/RxBus.kt | 6 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 6 +- 10 files changed, 225 insertions(+), 91 deletions(-) diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 74e782ff..89814f83 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -34,7 +34,7 @@ android { minifyEnabled false multiDexEnabled true testCoverageEnabled true - applicationIdSuffix ".debug" + applicationIdSuffix '.debug' } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 62612082..bca305aa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -62,8 +62,6 @@ class TrackViewBinder( diffAdapter.isSelected(item.longId) ) - // Timber.v("Setting listeners") - holder.itemView.setOnLongClickListener { if (onContextMenuClick != null) { val popup = createContextMenu(holder.itemView, track) @@ -116,8 +114,6 @@ class TrackViewBinder( if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus } - - // Timber.v("Setting listeners done") } override fun onViewRecycled(holder: TrackViewHolder) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 630c00cf..605b66d6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -131,7 +131,7 @@ class TrackViewHolder(val view: View) : // Create new Disposable for the new Subscriptions rxBusSubscription = CompositeDisposable() rxBusSubscription!! += RxBus.playerStateObservable.subscribe { - setPlayIcon(it.index == bindingAdapterPosition && it.track?.id == song.id) + setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition) } rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index 9ca26871..de223e35 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -148,10 +148,10 @@ class NowPlayingFragment : Fragment() { if (abs(deltaX) > MIN_DISTANCE) { // left or right if (deltaX < 0) { - mediaPlayerController.previous() + mediaPlayerController.seekToPrevious() } if (deltaX > 0) { - mediaPlayerController.next() + mediaPlayerController.seekToNext() } } else if (abs(deltaY) > MIN_DISTANCE) { if (deltaY < 0) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 4857f708..8797d48f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -154,6 +154,8 @@ class PlayerFragment : private lateinit var pauseButton: View private lateinit var stopButton: View private lateinit var playButton: View + private lateinit var previousButton: MaterialButton + private lateinit var nextButton: MaterialButton private lateinit var shuffleButton: View private lateinit var repeatButton: MaterialButton private lateinit var progressBar: SeekBar @@ -196,6 +198,8 @@ class PlayerFragment : pauseButton = view.findViewById(R.id.button_pause) stopButton = view.findViewById(R.id.button_stop) playButton = view.findViewById(R.id.button_start) + nextButton = view.findViewById(R.id.button_next) + previousButton = view.findViewById(R.id.button_previous) repeatButton = view.findViewById(R.id.button_repeat) fiveStar1ImageView = view.findViewById(R.id.song_five_star_1) fiveStar2ImageView = view.findViewById(R.id.song_five_star_2) @@ -259,9 +263,7 @@ class PlayerFragment : previousButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.previous() - onCurrentChanged() - onSliderProgressChanged() + mediaPlayerController.seekToPrevious() } } @@ -272,9 +274,7 @@ class PlayerFragment : nextButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.next() - onCurrentChanged() - onSliderProgressChanged() + mediaPlayerController.seekToNext() } } @@ -285,16 +285,12 @@ class PlayerFragment : pauseButton.setOnClickListener { launch(CommunicationError.getHandler(context)) { mediaPlayerController.pause() - onCurrentChanged() - onSliderProgressChanged() } } stopButton.setOnClickListener { launch(CommunicationError.getHandler(context)) { mediaPlayerController.reset() - onCurrentChanged() - onSliderProgressChanged() } } @@ -304,8 +300,6 @@ class PlayerFragment : launch(CommunicationError.getHandler(context)) { mediaPlayerController.play() - onCurrentChanged() - onSliderProgressChanged() } } @@ -342,7 +336,6 @@ class PlayerFragment : override fun onStopTrackingTouch(seekBar: SeekBar) { launch(CommunicationError.getHandler(context)) { mediaPlayerController.seekTo(progressBar.progress) - onSliderProgressChanged() } } @@ -367,11 +360,13 @@ class PlayerFragment : // Observe playlist changes and update the UI rxBusSubscription += RxBus.playlistObservable.subscribe { onPlaylistChanged() - onSliderProgressChanged() + updateSeekBar() } rxBusSubscription += RxBus.playerStateObservable.subscribe { update() + updateTitle(it.state) + updateButtonStates(it.state) } // Query the Jukebox state in an IO Context @@ -432,7 +427,7 @@ class PlayerFragment : } else { // Download list and Album art must be updated when resumed onPlaylistChanged() - onCurrentChanged() + onTrackChanged() } val handler = Handler(Looper.getMainLooper()) @@ -764,10 +759,9 @@ class PlayerFragment : if (cancel?.isCancellationRequested == true) return val mediaPlayerController = mediaPlayerController if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) { - onCurrentChanged() + onTrackChanged() } - onSliderProgressChanged() - requireActivity().invalidateOptionsMenu() + updateSeekBar() } private fun savePlaylistInBackground(playlistName: String) { @@ -827,12 +821,9 @@ class PlayerFragment : } // Create listener - val clickHandler: ((Track, Int) -> Unit) = { _, pos -> - mediaPlayerController.seekTo(pos, 0) - mediaPlayerController.prepare() - mediaPlayerController.play() - onCurrentChanged() - onSliderProgressChanged() + val clickHandler: ((Track, Int) -> Unit) = { _, listPos -> + val mediaIndex = mediaPlayerController.getUnshuffledIndexOf(listPos) + mediaPlayerController.play(mediaIndex) } viewAdapter.register( @@ -931,6 +922,7 @@ class PlayerFragment : if (actionState == ACTION_STATE_IDLE && dragging) { dragging = false // Move the item in the playlist separately + Timber.i("Moving item %s to %s", startPosition, endPosition) mediaPlayerController.moveItemInPlaylist(startPosition, endPosition) } } @@ -1010,7 +1002,8 @@ class PlayerFragment : private fun onPlaylistChanged() { val mediaPlayerController = mediaPlayerController - val list = mediaPlayerController.playlist + // Try to display playlist in play order + val list = mediaPlayerController.playlistInPlayOrder emptyTextView.setText(R.string.playlist_empty) viewAdapter.submitList(list.map(MediaItem::toTrack)) @@ -1020,7 +1013,7 @@ class PlayerFragment : updateRepeatButtonState(mediaPlayerController.repeatMode) } - private fun onCurrentChanged() { + private fun onTrackChanged() { currentSong = mediaPlayerController.currentMediaItem?.toTrack() scrollToCurrent() @@ -1064,7 +1057,7 @@ class PlayerFragment : it.loadImage(albumArtImageView, currentSong, true, 0) } - displaySongRating() + updateSongRating() } else { currentSong = null songTitleTextView.text = null @@ -1078,24 +1071,27 @@ class PlayerFragment : it.loadImage(albumArtImageView, null, true, 0) } } + + // TODO: It would be a lot nicer if MediaPlayerController would send an event + // when this is necessary instead of updating every time + updateSongRating() + + nextButton.isEnabled = mediaPlayerController.canSeekToNext() + previousButton.isEnabled = mediaPlayerController.canSeekToPrevious() } - @Suppress("LongMethod") @Synchronized - private fun onSliderProgressChanged() { - + private fun updateSeekBar() { + Timber.i("Calling updateSeekBar") val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled val millisPlayed: Int = max(0, mediaPlayerController.playerPosition) val duration: Int = mediaPlayerController.playerDuration val playbackState: Int = mediaPlayerController.playbackState - val isPlaying = mediaPlayerController.isPlaying - if (cancellationToken.isCancellationRequested) return if (currentSong != null) { positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true) durationTextView.text = Util.formatTotalDuration(duration.toLong(), true) - progressBar.max = - if (duration == 0) 100 else duration // Work-around for apparent bug. + progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug. progressBar.progress = millisPlayed progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled } else { @@ -1107,18 +1103,18 @@ class PlayerFragment : } val progress = mediaPlayerController.bufferedPercentage + updateBufferProgress(playbackState, progress) + } + private fun updateTitle(playbackState: Int) { when (playbackState) { Player.STATE_BUFFERING -> { - val downloadStatus = resources.getString( R.string.download_playerstate_loading ) - progressBar.secondaryProgress = progress setTitle(this@PlayerFragment, downloadStatus) } Player.STATE_READY -> { - progressBar.secondaryProgress = progress if (mediaPlayerController.isShufflePlayEnabled) { setTitle( this@PlayerFragment, @@ -1128,13 +1124,22 @@ class PlayerFragment : setTitle(this@PlayerFragment, R.string.common_appname) } } - Player.STATE_IDLE, - Player.STATE_ENDED, - -> { - } + Player.STATE_IDLE, Player.STATE_ENDED -> {} else -> setTitle(this@PlayerFragment, R.string.common_appname) } + } + private fun updateBufferProgress(playbackState: Int, progress: Int) { + when (playbackState) { + Player.STATE_BUFFERING, Player.STATE_READY -> { + progressBar.secondaryProgress = progress + } + else -> { } + } + } + + private fun updateButtonStates(playbackState: Int) { + val isPlaying = mediaPlayerController.isPlaying when (playbackState) { Player.STATE_READY -> { pauseButton.isVisible = isPlaying @@ -1152,10 +1157,6 @@ class PlayerFragment : playButton.isVisible = true } } - - // TODO: It would be a lot nicer if MediaPlayerController would send an event - // when this is necessary instead of updating every time - displaySongRating() } private fun seek(forward: Boolean) { @@ -1189,18 +1190,14 @@ class PlayerFragment : // Right to Left swipe if (e1X - e2X > swipeDistance && absX > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.next() - onCurrentChanged() - onSliderProgressChanged() + mediaPlayerController.seekToNext() return true } // Left to Right swipe if (e2X - e1X > swipeDistance && absX > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.previous() - onCurrentChanged() - onSliderProgressChanged() + mediaPlayerController.seekToPrevious() return true } @@ -1208,7 +1205,6 @@ class PlayerFragment : if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000) - onSliderProgressChanged() return true } @@ -1216,7 +1212,6 @@ class PlayerFragment : if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000) - onSliderProgressChanged() return true } return false @@ -1237,7 +1232,7 @@ class PlayerFragment : return false } - private fun displaySongRating() { + private fun updateSongRating() { var rating = 0 if (currentSong?.userRating != null) { @@ -1253,7 +1248,7 @@ class PlayerFragment : private fun setSongRating(rating: Int) { if (currentSong == null) return - displaySongRating() + updateSongRating() mediaPlayerController.setSongRating(rating) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 019263db..7603f94f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -26,9 +26,12 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder +import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import io.reactivex.rxjava3.disposables.CompositeDisposable +import java.util.Random import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -175,6 +178,27 @@ class PlaybackService : player.setWakeMode(getWakeModeFlag()) } + // Set a listener to reset the ShuffleOrder + rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle -> + val len = player.currentTimeline.windowCount + + Timber.i("Resetting shuffle order, isShuffled: %s", shuffle) + + // If disabling Shuffle return early + if (!shuffle) { + return@subscribe player.setShuffleOrder(UnshuffledShuffleOrder(len)) + } + + // Get the position of the current track in the unshuffled order + val cur = player.currentMediaItemIndex + val seed = System.currentTimeMillis() + val random = Random(seed) + + val list = createShuffleListFromCurrentIndex(cur, len, random) + Timber.i("New Shuffle order: %s", list.joinToString { it.toString() }) + player.setShuffleOrder(DefaultShuffleOrder(list, seed)) + } + // Listen to the shutdown command rxBusSubscription += RxBus.shutdownCommandObservable.subscribe { Timber.i("Received destroy command via Rx") @@ -185,6 +209,24 @@ class PlaybackService : isStarted = true } + fun createShuffleListFromCurrentIndex( + currentIndex: Int, + length: Int, + random: Random + ): IntArray { + val list = IntArray(length) { it } + + // Shuffle the remaining items using a swapping algorithm + for (i in currentIndex + 1 until length) { + val swapIndex = (currentIndex + 1) + random.nextInt(i - currentIndex) + val swapItem = list[i] + list[i] = list[swapIndex] + list[swapIndex] = swapItem + } + + return list + } + private val listener: Player.Listener = object : Player.Listener { override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { cacheNextSongs() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index b7f81c09..c421a122 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -11,11 +11,14 @@ import android.content.Context import android.os.Handler import android.os.Looper import android.widget.Toast +import androidx.annotation.IntRange import androidx.media3.common.C import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player +import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT +import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Timeline @@ -30,6 +33,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.app.UApp @@ -60,6 +64,7 @@ class MediaPlayerController( private val externalStorageMonitor: ExternalStorageMonitor, val context: Context ) : KoinComponent { + private val activeServerProvider: ActiveServerProvider by inject() private var created = false @@ -96,6 +101,14 @@ class MediaPlayerController( * We run the event through RxBus in order to throttle them */ override fun onTimelineChanged(timeline: Timeline, reason: Int) { + val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled) + Timber.w("On timeline changed. First shuffle play at index: %s", start) + deferredPlay?.let { + Timber.w("Executing deferred shuffle play") + it() + deferredPlay = null + } + RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack)) } @@ -150,19 +163,21 @@ class MediaPlayerController( override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { val timeline: Timeline = controller!!.currentTimeline - var windowIndex = timeline.getFirstWindowIndex( /* shuffleModeEnabled= */true) + var windowIndex = timeline.getFirstWindowIndex(true) var count = 0 Timber.d("Shuffle: windowIndex: $windowIndex, at: $count") while (windowIndex != C.INDEX_UNSET) { count++ windowIndex = timeline.getNextWindowIndex( - windowIndex, REPEAT_MODE_OFF, /* shuffleModeEnabled= */true + windowIndex, REPEAT_MODE_OFF, true ) Timber.d("Shuffle: windowIndex: $windowIndex, at: $count") } } } + private var deferredPlay: (() -> Unit)? = null + private var cachedMediaItem: MediaItem? = null fun onCreate(onCreated: () -> Unit) { @@ -259,7 +274,7 @@ class MediaPlayerController( private fun publishPlaybackState() { val newState = RxBus.StateWithTrack( track = currentMediaItem?.toTrack(), - index = currentMediaItemIndex, + index = if (isShufflePlayEnabled) getCurrentShuffleIndex() else currentMediaItemIndex, isPlaying = isPlaying, state = playbackState ) @@ -316,6 +331,8 @@ class MediaPlayerController( @Synchronized fun play(index: Int) { controller?.seekTo(index, 0L) + // FIXME CHECK ITS NOT MAKING PROBLEMS + controller?.prepare() controller?.play() } @@ -404,6 +421,7 @@ class MediaPlayerController( } if (shuffle) isShufflePlayEnabled = true + Timber.w("Adding ${mediaItems.size} media items") controller?.addMediaItems(insertAt, mediaItems) prepare() @@ -411,10 +429,19 @@ class MediaPlayerController( // Playback doesn't start correctly when the player is in STATE_ENDED. // So we need to call seek before (this is what play(0,0)) does. // We can't just use play(0,0) then all random playlists will start with the first track. - // This means that we need to generate the random first track ourselves. + // Additionally the shuffle order becomes clear on after some time, so we need to wait for + // the right event, and can start playback only then. if (autoPlay) { - val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled) ?: 0 - play(start) + if (isShufflePlayEnabled) { + deferredPlay = { + val start = controller?.currentTimeline + ?.getFirstWindowIndex(isShufflePlayEnabled) ?: 0 + Timber.i("Deferred shuffle play starting now at index: %s", start) + play(start) + } + } else { + play(0) + } } } @@ -422,6 +449,8 @@ class MediaPlayerController( var isShufflePlayEnabled: Boolean get() = controller?.shuffleModeEnabled == true set(enabled) { + Timber.i("Shuffle is now enabled: %s", enabled) + RxBus.shufflePlayPublisher.onNext(enabled) controller?.shuffleModeEnabled = enabled } @@ -431,11 +460,17 @@ class MediaPlayerController( return isShufflePlayEnabled } + /** + * Returns an estimate of the percentage in the current content up to which data is + * buffered, or 0 if no estimate is available. + */ + @get:IntRange(from = 0, to = 100) val bufferedPercentage: Int get() = controller?.bufferedPercentage ?: 0 @Synchronized fun moveItemInPlaylist(oldPos: Int, newPos: Int) { + // TODO: This currently does not care about shuffle position. controller?.moveMediaItem(oldPos, newPos) } @@ -494,15 +529,25 @@ class MediaPlayerController( } @Synchronized - fun previous() { + fun seekToPrevious() { controller?.seekToPrevious() } @Synchronized - operator fun next() { + fun canSeekToPrevious(): Boolean { + return controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true + } + + @Synchronized + fun seekToNext() { controller?.seekToNext() } + @Synchronized + fun canSeekToNext(): Boolean { + return controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true + } + @Synchronized fun reset() { controller?.clearMediaItems() @@ -693,15 +738,15 @@ class MediaPlayerController( if (currentMediaItem == null) return val song = currentMediaItem!!.toTrack() song.userRating = rating - Thread { - try { - getMusicService().setRating(song.id, rating) - } catch (e: Exception) { - Timber.e(e) + mainScope.launch { + withContext(Dispatchers.IO) { + try { + getMusicService().setRating(song.id, rating) + } catch (e: Exception) { + Timber.e(e) + } } - }.start() - // TODO this would be better handled with a Rx command - // updateNotification() + } } val currentMediaItem: MediaItem? @@ -710,9 +755,65 @@ class MediaPlayerController( val currentMediaItemIndex: Int get() = controller?.currentMediaItemIndex ?: -1 + fun getCurrentShuffleIndex(): Int { + val currentMediaItemIndex = controller?.currentMediaItemIndex ?: return -1 + return getShuffledIndexOf(currentMediaItemIndex) + } + + /** + * Loops over the timeline windows to find the entry which matches the given closure. + * + * @param searchClosure Determines the condition which the searched for window needs to match. + * @param timeline the timeline to search in. + * @return the index of the window that satisfies the search condition, + * or [C.INDEX_UNSET] if not found. + */ + private fun getWindowIndexWhere(searchClosure: (Int, Int) -> Boolean): Int { + val timeline = controller?.currentTimeline!! + var windowIndex = timeline.getFirstWindowIndex(true) + var count = 0 + while (windowIndex != C.INDEX_UNSET) { + if (searchClosure(count, windowIndex)) return count + count++ + windowIndex = timeline.getNextWindowIndex( + windowIndex, REPEAT_MODE_OFF, true + ) + } + + return C.INDEX_UNSET + } + + /** + * Returns the index of the shuffled position of the current playback item given its original + * position in the unshuffled timeline. + * + * @param searchPosition The index of the item in the unshuffled timeline to search for + * in the shuffled timeline. + * @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found. + */ + fun getShuffledIndexOf(searchPosition: Int): Int { + return getWindowIndexWhere { _, windowIndex -> windowIndex == searchPosition } + } + + /** + * Returns the index of the unshuffled position of the current playback item given its shuffled + * position in the shuffled timeline. + * + * @param shufflePosition the index of the item in the shuffled timeline to search for in the + * unshuffled timeline. + * @return the index of the item in the unshuffled timeline, or [C.INDEX_UNSET] if not found. + */ + fun getUnshuffledIndexOf(shufflePosition: Int): Int { + return getWindowIndexWhere { count, _ -> count == shufflePosition } + } + val mediaItemCount: Int get() = controller?.mediaItemCount ?: 0 + fun getMediaItemAt(index: Int): MediaItem? { + return controller?.getMediaItemAt(index) + } + val playlistSize: Int get() = controller?.currentTimeline?.windowCount ?: 0 @@ -721,10 +822,6 @@ class MediaPlayerController( return Util.getPlayListFromTimeline(controller?.currentTimeline, false) } - fun getMediaItemAt(index: Int): MediaItem? { - return controller?.getMediaItemAt(index) - } - val playlistInPlayOrder: List get() { return Util.getPlayListFromTimeline( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index d412d0e6..c05805e5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -182,8 +182,8 @@ class MediaPlayerLifecycleSupport : KoinComponent { when (keyCode) { KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause() - KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.previous() - KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.next() + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.seekToPrevious() + KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.seekToNext() KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop() KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play() KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() @@ -226,8 +226,8 @@ class MediaPlayerLifecycleSupport : KoinComponent { // no need to call anything if (isRunning) mediaPlayerController.resumeOrPlay() - Constants.CMD_NEXT -> mediaPlayerController.next() - Constants.CMD_PREVIOUS -> mediaPlayerController.previous() + Constants.CMD_NEXT -> mediaPlayerController.seekToNext() + Constants.CMD_PREVIOUS -> mediaPlayerController.seekToPrevious() Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause() Constants.CMD_STOP -> mediaPlayerController.stop() Constants.CMD_PAUSE -> mediaPlayerController.pause() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index 48a1061f..f4b62de0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -20,9 +20,13 @@ class RxBus { private fun mainThread() = AndroidSchedulers.from(Looper.getMainLooper()) + val shufflePlayPublisher: PublishSubject = + PublishSubject.create() + val shufflePlayObservable: Observable = + shufflePlayPublisher + var activeServerChangingPublisher: PublishSubject = PublishSubject.create() - // Subscribers should be called synchronously, not on another thread var activeServerChangingObservable: Observable = activeServerChangingPublisher diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 6ba28594..cdcd5443 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -757,7 +757,7 @@ object Util { fun getPlayListFromTimeline( timeline: Timeline?, - shuffle: Boolean, + isShuffled: Boolean, firstIndex: Int? = null, count: Int? = null ): List { @@ -765,13 +765,13 @@ object Util { if (timeline.windowCount < 1) return emptyList() val playlist: MutableList = mutableListOf() - var i = firstIndex ?: timeline.getFirstWindowIndex(false) + var i = firstIndex ?: timeline.getFirstWindowIndex(isShuffled) if (i == C.INDEX_UNSET) return emptyList() while (i != C.INDEX_UNSET && (count != playlist.count())) { val window = timeline.getWindow(i, Timeline.Window()) playlist.add(window.mediaItem) - i = timeline.getNextWindowIndex(i, Player.REPEAT_MODE_OFF, shuffle) + i = timeline.getNextWindowIndex(i, Player.REPEAT_MODE_OFF, isShuffled) } return playlist } From e9b602890a0e4ed177b08e601d659ef3bb31ec2e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 20 Apr 2023 11:34:31 +0000 Subject: [PATCH 09/69] Update media3 to v1.0.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e8244f7..92c7607c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ ktlint = "0.43.2" ktlintGradle = "11.3.1" detekt = "1.22.0" preferences = "1.2.0" -media3 = "1.0.0" +media3 = "1.0.1" androidSupport = "1.6.0" materialDesign = "1.8.0" From e62b8972e7b77fdcae32a5aa2f6f400fef7a3aca Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 21 Apr 2023 13:34:02 +0000 Subject: [PATCH 10/69] Update dependency gradle to v8.1.1 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a21c6ebe..8707e8b5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 5fcbf59e0e365edb53deab02da4f9da24a2e490d Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 22 Apr 2023 11:25:31 +0200 Subject: [PATCH 11/69] Remove Jitpack repo (was from custom media3 build) --- build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index a4f4bb4b..d9cf0212 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,6 @@ buildscript { google() mavenCentral() maven { url "https://plugins.gradle.org/m2/" } - maven { url 'https://jitpack.io' } } dependencies { classpath libs.gradle @@ -34,7 +33,6 @@ allprojects { repositories { mavenCentral() google() - maven { url 'https://jitpack.io' } } // Set Kotlin JVM target to the same for all subprojects From f59e039c497eeeb849d853f282920f1b20251ca2 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 23 Apr 2023 11:55:08 +0200 Subject: [PATCH 12/69] Fix missing context --- .../org/moire/ultrasonic/adapters/HeaderViewBinder.kt | 7 +++++-- .../org/moire/ultrasonic/fragment/NowPlayingFragment.kt | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt index 1d0efedd..c2d25b0c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt @@ -51,11 +51,14 @@ class HeaderViewBinder( val resources = context.resources val artworkSelection = random.nextInt(item.childCount) + val size = Util.getAlbumImageSize(context) imageLoaderProvider.executeOn { it.loadImage( - holder.coverArtView, item.entries[artworkSelection], false, - Util.getAlbumImageSize(context) + holder.coverArtView, + item.entries[artworkSelection], + false, + size ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index de223e35..eb192af7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -96,13 +96,14 @@ class NowPlayingFragment : Fragment() { if (file != null) { val title = file.title val artist = file.artist + val size = getNotificationImageSize(requireContext()) imageLoaderProvider.executeOn { it.loadImage( nowPlayingAlbumArtImage, file, false, - getNotificationImageSize(requireContext()) + size ) } From 10767d2d5bf29b5dbe8d9f74be7a3d8d225243b0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 25 Apr 2023 05:32:25 +0000 Subject: [PATCH 13/69] Update kotlin monorepo to v1.8.21 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92c7607c..5f1b6f0e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ materialDesign = "1.8.0" constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.1" -kotlin = "1.8.20" +kotlin = "1.8.21" kotlinxCoroutines = "1.6.4" kotlinxGuava = "1.6.4" viewModelKtx = "2.6.1" From 7b414a3a23b3e88aa777e7b20fcd423142e48b89 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 25 Apr 2023 14:32:15 +0000 Subject: [PATCH 14/69] Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.3.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92c7607c..552e2d83 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ navigation = "2.5.3" gradlePlugin = "8.0.0" androidxcore = "1.10.0" ktlint = "0.43.2" -ktlintGradle = "11.3.1" +ktlintGradle = "11.3.2" detekt = "1.22.0" preferences = "1.2.0" media3 = "1.0.1" From ca2c5483c09f34d71acbfc0cb4071f0b8656f839 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 26 Apr 2023 07:32:20 +0000 Subject: [PATCH 15/69] Update dependency org.junit.vintage:junit-vintage-engine to v5.9.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92c7607c..da96e31f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ koin = "3.3.2" picasso = "2.8" junit4 = "4.13.2" -junit5 = "5.9.2" +junit5 = "5.9.3" mockito = "5.2.0" mockitoKotlin = "4.1.0" kluent = "1.72" From 7abca537c9b43970a2cf2a6b49b5f7d7c086e123 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 29 Apr 2023 14:32:22 +0000 Subject: [PATCH 16/69] Update kluent to v1.73 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92c7607c..a2374504 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ junit4 = "4.13.2" junit5 = "5.9.2" mockito = "5.2.0" mockitoKotlin = "4.1.0" -kluent = "1.72" +kluent = "1.73" apacheCodecs = "1.15" robolectric = "4.10" timber = "5.0.1" From d750c8460692765beeede3dfe64d9599526a76a7 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 1 May 2023 16:32:12 +0000 Subject: [PATCH 17/69] Update dependency com.android.tools.build:gradle to v8.0.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92c7607c..88e5a76a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ gradle = "7.6" navigation = "2.5.3" -gradlePlugin = "8.0.0" +gradlePlugin = "8.0.1" androidxcore = "1.10.0" ktlint = "0.43.2" ktlintGradle = "11.3.1" From 0cb795294301ec1240ae019f9d2435475898a60f Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 4 May 2023 19:32:12 +0000 Subject: [PATCH 18/69] Update dependency org.robolectric:robolectric to v4.10.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92c7607c..beb567d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ mockito = "5.2.0" mockitoKotlin = "4.1.0" kluent = "1.72" apacheCodecs = "1.15" -robolectric = "4.10" +robolectric = "4.10.2" timber = "5.0.1" fastScroll = "2.0.1" colorPicker = "2.2.4" From 96073125ca07010244430c6ceec869ad124cedd7 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 7 May 2023 11:19:01 +0200 Subject: [PATCH 19/69] Correctly enable Artists pictures by default (was enabled in Settings mit not in Code) --- .../src/main/kotlin/org/moire/ultrasonic/util/Settings.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index 501573eb..65dbd92b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -181,7 +181,7 @@ object Settings { var firstRunExecuted by BooleanSetting(getKey(R.string.setting_key_first_run_executed), false) val shouldShowArtistPicture - by BooleanSetting(getKey(R.string.setting_key_show_artist_picture), false) + by BooleanSetting(getKey(R.string.setting_key_show_artist_picture), true) @JvmStatic var chatRefreshInterval by StringIntSetting( From 2a02c94c8fc5db8445d130ca24c49f624b7a59ae Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sun, 7 May 2023 09:27:24 +0000 Subject: [PATCH 20/69] Introduce a RatingManager that takes care of receiving and passing ratings... --- .../ultrasonic/adapters/AlbumRowDelegate.kt | 34 ++--- .../ultrasonic/adapters/TrackViewHolder.kt | 52 +++----- .../org/moire/ultrasonic/data/RatingUpdate.kt | 16 +++ .../ultrasonic/fragment/PlayerFragment.kt | 119 ++++++++++-------- .../playback/AutoMediaBrowserCallback.kt | 31 +++-- .../service/MediaPlayerController.kt | 101 +++++++-------- .../service/MediaPlayerLifecycleSupport.kt | 14 ++- .../moire/ultrasonic/service/MusicService.kt | 4 +- .../moire/ultrasonic/service/RatingManager.kt | 87 +++++++++++++ .../org/moire/ultrasonic/service/RxBus.kt | 13 ++ ultrasonic/src/main/res/menu/nowplaying.xml | 7 ++ ultrasonic/src/main/res/values/strings.xml | 4 +- 12 files changed, 299 insertions(+), 183 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/RatingUpdate.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RatingManager.kt diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt index dfbce65a..c54cb32a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt @@ -15,17 +15,17 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.media3.common.HeartRating import androidx.recyclerview.widget.RecyclerView import com.drakeet.multitype.ItemViewDelegate import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.domain.Album -import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.LayoutType -import org.moire.ultrasonic.util.Settings.shouldUseId3Tags -import timber.log.Timber /** * Creates a Row in a RecyclerView which contains the details of an Album @@ -112,27 +112,13 @@ open class AlbumRowDelegate( private fun onStarClick(entry: Album, star: ImageView) { entry.starred = !entry.starred star.setImageResource(if (entry.starred) starDrawable else starHollowDrawable) - val musicService = getMusicService() - Thread { - val useId3 = shouldUseId3Tags - try { - if (entry.starred) { - musicService.star( - if (!useId3) entry.id else null, - if (useId3) entry.id else null, - null - ) - } else { - musicService.unstar( - if (!useId3) entry.id else null, - if (useId3) entry.id else null, - null - ) - } - } catch (all: Exception) { - Timber.e(all) - } - }.start() + + RxBus.ratingSubmitter.onNext( + RatingUpdate( + entry.id, + HeartRating(entry.starred) + ) + ) } override fun onCreateViewHolder(context: Context, parent: ViewGroup): ListViewHolder { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 605b66d6..79b74069 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -10,6 +10,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.MutableLiveData +import androidx.media3.common.HeartRating import androidx.recyclerview.widget.RecyclerView import com.google.android.material.progressindicator.CircularProgressIndicator import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -19,10 +20,10 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.DownloadService import org.moire.ultrasonic.service.DownloadState -import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.util.Settings @@ -81,7 +82,6 @@ class TrackViewHolder(val view: View) : draggable: Boolean, isSelected: Boolean = false ) { - // Timber.v("Setting song") val useFiveStarRating = Settings.useFiveStarRating entry = song @@ -118,9 +118,9 @@ class TrackViewHolder(val view: View) : } if (useFiveStarRating) { - setFiveStars(entry?.userRating ?: 0) + updateFiveStars(entry?.userRating ?: 0) } else { - setSingleStar(entry!!.starred) + updateSingleStar(entry!!.starred) } if (song.isVideo) { @@ -165,48 +165,32 @@ class TrackViewHolder(val view: View) : } } - private fun setupStarButtons(song: Track, useFiveStarRating: Boolean) { + private fun setupStarButtons(track: Track, useFiveStarRating: Boolean) { if (useFiveStarRating) { // Hide single star star.isGone = true rating.isVisible = true - val rating = if (song.userRating == null) 0 else song.userRating!! - setFiveStars(rating) + val rating = if (track.userRating == null) 0 else track.userRating!! + updateFiveStars(rating) + + // Five star rating has no click handler because in the + // track view theres not enough space } else { star.isVisible = true rating.isGone = true - setSingleStar(song.starred) + updateSingleStar(track.starred) star.setOnClickListener { - val isStarred = song.starred - val id = song.id - - if (!isStarred) { - star.setImageResource(R.drawable.ic_star_full) - song.starred = true - } else { - star.setImageResource(R.drawable.ic_star_hollow) - song.starred = false - } - - // Should this be done here ? - Thread { - val musicService = MusicServiceFactory.getMusicService() - try { - if (!isStarred) { - musicService.star(id, null, null) - } else { - musicService.unstar(id, null, null) - } - } catch (all: Exception) { - Timber.e(all) - } - }.start() + track.starred = !track.starred + updateSingleStar(track.starred) + RxBus.ratingSubmitter.onNext( + RatingUpdate(track.id, HeartRating(track.starred)) + ) } } } @Suppress("MagicNumber") - private fun setFiveStars(rating: Int) { + private fun updateFiveStars(rating: Int) { fiveStar1.setImageResource( if (rating > 0) R.drawable.ic_star_full else R.drawable.ic_star_hollow ) @@ -224,7 +208,7 @@ class TrackViewHolder(val view: View) : ) } - private fun setSingleStar(starred: Boolean) { + private fun updateSingleStar(starred: Boolean) { if (starred) { star.setImageResource(R.drawable.ic_star_full) } else { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/RatingUpdate.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/RatingUpdate.kt new file mode 100644 index 00000000..93faedee --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/RatingUpdate.kt @@ -0,0 +1,16 @@ +/* + * RatingUpdate.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.data + +import androidx.media3.common.Rating + +data class RatingUpdate( + val id: String, + val rating: Rating, + val success: Boolean? = null +) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 8797d48f..2dd60683 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -35,11 +35,15 @@ import android.widget.TextView import android.widget.Toast import android.widget.ViewFlipper import androidx.core.content.res.ResourcesCompat +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.Player -import androidx.media3.session.SessionResult +import androidx.media3.common.StarRating import androidx.navigation.Navigation import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.ItemTouchHelper @@ -49,8 +53,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton -import com.google.common.util.concurrent.FutureCallback -import com.google.common.util.concurrent.Futures import io.reactivex.rxjava3.disposables.CompositeDisposable import java.text.DateFormat import java.text.SimpleDateFormat @@ -76,6 +78,7 @@ import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track @@ -98,7 +101,7 @@ import timber.log.Timber /** * Contains the Music Player screen of Ultrasonic with playback controls and the playlist - * TODO: Add timeline lister -> updateProgressBar(). + * */ @Suppress("LargeClass", "TooManyFunctions", "MagicNumber") class PlayerFragment : @@ -132,7 +135,6 @@ class PlayerFragment : // Views and UI Elements private lateinit var playlistNameView: EditText - private lateinit var starMenuItem: MenuItem private lateinit var fiveStar1ImageView: ImageView private lateinit var fiveStar2ImageView: ImageView private lateinit var fiveStar3ImageView: ImageView @@ -230,7 +232,13 @@ class PlayerFragment : height = size.y } - setHasOptionsMenu(true) + // Register our options menu + (requireActivity() as MenuHost).addMenuProvider( + menuProvider, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) + useFiveStarRating = Settings.useFiveStarRating swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100 swipeVelocity = swipeDistance @@ -467,23 +475,55 @@ class PlayerFragment : super.onDestroyView() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.nowplaying, menu) - super.onCreateOptionsMenu(menu, inflater) + private val menuProvider: MenuProvider = object : MenuProvider { + override fun onPrepareMenu(menu: Menu) { + setupOptionsMenu(menu) + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.nowplaying, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return menuItemSelected(menuItem.itemId, currentSong) + } } @Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth") - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) + fun setupOptionsMenu(menu: Menu) { + // Seems there is nothing like ViewBinding for Menus val screenOption = menu.findItem(R.id.menu_item_screen_on_off) + val goToAlbum = menu.findItem(R.id.menu_show_album) + val goToArtist = menu.findItem(R.id.menu_show_artist) val jukeboxOption = menu.findItem(R.id.menu_item_jukebox) val equalizerMenuItem = menu.findItem(R.id.menu_item_equalizer) val shareMenuItem = menu.findItem(R.id.menu_item_share) val shareSongMenuItem = menu.findItem(R.id.menu_item_share_song) - starMenuItem = menu.findItem(R.id.menu_item_star) + val starMenuItem = menu.findItem(R.id.menu_item_star) val bookmarkMenuItem = menu.findItem(R.id.menu_item_bookmark_set) val bookmarkRemoveMenuItem = menu.findItem(R.id.menu_item_bookmark_delete) + // Listen to rating changes and update the UI + rxBusSubscription += RxBus.ratingPublishedObservable.subscribe { update -> + + // Ignore updates which are not for the current song + if (update.id != currentSong?.id) return@subscribe + + // Ensure UI thread + launch { + if (update.success == true && update.rating is HeartRating) { + if (update.rating.isHeart) { + starMenuItem.setIcon(fullStar) + } else { + starMenuItem.setIcon(hollowStar) + } + } else if (update.success == false) { + Toast.makeText(context, "Setting rating failed", Toast.LENGTH_SHORT) + .show() + } + } + } + if (isOffline()) { if (shareMenuItem != null) { shareMenuItem.isVisible = false @@ -500,6 +540,7 @@ class PlayerFragment : equalizerMenuItem.isEnabled = isEqualizerAvailable equalizerMenuItem.isVisible = isEqualizerAvailable } + val mediaPlayerController = mediaPlayerController val track = mediaPlayerController.currentMediaItem?.toTrack() @@ -512,9 +553,13 @@ class PlayerFragment : if (currentSong != null) { starMenuItem.setIcon(if (currentSong!!.starred) fullStar else hollowStar) shareSongMenuItem.isVisible = true + goToAlbum.isVisible = true + goToArtist.isVisible = true } else { starMenuItem.setIcon(hollowStar) shareSongMenuItem.isVisible = false + goToAlbum.isVisible = false + goToArtist.isVisible = false } if (mediaPlayerController.keepScreenOn) { @@ -555,10 +600,6 @@ class PlayerFragment : return popup } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return menuItemSelected(item.itemId, currentSong) || super.onOptionsItemSelected(item) - } - private fun onContextMenuItemSelected( menuItem: MenuItem, item: MusicDirectory.Child @@ -655,31 +696,11 @@ class PlayerFragment : } R.id.menu_item_star -> { if (track == null) return true + track.starred = !track.starred - val isStarred = track.starred - - mediaPlayerController.toggleSongStarred()?.let { - Futures.addCallback( - it, - object : FutureCallback { - override fun onSuccess(result: SessionResult?) { - if (isStarred) { - starMenuItem.setIcon(hollowStar) - track.starred = false - } else { - starMenuItem.setIcon(fullStar) - track.starred = true - } - } - - override fun onFailure(t: Throwable) { - Toast.makeText(context, "SetRating failed", Toast.LENGTH_SHORT) - .show() - } - }, - this.executorService - ) - } + RxBus.ratingSubmitter.onNext( + RatingUpdate(track.id, HeartRating(track.starred)) + ) return true } @@ -1072,8 +1093,6 @@ class PlayerFragment : } } - // TODO: It would be a lot nicer if MediaPlayerController would send an event - // when this is necessary instead of updating every time updateSongRating() nextButton.isEnabled = mediaPlayerController.canSeekToNext() @@ -1082,7 +1101,6 @@ class PlayerFragment : @Synchronized private fun updateSeekBar() { - Timber.i("Calling updateSeekBar") val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled val millisPlayed: Int = max(0, mediaPlayerController.playerPosition) val duration: Int = mediaPlayerController.playerDuration @@ -1233,11 +1251,7 @@ class PlayerFragment : } private fun updateSongRating() { - var rating = 0 - - if (currentSong?.userRating != null) { - rating = currentSong!!.userRating!! - } + val rating = currentSong?.userRating ?: 0 fiveStar1ImageView.setImageResource(if (rating > 0) fullStar else hollowStar) fiveStar2ImageView.setImageResource(if (rating > 1) fullStar else hollowStar) @@ -1248,8 +1262,15 @@ class PlayerFragment : private fun setSongRating(rating: Int) { if (currentSong == null) return + currentSong?.userRating = rating updateSongRating() - mediaPlayerController.setSongRating(rating) + + RxBus.ratingSubmitter.onNext( + RatingUpdate( + currentSong!!.id, + StarRating(5, rating.toFloat()) + ) + ) } @SuppressLint("InflateParams") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index fcf0b9f8..8921bb31 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -26,8 +26,6 @@ import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult -import androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE -import androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN import androidx.media3.session.SessionResult.RESULT_SUCCESS import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.FutureCallback @@ -44,12 +42,14 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.service.RatingManager import org.moire.ultrasonic.util.MainThreadExecutor import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util @@ -306,21 +306,18 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr rating: Rating ): ListenableFuture { return serviceScope.future { - if (rating is HeartRating) { - try { - if (rating.isHeart) { - musicService.star(mediaId, null, null) - } else { - musicService.unstar(mediaId, null, null) - } - } catch (all: Exception) { - Timber.e(all) - // TODO: Better handle exception - return@future SessionResult(RESULT_ERROR_UNKNOWN) - } - return@future SessionResult(RESULT_SUCCESS) - } - return@future SessionResult(RESULT_ERROR_BAD_VALUE) + Timber.i(controller.packageName) + // This function even though its declared in AutoMediaBrowserCallback.kt is + // actually called every time we set the rating on an MediaItem. + // To avoid an event loop it does not emit a RatingUpdate event, + // but calls the Manager directly + RatingManager.instance.submitRating( + RatingUpdate( + id = mediaId, + rating = rating + ) + ) + return@future SessionResult(RESULT_SUCCESS) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index c421a122..611a2c4c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -10,7 +10,6 @@ import android.content.ComponentName import android.content.Context import android.os.Handler import android.os.Looper -import android.widget.Toast import androidx.annotation.IntRange import androidx.media3.common.C import androidx.media3.common.HeartRating @@ -21,28 +20,26 @@ import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO import androidx.media3.common.Player.REPEAT_MODE_OFF +import androidx.media3.common.Rating +import androidx.media3.common.StarRating import androidx.media3.common.Timeline import androidx.media3.session.MediaController -import androidx.media3.session.SessionResult import androidx.media3.session.SessionToken -import com.google.common.util.concurrent.FutureCallback -import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID +import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.playback.PlaybackService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.MainThreadExecutor import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.setPin @@ -231,11 +228,21 @@ class MediaPlayerController( clear(false) onDestroy() } + rxBusSubscription += RxBus.stopServiceCommandObservable.subscribe { clear(false) onDestroy() } + rxBusSubscription += RxBus.ratingSubmitterObservable.subscribe { + // Ensure correct thread + mainScope.launch { + // This deals only with the current track! + if (it.id != currentMediaItem?.toTrack()?.id) return@launch + setRating(it.rating) + } + } + created = true Timber.i("MediaPlayerController started") } @@ -701,52 +708,49 @@ class MediaPlayerController( controller?.volume = volume } - fun toggleSongStarred(): ListenableFuture? { - if (currentMediaItem == null) return null - val song = currentMediaItem!!.toTrack() - - return (controller as? MediaController)?.setRating( - HeartRating(!song.starred) - )?.let { - Futures.addCallback( - it, - object : FutureCallback { - override fun onSuccess(result: SessionResult?) { - // Trigger an update - // TODO Update Metadata of MediaItem... - // localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying) - song.starred = !song.starred - } - - override fun onFailure(t: Throwable) { - Toast.makeText( - context, - "There was an error updating the rating", - Toast.LENGTH_SHORT - ).show() - } - }, - MainThreadExecutor() - ) - it + /* + * Sets the rating of the current track + */ + fun setRating(rating: Rating) { + if (controller is MediaController) { + (controller as MediaController).setRating(rating) } } - @Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions - fun setSongRating(rating: Int) { - if (!Settings.useFiveStarRating) return + /* + * This legacy function simply emits a rating update, + * which will then be processed by both the RatingManager as well as the controller + */ + fun legacyToggleStar() { if (currentMediaItem == null) return - val song = currentMediaItem!!.toTrack() - song.userRating = rating - mainScope.launch { - withContext(Dispatchers.IO) { - try { - getMusicService().setRating(song.id, rating) - } catch (e: Exception) { - Timber.e(e) - } - } - } + val track = currentMediaItem!!.toTrack() + track.starred = !track.starred + val rating = HeartRating(track.starred) + + RxBus.ratingSubmitter.onNext( + RatingUpdate( + track.id, + rating + ) + ) + } + + /* + * This legacy function simply emits a rating update, + * which will then be processed by both the RatingManager as well as the controller + */ + fun legacySetRating(num: Int) { + if (currentMediaItem == null) return + val track = currentMediaItem!!.toTrack() + track.userRating = num + val rating = StarRating(5, num.toFloat()) + + RxBus.ratingSubmitter.onNext( + RatingUpdate( + track.id, + rating + ) + ) } val currentMediaItem: MediaItem? @@ -764,7 +768,6 @@ class MediaPlayerController( * Loops over the timeline windows to find the entry which matches the given closure. * * @param searchClosure Determines the condition which the searched for window needs to match. - * @param timeline the timeline to search in. * @return the index of the window that satisfies the search condition, * or [C.INDEX_UNSET] if not found. */ diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index c05805e5..3c5d4df3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -30,6 +30,7 @@ import timber.log.Timber * This class is responsible for handling received events for the Media Player implementation */ class MediaPlayerLifecycleSupport : KoinComponent { + private lateinit var ratingManager: RatingManager private val playbackStateSerializer by inject() private val mediaPlayerController by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() @@ -71,6 +72,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { CacheCleaner().clean() created = true + ratingManager = RatingManager.instance Timber.i("LifecycleSupport created") } @@ -187,12 +189,12 @@ class MediaPlayerLifecycleSupport : KoinComponent { KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop() KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play() KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() - KeyEvent.KEYCODE_1 -> mediaPlayerController.setSongRating(1) - KeyEvent.KEYCODE_2 -> mediaPlayerController.setSongRating(2) - KeyEvent.KEYCODE_3 -> mediaPlayerController.setSongRating(3) - KeyEvent.KEYCODE_4 -> mediaPlayerController.setSongRating(4) - KeyEvent.KEYCODE_5 -> mediaPlayerController.setSongRating(5) - KeyEvent.KEYCODE_STAR -> mediaPlayerController.toggleSongStarred() + KeyEvent.KEYCODE_1 -> mediaPlayerController.legacySetRating(1) + KeyEvent.KEYCODE_2 -> mediaPlayerController.legacySetRating(2) + KeyEvent.KEYCODE_3 -> mediaPlayerController.legacySetRating(3) + KeyEvent.KEYCODE_4 -> mediaPlayerController.legacySetRating(4) + KeyEvent.KEYCODE_5 -> mediaPlayerController.legacySetRating(5) + KeyEvent.KEYCODE_STAR -> mediaPlayerController.legacyToggleStar() else -> { } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index eb3c8bb9..2d37a648 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -39,10 +39,10 @@ interface MusicService { fun getGenres(refresh: Boolean): List @Throws(Exception::class) - fun star(id: String?, albumId: String?, artistId: String?) + fun star(id: String?, albumId: String? = null, artistId: String? = null) @Throws(Exception::class) - fun unstar(id: String?, albumId: String?, artistId: String?) + fun unstar(id: String?, albumId: String? = null, artistId: String? = null) @Throws(Exception::class) fun setRating(id: String, rating: Int) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RatingManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RatingManager.kt new file mode 100644 index 00000000..dd2bd38b --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RatingManager.kt @@ -0,0 +1,87 @@ +/* + * RatingManager.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.service + +import androidx.media3.common.HeartRating +import androidx.media3.common.StarRating +import io.reactivex.rxjava3.disposables.CompositeDisposable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.moire.ultrasonic.data.RatingUpdate +import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import timber.log.Timber + +/* +* This class subscribes to RatingEvents and submits them to the server. +* In the future it could be extended to store the ratings when offline +* and submit them when back online. +* Only the manager should publish RatingSubmitted events + */ +class RatingManager : CoroutineScope by CoroutineScope(Dispatchers.Default) { + private val rxBusSubscription: CompositeDisposable = CompositeDisposable() + + var lastUpdate: RatingUpdate? = null + + init { + rxBusSubscription += RxBus.ratingSubmitterObservable.subscribe { + submitRating(it) + } + } + + internal fun submitRating(update: RatingUpdate) { + // Don't submit the same rating twice + if (update.id == lastUpdate?.id && update.rating == lastUpdate?.rating) return + + val service = getMusicService() + val id = update.id + + Timber.i("Submitting rating to server: ${update.rating} for $id") + + if (update.rating is HeartRating) { + launch { + var success = false + withContext(Dispatchers.IO) { + try { + if (update.rating.isHeart) service.star(id) + else service.unstar(id) + success = true + } catch (all: Exception) { + Timber.e(all) + } + } + RxBus.ratingPublished.onNext( + update.copy(success = success) + ) + } + } else if (update.rating is StarRating) { + launch { + var success = false + withContext(Dispatchers.IO) { + try { + getMusicService().setRating(id, update.rating.starRating.toInt()) + success = true + } catch (all: Exception) { + Timber.e(all) + } + } + RxBus.ratingPublished.onNext( + update.copy(success = success) + ) + } + } + lastUpdate = update + } + + companion object { + val instance: RatingManager by lazy { + RatingManager() + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt index f4b62de0..9c87c519 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RxBus.kt @@ -7,6 +7,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.subjects.PublishSubject import java.util.concurrent.TimeUnit +import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.domain.Track class RxBus { @@ -75,6 +76,18 @@ class RxBus { val trackDownloadStateObservable: Observable = trackDownloadStatePublisher.observeOn(mainThread()) + // Sends a RatingUpdate which was just triggered by the user + val ratingSubmitter: PublishSubject = + PublishSubject.create() + val ratingSubmitterObservable: Observable = + ratingSubmitter + + // Sends a RatingUpdate which was successfully submitted to the server or database + val ratingPublished: PublishSubject = + PublishSubject.create() + val ratingPublishedObservable: Observable = + ratingPublished + // Commands val dismissNowPlayingCommandPublisher: PublishSubject = PublishSubject.create() diff --git a/ultrasonic/src/main/res/menu/nowplaying.xml b/ultrasonic/src/main/res/menu/nowplaying.xml index 67b7f3ff..39126689 100644 --- a/ultrasonic/src/main/res/menu/nowplaying.xml +++ b/ultrasonic/src/main/res/menu/nowplaying.xml @@ -15,6 +15,13 @@ app:showAsAction="ifRoom|withText" a:title="@string/download.menu_star"/> + + + Save Playlist Screen Off Screen On - Show Album + Go to Album Shuffle Shuffle mode enabled Shuffle mode disabled @@ -367,7 +367,7 @@ Check out this music I shared from %s Share songs via Share - Show Artist + Go to Artist Album artwork Multiple Years Show confirmation dialog From a0314a865ce44a184245980648dbf679ae9e2e05 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 7 May 2023 09:32:30 +0000 Subject: [PATCH 21/69] Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.7.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c27317d..5519a16f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.1" kotlin = "1.8.21" -kotlinxCoroutines = "1.6.4" +kotlinxCoroutines = "1.7.0" kotlinxGuava = "1.6.4" viewModelKtx = "2.6.1" swipeRefresh = "1.1.0" From f37301e738da6279532d4e3ae5f397f95b6005dc Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sun, 7 May 2023 09:47:24 +0000 Subject: [PATCH 22/69] Merge changes from master back to dev. (4.3.3 release) --- fastlane/metadata/android/en-US/changelogs/116.txt | 8 ++++++++ ultrasonic/build.gradle | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/116.txt diff --git a/fastlane/metadata/android/en-US/changelogs/116.txt b/fastlane/metadata/android/en-US/changelogs/116.txt new file mode 100644 index 00000000..b71f8806 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/116.txt @@ -0,0 +1,8 @@ +Bug fixes +- Fix various crashes + +Changes since 4.2.0 +- #827: Make app full compliant Android Auto to publish in Play Store. +- #878: "Play shuffled" option for playlists always begins with the first track. +- #891: Dump config to log file when logging is enabled. +- #854: Remove Videos menu option for servers which don't support it. diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index dc4f7cc2..57562d4a 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { applicationId "org.moire.ultrasonic" - versionCode 115 - versionName "4.3.2" + versionCode 116 + versionName "4.3.3" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk From 4f5d503ceb06c0c336c86c43450c009c240dd8dc Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 7 May 2023 09:56:19 +0000 Subject: [PATCH 23/69] Update dependency org.mockito:mockito-core to v5.3.1 --- core/subsonic-api/build.gradle | 1 - gradle/libs.versions.toml | 3 +-- ultrasonic/build.gradle | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/core/subsonic-api/build.gradle b/core/subsonic-api/build.gradle index a70a11a2..9f044c21 100644 --- a/core/subsonic-api/build.gradle +++ b/core/subsonic-api/build.gradle @@ -13,7 +13,6 @@ dependencies { testImplementation libs.kotlinJunit testImplementation libs.mockito - testImplementation libs.mockitoInline testImplementation libs.mockitoKotlin testImplementation libs.kluent testImplementation libs.mockWebServer diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c27317d..f69225ae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ picasso = "2.8" junit4 = "4.13.2" junit5 = "5.9.3" -mockito = "5.2.0" +mockito = "5.3.1" mockitoKotlin = "4.1.0" kluent = "1.73" apacheCodecs = "1.15" @@ -95,7 +95,6 @@ junitVintage = { module = "org.junit.vintage:junit-vintage-engine", v kotlinJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } mockitoKotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" } -mockitoInline = { module = "org.mockito:mockito-inline", version.ref = "mockito" } kluent = { module = "org.amshove.kluent:kluent", version.ref = "kluent" } kluentAndroid = { module = "org.amshove.kluent:kluent-android", version.ref = "kluent" } mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index dc4f7cc2..4b96f682 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -137,7 +137,6 @@ dependencies { testImplementation libs.kotlinJunit testImplementation libs.kluent testImplementation libs.mockito - testImplementation libs.mockitoInline testImplementation libs.mockitoKotlin testImplementation libs.robolectric From 83c9c188e9b97f51bbd848a5d9eb28ece4f73810 Mon Sep 17 00:00:00 2001 From: Kaiyang Wu Date: Sat, 29 Apr 2023 07:29:32 +0000 Subject: [PATCH 24/69] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (426 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hans/ --- ultrasonic/src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index ac3c0deb..fea8126c 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -199,7 +199,7 @@ 按光盘编号和曲目编号对歌曲列表进行排序 展示比特率和文件后缀 在艺术家姓名后追加比特率和文件后缀 - 正在播放 + 播放时显示正在播放界面 隐藏来自其他应用的音乐。 隐藏其他来源 在安卓系统下次扫描音乐时生效。 From 218f14484802e567ea716c7351a130f6dc221198 Mon Sep 17 00:00:00 2001 From: Kaiyang Wu Date: Sat, 29 Apr 2023 00:23:07 +0000 Subject: [PATCH 25/69] Translated using Weblate (Chinese (Traditional)) Currently translated at 53.0% (226 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hant/ --- ultrasonic/src/main/res/values-zh-rTW/strings.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index d36c4e57..aaa29509 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -213,4 +213,19 @@ 離線媒體 更新資訊 不允許遠端控制。請在您在 Subsonic 伺服器上的 使用者 > 設定 中啟用點唱機模式。 + 目錄快取時間 + 依光碟編號和曲目編號對歌曲清單進行排序 + 清空搜尋記錄 + 聊天訊息刷新時間間隔 + 在藝術家名稱後附加位元速率和檔案後綴 + 外觀 + 歌曲播放完畢後清除書籤 + 依光碟排序歌曲 + 錯誤:無可用的 SD 卡。 + 警告:目前沒有可用的網路。 +\n 如果您要使用行動數據,您需要在設定中允許使用計量付費網路連線下載。 + 播放全部 + 所有資料夾 + 選擇資料夾 + 伺服器上沒有已保存的播放清單 \ No newline at end of file From 0650ce0bbab26d53d507c7c8d7bdc0044190692a Mon Sep 17 00:00:00 2001 From: Kaiyang Wu Date: Wed, 3 May 2023 06:40:23 +0000 Subject: [PATCH 26/69] Translated using Weblate (Chinese (Traditional)) Currently translated at 54.2% (231 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hant/ --- ultrasonic/src/main/res/values-zh-rTW/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index aaa29509..8fdfccfd 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -228,4 +228,9 @@ 所有資料夾 選擇資料夾 伺服器上沒有已保存的播放清單 + 隱藏其他來源 + 隱藏來自其他應用程式的音樂檔案。 + 在 Android 系統下次掃描裝置內音樂時生效。 + 播放時顯示正在播放介面 + 在媒體庫介面開始播放後切換到正在播放介面 \ No newline at end of file From 1beb67c497396b66cbfe517263bd0687e5258c2d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 7 May 2023 10:32:39 +0000 Subject: [PATCH 27/69] Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-guava to v1.7.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ae736d9..efa08e5b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ multidex = "2.0.1" room = "2.5.1" kotlin = "1.8.21" kotlinxCoroutines = "1.7.0" -kotlinxGuava = "1.6.4" +kotlinxGuava = "1.7.0" viewModelKtx = "2.6.1" swipeRefresh = "1.1.0" From 751b946092b57db98647e97da4c8c37f94745ed9 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 7 May 2023 12:57:15 +0200 Subject: [PATCH 28/69] Revert Jackson to 2.13.5 for compatibility with older APIs --- gradle/libs.versions.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index efa08e5b..ce6ce6f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,8 @@ viewModelKtx = "2.6.1" swipeRefresh = "1.1.0" retrofit = "2.9.0" -jackson = "2.14.2" +## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24 +jackson = "2.13.5" okhttp = "4.10.0" koin = "3.3.2" picasso = "2.8" From 82fb45bd55410caf3430ec7e245996b565982c17 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sun, 7 May 2023 15:23:57 +0000 Subject: [PATCH 29/69] Fix a bunch of Exceptions collected through Play Store reporting --- .../moire/ultrasonic/view/GenreAdapter.java | 2 +- .../fragment/TrackCollectionFragment.kt | 2 ++ .../fragment/legacy/SelectGenreFragment.kt | 4 ++- .../ultrasonic/imageloader/ImageLoader.kt | 2 ++ .../ultrasonic/subsonic/DownloadHandler.kt | 16 +++++++++- .../org/moire/ultrasonic/util/Dialogs.kt | 21 +++++++++----- .../org/moire/ultrasonic/util/StorageFile.kt | 2 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 29 +++++++++++-------- 8 files changed, 55 insertions(+), 23 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java index 475bb602..eeb1c48b 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/GenreAdapter.java @@ -46,7 +46,7 @@ public class GenreAdapter extends ArrayAdapter implements SectionIndexer private final Object[] sections; private final Integer[] positions; - public GenreAdapter(Context context, List genres) + public GenreAdapter(@NonNull Context context, List genres) { super(context, R.layout.list_item_generic, genres); diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index b67ec05b..6664f17e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -401,6 +401,8 @@ open class TrackCollectionFragment( ) { // We are coming back from unknown context // and need to ensure Main Thread in order to manipulate the UI + // If view is null, our view was disposed in the meantime + if (view == null) return viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { val multipleSelection = viewAdapter.hasMultipleSelection() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt index 31790ef3..0a876b45 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt @@ -102,7 +102,9 @@ class SelectGenreFragment : Fragment() { override fun done(result: List) { emptyView!!.isVisible = result.isEmpty() - genreListView!!.adapter = GenreAdapter(context, result) + if (context != null) { + genreListView!!.adapter = GenreAdapter(context!!, result) + } } } task.execute() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index c814eeec..06bc30ff 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -240,6 +240,8 @@ class ImageLoader( } finally { inputStream.safeClose() } + } catch (all: Exception) { + Timber.w(all) } finally { cacheInProgress.remove(file)?.countDown() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 286ce8f2..ce14c811 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -7,11 +7,14 @@ package org.moire.ultrasonic.subsonic +import android.os.Handler +import android.os.Looper import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import java.util.Collections import java.util.LinkedList import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -28,6 +31,7 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util +import timber.log.Timber /** * Retrieves a list of songs and adds them to the now playing list @@ -39,6 +43,16 @@ class DownloadHandler( ) : CoroutineScope by CoroutineScope(Dispatchers.IO) { private val maxSongs = 500 + /** + * Exception Handler for Coroutines + */ + val exceptionHandler = CoroutineExceptionHandler { _, exception -> + Handler(Looper.getMainLooper()).post { + Timber.w(exception) + } + } + + // TODO: Use coroutine here (with proper exception handler) fun download( fragment: Fragment, append: Boolean, @@ -210,7 +224,7 @@ class DownloadHandler( isArtist: Boolean ) { // Launch the Job - val job = launch { + val job = launch(exceptionHandler) { val songs: MutableList = getTracksFromServer(isArtist, id, isDirectory, name, isShare) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Dialogs.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Dialogs.kt index a33dae82..7988dc3c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Dialogs.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Dialogs.kt @@ -10,7 +10,9 @@ package org.moire.ultrasonic.util import android.app.Activity import android.content.Context import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.lang.ref.WeakReference import org.moire.ultrasonic.R +import timber.log.Timber /* * InfoDialog can be used to show some information to the user. Typically it cannot be cancelled, @@ -19,24 +21,30 @@ import org.moire.ultrasonic.R open class InfoDialog( context: Context, message: CharSequence?, - private val activity: Activity? = null, + activity: Activity? = null, private val finishActivityOnClose: Boolean = false ) { - - open var builder: MaterialAlertDialogBuilder = Builder(activity ?: context, message) + private val activityRef: WeakReference = WeakReference(activity) + open var builder: MaterialAlertDialogBuilder = Builder(activityRef.get() ?: context, message) fun show() { builder.setOnCancelListener { if (finishActivityOnClose) { - activity!!.finish() + activityRef.get()?.finish() } } builder.setPositiveButton(R.string.common_ok) { _, _ -> if (finishActivityOnClose) { - activity!!.finish() + activityRef.get()?.finish() } } - builder.create().show() + + // If the app was put into the background in the meantime this would fail + try { + builder.create().show() + } catch (all: Exception) { + Timber.w(all, "Failed to create dialog") + } } class Builder(context: Context) : MaterialAlertDialogBuilder(context) { @@ -93,7 +101,6 @@ class ConfirmationDialog( activity: Activity? = null, finishActivityOnClose: Boolean = false ) : InfoDialog(context, message, activity, finishActivityOnClose) { - override var builder: MaterialAlertDialogBuilder = Builder(activity ?: context, message) class Builder(context: Context) : MaterialAlertDialogBuilder(context) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt index 09b12f3c..5d1eb983 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt @@ -273,7 +273,7 @@ class StorageFile( } private fun getStorageFileForParentDirectory(path: String): StorageFile? { - val parentPath = FileUtil.getParentPath(path)!! + val parentPath = FileUtil.getParentPath(path) ?: return null if (storageFilePathDictionary.containsKey(parentPath)) return storageFilePathDictionary[parentPath]!! if (notExistingPathDictionary.contains(parentPath)) return null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index cdcd5443..b9f2368f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -133,19 +133,24 @@ object Util { @JvmStatic @SuppressLint("ShowToast") // Invalid warning fun toast(context: Context?, message: CharSequence?, shortDuration: Boolean) { - if (toast == null) { - toast = Toast.makeText( - context, - message, - if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG - ) - toast!!.setGravity(Gravity.CENTER, 0, 0) - } else { - toast!!.setText(message) - toast!!.duration = - if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG + // If called after doing some background processing, our context might have expired! + try { + if (toast == null) { + toast = Toast.makeText( + context, + message, + if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG + ) + toast!!.setGravity(Gravity.CENTER, 0, 0) + } else { + toast!!.setText(message) + toast!!.duration = + if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG + } + toast!!.show() + } catch (_: Exception) { + // Ignore } - toast!!.show() } /** From 338fb618b9ad7d7cc54a9ccbce72537896e5944a Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sun, 7 May 2023 15:48:32 +0000 Subject: [PATCH 30/69] Translated using Weblate (Spanish) Currently translated at 100.0% (426 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/ --- ultrasonic/src/main/res/values-es/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 5d9d7e56..04ac76bd 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -70,7 +70,7 @@ Guardar lista de reproducción Pantalla apagada Pantalla encendida - Mostrar Álbum + Ir al álbum Aleatorio Modo aleatorio activado Modo aleatorio desactivado @@ -361,7 +361,7 @@ Echa un vistazo a esta música que te comparto desde %s Compartir canciones vía Compartir - Mostrar artista + Ir al artista Portadas de álbumes Múltiples años Mostrar diálogo de confirmación From cd982814cf6a2f9aaad6cbfe3cfa06d35d8c66da Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Tue, 9 May 2023 09:34:15 +0000 Subject: [PATCH 31/69] Use Coroutines for triggering the download or playback of music through the context menus --- gradle/libs.versions.toml | 2 +- .../ultrasonic/fragment/EntryListFragment.kt | 46 +-- .../ultrasonic/fragment/SearchFragment.kt | 40 +- .../fragment/TrackCollectionFragment.kt | 124 +++--- .../fragment/legacy/PlaylistsFragment.kt | 43 +- .../fragment/legacy/SharesFragment.kt | 79 ++-- .../service/MediaPlayerController.kt | 4 - .../ultrasonic/subsonic/DownloadHandler.kt | 380 ++++++------------ .../ultrasonic/util/CoroutinePatterns.kt | 82 ++++ .../kotlin/org/moire/ultrasonic/util/Util.kt | 4 +- 10 files changed, 329 insertions(+), 475 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce6ce6f2..dc011630 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # You need to run ./gradlew wrapper after updating the version -gradle = "7.6" +gradle = "8.1.1" navigation = "2.5.3" gradlePlugin = "8.0.1" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 8b0f1241..80a78416 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -21,6 +21,7 @@ import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign +import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.Settings @@ -129,81 +130,54 @@ abstract class EntryListFragment : MultiListFragment() { ): Boolean { when (menuItem.itemId) { R.id.menu_play_now -> - downloadHandler.downloadRecursively( + downloadHandler.fetchTracksAndAddToController( fragment, item.id, - save = false, append = false, autoPlay = true, shuffle = false, - background = false, playNext = false, - unpin = false, isArtist = isArtist ) R.id.menu_play_next -> - downloadHandler.downloadRecursively( + downloadHandler.fetchTracksAndAddToController( fragment, item.id, - save = false, append = false, autoPlay = true, shuffle = true, - background = false, playNext = true, - unpin = false, isArtist = isArtist ) R.id.menu_play_last -> - downloadHandler.downloadRecursively( + downloadHandler.fetchTracksAndAddToController( fragment, item.id, - save = false, append = true, autoPlay = false, shuffle = false, - background = false, playNext = false, - unpin = false, isArtist = isArtist ) R.id.menu_pin -> - downloadHandler.downloadRecursively( + downloadHandler.justDownload( + action = DownloadAction.PIN, fragment, item.id, - save = true, - append = true, - autoPlay = false, - shuffle = false, - background = false, - playNext = false, - unpin = false, isArtist = isArtist ) R.id.menu_unpin -> - downloadHandler.downloadRecursively( + downloadHandler.justDownload( + action = DownloadAction.UNPIN, fragment, item.id, - save = false, - append = false, - autoPlay = false, - shuffle = false, - background = false, - playNext = false, - unpin = true, isArtist = isArtist ) R.id.menu_download -> - downloadHandler.downloadRecursively( + downloadHandler.justDownload( + action = DownloadAction.DOWNLOAD, fragment, item.id, - save = false, - append = false, - autoPlay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false, isArtist = isArtist ) else -> return false diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 323f8d05..9dfcc1ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -309,7 +309,6 @@ class SearchFragment : MultiListFragment(), KoinComponent { } mediaPlayerController.addToPlaylist( listOf(song), - cachePermanently = false, autoPlay = false, shuffle = false, insertionMode = MediaPlayerController.InsertionMode.APPEND @@ -367,40 +366,37 @@ class SearchFragment : MultiListFragment(), KoinComponent { when (menuItem.itemId) { R.id.song_menu_play_now -> { songs.add(item) - downloadHandler.download( - fragment = this, - append = false, - save = false, - autoPlay = true, - playNext = false, - shuffle = false, + downloadHandler.addTracksToMediaController( songs = songs, + append = false, + playNext = false, + autoPlay = true, + shuffle = false, + fragment = this, playlistName = null ) } R.id.song_menu_play_next -> { songs.add(item) - downloadHandler.download( - fragment = this, - append = true, - save = false, - autoPlay = false, - playNext = true, - shuffle = false, + downloadHandler.addTracksToMediaController( songs = songs, + append = true, + playNext = true, + autoPlay = false, + shuffle = false, + fragment = this, playlistName = null ) } R.id.song_menu_play_last -> { songs.add(item) - downloadHandler.download( - fragment = this, - append = true, - save = false, - autoPlay = false, - playNext = false, - shuffle = false, + downloadHandler.addTracksToMediaController( songs = songs, + append = true, + playNext = false, + autoPlay = false, + shuffle = false, + fragment = this, playlistName = null ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 6664f17e..85e466dd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -40,11 +40,10 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.model.TrackCollectionModel -import org.moire.ultrasonic.service.DownloadService import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign -import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker +import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.VideoPlayer import org.moire.ultrasonic.util.CancellationToken @@ -84,7 +83,6 @@ open class TrackCollectionFragment( private var shareButton: MenuItem? = null internal val mediaPlayerController: MediaPlayerController by inject() - private val networkAndStorageChecker: NetworkAndStorageChecker by inject() private val shareHandler: ShareHandler by inject() internal var cancellationToken: CancellationToken? = null @@ -211,11 +209,14 @@ open class TrackCollectionFragment( } playNextButton?.setOnClickListener { - downloadHandler.download( - this@TrackCollectionFragment, append = true, - save = false, autoPlay = false, playNext = true, shuffle = false, + downloadHandler.addTracksToMediaController( songs = getSelectedSongs(), - playlistName = navArgs.playlistName + append = true, + playNext = true, + autoPlay = false, + shuffle = false, + playlistName = navArgs.playlistName, + this@TrackCollectionFragment ) } @@ -304,9 +305,14 @@ open class TrackCollectionFragment( selectedSongs: List = getSelectedSongs() ) { if (selectedSongs.isNotEmpty()) { - downloadHandler.download( - this, append, false, !append, playNext = false, - shuffle = false, songs = selectedSongs, null + downloadHandler.addTracksToMediaController( + songs = selectedSongs, + append = append, + playNext = false, + autoPlay = !append, + shuffle = false, + playlistName = null, + fragment = this ) } else { playAll(false, append) @@ -337,31 +343,29 @@ open class TrackCollectionFragment( } val isArtist = navArgs.isArtist - val id = navArgs.id + + // Need a valid id to download stuff + val id = navArgs.id ?: return if (hasSubFolders) { - downloadHandler.downloadRecursively( + downloadHandler.fetchTracksAndAddToController( fragment = this, id = id, - save = false, append = append, autoPlay = !append, shuffle = shuffle, - background = false, playNext = false, - unpin = false, isArtist = isArtist ) } else { - downloadHandler.download( - fragment = this, - append = append, - save = false, - autoPlay = !append, - playNext = false, - shuffle = shuffle, + downloadHandler.addTracksToMediaController( songs = getAllSongs(), - playlistName = navArgs.playlistName + append = append, + playNext = false, + autoPlay = !append, + shuffle = shuffle, + playlistName = navArgs.playlistName, + fragment = this ) } } @@ -416,62 +420,35 @@ open class TrackCollectionFragment( } } - private fun downloadBackground(save: Boolean) { - var songs = getSelectedSongs() + private fun downloadBackground(save: Boolean, tracks: List = getSelectedSongs()) { + var songs = tracks if (songs.isEmpty()) { songs = getAllSongs() } - downloadBackground(save, songs) - } - - private fun downloadBackground( - save: Boolean, - songs: List - ) { - val onValid = Runnable { - networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - DownloadService.download(songs.filterNotNull(), save) - - if (save) { - Util.toast( - context, - resources.getQuantityString( - R.plurals.select_album_n_songs_pinned, songs.size, songs.size - ) - ) - } else { - Util.toast( - context, - resources.getQuantityString( - R.plurals.select_album_n_songs_downloaded, songs.size, songs.size - ) - ) - } - } - onValid.run() + val action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD + downloadHandler.justDownload( + action = action, + fragment = this, + tracks = songs + ) } internal fun delete(songs: List = getSelectedSongs()) { - Util.toast( - context, - resources.getQuantityString( - R.plurals.select_album_n_songs_deleted, songs.size, songs.size - ) + downloadHandler.justDownload( + action = DownloadAction.DELETE, + fragment = this, + tracks = songs ) - - DownloadService.delete(songs) } internal fun unpin(songs: List = getSelectedSongs()) { - Util.toast( - context, - resources.getQuantityString( - R.plurals.select_album_n_songs_unpinned, songs.size, songs.size - ) + downloadHandler.justDownload( + action = DownloadAction.UNPIN, + fragment = this, + tracks = songs ) - DownloadService.unpin(songs) } override val defaultObserver: (List) -> Unit = { @@ -637,15 +614,14 @@ open class TrackCollectionFragment( playNow(false, songs) } R.id.song_menu_play_next -> { - downloadHandler.download( - fragment = this@TrackCollectionFragment, - append = true, - save = false, - autoPlay = false, - playNext = true, - shuffle = false, + downloadHandler.addTracksToMediaController( songs = songs, - playlistName = navArgs.playlistName + append = true, + playNext = true, + autoPlay = false, + shuffle = false, + playlistName = navArgs.playlistName, + fragment = this@TrackCollectionFragment ) } R.id.song_menu_play_last -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt index ff2f8a73..3a077b45 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt @@ -38,6 +38,7 @@ import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.OfflineException +import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.CacheCleaner @@ -147,45 +148,33 @@ class PlaylistsFragment : Fragment() { val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist when (menuItem.itemId) { R.id.playlist_menu_pin -> { - downloadHandler.value.downloadPlaylist( - this, + downloadHandler.value.justDownload( + DownloadAction.PIN, + fragment = this, id = playlist.id, name = playlist.name, - save = true, - append = true, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false + isShare = false, + isDirectory = false ) } R.id.playlist_menu_unpin -> { - downloadHandler.value.downloadPlaylist( - this, + downloadHandler.value.justDownload( + DownloadAction.UNPIN, + fragment = this, id = playlist.id, name = playlist.name, - save = false, - append = false, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = true + isShare = false, + isDirectory = false ) } R.id.playlist_menu_download -> { - downloadHandler.value.downloadPlaylist( - this, + downloadHandler.value.justDownload( + DownloadAction.DOWNLOAD, + fragment = this, id = playlist.id, name = playlist.name, - save = false, - append = false, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false + isShare = false, + isDirectory = false ) } R.id.playlist_menu_play_now -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt index eae31252..49c33749 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt @@ -28,7 +28,8 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import java.util.Locale -import org.koin.java.KoinJavaComponent +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException @@ -36,6 +37,7 @@ import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.fragment.FragmentTitle import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.OfflineException +import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.CancellationToken @@ -50,14 +52,12 @@ import org.moire.ultrasonic.view.ShareAdapter * * TODO: This file has been converted from Java, but not modernized yet. */ -class SharesFragment : Fragment() { +class SharesFragment : Fragment(), KoinComponent { private var refreshSharesListView: SwipeRefreshLayout? = null private var sharesListView: ListView? = null private var emptyTextView: View? = null private var shareAdapter: ShareAdapter? = null - private val downloadHandler = KoinJavaComponent.inject( - DownloadHandler::class.java - ) + private val downloadHandler = inject() private var cancellationToken: CancellationToken? = null override fun onCreate(savedInstanceState: Bundle?) { Util.applyTheme(this.context) @@ -72,7 +72,6 @@ class SharesFragment : Fragment() { return inflater.inflate(R.layout.select_share, container, false) } - @Suppress("NAME_SHADOWING") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { cancellationToken = CancellationToken() refreshSharesListView = view.findViewById(R.id.select_share_refresh) @@ -132,73 +131,55 @@ class SharesFragment : Fragment() { val share = sharesListView!!.getItemAtPosition(info.position) as Share when (menuItem.itemId) { R.id.share_menu_pin -> { - downloadHandler.value.downloadShare( - this, - share.id, - share.name, - save = true, - append = true, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false + downloadHandler.value.justDownload( + DownloadAction.PIN, + fragment = this, + id = share.id, + name = share.name, + isShare = true, + isDirectory = false ) } R.id.share_menu_unpin -> { - downloadHandler.value.downloadShare( - this, - share.id, - share.name, - save = false, - append = false, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = true + downloadHandler.value.justDownload( + DownloadAction.UNPIN, + fragment = this, + id = share.id, + name = share.name, + isShare = true, + isDirectory = false ) } R.id.share_menu_download -> { - downloadHandler.value.downloadShare( - this, - share.id, - share.name, - save = false, - append = false, - autoplay = false, - shuffle = false, - background = true, - playNext = false, - unpin = false + downloadHandler.value.justDownload( + DownloadAction.DOWNLOAD, + fragment = this, + id = share.id, + name = share.name, + isShare = true, + isDirectory = false ) } R.id.share_menu_play_now -> { - downloadHandler.value.downloadShare( + downloadHandler.value.fetchTracksAndAddToController( this, share.id, share.name, - save = false, append = false, - autoplay = true, + autoPlay = true, shuffle = false, - background = false, playNext = false, - unpin = false ) } R.id.share_menu_play_shuffled -> { - downloadHandler.value.downloadShare( + downloadHandler.value.fetchTracksAndAddToController( this, share.id, share.name, - save = false, append = false, - autoplay = true, + autoPlay = true, shuffle = true, - background = false, playNext = false, - unpin = false ) } R.id.share_menu_delete -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 611a2c4c..2be0f26f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -42,7 +42,6 @@ import org.moire.ultrasonic.playback.PlaybackService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util -import org.moire.ultrasonic.util.setPin import org.moire.ultrasonic.util.toMediaItem import org.moire.ultrasonic.util.toTrack import timber.log.Timber @@ -314,7 +313,6 @@ class MediaPlayerController( addToPlaylist( state.songs, - cachePermanently = false, autoPlay = false, shuffle = false, insertionMode = insertionMode @@ -408,7 +406,6 @@ class MediaPlayerController( @Synchronized fun addToPlaylist( songs: List, - cachePermanently: Boolean, autoPlay: Boolean, shuffle: Boolean, insertionMode: InsertionMode @@ -423,7 +420,6 @@ class MediaPlayerController( val mediaItems: List = songs.map { val result = it.toMediaItem() - if (cachePermanently) result.setPin(true) result } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index ce14c811..c327207b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -7,17 +7,11 @@ package org.moire.ultrasonic.subsonic -import android.os.Handler -import android.os.Looper import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import java.util.Collections import java.util.LinkedList -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline @@ -26,12 +20,8 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.DownloadService import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.CommunicationError -import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator -import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.Util -import timber.log.Timber +import org.moire.ultrasonic.util.executeTaskWithToast /** * Retrieves a list of songs and adds them to the now playing list @@ -39,279 +29,145 @@ import timber.log.Timber @Suppress("LongParameterList") class DownloadHandler( val mediaPlayerController: MediaPlayerController, - val networkAndStorageChecker: NetworkAndStorageChecker + private val networkAndStorageChecker: NetworkAndStorageChecker ) : CoroutineScope by CoroutineScope(Dispatchers.IO) { private val maxSongs = 500 - /** - * Exception Handler for Coroutines - */ - val exceptionHandler = CoroutineExceptionHandler { _, exception -> - Handler(Looper.getMainLooper()).post { - Timber.w(exception) - } - } - - // TODO: Use coroutine here (with proper exception handler) - fun download( + fun justDownload( + action: DownloadAction, fragment: Fragment, - append: Boolean, - save: Boolean, - autoPlay: Boolean, - playNext: Boolean, - shuffle: Boolean, - songs: List, - playlistName: String?, + id: String? = null, + name: String? = "", + isShare: Boolean = false, + isDirectory: Boolean = true, + isArtist: Boolean = false, + tracks: List? = null ) { - val onValid = Runnable { - // TODO: The logic here is different than in the controller... - val insertionMode = when { - playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT - append -> MediaPlayerController.InsertionMode.APPEND - else -> MediaPlayerController.InsertionMode.CLEAR - } + var successString: String? = null - networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.addToPlaylist( - songs, - save, - autoPlay, - shuffle, - insertionMode - ) - - if (playlistName != null) { - mediaPlayerController.suggestedPlaylistName = playlistName - } - if (autoPlay) { - if (Settings.shouldTransitionOnPlayback) { - fragment.findNavController().popBackStack(R.id.playerFragment, true) - fragment.findNavController().navigate(R.id.playerFragment) - } - } else if (save) { - Util.toast( - fragment.context, - fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_pinned, - songs.size, - songs.size - ) - ) - } else if (playNext) { - Util.toast( - fragment.context, - fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_play_next, - songs.size, - songs.size - ) - ) - } else if (append) { - Util.toast( - fragment.context, - fragment.resources.getQuantityString( - R.plurals.select_album_n_songs_added, - songs.size, - songs.size - ) - ) - } - } - onValid.run() - } - - fun downloadPlaylist( - fragment: Fragment, - id: String, - name: String?, - save: Boolean, - append: Boolean, - autoplay: Boolean, - shuffle: Boolean, - background: Boolean, - playNext: Boolean, - unpin: Boolean - ) { - downloadRecursively( - fragment, - id, - name, - isShare = false, - isDirectory = false, - save = save, - append = append, - autoPlay = autoplay, - shuffle = shuffle, - background = background, - playNext = playNext, - unpin = unpin, - isArtist = false - ) - } - - fun downloadShare( - fragment: Fragment, - id: String, - name: String?, - save: Boolean, - append: Boolean, - autoplay: Boolean, - shuffle: Boolean, - background: Boolean, - playNext: Boolean, - unpin: Boolean - ) { - downloadRecursively( - fragment, - id, - name, - isShare = true, - isDirectory = false, - save = save, - append = append, - autoPlay = autoplay, - shuffle = shuffle, - background = background, - playNext = playNext, - unpin = unpin, - isArtist = false - ) - } - - fun downloadRecursively( - fragment: Fragment, - id: String?, - save: Boolean, - append: Boolean, - autoPlay: Boolean, - shuffle: Boolean, - background: Boolean, - playNext: Boolean, - unpin: Boolean, - isArtist: Boolean - ) { - if (id.isNullOrEmpty()) return - downloadRecursively( - fragment, - id, - "", - isShare = false, - isDirectory = true, - save = save, - append = append, - autoPlay = autoPlay, - shuffle = shuffle, - background = background, - playNext = playNext, - unpin = unpin, - isArtist = isArtist - ) - } - - private fun downloadRecursively( - fragment: Fragment, - id: String, - name: String?, - isShare: Boolean, - isDirectory: Boolean, - save: Boolean, - append: Boolean, - autoPlay: Boolean, - shuffle: Boolean, - background: Boolean, - playNext: Boolean, - unpin: Boolean, - isArtist: Boolean - ) { // Launch the Job - val job = launch(exceptionHandler) { + executeTaskWithToast(fragment, { + val tracksToDownload: List = tracks + ?: getTracksFromServer(isArtist, id!!, isDirectory, name, isShare) + + withContext(Dispatchers.Main) { + // If we are just downloading tracks we don't need to add them to the controller + when (action) { + DownloadAction.DOWNLOAD -> DownloadService.download(tracksToDownload, false) + DownloadAction.PIN -> DownloadService.download(tracksToDownload, true) + DownloadAction.UNPIN -> DownloadService.unpin(tracksToDownload) + DownloadAction.DELETE -> DownloadService.delete(tracksToDownload) + } + successString = when (action) { + DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_downloaded, + tracksToDownload.size, + tracksToDownload.size + ) + DownloadAction.UNPIN -> { + fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_unpinned, + tracksToDownload.size, + tracksToDownload.size + ) + } + DownloadAction.PIN -> { + fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_pinned, + tracksToDownload.size, + tracksToDownload.size + ) + } + DownloadAction.DELETE -> { + fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_deleted, + tracksToDownload.size, + tracksToDownload.size + ) + } + } + } + }) { successString } + } + + fun fetchTracksAndAddToController( + fragment: Fragment, + id: String, + name: String? = "", + isShare: Boolean = false, + isDirectory: Boolean = true, + append: Boolean, + autoPlay: Boolean, + shuffle: Boolean, + playNext: Boolean, + isArtist: Boolean = false + ) { + var successString: String? = null + // Launch the Job + executeTaskWithToast(fragment, { val songs: MutableList = getTracksFromServer(isArtist, id, isDirectory, name, isShare) withContext(Dispatchers.Main) { addTracksToMediaController( - songs, - background, - unpin, - append, - playNext, - save, - autoPlay, - shuffle, - fragment + songs = songs, + append = append, + playNext = playNext, + autoPlay = autoPlay, + shuffle = shuffle, + playlistName = null, + fragment = fragment ) + // Play Now doesn't get a Toast :) + if (playNext) { + successString = fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_play_next, + songs.size, + songs.size + ) + } else if (append) { + successString = fragment.resources.getQuantityString( + R.plurals.select_album_n_songs_added, + songs.size, + songs.size + ) + } } - } - - // Create the dialog - val builder = InfoDialog.Builder(fragment.requireContext()) - builder.setTitle(R.string.background_task_wait) - builder.setMessage(R.string.background_task_loading) - builder.setOnCancelListener { job.cancel() } - builder.setPositiveButton(R.string.common_cancel) { _, i -> job.cancel() } - val dialog = builder.create() - dialog.show() - - job.invokeOnCompletion { - dialog.dismiss() - if (it != null && it !is CancellationException) { - Util.toast( - fragment.requireContext(), - CommunicationError.getErrorMessage(it, fragment.requireContext()) - ) - } - } + }) { successString } } - private fun addTracksToMediaController( - songs: MutableList, - background: Boolean, - unpin: Boolean, + fun addTracksToMediaController( + songs: List, append: Boolean, playNext: Boolean, - save: Boolean, autoPlay: Boolean, shuffle: Boolean, + playlistName: String? = null, fragment: Fragment ) { if (songs.isEmpty()) return - if (Settings.shouldSortByDisc) { - Collections.sort(songs, EntryByDiscAndTrackComparator()) - } + networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - if (!background) { - if (unpin) { - DownloadService.unpin(songs) - } else { - val insertionMode = when { - append -> MediaPlayerController.InsertionMode.APPEND - playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT - else -> MediaPlayerController.InsertionMode.CLEAR - } - mediaPlayerController.addToPlaylist( - songs, - save, - autoPlay, - shuffle, - insertionMode - ) - if ( - !append && - Settings.shouldTransitionOnPlayback - ) { - fragment.findNavController().popBackStack( - R.id.playerFragment, - true - ) - fragment.findNavController().navigate(R.id.playerFragment) - } - } - } else { - if (unpin) { - DownloadService.unpin(songs) - } else { - DownloadService.download(songs, save) - } + + val insertionMode = when { + append -> MediaPlayerController.InsertionMode.APPEND + playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT + else -> MediaPlayerController.InsertionMode.CLEAR + } + + if (playlistName != null) { + mediaPlayerController.suggestedPlaylistName = playlistName + } + + mediaPlayerController.addToPlaylist( + songs, + autoPlay, + shuffle, + insertionMode + ) + if (Settings.shouldTransitionOnPlayback && (!append || autoPlay)) { + fragment.findNavController().popBackStack(R.id.playerFragment, true) + fragment.findNavController().navigate(R.id.playerFragment) } } @@ -396,3 +252,7 @@ class DownloadHandler( } } } + +enum class DownloadAction { + DOWNLOAD, PIN, UNPIN, DELETE +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt new file mode 100644 index 00000000..c0a95bd9 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt @@ -0,0 +1,82 @@ +/* + * CoroutinePatterns.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.util + +import android.os.Handler +import android.os.Looper +import androidx.fragment.app.Fragment +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.moire.ultrasonic.R +import timber.log.Timber + +object CoroutinePatterns { + val loggingExceptionHandler by lazy { + CoroutineExceptionHandler { _, exception -> + Handler(Looper.getMainLooper()).post { + Timber.w(exception) + } + } + } +} + +fun CoroutineScope.executeTaskWithToast( + fragment: Fragment, + task: suspend CoroutineScope.() -> Unit, + successString: () -> String? +): Job { + // Launch the Job + val job = launch(CoroutinePatterns.loggingExceptionHandler, block = task) + + // Setup a handler when the job is done + job.invokeOnCompletion { + val toastString = if (it != null && it !is CancellationException) { + CommunicationError.getErrorMessage(it, fragment.context) + } else { + successString() + } + + // Return early if nothing to post + if (toastString == null) return@invokeOnCompletion + + launch(Dispatchers.Main) { + Util.toast(fragment.context, toastString) + } + } + + return job +} + +fun CoroutineScope.executeTaskWithModalDialog( + fragment: Fragment, + task: suspend CoroutineScope.() -> Unit, + successString: () -> String +) { + // Create the job + val job = executeTaskWithToast(fragment, task, successString) + + // Create the dialog + val builder = InfoDialog.Builder(fragment.requireContext()) + builder.setTitle(R.string.background_task_wait) + builder.setMessage(R.string.background_task_loading) + builder.setOnCancelListener { job.cancel() } + builder.setPositiveButton(R.string.common_cancel) { _, _ -> job.cancel() } + val dialog = builder.create() + dialog.show() + + // Add additional handler to close the dialog + job.invokeOnCompletion { + launch(Dispatchers.Main) { + dialog.dismiss() + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index b9f2368f..40739ee7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -148,8 +148,8 @@ object Util { if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG } toast!!.show() - } catch (_: Exception) { - // Ignore + } catch (all: Exception) { + Timber.w(all) } } From 0e2171b872bd7933fdc327911857292874ebd74a Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 9 May 2023 11:48:08 +0200 Subject: [PATCH 32/69] Fix the warning 'ID must not be null' --- .../moire/ultrasonic/fragment/TrackCollectionFragment.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 85e466dd..7d38ba24 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -574,14 +574,14 @@ open class TrackCollectionFragment( } else if (getVideos) { setTitle(R.string.main_videos) listModel.getVideos(refresh2) - } else if (getRandomTracks) { + } else if (id == null || getRandomTracks) { + // There seems to be a bug in ViewPager when resuming the Actitivy that subfragments + // arguments are empty. If we have no id, just show some random tracks setTitle(R.string.main_songs_random) listModel.getRandom(size, append) } else { setTitle(name) - requireNotNull(id) { - "ID must be set. NavArgs: ${navArgs.toBundle()}" - } + if (ActiveServerProvider.isID3Enabled()) { if (isAlbum) { listModel.getAlbum(refresh2, id, name) From 50aa2d0a2d614db14d09c801c54be4469924341c Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Tue, 9 May 2023 10:05:18 +0000 Subject: [PATCH 33/69] Mergeback --- fastlane/metadata/android/en-US/changelogs/117.txt | 8 ++++++++ ultrasonic/build.gradle | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/117.txt diff --git a/fastlane/metadata/android/en-US/changelogs/117.txt b/fastlane/metadata/android/en-US/changelogs/117.txt new file mode 100644 index 00000000..557e9b8b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/117.txt @@ -0,0 +1,8 @@ +Bug fixes +- Fix more exceptions + +Changes since 4.2.0 +- #827: Make app full compliant Android Auto to publish in Play Store. +- #878: "Play shuffled" option for playlists always begins with the first track. +- #891: Dump config to log file when logging is enabled. +- #854: Remove Videos menu option for servers which don't support it. diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index c912aea0..8583b11d 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { applicationId "org.moire.ultrasonic" - versionCode 116 - versionName "4.3.3" + versionCode 117 + versionName "4.3.4" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk From 58de991d64698ce3381c918a2a87a86b8bbf0c8c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 10 May 2023 17:32:09 +0000 Subject: [PATCH 34/69] Update dependency androidx.core:core-ktx to v1.10.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc011630..949a0675 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ gradle = "8.1.1" navigation = "2.5.3" gradlePlugin = "8.0.1" -androidxcore = "1.10.0" +androidxcore = "1.10.1" ktlint = "0.43.2" ktlintGradle = "11.3.2" detekt = "1.22.0" From 70d02f44936184135366e934f55688658fe93ddc Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 May 2023 05:32:15 +0000 Subject: [PATCH 35/69] Update dependency com.google.android.material:material to v1.9.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc011630..c53e7636 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ preferences = "1.2.0" media3 = "1.0.1" androidSupport = "1.6.0" -materialDesign = "1.8.0" +materialDesign = "1.9.0" constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.1" From 5daeddcc63355662eb07a8a31b5da67acf3c2456 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 11 May 2023 05:32:21 +0000 Subject: [PATCH 36/69] Update okhttp to v4.11.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc011630..cd4db9bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ swipeRefresh = "1.1.0" retrofit = "2.9.0" ## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24 jackson = "2.13.5" -okhttp = "4.10.0" +okhttp = "4.11.0" koin = "3.3.2" picasso = "2.8" From e21477a5eeaf4ef4ea95d99bedf6dd59a5dbf9f3 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sun, 14 May 2023 14:28:05 +0000 Subject: [PATCH 37/69] Use fixed version of the CI image --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7b3bdb7f..b79f63b0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ default: - image: registry.gitlab.com/ultrasonic/ci-android:latest + image: registry.gitlab.com/ultrasonic/ci-android:1.1.0 cache: &global_cache key: files: From 8337f4a7e443b9e40b14beda3c93c904abccc296 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 15 May 2023 08:20:16 +0000 Subject: [PATCH 38/69] Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.7.1 --- gradle/libs.versions.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc011630..ab4f3a28 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,8 +17,7 @@ constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.1" kotlin = "1.8.21" -kotlinxCoroutines = "1.7.0" -kotlinxGuava = "1.7.0" +kotlinxCoroutines = "1.7.1" viewModelKtx = "2.6.1" swipeRefresh = "1.1.0" @@ -74,7 +73,7 @@ swipeRefresh = { module = "androidx.swiperefreshlayout:swiperefreshla kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } -kotlinxGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxGuava"} +kotlinxGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutines"} retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" } From e8bfa5dc049116ced3258fdf3990fbf68fa71766 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Tue, 16 May 2023 07:37:36 +0000 Subject: [PATCH 39/69] Fix missing bluetooth permissions --- ultrasonic/src/main/AndroidManifest.xml | 1 + .../receiver/BluetoothIntentReceiver.java | 100 -------------- .../receiver/BluetoothIntentReceiver.kt | 127 ++++++++++++++++++ 3 files changed, 128 insertions(+), 100 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java create mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index a9b35248..75fb06d2 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:installLocation="auto"> + diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java deleted file mode 100644 index cf7844e7..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic 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. - - Subsonic 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 Subsonic. If not, see . - - Copyright 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.receiver; - -import android.annotation.SuppressLint; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothProfile; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import org.moire.ultrasonic.util.Constants; -import org.moire.ultrasonic.util.Settings; - -import timber.log.Timber; - -/** - * Resume or pause playback on Bluetooth A2DP connect/disconnect. - * - * @author Sindre Mehus - */ -@SuppressLint("MissingPermission") -public class BluetoothIntentReceiver extends BroadcastReceiver -{ - @Override - public void onReceive(Context context, Intent intent) - { - int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - String action = intent.getAction(); - String name = device != null ? device.getName() : "Unknown"; - String address = device != null ? device.getAddress() : "Unknown"; - - Timber.d("A2DP State: %d; Action: %s; Device: %s; Address: %s", state, action, name, address); - - boolean actionBluetoothDeviceConnected = false; - boolean actionBluetoothDeviceDisconnected = false; - boolean actionA2dpConnected = false; - boolean actionA2dpDisconnected = false; - - if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) - { - actionBluetoothDeviceConnected = true; - } - else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action) || BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED.equals(action)) - { - actionBluetoothDeviceDisconnected = true; - } - - if (state == android.bluetooth.BluetoothA2dp.STATE_CONNECTED) actionA2dpConnected = true; - else if (state == android.bluetooth.BluetoothA2dp.STATE_DISCONNECTED) actionA2dpDisconnected = true; - - boolean resume = false; - boolean pause = false; - - switch (Settings.getResumeOnBluetoothDevice()) - { - case Constants.PREFERENCE_VALUE_ALL: resume = actionA2dpConnected || actionBluetoothDeviceConnected; - break; - case Constants.PREFERENCE_VALUE_A2DP: resume = actionA2dpConnected; - break; - } - - switch (Settings.getPauseOnBluetoothDevice()) - { - case Constants.PREFERENCE_VALUE_ALL: pause = actionA2dpDisconnected || actionBluetoothDeviceDisconnected; - break; - case Constants.PREFERENCE_VALUE_A2DP: pause = actionA2dpDisconnected; - break; - } - - if (resume) - { - Timber.i("Connected to Bluetooth device %s address %s, resuming playback.", name, address); - context.sendBroadcast(new Intent(Constants.CMD_RESUME_OR_PLAY).setPackage(context.getPackageName())); - } - - if (pause) - { - Timber.i("Disconnected from Bluetooth device %s address %s, requesting pause.", name, address); - context.sendBroadcast(new Intent(Constants.CMD_PAUSE).setPackage(context.getPackageName())); - } - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt new file mode 100644 index 00000000..31fc0210 --- /dev/null +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/BluetoothIntentReceiver.kt @@ -0,0 +1,127 @@ +/* + * BluetoothIntentReceiver.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.receiver + +import android.Manifest +import android.bluetooth.BluetoothA2dp +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothDevice.ACTION_ACL_CONNECTED +import android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECTED +import android.bluetooth.BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED +import android.bluetooth.BluetoothProfile +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.ActivityCompat +import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_A2DP +import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_ALL +import org.moire.ultrasonic.util.Constants.PREFERENCE_VALUE_DISABLED +import org.moire.ultrasonic.util.Settings +import timber.log.Timber + +/** + * Resume or pause playback on Bluetooth A2DP connect/disconnect. + */ +class BluetoothIntentReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1) + val device = intent.getBluetoothDevice() + val action = intent.action + + // Whether to log the name of the bluetooth device + val name = device.getNameSafely() + Timber.d("Bluetooth device: $name; State: $state; Action: $action") + + // In these flags we store what kind of device (any or a2dp) has (dis)connected + var connectionStatus = PREFERENCE_VALUE_DISABLED + var disconnectionStatus = PREFERENCE_VALUE_DISABLED + + // First check for general devices + when (action) { + ACTION_ACL_CONNECTED -> { + connectionStatus = PREFERENCE_VALUE_ALL + } + ACTION_ACL_DISCONNECTED, + ACTION_ACL_DISCONNECT_REQUESTED -> { + disconnectionStatus = PREFERENCE_VALUE_ALL + } + } + + // Then check for A2DP devices + when (state) { + BluetoothA2dp.STATE_CONNECTED -> { + connectionStatus = PREFERENCE_VALUE_A2DP + } + BluetoothA2dp.STATE_DISCONNECTED -> { + disconnectionStatus = PREFERENCE_VALUE_A2DP + } + } + + // Flags to store which action should be performed + var shouldResume = false + var shouldPause = false + + // Now check the settings and set the appropriate flags + when (Settings.resumeOnBluetoothDevice) { + PREFERENCE_VALUE_ALL -> { + shouldResume = (connectionStatus != PREFERENCE_VALUE_DISABLED) + } + PREFERENCE_VALUE_A2DP -> { + shouldResume = (connectionStatus == PREFERENCE_VALUE_A2DP) + } + } + + when (Settings.pauseOnBluetoothDevice) { + PREFERENCE_VALUE_ALL -> { + shouldPause = (disconnectionStatus != PREFERENCE_VALUE_DISABLED) + } + PREFERENCE_VALUE_A2DP -> { + shouldPause = (disconnectionStatus == PREFERENCE_VALUE_A2DP) + } + } + + if (shouldResume) { + Timber.i("Connected to Bluetooth device $name; Resuming playback.") + context.sendBroadcast( + Intent(Constants.CMD_RESUME_OR_PLAY) + .setPackage(context.packageName) + ) + } + + if (shouldPause) { + Timber.i("Disconnected from Bluetooth device $name; Requesting pause.") + context.sendBroadcast( + Intent(Constants.CMD_PAUSE) + .setPackage(context.packageName) + ) + } + } +} + +private fun BluetoothDevice?.getNameSafely(): String? { + val logBluetoothName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + ( + ActivityCompat.checkSelfPermission( + UApp.applicationContext(), Manifest.permission.BLUETOOTH_CONNECT + ) != PackageManager.PERMISSION_GRANTED + ) + + return if (logBluetoothName) this?.name else "Unknown" +} + +private fun Intent.getBluetoothDevice(): BluetoothDevice? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) + } else { + getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + } +} From a97c6e15e96d5dbd5ca3911098b3377ea406cf83 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 16 May 2023 09:59:20 +0200 Subject: [PATCH 40/69] Don't sort playlists even when Sort by Disc is activated --- .../fragment/legacy/PlaylistsFragment.kt | 15 +++++++------- .../ultrasonic/model/TrackCollectionModel.kt | 20 +++++++++---------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt index 3a077b45..ebffc3cc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt @@ -29,7 +29,8 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import java.util.Locale -import org.koin.java.KoinJavaComponent.inject +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException @@ -55,15 +56,13 @@ import org.moire.ultrasonic.util.Util.toast * * TODO: This file has been converted from Java, but not modernized yet. */ -class PlaylistsFragment : Fragment() { +class PlaylistsFragment : Fragment(), KoinComponent { private var refreshPlaylistsListView: SwipeRefreshLayout? = null private var playlistsListView: ListView? = null private var emptyTextView: View? = null private var playlistAdapter: ArrayAdapter? = null - private val downloadHandler = inject( - DownloadHandler::class.java - ) + private val downloadHandler by inject() private var cancellationToken: CancellationToken? = null @@ -148,7 +147,7 @@ class PlaylistsFragment : Fragment() { val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist when (menuItem.itemId) { R.id.playlist_menu_pin -> { - downloadHandler.value.justDownload( + downloadHandler.justDownload( DownloadAction.PIN, fragment = this, id = playlist.id, @@ -158,7 +157,7 @@ class PlaylistsFragment : Fragment() { ) } R.id.playlist_menu_unpin -> { - downloadHandler.value.justDownload( + downloadHandler.justDownload( DownloadAction.UNPIN, fragment = this, id = playlist.id, @@ -168,7 +167,7 @@ class PlaylistsFragment : Fragment() { ) } R.id.playlist_menu_download -> { - downloadHandler.value.justDownload( + downloadHandler.justDownload( DownloadAction.DOWNLOAD, fragment = this, id = playlist.id, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index 8c2e65f9..252575f5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -40,7 +40,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getMusicDirectory(id, name, refresh) - + currentListIsSortable = true updateList(musicDirectory) } } @@ -51,7 +51,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val service = MusicServiceFactory.getMusicService() val musicDirectory: MusicDirectory = service.getAlbumAsDir(id, name, refresh) - + currentListIsSortable = true updateList(musicDirectory) } } @@ -60,6 +60,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getSongsByGenre(genre, count, offset) + currentListIsSortable = false updateList(musicDirectory, append) } } @@ -76,7 +77,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } else { Util.getSongsFromSearchResult(service.getStarred()) } - + currentListIsSortable = false updateList(musicDirectory) } } @@ -87,8 +88,8 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val videos = service.getVideos(refresh) - if (videos != null) { + currentListIsSortable = false updateList(videos) } } @@ -99,19 +100,16 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getRandomSongs(size) - currentListIsSortable = false - updateList(musicDirectory, append) } } suspend fun getPlaylist(playlistId: String, playlistName: String) { - withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getPlaylist(playlistId, playlistName) - + currentListIsSortable = false updateList(musicDirectory) } } @@ -121,8 +119,8 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getPodcastEpisodes(podcastChannelId) - if (musicDirectory != null) { + currentListIsSortable = false updateList(musicDirectory) } } @@ -144,7 +142,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat break } } - + currentListIsSortable = false updateList(musicDirectory) } } @@ -153,7 +151,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = Util.getSongsFromBookmarks(service.getBookmarks()) - + currentListIsSortable = false updateList(musicDirectory) } } From c118bd70f9bc78e5403af1eed8d0d8abd98fc973 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Tue, 16 May 2023 15:47:03 +0000 Subject: [PATCH 41/69] Update README.md --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 27ded22e..7a7579cb 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,6 @@ If you want to use the version downloaded from F-Droid or from GitLab with First, see if your issue haven’t been yet reported [here][issues], otherwise open [a new issue][newissue]. -### Known (not our) bugs - -If you are using *Madsonic 5.1.X* several sections of Ultrasonic will not -work. This is caused by bad implementation of Subsonic API by Madsonic. For -more info about this you can read [this bug][madbug]. - ## Contributing See [CONTRIBUTING](CONTRIBUTING.md). @@ -62,7 +56,6 @@ Full text of the license is available in the [LICENSE](LICENSE) file and [wikiaa]: https://gitlab.com/ultrasonic/ultrasonic/-/wikis/Using-Ultrasonic-with-Android-Auto [issues]: https://gitlab.com/ultrasonic/ultrasonic/-/issues [newissue]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/new -[madbug]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/129 [subsonic]: http://www.subsonic.org/ [subapi]: http://www.subsonic.org/pages/api.jsp [airsonic]: https://github.com/airsonic-advanced/airsonic-advanced From 4faf2db11fc0ec3d454f731b5dd30abd33419562 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 16 May 2023 17:56:14 +0200 Subject: [PATCH 42/69] Revert Material to 1.8.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f664e51..4738ab32 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ preferences = "1.2.0" media3 = "1.0.1" androidSupport = "1.6.0" -materialDesign = "1.9.0" +materialDesign = "1.8.0" constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.5.1" From 7b56017844711ed5c60fe710428b3864a6e3ade4 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Tue, 16 May 2023 18:09:03 +0000 Subject: [PATCH 43/69] Clarify the naming around the ID3 settings and methods, --- .../ultrasonic/adapters/ArtistRowBinder.kt | 2 +- .../ultrasonic/data/ActiveServerProvider.kt | 4 +-- .../ultrasonic/fragment/AlbumListFragment.kt | 4 +-- .../ultrasonic/fragment/EntryListFragment.kt | 4 +-- .../ultrasonic/fragment/NowPlayingFragment.kt | 2 +- .../ultrasonic/fragment/PlayerFragment.kt | 11 ++++---- .../ultrasonic/fragment/SettingsFragment.kt | 6 ++--- .../fragment/TrackCollectionFragment.kt | 2 +- .../moire/ultrasonic/model/AlbumListModel.kt | 6 ++--- .../moire/ultrasonic/model/ArtistListModel.kt | 2 +- .../ultrasonic/model/GenericListModel.kt | 3 +-- .../ultrasonic/model/TrackCollectionModel.kt | 4 +-- .../playback/AutoMediaBrowserCallback.kt | 12 ++++----- .../ultrasonic/service/RESTMusicService.kt | 5 ++-- .../ultrasonic/subsonic/DownloadHandler.kt | 27 +++++++++---------- .../org/moire/ultrasonic/util/Settings.kt | 4 +-- 16 files changed, 46 insertions(+), 52 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt index 7e73e0b6..a50f082f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -115,7 +115,7 @@ class ArtistRowBinder( } private fun showArtistPicture(): Boolean { - return ActiveServerProvider.isID3Enabled() && Settings.shouldShowArtistPicture + return ActiveServerProvider.shouldUseId3Tags() && Settings.shouldShowArtistPicture } /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index 04ff6b8e..06672dc1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -270,8 +270,8 @@ class ActiveServerProvider( /** * Queries if ID3 tags should be used */ - fun isID3Enabled(): Boolean { - return Settings.shouldUseId3Tags && (!isOffline() || Settings.useId3TagsOffline) + fun shouldUseId3Tags(): Boolean { + return Settings.id3TagsEnabledOnline && (!isOffline() || Settings.id3TagsEnabledOffline) } /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 7fb227be..9e7cb6c5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -138,8 +138,8 @@ class AlbumListFragment( ) private fun getListOfSortOrders(): List { - val useId3 = Settings.shouldUseId3Tags - val useId3Offline = Settings.useId3TagsOffline + val useId3 = Settings.id3TagsEnabledOnline + val useId3Offline = Settings.id3TagsEnabledOffline val isOnline = !ActiveServerProvider.isOffline() val supported = mutableListOf() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 80a78416..dca68e30 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -16,6 +16,7 @@ import androidx.navigation.fragment.findNavController import io.reactivex.rxjava3.disposables.CompositeDisposable import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.FolderSelectorBinder +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.Identifiable @@ -23,7 +24,6 @@ import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.subsonic.DownloadHandler -import org.moire.ultrasonic.util.Settings /** * An extension of the MultiListFragment, with a few helper functions geared @@ -39,7 +39,7 @@ abstract class EntryListFragment : MultiListFragment() { */ private fun showFolderHeader(): Boolean { return listModel.showSelectFolderHeader() && !listModel.isOffline() && - !Settings.shouldUseId3Tags + !ActiveServerProvider.shouldUseId3Tags() } override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index eb192af7..b6020f56 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -111,7 +111,7 @@ class NowPlayingFragment : Fragment() { nowPlayingArtist!!.text = artist nowPlayingAlbumArtImage!!.setOnClickListener { - val id3 = Settings.shouldUseId3Tags + val id3 = Settings.id3TagsEnabledOnline val action = NavigationGraphDirections.toTrackCollection( isAlbum = id3, id = if (id3) file.albumId else file.parent, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 2dd60683..446b72d0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -78,6 +78,7 @@ import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory @@ -591,10 +592,10 @@ class PlayerFragment : } } - if (isOffline() || !Settings.shouldUseId3Tags) { - popup.menu.findItem(R.id.menu_show_artist)?.isVisible = false - } + // Only show the menu if the ID3 tags are available + popup.menu.findItem(R.id.menu_show_artist)?.isVisible = shouldUseId3Tags() + // Only show the lyrics when the user is online popup.menu.findItem(R.id.menu_lyrics)?.isVisible = !isOffline() popup.show() return popup @@ -614,7 +615,7 @@ class PlayerFragment : R.id.menu_show_artist -> { if (track == null) return false - if (Settings.shouldUseId3Tags) { + if (Settings.id3TagsEnabledOnline) { val action = PlayerFragmentDirections.playerToAlbumsList( type = AlbumListType.SORTED_BY_NAME, byArtist = true, @@ -630,7 +631,7 @@ class PlayerFragment : R.id.menu_show_album -> { if (track == null) return false - val albumId = if (Settings.shouldUseId3Tags) track.albumId else track.parent + val albumId = if (shouldUseId3Tags()) track.albumId else track.parent val action = PlayerFragmentDirections.playerToSelectAlbum( id = albumId, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 4c25e30c..6245ce45 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -39,8 +39,8 @@ import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.FileUtil.ultrasonicDirectory import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.Settings +import org.moire.ultrasonic.util.Settings.id3TagsEnabledOnline import org.moire.ultrasonic.util.Settings.preferences -import org.moire.ultrasonic.util.Settings.shouldUseId3Tags import org.moire.ultrasonic.util.Storage import org.moire.ultrasonic.util.TimeSpanPreference import org.moire.ultrasonic.util.TimeSpanPreferenceDialogFragmentCompat @@ -354,8 +354,8 @@ class SettingsFragment : debugLogToFile?.summary = "" } - showArtistPicture?.isEnabled = shouldUseId3Tags - useId3TagsOffline?.isEnabled = shouldUseId3Tags + showArtistPicture?.isEnabled = id3TagsEnabledOnline + useId3TagsOffline?.isEnabled = id3TagsEnabledOnline } private fun setHideMedia(hide: Boolean) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 7d38ba24..48f39ffb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -582,7 +582,7 @@ open class TrackCollectionFragment( } else { setTitle(name) - if (ActiveServerProvider.isID3Enabled()) { + if (ActiveServerProvider.shouldUseId3Tags()) { if (isAlbum) { listModel.getAlbum(refresh2, id, name) } else { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index 56f82ab3..4fda6e60 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -12,9 +12,9 @@ import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.moire.ultrasonic.api.subsonic.models.AlbumListType +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.service.MusicServiceFactory -import org.moire.ultrasonic.util.Settings class AlbumListModel(application: Application) : GenericListModel(application) { @@ -69,7 +69,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { // If appending the existing list, set the offset from where to load if (append) offset += (size + loadedUntil) - musicDirectory = if (Settings.shouldUseId3Tags) { + musicDirectory = if (ActiveServerProvider.shouldUseId3Tags()) { service.getAlbumList2( albumListType, size, offset, musicFolderId @@ -119,7 +119,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) { val isAlphabetical = (lastType == AlbumListType.SORTED_BY_NAME) || (lastType == AlbumListType.SORTED_BY_ARTIST) - return !isOffline() && !Settings.shouldUseId3Tags && isAlphabetical + return !isOffline() && !ActiveServerProvider.shouldUseId3Tags() && isAlphabetical } private fun isCollectionSortable(albumListType: AlbumListType): Boolean { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt index 6dd042ce..801f6feb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt @@ -43,7 +43,7 @@ class ArtistListModel(application: Application) : GenericListModel(application) val musicFolderId = activeServer.musicFolderId - val result = if (ActiveServerProvider.isID3Enabled()) { + val result = if (ActiveServerProvider.shouldUseId3Tags()) { musicService.getArtists(refresh) } else { musicService.getIndexes(musicFolderId, refresh) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index 813dcce8..b0ab5481 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -26,7 +26,6 @@ import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.CommunicationError -import org.moire.ultrasonic.util.Settings /** * An abstract Model, which can be extended to retrieve a list of items from the API @@ -89,7 +88,7 @@ open class GenericListModel(application: Application) : withContext(Dispatchers.IO) { val musicService = MusicServiceFactory.getMusicService() val isOffline = ActiveServerProvider.isOffline() - val useId3Tags = Settings.shouldUseId3Tags + val useId3Tags = ActiveServerProvider.shouldUseId3Tags() try { load(isOffline, useId3Tags, musicService, refresh) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index 252575f5..df0cb4aa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -13,12 +13,12 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.DownloadService import org.moire.ultrasonic.service.DownloadState import org.moire.ultrasonic.service.MusicServiceFactory -import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util /* @@ -72,7 +72,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat val service = MusicServiceFactory.getMusicService() val musicDirectory: MusicDirectory - musicDirectory = if (Settings.shouldUseId3Tags) { + musicDirectory = if (ActiveServerProvider.shouldUseId3Tags()) { Util.getSongsFromSearchResult(service.getStarred2()) } else { Util.getSongsFromSearchResult(service.getStarred()) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index 8921bb31..d4f12bbc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -51,7 +51,6 @@ import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.RatingManager import org.moire.ultrasonic.util.MainThreadExecutor -import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.buildMediaItem import org.moire.ultrasonic.util.toMediaItem @@ -119,7 +118,6 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr private val musicService get() = MusicServiceFactory.getMusicService() private val isOffline get() = ActiveServerProvider.isOffline() - private val useId3Tags get() = Settings.shouldUseId3Tags private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId /** @@ -661,7 +659,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr var childMediaId: String = MEDIA_ARTIST_ITEM var artists = serviceScope.future { - if (!isOffline && useId3Tags) { + if (ActiveServerProvider.shouldUseId3Tags()) { // TODO this list can be big so we're not refreshing. // Maybe a refresh menu item can be added callWithErrorHandling { musicService.getArtists(false) } @@ -716,7 +714,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr return mainScope.future { val albums = serviceScope.future { - if (!isOffline && useId3Tags) { + if (ActiveServerProvider.shouldUseId3Tags()) { callWithErrorHandling { musicService.getAlbumsOfArtist(id, name, false) } } else { callWithErrorHandling { @@ -788,7 +786,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr val offset = (page ?: 0) * DISPLAY_LIMIT val albums = serviceScope.future { - if (useId3Tags) { + if (ActiveServerProvider.shouldUseId3Tags()) { callWithErrorHandling { musicService.getAlbumList2( type, DISPLAY_LIMIT, offset, null @@ -1190,7 +1188,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr private fun listSongsInMusicService(id: String, name: String?): MusicDirectory? { return serviceScope.future { - if (!ActiveServerProvider.isOffline() && Settings.shouldUseId3Tags) { + if (ActiveServerProvider.shouldUseId3Tags()) { callWithErrorHandling { musicService.getAlbumAsDir(id, name, false) } } else { callWithErrorHandling { musicService.getMusicDirectory(id, name, false) } @@ -1200,7 +1198,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr private fun listStarredSongsInMusicService(): SearchResult? { return serviceScope.future { - if (Settings.shouldUseId3Tags) { + if (ActiveServerProvider.shouldUseId3Tags()) { callWithErrorHandling { musicService.getStarred2() } } else { callWithErrorHandling { musicService.getStarred() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index eddf1d9f..a31ea7c2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -18,7 +18,7 @@ import org.moire.ultrasonic.api.subsonic.models.JukeboxAction import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.api.subsonic.toStreamResponse import org.moire.ultrasonic.data.ActiveServerProvider -import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Bookmark @@ -44,7 +44,6 @@ import org.moire.ultrasonic.domain.toIndexList import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity import org.moire.ultrasonic.domain.toTrackEntity import org.moire.ultrasonic.util.FileUtil -import org.moire.ultrasonic.util.Settings import timber.log.Timber /** @@ -181,7 +180,7 @@ open class RESTMusicService( criteria: SearchCriteria ): SearchResult { return try { - if (!isOffline() && Settings.shouldUseId3Tags) { + if (shouldUseId3Tags()) { search3(criteria) } else { search2(criteria) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index c327207b..4ba976ed 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.moire.ultrasonic.R -import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.DownloadService @@ -181,11 +181,11 @@ class DownloadHandler( val musicService = getMusicService() val songs: MutableList = LinkedList() val root: MusicDirectory - if (!isOffline() && isArtist && Settings.shouldUseId3Tags) { - getSongsForArtist(id, songs) + if (shouldUseId3Tags() && isArtist) { + return getSongsForArtist(id) } else { if (isDirectory) { - root = if (!isOffline() && Settings.shouldUseId3Tags) + root = if (shouldUseId3Tags()) musicService.getAlbumAsDir(id, name, false) else musicService.getMusicDirectory(id, name, false) @@ -219,23 +219,19 @@ class DownloadHandler( } val musicService = getMusicService() for ((id1, _, _, title) in parent.getAlbums()) { - val root: MusicDirectory = if ( - !isOffline() && - Settings.shouldUseId3Tags - ) musicService.getAlbumAsDir(id1, title, false) - else musicService.getMusicDirectory(id1, title, false) + val root: MusicDirectory = if (shouldUseId3Tags()) + musicService.getAlbumAsDir(id1, title, false) + else + musicService.getMusicDirectory(id1, title, false) getSongsRecursively(root, songs) } } @Throws(Exception::class) private fun getSongsForArtist( - id: String, - songs: MutableCollection - ) { - if (songs.size > maxSongs) { - return - } + id: String + ): MutableList { + val songs: MutableList = LinkedList() val musicService = getMusicService() val artist = musicService.getAlbumsOfArtist(id, "", false) for ((id1) in artist) { @@ -250,6 +246,7 @@ class DownloadHandler( } } } + return songs } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index 65dbd92b..c8ebad0b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -168,11 +168,11 @@ object Settings { // Normally you don't need to use these Settings directly, // use ActiveServerProvider.isID3Enabled() instead @JvmStatic - var shouldUseId3Tags by BooleanSetting(getKey(R.string.setting_key_id3_tags), true) + var id3TagsEnabledOnline by BooleanSetting(getKey(R.string.setting_key_id3_tags), true) // See comment above. @JvmStatic - var useId3TagsOffline by BooleanSetting(getKey(R.string.setting_key_id3_tags_offline), true) + var id3TagsEnabledOffline by BooleanSetting(getKey(R.string.setting_key_id3_tags_offline), true) var activeServer by IntSetting(getKey(R.string.setting_key_server_instance), -1) From b5dd0fdca26c74f5caa98fc7ba3b329d93c14154 Mon Sep 17 00:00:00 2001 From: tzugen Date: Tue, 16 May 2023 20:39:17 +0200 Subject: [PATCH 44/69] Add loading indicator to playlist view --- .../ultrasonic/fragment/PlayerFragment.kt | 26 ++++++++++++++----- .../main/res/layout-land/current_playing.xml | 2 +- .../src/main/res/layout/current_playing.xml | 2 +- .../src/main/res/layout/current_playlist.xml | 16 +++++++----- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 446b72d0..65f6b442 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -34,6 +34,7 @@ import android.widget.SeekBar.OnSeekBarChangeListener import android.widget.TextView import android.widget.Toast import android.widget.ViewFlipper +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.ResourcesCompat import androidx.core.view.MenuHost import androidx.core.view.MenuProvider @@ -53,6 +54,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton +import com.google.android.material.progressindicator.CircularProgressIndicator import io.reactivex.rxjava3.disposables.CompositeDisposable import java.text.DateFormat import java.text.SimpleDateFormat @@ -80,6 +82,7 @@ import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags import org.moire.ultrasonic.data.RatingUpdate +import org.moire.ultrasonic.databinding.CurrentPlayingBinding import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track @@ -143,6 +146,7 @@ class PlayerFragment : private lateinit var fiveStar5ImageView: ImageView private lateinit var playlistFlipper: ViewFlipper private lateinit var emptyTextView: TextView + private lateinit var emptyView: ConstraintLayout private lateinit var songTitleTextView: TextView private lateinit var artistTextView: TextView private lateinit var albumTextView: TextView @@ -162,9 +166,15 @@ class PlayerFragment : private lateinit var shuffleButton: View private lateinit var repeatButton: MaterialButton private lateinit var progressBar: SeekBar + private lateinit var progressIndicator: CircularProgressIndicator private val hollowStar = R.drawable.ic_star_hollow private val fullStar = R.drawable.ic_star_full + private var _binding: CurrentPlayingBinding? = null + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + private val viewAdapter: BaseAdapter by lazy { BaseAdapter() } @@ -178,13 +188,17 @@ class PlayerFragment : inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.current_playing, container, false) + ): View { + _binding = CurrentPlayingBinding.inflate(layoutInflater, container, false) + return binding.root } + // TODO: Switch them all over to use the view binding private fun findViews(view: View) { playlistFlipper = view.findViewById(R.id.current_playing_playlist_flipper) - emptyTextView = view.findViewById(R.id.playlist_empty) + emptyTextView = view.findViewById(R.id.empty_list_text) + emptyView = view.findViewById(R.id.emptyListView) + progressIndicator = view.findViewById(R.id.progress_indicator) songTitleTextView = view.findViewById(R.id.current_playing_song) artistTextView = view.findViewById(R.id.current_playing_artist) albumTextView = view.findViewById(R.id.current_playing_album) @@ -473,6 +487,7 @@ class PlayerFragment : rxBusSubscription.dispose() cancel("CoroutineScope cancelled because the view was destroyed") cancellationToken.cancel() + _binding = null super.onDestroyView() } @@ -1027,10 +1042,9 @@ class PlayerFragment : // Try to display playlist in play order val list = mediaPlayerController.playlistInPlayOrder emptyTextView.setText(R.string.playlist_empty) - viewAdapter.submitList(list.map(MediaItem::toTrack)) - - emptyTextView.isVisible = list.isEmpty() + progressIndicator.isVisible = false + emptyView.isVisible = list.isEmpty() updateRepeatButtonState(mediaPlayerController.repeatMode) } diff --git a/ultrasonic/src/main/res/layout-land/current_playing.xml b/ultrasonic/src/main/res/layout-land/current_playing.xml index 923ec7f2..575f62c2 100644 --- a/ultrasonic/src/main/res/layout-land/current_playing.xml +++ b/ultrasonic/src/main/res/layout-land/current_playing.xml @@ -119,7 +119,7 @@ - + diff --git a/ultrasonic/src/main/res/layout/current_playing.xml b/ultrasonic/src/main/res/layout/current_playing.xml index 4ed71bce..c73d42ce 100644 --- a/ultrasonic/src/main/res/layout/current_playing.xml +++ b/ultrasonic/src/main/res/layout/current_playing.xml @@ -112,7 +112,7 @@ - + diff --git a/ultrasonic/src/main/res/layout/current_playlist.xml b/ultrasonic/src/main/res/layout/current_playlist.xml index 1d95e539..e3168e2c 100644 --- a/ultrasonic/src/main/res/layout/current_playlist.xml +++ b/ultrasonic/src/main/res/layout/current_playlist.xml @@ -5,13 +5,17 @@ a:layout_height="fill_parent" a:orientation="vertical"> - + + Date: Wed, 17 May 2023 17:31:43 +0000 Subject: [PATCH 45/69] Update media3 to v1.0.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4738ab32..060b3bd0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ ktlint = "0.43.2" ktlintGradle = "11.3.2" detekt = "1.22.0" preferences = "1.2.0" -media3 = "1.0.1" +media3 = "1.0.2" androidSupport = "1.6.0" materialDesign = "1.8.0" From fe696943a42b517342c5e7359ceebd3c96e79918 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 17 May 2023 21:32:36 +0000 Subject: [PATCH 46/69] Update dependency org.robolectric:robolectric to v4.10.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4738ab32..bc180d7c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ mockito = "5.3.1" mockitoKotlin = "4.1.0" kluent = "1.73" apacheCodecs = "1.15" -robolectric = "4.10.2" +robolectric = "4.10.3" timber = "5.0.1" fastScroll = "2.0.1" colorPicker = "2.2.4" From ddd9c29d7a27fedcfc01122dfcd64a91865cdeb1 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 18 May 2023 12:22:40 +0200 Subject: [PATCH 47/69] Apply suggested Kotlin Gradle updates --- core/domain/build.gradle | 1 + gradle_scripts/android-module-bootstrap.gradle | 4 ++-- gradle_scripts/kotlin-module-bootstrap.gradle | 2 +- ultrasonic/build.gradle | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/domain/build.gradle b/core/domain/build.gradle index b462ae9c..75d3d4a5 100644 --- a/core/domain/build.gradle +++ b/core/domain/build.gradle @@ -2,6 +2,7 @@ apply from: bootstrap.androidModule apply plugin: 'kotlin-kapt' dependencies { + implementation libs.core implementation libs.roomRuntime implementation libs.roomKtx kapt libs.room diff --git a/gradle_scripts/android-module-bootstrap.gradle b/gradle_scripts/android-module-bootstrap.gradle index eeb4fe6d..f84d8fe5 100644 --- a/gradle_scripts/android-module-bootstrap.gradle +++ b/gradle_scripts/android-module-bootstrap.gradle @@ -2,9 +2,9 @@ * This module provides a base for for submodules which depend on the Android runtime */ apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +apply plugin: 'org.jetbrains.kotlin.android' apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" -apply plugin: 'kotlin-kapt' +apply plugin: 'org.jetbrains.kotlin.kapt' android { compileSdkVersion versions.compileSdk diff --git a/gradle_scripts/kotlin-module-bootstrap.gradle b/gradle_scripts/kotlin-module-bootstrap.gradle index d440dec5..c71044e9 100644 --- a/gradle_scripts/kotlin-module-bootstrap.gradle +++ b/gradle_scripts/kotlin-module-bootstrap.gradle @@ -2,7 +2,7 @@ * This module provides a base for for pure kotlin modules */ apply plugin: 'kotlin' -apply plugin: 'kotlin-kapt' +apply plugin: 'org.jetbrains.kotlin.kapt' apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" sourceSets { diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 8583b11d..8aab2f76 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'org.jetbrains.kotlin.kapt' apply plugin: "androidx.navigation.safeargs.kotlin" apply from: "../gradle_scripts/code_quality.gradle" From 7a179368556c25f1c0c2ccd8891ca44f339b0387 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Thu, 18 May 2023 10:32:17 +0000 Subject: [PATCH 48/69] Modernize Activity launching to set custom cache location --- .../ultrasonic/fragment/SettingsFragment.kt | 91 +++++-------------- .../util/SelectCacheActivityContract.kt | 49 ++++++++++ .../org/moire/ultrasonic/util/Settings.kt | 6 +- .../kotlin/org/moire/ultrasonic/util/Util.kt | 1 + 4 files changed, 76 insertions(+), 71 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SelectCacheActivityContract.kt diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 6245ce45..295885ea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -1,14 +1,11 @@ package org.moire.ultrasonic.fragment -import android.app.Activity import android.content.DialogInterface import android.content.Intent import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.net.Uri -import android.os.Build import android.os.Bundle -import android.provider.DocumentsContract import android.provider.SearchRecentSuggestions import android.view.View import androidx.annotation.StringRes @@ -38,6 +35,7 @@ import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.FileUtil.ultrasonicDirectory import org.moire.ultrasonic.util.InfoDialog +import org.moire.ultrasonic.util.SelectCacheActivityContract import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings.id3TagsEnabledOnline import org.moire.ultrasonic.util.Settings.preferences @@ -100,64 +98,14 @@ class SettingsFragment : updateCustomPreferences() } - /** - * This function will be called when we return from the file picker - * with a new custom cache location - * - * TODO: This method has been deprecated in favor of using the Activity Result API - * which brings increased type safety via an ActivityResultContract and the prebuilt - * contracts for common intents available in - * androidx.activity.result.contract.ActivityResultContracts, - * provides hooks for testing, and allow receiving results in separate, - * testable classes independent from your fragment. - * Use registerForActivityResult(ActivityResultContract, ActivityResultCallback) with the - * appropriate ActivityResultContract and handling the result in the callback. - */ - override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { - if ( - requestCode == SELECT_CACHE_ACTIVITY && - resultCode == Activity.RESULT_OK && - resultData != null - ) { - val read = (resultData.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0 - val write = (resultData.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 - val persist = (resultData.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0 - - if (read && write && persist) { - if (resultData.data != null) { - // The result data contains a URI for the document or directory that - // the user selected. - val uri = resultData.data!! - val contentResolver = UApp.applicationContext().contentResolver - - contentResolver.takePersistableUriPermission(uri, RW_FLAG) - setCacheLocation(uri.toString()) - setupCacheLocationPreference() - return - } - } - ErrorDialog.Builder(requireContext()) - .setMessage(R.string.settings_cache_location_error) - .show() - } - - if (Settings.cacheLocationUri == "") { - Settings.customCacheLocation = false - customCacheLocation?.isChecked = false - setupCacheLocationPreference() - } - } - override fun onResume() { super.onResume() - val preferences = preferences preferences.registerOnSharedPreferenceChangeListener(this) } override fun onPause() { super.onPause() - val prefs = preferences - prefs.unregisterOnSharedPreferenceChangeListener(this) + preferences.unregisterOnSharedPreferenceChangeListener(this) } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { @@ -249,19 +197,31 @@ class SettingsFragment : } private fun selectCacheLocation() { - // Choose a directory using the system's file picker. - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - - if (Settings.cacheLocationUri != "" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Settings.cacheLocationUri) - } - - intent.addFlags(RW_FLAG) - intent.addFlags(PERSISTABLE_FLAG) - - startActivityForResult(intent, SELECT_CACHE_ACTIVITY) + // Start the activity to pick a directory using the system's file picker. + selectCacheActivityContract.launch(Settings.cacheLocationUri) } + // Custom activity result contract + private val selectCacheActivityContract = + registerForActivityResult(SelectCacheActivityContract()) { uri -> + // parseResult will return the chosen path as an Uri + if (uri != null) { + val contentResolver = UApp.applicationContext().contentResolver + contentResolver.takePersistableUriPermission(uri, RW_FLAG) + setCacheLocation(uri.toString()) + setupCacheLocationPreference() + } else { + ErrorDialog.Builder(requireContext()) + .setMessage(R.string.settings_cache_location_error) + .show() + if (Settings.cacheLocationUri == "") { + Settings.customCacheLocation = false + customCacheLocation?.isChecked = false + setupCacheLocationPreference() + } + } + } + private fun setupBluetoothDevicePreferences() { val resumeSetting = Settings.resumeOnBluetoothDevice val pauseSetting = Settings.pauseOnBluetoothDevice @@ -425,7 +385,6 @@ class SettingsFragment : } companion object { - const val SELECT_CACHE_ACTIVITY = 161161 const val RW_FLAG = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION const val PERSISTABLE_FLAG = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SelectCacheActivityContract.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SelectCacheActivityContract.kt new file mode 100644 index 00000000..88437aca --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SelectCacheActivityContract.kt @@ -0,0 +1,49 @@ +/* + * SelectCacheActivityContract.kt + * Copyright (C) 2009-2023 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.util + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.DocumentsContract +import androidx.activity.result.contract.ActivityResultContract +import org.moire.ultrasonic.fragment.SettingsFragment + +class SelectCacheActivityContract : ActivityResultContract() { + override fun createIntent(context: Context, input: String?): Intent { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + if (Settings.cacheLocationUri != "" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input) + } + intent.addFlags(SettingsFragment.RW_FLAG) + intent.addFlags(SettingsFragment.PERSISTABLE_FLAG) + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + if ( + resultCode == Activity.RESULT_OK && + intent != null + ) { + val read = (intent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0 + val write = (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0 + val persist = (intent.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0 + + if (read && write && persist) { + if (intent.data != null) { + // The result data contains a URI for the document or directory that + // the user selected. + return intent.data!! + } + } + } + return null + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index c8ebad0b..e1f421f0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -48,7 +48,6 @@ object Settings { @JvmStatic val preloadCount: Int get() { - val preferences = preferences val preloadCount = preferences.getString(getKey(R.string.setting_key_preload_count), "-1")!! .toInt() @@ -60,7 +59,6 @@ object Settings { @JvmStatic val cacheSizeMB: Int get() { - val preferences = preferences val cacheSize = preferences.getString( getKey(R.string.setting_key_cache_size), "-1" @@ -209,7 +207,6 @@ object Settings { @JvmStatic val shareGreeting: String? get() { - val preferences = preferences val context = Util.appContext() val defaultVal = String.format( context.resources.getString(R.string.share_default_greeting), @@ -278,8 +275,7 @@ object Settings { } fun getAllKeys(): List { - val prefs = PreferenceManager.getDefaultSharedPreferences(UApp.applicationContext()) - return prefs.all.keys.toList() + return preferences.all.keys.toList() } private val appContext: Context diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index 40739ee7..83ecb11b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -833,6 +833,7 @@ object Util { Timber.d("Current user preferences") Timber.d("========================") val keys = Settings.preferences.all + keys.forEach { Timber.d("${it.key}: ${it.value}") } From 13091948ea1e69b74088b2cd036f3dafd1218a35 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Thu, 18 May 2023 10:54:16 +0000 Subject: [PATCH 49/69] Use default locations for Detekt config and baseline. --- detekt-config.yml => config/detekt/detekt.yml | 0 gradle_scripts/code_quality.gradle | 2 -- detekt-baseline.xml => ultrasonic/detekt-baseline.xml | 6 +++--- .../moire/ultrasonic/receiver/UltrasonicIntentReceiver.java | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) rename detekt-config.yml => config/detekt/detekt.yml (100%) rename detekt-baseline.xml => ultrasonic/detekt-baseline.xml (86%) diff --git a/detekt-config.yml b/config/detekt/detekt.yml similarity index 100% rename from detekt-config.yml rename to config/detekt/detekt.yml diff --git a/gradle_scripts/code_quality.gradle b/gradle_scripts/code_quality.gradle index 8738567e..f5880485 100644 --- a/gradle_scripts/code_quality.gradle +++ b/gradle_scripts/code_quality.gradle @@ -25,8 +25,6 @@ if (isCodeQualityEnabled) { // Builds the AST in parallel. Rules are always executed in parallel. // Can lead to speedups in larger projects. parallel = true - baseline = file("${rootProject.projectDir}/detekt-baseline.xml") - config = files("${rootProject.projectDir}/detekt-config.yml") } } tasks.detekt.jvmTarget = "17" diff --git a/detekt-baseline.xml b/ultrasonic/detekt-baseline.xml similarity index 86% rename from detekt-baseline.xml rename to ultrasonic/detekt-baseline.xml index 3acd3952..a8789e05 100644 --- a/detekt-baseline.xml +++ b/ultrasonic/detekt-baseline.xml @@ -1,9 +1,9 @@ - + TooManyFunctions:PlaybackService.kt$PlaybackService : MediaLibraryServiceKoinComponentCoroutineScope UnusedPrivateMember:UApp.kt$private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder - ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) ) + ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) ) ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file) ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name) ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name) @@ -13,7 +13,7 @@ LongMethod:PlaylistsFragment.kt$PlaylistsFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken, additionalId: String? ) LongMethod:SharesFragment.kt$SharesFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) ) + LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) ) MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192 MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50 diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java index 5eb8e214..9297b5d8 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java @@ -13,7 +13,7 @@ import static org.koin.java.KoinJavaComponent.inject; public class UltrasonicIntentReceiver extends BroadcastReceiver { - private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); + private final Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); @Override public void onReceive(Context context, Intent intent) From bdcb1a505ba68a60510fd12f9bf8999b3d63da91 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Fri, 19 May 2023 21:37:31 +0000 Subject: [PATCH 50/69] Use the JukeboxPlayer as a Player instead of an Controller --- ultrasonic/lint-baseline.xml | 81 +--- ultrasonic/src/main/AndroidManifest.xml | 7 - .../receiver/BluetoothIntentReceiver.kt | 1 + .../receiver/UltrasonicIntentReceiver.java | 2 +- .../ultrasonic/activity/NavigationActivity.kt | 23 +- .../moire/ultrasonic/di/MediaPlayerModule.kt | 4 +- .../ultrasonic/fragment/BookmarksFragment.kt | 2 +- .../ultrasonic/fragment/NowPlayingFragment.kt | 14 +- .../ultrasonic/fragment/PlayerFragment.kt | 130 ++++--- .../ultrasonic/fragment/SearchFragment.kt | 12 +- .../ultrasonic/fragment/SettingsFragment.kt | 6 +- .../fragment/TrackCollectionFragment.kt | 4 +- .../moire/ultrasonic/model/EditServerModel.kt | 4 +- .../playback/AutoMediaBrowserCallback.kt | 12 +- .../playback/CustomNotificationProvider.kt | 6 +- .../ultrasonic/playback/PlaybackService.kt | 157 +++++--- .../ultrasonic/service/JukeboxMediaPlayer.kt | 367 ++++++++---------- .../JukeboxNotificationActionFactory.kt | 97 ----- .../service/JukeboxUnimplementedFunctions.kt | 23 +- .../service/MediaPlayerLifecycleSupport.kt | 58 +-- ...yerController.kt => MediaPlayerManager.kt} | 151 +++---- .../ultrasonic/subsonic/DownloadHandler.kt | 14 +- .../org/moire/ultrasonic/util/CacheCleaner.kt | 6 +- .../org/moire/ultrasonic/util/Settings.kt | 3 + .../src/main/res/layout/jukebox_volume.xml | 28 -- ultrasonic/src/main/res/values-cs/strings.xml | 1 - ultrasonic/src/main/res/values-de/strings.xml | 1 - ultrasonic/src/main/res/values-es/strings.xml | 1 - ultrasonic/src/main/res/values-fr/strings.xml | 1 - ultrasonic/src/main/res/values-hu/strings.xml | 1 - ultrasonic/src/main/res/values-it/strings.xml | 1 - ultrasonic/src/main/res/values-ja/strings.xml | 1 - .../src/main/res/values-nb-rNO/strings.xml | 1 - ultrasonic/src/main/res/values-nl/strings.xml | 1 - ultrasonic/src/main/res/values-pl/strings.xml | 1 - .../src/main/res/values-pt-rBR/strings.xml | 1 - ultrasonic/src/main/res/values-pt/strings.xml | 1 - ultrasonic/src/main/res/values-ru/strings.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 1 - .../src/main/res/values-zh-rTW/strings.xml | 1 - ultrasonic/src/main/res/values/strings.xml | 1 - 41 files changed, 470 insertions(+), 758 deletions(-) delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxNotificationActionFactory.kt rename ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/{MediaPlayerController.kt => MediaPlayerManager.kt} (87%) delete mode 100644 ultrasonic/src/main/res/layout/jukebox_volume.xml diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index 2458dc9d..f77f8b2f 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -1,27 +1,5 @@ - - - - - - - - - + @@ -48,50 +26,6 @@ file="../core/subsonic-api/build/libs/subsonic-api.jar"/> - - - - - - - - - - - - - - - - - - - - - - - = Build.VERSION_CODES.TIRAMISU) { getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) } else { + @Suppress("DEPRECATION") getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java index 9297b5d8..5eb8e214 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/receiver/UltrasonicIntentReceiver.java @@ -13,7 +13,7 @@ import static org.koin.java.KoinJavaComponent.inject; public class UltrasonicIntentReceiver extends BroadcastReceiver { - private final Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); + private Lazy lifecycleSupport = inject(MediaPlayerLifecycleSupport.class); @Override public void onReceive(Context context, Intent intent) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 6ba4f89d..8ed8c8bc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -17,7 +17,6 @@ import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.provider.SearchRecentSuggestions -import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.View @@ -55,8 +54,8 @@ import org.moire.ultrasonic.data.ServerSettingDao import org.moire.ultrasonic.fragment.OnBackPressedHandler import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.provider.SearchSuggestionProvider -import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.util.Constants @@ -98,7 +97,7 @@ class NavigationActivity : AppCompatActivity() { private val serverSettingsModel: ServerSettingsModel by viewModel() private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() private val activeServerProvider: ActiveServerProvider by inject() private val serverRepository: ServerSettingDao by inject() @@ -274,18 +273,6 @@ class NavigationActivity : AppCompatActivity() { } } - override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - val isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN - val isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP - val isVolumeAdjust = isVolumeDown || isVolumeUp - val isJukebox = mediaPlayerController.isJukeboxEnabled - if (isVolumeAdjust && isJukebox) { - mediaPlayerController.adjustVolume(isVolumeUp) - return true - } - return super.onKeyDown(keyCode, event) - } - private fun setupNavigationMenu(navController: NavController) { navigationView?.setupWithNavController(navController) @@ -308,7 +295,7 @@ class NavigationActivity : AppCompatActivity() { } R.id.menu_exit -> { setResult(Constants.RESULT_CLOSE_ALL) - mediaPlayerController.onDestroy() + mediaPlayerManager.onDestroy() finish() exit() } @@ -475,9 +462,9 @@ class NavigationActivity : AppCompatActivity() { } if (nowPlayingView != null) { - val playerState: Int = mediaPlayerController.playbackState + val playerState: Int = mediaPlayerManager.playbackState if (playerState == STATE_BUFFERING || playerState == STATE_READY) { - val item: MediaItem? = mediaPlayerController.currentMediaItem + val item: MediaItem? = mediaPlayerManager.currentMediaItem if (item != null) { nowPlayingView?.visibility = View.VISIBLE } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt index 6ec150c6..823a84fa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt @@ -2,8 +2,8 @@ package org.moire.ultrasonic.di import org.koin.dsl.module import org.moire.ultrasonic.service.ExternalStorageMonitor -import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.PlaybackStateSerializer /** @@ -15,5 +15,5 @@ val mediaPlayerModule = module { single { ExternalStorageMonitor() } // TODO Ideally this can be cleaned up when all circular references are removed. - single { MediaPlayerController(get(), get(), get()) } + single { MediaPlayerManager(get(), get(), get()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index ed644e72..5f0ef215 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -72,7 +72,7 @@ class BookmarksFragment : TrackCollectionFragment() { currentPlayingPosition = songs[0].bookmarkPosition ) - mediaPlayerController.restore( + mediaPlayerManager.restore( state = state, autoPlay = true, newPlaylist = true diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt index b6020f56..70eaf028 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/NowPlayingFragment.kt @@ -25,7 +25,7 @@ import kotlin.math.abs import org.koin.android.ext.android.inject import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Settings @@ -48,7 +48,7 @@ class NowPlayingFragment : Fragment() { private var nowPlayingArtist: TextView? = null private var rxBusSubscription: Disposable? = null - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() override fun onCreate(savedInstanceState: Bundle?) { @@ -85,13 +85,13 @@ class NowPlayingFragment : Fragment() { @SuppressLint("ClickableViewAccessibility") private fun update() { try { - if (mediaPlayerController.isPlaying) { + if (mediaPlayerManager.isPlaying) { playButton!!.setIconResource(R.drawable.media_pause) } else { playButton!!.setIconResource(R.drawable.media_start) } - val file = mediaPlayerController.currentMediaItem?.toTrack() + val file = mediaPlayerManager.currentMediaItem?.toTrack() if (file != null) { val title = file.title @@ -127,7 +127,7 @@ class NowPlayingFragment : Fragment() { // This empty onClickListener is necessary for the onTouchListener to work requireView().setOnClickListener { } - playButton!!.setOnClickListener { mediaPlayerController.togglePlayPause() } + playButton!!.setOnClickListener { mediaPlayerManager.togglePlayPause() } } catch (all: Exception) { Timber.w(all, "Failed to get notification cover art") } @@ -149,10 +149,10 @@ class NowPlayingFragment : Fragment() { if (abs(deltaX) > MIN_DISTANCE) { // left or right if (deltaX < 0) { - mediaPlayerController.seekToPrevious() + mediaPlayerManager.seekToPrevious() } if (deltaX > 0) { - mediaPlayerController.seekToNext() + mediaPlayerManager.seekToNext() } } else if (abs(deltaY) > MIN_DISTANCE) { if (deltaY < 0) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 65f6b442..bdc08eec 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -87,7 +87,7 @@ import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign @@ -128,7 +128,7 @@ class PlayerFragment : // Data & Services private val networkAndStorageChecker: NetworkAndStorageChecker by inject() - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() private val shareHandler: ShareHandler by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() private var currentSong: Track? = null @@ -263,8 +263,8 @@ class PlayerFragment : val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous) val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next) shuffleButton = view.findViewById(R.id.button_shuffle) - updateShuffleButtonState(mediaPlayerController.isShufflePlayEnabled) - updateRepeatButtonState(mediaPlayerController.repeatMode) + updateShuffleButtonState(mediaPlayerManager.isShufflePlayEnabled) + updateRepeatButtonState(mediaPlayerManager.repeatMode) val ratingLinearLayout = view.findViewById(R.id.song_rating) if (!useFiveStarRating) ratingLinearLayout.isVisible = false @@ -286,7 +286,7 @@ class PlayerFragment : previousButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.seekToPrevious() + mediaPlayerManager.seekToPrevious() } } @@ -297,7 +297,7 @@ class PlayerFragment : nextButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.seekToNext() + mediaPlayerManager.seekToNext() } } @@ -307,22 +307,22 @@ class PlayerFragment : pauseButton.setOnClickListener { launch(CommunicationError.getHandler(context)) { - mediaPlayerController.pause() + mediaPlayerManager.pause() } } stopButton.setOnClickListener { launch(CommunicationError.getHandler(context)) { - mediaPlayerController.reset() + mediaPlayerManager.reset() } } playButton.setOnClickListener { - if (!mediaPlayerController.isJukeboxEnabled) + if (!mediaPlayerManager.isJukeboxEnabled) networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() launch(CommunicationError.getHandler(context)) { - mediaPlayerController.play() + mediaPlayerManager.play() } } @@ -331,12 +331,12 @@ class PlayerFragment : } repeatButton.setOnClickListener { - var newRepeat = mediaPlayerController.repeatMode + 1 + var newRepeat = mediaPlayerManager.repeatMode + 1 if (newRepeat == 3) { newRepeat = 0 } - mediaPlayerController.repeatMode = newRepeat + mediaPlayerManager.repeatMode = newRepeat onPlaylistChanged() @@ -358,7 +358,7 @@ class PlayerFragment : progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { override fun onStopTrackingTouch(seekBar: SeekBar) { launch(CommunicationError.getHandler(context)) { - mediaPlayerController.seekTo(progressBar.progress) + mediaPlayerManager.seekTo(progressBar.progress) } } @@ -395,12 +395,19 @@ class PlayerFragment : // Query the Jukebox state in an IO Context ioScope.launch(CommunicationError.getHandler(context)) { try { - jukeboxAvailable = mediaPlayerController.isJukeboxAvailable + jukeboxAvailable = mediaPlayerManager.isJukeboxAvailable } catch (all: Exception) { Timber.e(all) } } + // Subscribe to change in command availability + mediaPlayerManager.addListener(object : Player.Listener { + override fun onAvailableCommandsChanged(availableCommands: Player.Commands) { + updateMediaButtonActivationState() + } + }) + view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) } } @@ -432,7 +439,7 @@ class PlayerFragment : } private fun toggleShuffle() { - val isEnabled = mediaPlayerController.toggleShuffle() + val isEnabled = mediaPlayerManager.toggleShuffle() if (isEnabled) { Util.toast(activity, R.string.download_menu_shuffle_on) @@ -445,7 +452,7 @@ class PlayerFragment : override fun onResume() { super.onResume() - if (mediaPlayerController.currentMediaItem == null) { + if (mediaPlayerManager.currentMediaItem == null) { playlistFlipper.displayedChild = 1 } else { // Download list and Album art must be updated when resumed @@ -458,7 +465,7 @@ class PlayerFragment : executorService = Executors.newSingleThreadScheduledExecutor() executorService.scheduleWithFixedDelay(runnable, 0L, 500L, TimeUnit.MILLISECONDS) - if (mediaPlayerController.keepScreenOn) { + if (mediaPlayerManager.keepScreenOn) { requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } else { requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) @@ -469,7 +476,7 @@ class PlayerFragment : // Scroll to current playing. private fun scrollToCurrent() { - val index = mediaPlayerController.currentMediaItemIndex + val index = mediaPlayerManager.currentMediaItemIndex if (index != -1) { val smoothScroller = LinearSmoothScroller(context) @@ -557,7 +564,7 @@ class PlayerFragment : equalizerMenuItem.isVisible = isEqualizerAvailable } - val mediaPlayerController = mediaPlayerController + val mediaPlayerController = mediaPlayerManager val track = mediaPlayerController.currentMediaItem?.toTrack() if (track != null) { @@ -666,12 +673,12 @@ class PlayerFragment : } R.id.menu_item_screen_on_off -> { val window = requireActivity().window - if (mediaPlayerController.keepScreenOn) { + if (mediaPlayerManager.keepScreenOn) { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - mediaPlayerController.keepScreenOn = false + mediaPlayerManager.keepScreenOn = false } else { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - mediaPlayerController.keepScreenOn = true + mediaPlayerManager.keepScreenOn = true } return true } @@ -684,8 +691,8 @@ class PlayerFragment : return true } R.id.menu_item_jukebox -> { - val jukeboxEnabled = !mediaPlayerController.isJukeboxEnabled - mediaPlayerController.isJukeboxEnabled = jukeboxEnabled + val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled + mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled Util.toast( context, if (jukeboxEnabled) R.string.download_jukebox_on @@ -699,13 +706,13 @@ class PlayerFragment : return true } R.id.menu_item_clear_playlist -> { - mediaPlayerController.isShufflePlayEnabled = false - mediaPlayerController.clear() + mediaPlayerManager.isShufflePlayEnabled = false + mediaPlayerManager.clear() onPlaylistChanged() return true } R.id.menu_item_save_playlist -> { - if (mediaPlayerController.playlistSize > 0) { + if (mediaPlayerManager.playlistSize > 0) { showSavePlaylistDialog() } return true @@ -724,7 +731,7 @@ class PlayerFragment : if (track == null) return true val songId = track.id - val playerPosition = mediaPlayerController.playerPosition + val playerPosition = mediaPlayerManager.playerPosition track.bookmarkPosition = playerPosition val bookmarkTime = Util.formatTotalDuration(playerPosition.toLong(), true) Thread { @@ -759,7 +766,7 @@ class PlayerFragment : return true } R.id.menu_item_share -> { - val mediaPlayerController = mediaPlayerController + val mediaPlayerController = mediaPlayerManager val tracks: MutableList = ArrayList() val playlist = mediaPlayerController.playlist for (item in playlist) { @@ -794,8 +801,7 @@ class PlayerFragment : private fun update(cancel: CancellationToken? = null) { if (cancel?.isCancellationRequested == true) return - val mediaPlayerController = mediaPlayerController - if (currentSong?.id != mediaPlayerController.currentMediaItem?.mediaId) { + if (currentSong?.id != mediaPlayerManager.currentMediaItem?.mediaId) { onTrackChanged() } updateSeekBar() @@ -803,10 +809,10 @@ class PlayerFragment : private fun savePlaylistInBackground(playlistName: String) { Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName)) - mediaPlayerController.suggestedPlaylistName = playlistName + mediaPlayerManager.suggestedPlaylistName = playlistName // The playlist can be acquired only from the main thread - val entries = mediaPlayerController.playlist.map { + val entries = mediaPlayerManager.playlist.map { it.toTrack() } @@ -859,8 +865,8 @@ class PlayerFragment : // Create listener val clickHandler: ((Track, Int) -> Unit) = { _, listPos -> - val mediaIndex = mediaPlayerController.getUnshuffledIndexOf(listPos) - mediaPlayerController.play(mediaIndex) + val mediaIndex = mediaPlayerManager.getUnshuffledIndexOf(listPos) + mediaPlayerManager.play(mediaIndex) } viewAdapter.register( @@ -924,7 +930,7 @@ class PlayerFragment : @SuppressLint("NotifyDataSetChanged") override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { val pos = viewHolder.bindingAdapterPosition - val item = mediaPlayerController.getMediaItemAt(pos) + val item = mediaPlayerManager.getMediaItemAt(pos) // Remove the item from the list quickly val items = viewAdapter.getCurrentList().toMutableList() @@ -940,7 +946,7 @@ class PlayerFragment : Util.toast(context, songRemoved) // Remove the item from the playlist - mediaPlayerController.removeFromPlaylist(pos) + mediaPlayerManager.removeFromPlaylist(pos) } override fun onSelectedChanged( @@ -960,7 +966,7 @@ class PlayerFragment : dragging = false // Move the item in the playlist separately Timber.i("Moving item %s to %s", startPosition, endPosition) - mediaPlayerController.moveItemInPlaylist(startPosition, endPosition) + mediaPlayerManager.moveItemInPlaylist(startPosition, endPosition) } } @@ -1038,7 +1044,7 @@ class PlayerFragment : } private fun onPlaylistChanged() { - val mediaPlayerController = mediaPlayerController + val mediaPlayerController = mediaPlayerManager // Try to display playlist in play order val list = mediaPlayerController.playlistInPlayOrder emptyTextView.setText(R.string.playlist_empty) @@ -1050,12 +1056,12 @@ class PlayerFragment : } private fun onTrackChanged() { - currentSong = mediaPlayerController.currentMediaItem?.toTrack() + currentSong = mediaPlayerManager.currentMediaItem?.toTrack() scrollToCurrent() - val totalDuration = mediaPlayerController.playListDuration - val totalSongs = mediaPlayerController.playlistSize - val currentSongIndex = mediaPlayerController.currentMediaItemIndex + 1 + val totalDuration = mediaPlayerManager.playListDuration + val totalSongs = mediaPlayerManager.playlistSize + val currentSongIndex = mediaPlayerManager.currentMediaItemIndex + 1 val duration = Util.formatTotalDuration(totalDuration) val trackFormat = String.format(Locale.getDefault(), "%d / %d", currentSongIndex, totalSongs) @@ -1110,23 +1116,27 @@ class PlayerFragment : updateSongRating() - nextButton.isEnabled = mediaPlayerController.canSeekToNext() - previousButton.isEnabled = mediaPlayerController.canSeekToPrevious() + updateMediaButtonActivationState() + } + + private fun updateMediaButtonActivationState() { + nextButton.isEnabled = mediaPlayerManager.canSeekToNext() + previousButton.isEnabled = mediaPlayerManager.canSeekToPrevious() } @Synchronized private fun updateSeekBar() { - val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled - val millisPlayed: Int = max(0, mediaPlayerController.playerPosition) - val duration: Int = mediaPlayerController.playerDuration - val playbackState: Int = mediaPlayerController.playbackState + val isJukeboxEnabled: Boolean = mediaPlayerManager.isJukeboxEnabled + val millisPlayed: Int = max(0, mediaPlayerManager.playerPosition) + val duration: Int = mediaPlayerManager.playerDuration + val playbackState: Int = mediaPlayerManager.playbackState if (currentSong != null) { positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true) durationTextView.text = Util.formatTotalDuration(duration.toLong(), true) progressBar.max = if (duration == 0) 100 else duration // Work-around for apparent bug. progressBar.progress = millisPlayed - progressBar.isEnabled = mediaPlayerController.isPlaying || isJukeboxEnabled + progressBar.isEnabled = mediaPlayerManager.isPlaying || isJukeboxEnabled } else { positionTextView.setText(R.string.util_zero_time) durationTextView.setText(R.string.util_no_time) @@ -1135,7 +1145,7 @@ class PlayerFragment : progressBar.isEnabled = false } - val progress = mediaPlayerController.bufferedPercentage + val progress = mediaPlayerManager.bufferedPercentage updateBufferProgress(playbackState, progress) } @@ -1148,7 +1158,7 @@ class PlayerFragment : setTitle(this@PlayerFragment, downloadStatus) } Player.STATE_READY -> { - if (mediaPlayerController.isShufflePlayEnabled) { + if (mediaPlayerManager.isShufflePlayEnabled) { setTitle( this@PlayerFragment, R.string.download_playerstate_playing_shuffle @@ -1172,7 +1182,7 @@ class PlayerFragment : } private fun updateButtonStates(playbackState: Int) { - val isPlaying = mediaPlayerController.isPlaying + val isPlaying = mediaPlayerManager.isPlaying when (playbackState) { Player.STATE_READY -> { pauseButton.isVisible = isPlaying @@ -1195,9 +1205,9 @@ class PlayerFragment : private fun seek(forward: Boolean) { launch(CommunicationError.getHandler(context)) { if (forward) { - mediaPlayerController.seekForward() + mediaPlayerManager.seekForward() } else { - mediaPlayerController.seekBack() + mediaPlayerManager.seekBack() } } } @@ -1223,28 +1233,28 @@ class PlayerFragment : // Right to Left swipe if (e1X - e2X > swipeDistance && absX > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.seekToNext() + mediaPlayerManager.seekToNext() return true } // Left to Right swipe if (e2X - e1X > swipeDistance && absX > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.seekToPrevious() + mediaPlayerManager.seekToPrevious() return true } // Top to Bottom swipe if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000) + mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition + 30000) return true } // Bottom to Top swipe if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000) + mediaPlayerManager.seekTo(mediaPlayerManager.playerPosition - 8000) return true } return false @@ -1309,7 +1319,7 @@ class PlayerFragment : builder.setView(layout) builder.setCancelable(true) val dialog = builder.create() - val playlistName = mediaPlayerController.suggestedPlaylistName + val playlistName = mediaPlayerManager.suggestedPlaylistName if (playlistName != null) { playlistNameView.setText(playlistName) } else { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 9dfcc1ef..e8a26e58 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -42,7 +42,7 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.model.SearchListModel import org.moire.ultrasonic.service.DownloadService -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo @@ -63,7 +63,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { private var searchRefresh: SwipeRefreshLayout? = null private var searchView: SearchView? = null - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() private val shareHandler: ShareHandler by inject() private val networkAndStorageChecker: NetworkAndStorageChecker by inject() @@ -305,15 +305,15 @@ class SearchFragment : MultiListFragment(), KoinComponent { private fun onSongSelected(song: Track, append: Boolean) { if (!append) { - mediaPlayerController.clear() + mediaPlayerManager.clear() } - mediaPlayerController.addToPlaylist( + mediaPlayerManager.addToPlaylist( listOf(song), autoPlay = false, shuffle = false, - insertionMode = MediaPlayerController.InsertionMode.APPEND + insertionMode = MediaPlayerManager.InsertionMode.APPEND ) - mediaPlayerController.play(mediaPlayerController.mediaItemCount - 1) + mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1) toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1)) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 295885ea..700bde05 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -28,7 +28,7 @@ import org.moire.ultrasonic.log.FileLoggerTree.Companion.getLogFileSizes import org.moire.ultrasonic.log.FileLoggerTree.Companion.plantToTimberForest import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest import org.moire.ultrasonic.provider.SearchSuggestionProvider -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.Constants @@ -62,7 +62,7 @@ class SettingsFragment : private var debugLogToFile: CheckBoxPreference? = null private var customCacheLocation: CheckBoxPreference? = null - private val mediaPlayerController: MediaPlayerController by inject() + private val mediaPlayerManager: MediaPlayerManager by inject() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.settings, rootKey) @@ -342,7 +342,7 @@ class SettingsFragment : Settings.cacheLocationUri = path // Clear download queue. - mediaPlayerController.clear() + mediaPlayerManager.clear() Storage.reset() Storage.ensureRootIsAvailable() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 48f39ffb..9e03df83 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -40,7 +40,7 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.model.TrackCollectionModel -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.subsonic.DownloadAction @@ -82,7 +82,7 @@ open class TrackCollectionFragment( private var playAllButton: MenuItem? = null private var shareButton: MenuItem? = null - internal val mediaPlayerController: MediaPlayerController by inject() + internal val mediaPlayerManager: MediaPlayerManager by inject() private val shareHandler: ShareHandler by inject() internal var cancellationToken: CancellationToken? = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt index c7e51723..622f7b99 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt @@ -10,7 +10,7 @@ package org.moire.ultrasonic.model import android.app.Application import androidx.lifecycle.AndroidViewModel import java.io.IOException -import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.flatMapMerge @@ -90,7 +90,7 @@ class EditServerModel(val app: Application) : AndroidViewModel(app), KoinCompone } } - @OptIn(FlowPreview::class) + @OptIn(ExperimentalCoroutinesApi::class) suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow { val client = buildTestClient(currentServerSetting) // One line of magic: diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index d4f12bbc..456f23ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -19,7 +19,6 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES -import androidx.media3.common.Player import androidx.media3.common.Rating import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService @@ -47,7 +46,7 @@ import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.RatingManager import org.moire.ultrasonic.util.MainThreadExecutor @@ -101,10 +100,10 @@ const val PLAY_COMMAND = "play " */ @Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") @SuppressLint("UnsafeOptInUsageError") -class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibraryService) : +class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : MediaLibraryService.MediaLibrarySession.Callback, KoinComponent { - private val mediaPlayerController by inject() + private val mediaPlayerManager by inject() private val activeServerProvider: ActiveServerProvider by inject() private val serviceJob = SupervisorJob() @@ -241,7 +240,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr * is stored in the track.starred value * See https://github.com/androidx/media/issues/33 */ - val track = mediaPlayerController.currentMediaItem?.toTrack() + val track = mediaPlayerManager.currentMediaItem?.toTrack() if (track != null) { customCommandFuture = onSetRating( session, @@ -254,12 +253,13 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr override fun onSuccess(result: SessionResult) { track.starred = !track.starred // This needs to be called on the main Thread + // TODO: This is a looping reference libraryService.onUpdateNotification(session) } override fun onFailure(t: Throwable) { Toast.makeText( - mediaPlayerController.context, + mediaPlayerManager.context, "There was an error updating the rating", LENGTH_SHORT ).show() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt index 65674111..0f283267 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt @@ -20,7 +20,7 @@ import com.google.common.collect.ImmutableList import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.util.toTrack @UnstableApi @@ -33,7 +33,7 @@ class CustomNotificationProvider(ctx: Context) : * is stored in the track.starred value. See https://github.com/androidx/media/issues/33 * TODO: Once the bug is fixed remove this circular reference! */ - private val mediaPlayerController by inject() + private val mediaPlayerManager by inject() override fun addNotificationActions( mediaSession: MediaSession, @@ -48,7 +48,7 @@ class CustomNotificationProvider(ctx: Context) : * is stored in the track.starred value * See https://github.com/androidx/media/issues/33 */ - val rating = mediaPlayerController.currentMediaItem?.toTrack()?.starred?.let { + val rating = mediaPlayerManager.currentMediaItem?.toTrack()?.starred?.let { HeartRating( it ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 7603f94f..6d0f0db6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -26,8 +26,7 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder -import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder +import androidx.media3.exoplayer.source.ShuffleOrder import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -37,6 +36,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.OkHttpClient import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.app.UApp @@ -46,6 +46,8 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider import org.moire.ultrasonic.service.DownloadService +import org.moire.ultrasonic.service.JukeboxMediaPlayer +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.plusAssign @@ -61,9 +63,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent, CoroutineScope by CoroutineScope(Dispatchers.IO) { - private lateinit var player: ExoPlayer + private lateinit var player: Player private lateinit var mediaLibrarySession: MediaLibrarySession private var equalizer: EqualizerController? = null + private val activeServerProvider: ActiveServerProvider by inject() private lateinit var librarySessionCallback: MediaLibrarySession.Callback @@ -76,6 +79,7 @@ class PlaybackService : super.onCreate() initializeSessionAndPlayer() setListener(MediaSessionServiceListener()) + instance = this } private fun getWakeModeFlag(): Int { @@ -99,6 +103,7 @@ class PlaybackService : } private fun releasePlayerAndSession() { + Timber.i("Releasing player and session") // Broadcast that the service is being shutdown RxBus.stopServiceCommandPublisher.onNext(Unit) @@ -127,6 +132,91 @@ class PlaybackService : setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext())) + player = if (activeServerProvider.getActiveServer().jukeboxByDefault) { + Timber.i("Jukebox enabled by default") + getJukeboxPlayer() + } else { + getLocalPlayer() + } + + // Create browser interface + librarySessionCallback = AutoMediaBrowserCallback(this) + + // This will need to use the AutoCalls + mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) + .setSessionActivity(getPendingIntentForContent()) + .setBitmapLoader(ArtworkBitmapLoader()) + .build() + + // Set a listener to update the API client when the active server has changed + rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { + // Set the player wake mode + (player as? ExoPlayer)?.setWakeMode(getWakeModeFlag()) + } + + // Set a listener to reset the ShuffleOrder + rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle -> + // This only applies for local playback + val exo = if (player is ExoPlayer) { + player as ExoPlayer + } else { + return@subscribe + } + val len = player.currentTimeline.windowCount + + Timber.i("Resetting shuffle order, isShuffled: %s", shuffle) + + // If disabling Shuffle return early + if (!shuffle) { + return@subscribe exo.setShuffleOrder( + ShuffleOrder.UnshuffledShuffleOrder(len) + ) + } + + // Get the position of the current track in the unshuffled order + val cur = player.currentMediaItemIndex + val seed = System.currentTimeMillis() + val random = Random(seed) + + val list = createShuffleListFromCurrentIndex(cur, len, random) + Timber.i("New Shuffle order: %s", list.joinToString { it.toString() }) + exo.setShuffleOrder(ShuffleOrder.DefaultShuffleOrder(list, seed)) + } + + // Listen to the shutdown command + rxBusSubscription += RxBus.shutdownCommandObservable.subscribe { + Timber.i("Received destroy command via Rx") + onDestroy() + } + + player.addListener(listener) + isStarted = true + } + + private fun updateBackend(newBackend: MediaPlayerManager.PlayerBackend) { + Timber.i("Switching player backends") + // Remove old listeners + player.removeListener(listener) + player.release() + + player = if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) { + getJukeboxPlayer() + } else { + getLocalPlayer() + } + + // Add fresh listeners + player.addListener(listener) + + mediaLibrarySession.player = player + actualBackend = newBackend + } + + private fun getJukeboxPlayer(): Player { + return JukeboxMediaPlayer() + } + + private fun getLocalPlayer(): Player { // Create a new plain OkHttpClient val builder = OkHttpClient.Builder() val client = builder.build() @@ -147,7 +237,7 @@ class PlaybackService : renderer.setEnableAudioOffload(true) // Create the player - player = ExoPlayer.Builder(this) + val player = ExoPlayer.Builder(this) .setAudioAttributes(getAudioAttributes(), true) .setWakeMode(getWakeModeFlag()) .setHandleAudioBecomingNoisy(true) @@ -157,59 +247,17 @@ class PlaybackService : .setSeekForwardIncrementMs(Settings.seekInterval.toLong()) .build() + // Setup Equalizer equalizer = EqualizerController.create(player.audioSessionId) // Enable audio offload if (Settings.useHwOffload) player.experimentalSetOffloadSchedulingEnabled(true) - // Create browser interface - librarySessionCallback = AutoMediaBrowserCallback(player, this) - - // This will need to use the AutoCalls - mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) - .setSessionActivity(getPendingIntentForContent()) - .setBitmapLoader(ArtworkBitmapLoader()) - .build() - - // Set a listener to update the API client when the active server has changed - rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { - // Set the player wake mode - player.setWakeMode(getWakeModeFlag()) - } - - // Set a listener to reset the ShuffleOrder - rxBusSubscription += RxBus.shufflePlayObservable.subscribe { shuffle -> - val len = player.currentTimeline.windowCount - - Timber.i("Resetting shuffle order, isShuffled: %s", shuffle) - - // If disabling Shuffle return early - if (!shuffle) { - return@subscribe player.setShuffleOrder(UnshuffledShuffleOrder(len)) - } - - // Get the position of the current track in the unshuffled order - val cur = player.currentMediaItemIndex - val seed = System.currentTimeMillis() - val random = Random(seed) - - val list = createShuffleListFromCurrentIndex(cur, len, random) - Timber.i("New Shuffle order: %s", list.joinToString { it.toString() }) - player.setShuffleOrder(DefaultShuffleOrder(list, seed)) - } - - // Listen to the shutdown command - rxBusSubscription += RxBus.shutdownCommandObservable.subscribe { - Timber.i("Received destroy command via Rx") - onDestroy() - } - - player.addListener(listener) - isStarted = true + return player } - fun createShuffleListFromCurrentIndex( + private fun createShuffleListFromCurrentIndex( currentIndex: Int, length: Int, random: Random @@ -244,6 +292,7 @@ class PlaybackService : } private fun cacheNextSongs() { + if (actualBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) return Timber.d("PlaybackService caching the next songs") val nextSongs = Util.getPlayListFromTimeline( player.currentTimeline, @@ -333,6 +382,16 @@ class PlaybackService : } companion object { + var actualBackend: MediaPlayerManager.PlayerBackend? = null + + private var desiredBackend: MediaPlayerManager.PlayerBackend? = null + fun setBackend(playerBackend: MediaPlayerManager.PlayerBackend) { + desiredBackend = playerBackend + instance?.updateBackend(playerBackend) + } + + var instance: PlaybackService? = null + private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic.error" private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages" private const val NOTIFICATION_ID = 3009 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt index d8550f09..bb67ab9b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -7,29 +7,12 @@ package org.moire.ultrasonic.service import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.content.pm.ServiceInfo -import android.os.Build import android.os.Handler -import android.os.IBinder import android.os.Looper -import android.view.Gravity -import android.view.KeyEvent -import android.view.KeyEvent.KEYCODE_MEDIA_NEXT -import android.view.KeyEvent.KEYCODE_MEDIA_PAUSE -import android.view.KeyEvent.KEYCODE_MEDIA_PLAY -import android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -import android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS -import android.view.KeyEvent.KEYCODE_MEDIA_STOP -import android.view.LayoutInflater -import android.view.View -import android.widget.ProgressBar -import android.widget.Toast -import androidx.core.app.NotificationManagerCompat import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.DeviceInfo +import androidx.media3.common.FlagSet import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackException @@ -39,34 +22,27 @@ import androidx.media3.common.Timeline import androidx.media3.common.TrackSelectionParameters import androidx.media3.common.VideoSize import androidx.media3.common.text.CueGroup +import androidx.media3.common.util.Clock +import androidx.media3.common.util.ListenerSet import androidx.media3.common.util.Size -import androidx.media3.session.MediaSession -import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.SettableFuture import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong -import kotlin.math.roundToInt import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.SubsonicRESTException import org.moire.ultrasonic.app.UApp.Companion.applicationContext import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.JukeboxStatus -import org.moire.ultrasonic.playback.CustomNotificationProvider import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService -import org.moire.ultrasonic.util.Util.getPendingIntentToShowPlayer +import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.sleepQuietly -import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification import timber.log.Timber private const val STATUS_UPDATE_INTERVAL_SECONDS = 5L -private const val SEEK_INCREMENT_SECONDS = 5L -private const val SEEK_START_AFTER_SECONDS = 5 private const val QUEUE_POLL_INTERVAL_SECONDS = 1L /** @@ -86,135 +62,64 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { private val timeOfLastUpdate = AtomicLong() private var jukeboxStatus: JukeboxStatus? = null private var previousJukeboxStatus: JukeboxStatus? = null - private var gain = 0.5f - private var volumeToast: VolumeToast? = null + private var gain = (MAX_GAIN / 3) + private val floatGain: Float + get() = gain.toFloat() / MAX_GAIN + private var serviceThread: Thread? = null - private var listeners: MutableList = mutableListOf() + private var listeners: ListenerSet private val playlist: MutableList = mutableListOf() - private var currentIndex: Int = 0 - private val notificationProvider = CustomNotificationProvider(applicationContext()) - private lateinit var mediaSession: MediaSession - private lateinit var notificationManagerCompat: NotificationManagerCompat - @Suppress("MagicNumber") - override fun onCreate() { - super.onCreate() - if (running.get()) return + private var _currentIndex: Int = 0 + private var currentIndex: Int + get() = _currentIndex + set(value) { + // This must never be smaller 0 + _currentIndex = if (value >= 0) value else 0 + } + + companion object { + // This is quite important, by setting the DeviceInfo the player is recognized by + // Android as being a remote playback surface + val DEVICE_INFO = DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 10) + val running = AtomicBoolean() + const val MAX_GAIN = 10 + } + + init { running.set(true) + listeners = ListenerSet( + applicationLooper, + Clock.DEFAULT + ) { listener: Player.Listener, flags: FlagSet? -> + listener.onEvents( + this, + Player.Events( + flags!! + ) + ) + } tasks.clear() updatePlaylist() stop() - - startFuture?.set(this) - startProcessTasks() - - notificationManagerCompat = NotificationManagerCompat.from(this) - mediaSession = MediaSession.Builder(applicationContext(), this) - .setId("jukebox") - .setSessionActivity(getPendingIntentToShowPlayer(this)) - .build() - val notification = notificationProvider.createNotification( - mediaSession, - ImmutableList.of(), - JukeboxNotificationActionFactory() - ) {} - - if (Build.VERSION.SDK_INT >= 29) { - startForeground( - notification.notificationId, - notification.notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK - ) - } else { - startForeground( - notification.notificationId, notification.notification - ) - } - - Timber.d("Started Jukebox Service") } + @Suppress("MagicNumber") - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - if (Intent.ACTION_MEDIA_BUTTON != intent?.action) return START_STICKY - - val extras = intent.extras - if ((extras != null) && extras.containsKey(Intent.EXTRA_KEY_EVENT)) { - val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - extras.getParcelable(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) - } else { - @Suppress("DEPRECATION") - extras.getParcelable(Intent.EXTRA_KEY_EVENT) - } - when (event?.keyCode) { - KEYCODE_MEDIA_PLAY -> play() - KEYCODE_MEDIA_PAUSE -> stop() - KEYCODE_MEDIA_STOP -> stop() - KEYCODE_MEDIA_PLAY_PAUSE -> if (isPlaying) stop() else play() - KEYCODE_MEDIA_PREVIOUS -> seekToPrevious() - KEYCODE_MEDIA_NEXT -> seekToNext() - } - } - return START_STICKY - } - - override fun onDestroy() { + override fun release() { tasks.clear() stop() if (!running.get()) return running.set(false) - serviceThread!!.join() + serviceThread?.join() - stopForegroundRemoveNotification() - mediaSession.release() - - super.onDestroy() Timber.d("Stopped Jukebox Service") } - override fun onBind(p0: Intent?): IBinder? { - return null - } - - fun requestStop() { - stopSelf() - } - - private fun updateNotification() { - val notification = notificationProvider.createNotification( - mediaSession, - ImmutableList.of(), - JukeboxNotificationActionFactory() - ) {} - notificationManagerCompat.notify(notification.notificationId, notification.notification) - } - - companion object { - val running = AtomicBoolean() - private var startFuture: SettableFuture? = null - - @JvmStatic - fun requestStart(): ListenableFuture? { - if (running.get()) return null - startFuture = SettableFuture.create() - val context = applicationContext() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService( - Intent(context, JukeboxMediaPlayer::class.java) - ) - } else { - context.startService(Intent(context, JukeboxMediaPlayer::class.java)) - } - Timber.i("JukeboxMediaPlayer starting...") - return startFuture - } - } - override fun addListener(listener: Player.Listener) { listeners.add(listener) } @@ -263,14 +168,20 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } tasks.add(Skip(mediaItemIndex, positionSeconds)) currentIndex = mediaItemIndex + updateAvailableCommands() } override fun seekBack() { - seekTo(0L.coerceAtMost((jukeboxStatus?.positionSeconds ?: 0) - SEEK_INCREMENT_SECONDS)) + seekTo( + 0L.coerceAtMost( + (jukeboxStatus?.positionSeconds ?: 0) - + Settings.seekIntervalMillis + ) + ) } override fun seekForward() { - seekTo((jukeboxStatus?.positionSeconds ?: 0) + SEEK_INCREMENT_SECONDS) + seekTo((jukeboxStatus?.positionSeconds ?: 0) + Settings.seekIntervalMillis) } override fun isCurrentMediaItemSeekable() = true @@ -292,8 +203,11 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { override fun getAvailableCommands(): Player.Commands { val commandsBuilder = Player.Commands.Builder().addAll( - Player.COMMAND_SET_VOLUME, - Player.COMMAND_GET_VOLUME + Player.COMMAND_CHANGE_MEDIA_ITEMS, + Player.COMMAND_GET_TIMELINE, + Player.COMMAND_GET_DEVICE_VOLUME, + Player.COMMAND_ADJUST_DEVICE_VOLUME, + Player.COMMAND_SET_DEVICE_VOLUME ) if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP) if (playlist.isNotEmpty()) { @@ -306,8 +220,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { Player.COMMAND_SEEK_FORWARD, Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, Player.COMMAND_SEEK_TO_MEDIA_ITEM, - ) - if (currentIndex > 0) commandsBuilder.addAll( + // Seeking back is always available Player.COMMAND_SEEK_TO_PREVIOUS, Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM ) @@ -323,6 +236,18 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { return availableCommands.contains(command) } + private fun updateAvailableCommands() { + Handler(Looper.getMainLooper()).post { + listeners.sendEvent( + Player.EVENT_AVAILABLE_COMMANDS_CHANGED + ) { listener: Player.Listener -> + listener.onAvailableCommandsChanged( + availableCommands + ) + } + } + } + override fun getPlayWhenReady(): Boolean { return isPlaying } @@ -358,21 +283,43 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {} - override fun setVolume(volume: Float) { + override fun setDeviceVolume(volume: Int) { gain = volume tasks.remove(SetGain::class.java) - tasks.add(SetGain(volume)) - val context = applicationContext() - if (volumeToast == null) volumeToast = VolumeToast(context) - volumeToast!!.setVolume(volume) + tasks.add(SetGain(floatGain)) + + // We must trigger an event so that the Controller knows the new volume + Handler(Looper.getMainLooper()).post { + listeners.queueEvent(Player.EVENT_DEVICE_VOLUME_CHANGED) { + it.onDeviceVolumeChanged( + gain, + false + ) + } + } + } + + override fun increaseDeviceVolume() { + gain = (gain + 1).coerceAtMost(MAX_GAIN) + deviceVolume = gain + } + + override fun decreaseDeviceVolume() { + gain = (gain - 1).coerceAtLeast(0) + deviceVolume = gain + } + + override fun setDeviceMuted(muted: Boolean) { + gain = 0 + deviceVolume = gain } override fun getVolume(): Float { - return gain + return floatGain } override fun getDeviceVolume(): Int { - return (gain * 100).toInt() + return gain } override fun addMediaItems(index: Int, mediaItems: MutableList) { @@ -444,7 +391,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } override fun seekToPrevious() { - if ((jukeboxStatus?.positionSeconds ?: 0) > SEEK_START_AFTER_SECONDS) { + if ((jukeboxStatus?.positionSeconds ?: 0) > (Settings.seekIntervalMillis)) { seekTo(currentIndex, 0) return } @@ -499,51 +446,63 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { @Suppress("LoopWithTooManyJumpStatements") private fun processTasks() { Timber.d("JukeboxMediaPlayer processTasks starting") - while (true) { + while (running.get()) { // Sleep a bit to spare processor time if we loop a lot sleepQuietly(10) // This is only necessary if Ultrasonic goes offline sooner than the thread stops if (isOffline()) continue var task: JukeboxTask? = null try { - task = tasks.poll() - // If running is false, exit when the queue is empty - if (task == null && !running.get()) break - if (task == null) continue + task = tasks.poll() ?: continue Timber.v("JukeBoxMediaPlayer processTasks processes Task %s", task::class) val status = task.execute() onStatusUpdate(status) - } catch (x: Throwable) { - onError(task, x) + } catch (all: Throwable) { + onError(task, all) } } Timber.d("JukeboxMediaPlayer processTasks stopped") } + // Jukebox status contains data received from the server, we need to validate it! private fun onStatusUpdate(jukeboxStatus: JukeboxStatus) { timeOfLastUpdate.set(System.currentTimeMillis()) previousJukeboxStatus = this.jukeboxStatus this.jukeboxStatus = jukeboxStatus + var shouldUpdateCommands = false + + // Ensure that the index is never smaller than 0 + // If -1 assume that this means we are not playing + if (jukeboxStatus.currentPlayingIndex != null && jukeboxStatus.currentPlayingIndex!! < 0) { + jukeboxStatus.currentPlayingIndex = 0 + jukeboxStatus.isPlaying = false + } currentIndex = jukeboxStatus.currentPlayingIndex ?: currentIndex if (jukeboxStatus.isPlaying != previousJukeboxStatus?.isPlaying) { + shouldUpdateCommands = true Handler(Looper.getMainLooper()).post { - listeners.forEach { + listeners.queueEvent(Player.EVENT_PLAYBACK_STATE_CHANGED) { it.onPlaybackStateChanged( if (jukeboxStatus.isPlaying) Player.STATE_READY else Player.STATE_IDLE ) + } + + listeners.queueEvent(Player.EVENT_IS_PLAYING_CHANGED) { it.onIsPlayingChanged(jukeboxStatus.isPlaying) } } } if (jukeboxStatus.currentPlayingIndex != previousJukeboxStatus?.currentPlayingIndex) { + shouldUpdateCommands = true currentIndex = jukeboxStatus.currentPlayingIndex ?: 0 val currentMedia = if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex] else MediaItem.EMPTY + Handler(Looper.getMainLooper()).post { - listeners.forEach { + listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) { it.onMediaItemTransition( currentMedia, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK @@ -552,44 +511,39 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } } - updateNotification() + if (shouldUpdateCommands) updateAvailableCommands() + + Handler(Looper.getMainLooper()).post { + listeners.flushEvents() + } } private fun onError(task: JukeboxTask?, x: Throwable) { + var exception: PlaybackException? = null if (x is ApiNotSupportedException && task !is Stop) { - Handler(Looper.getMainLooper()).post { - listeners.forEach { - it.onPlayerError( - PlaybackException( - "Jukebox server too old", - null, - R.string.download_jukebox_server_too_old - ) - ) - } - } + exception = PlaybackException( + "Jukebox server too old", + null, + R.string.download_jukebox_server_too_old + ) } else if (x is OfflineException && task !is Stop) { - Handler(Looper.getMainLooper()).post { - listeners.forEach { - it.onPlayerError( - PlaybackException( - "Jukebox offline", - null, - R.string.download_jukebox_offline - ) - ) - } - } + exception = PlaybackException( + "Jukebox offline", + null, + R.string.download_jukebox_offline + ) } else if (x is SubsonicRESTException && x.code == 50 && task !is Stop) { + exception = PlaybackException( + "Jukebox not authorized", + null, + R.string.download_jukebox_not_authorized + ) + } + + if (exception != null) { Handler(Looper.getMainLooper()).post { - listeners.forEach { - it.onPlayerError( - PlaybackException( - "Jukebox not authorized", - null, - R.string.download_jukebox_not_authorized - ) - ) + listeners.sendEvent(Player.EVENT_PLAYER_ERROR) { + it.onPlayerError(exception) } } } else { @@ -608,8 +562,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } tasks.add(SetPlaylist(ids)) Handler(Looper.getMainLooper()).post { - listeners.forEach { - it.onTimelineChanged( + listeners.sendEvent( + Player.EVENT_TIMELINE_CHANGED + ) { listener: Player.Listener -> + listener.onTimelineChanged( PlaylistTimeline(playlist), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED ) @@ -719,25 +675,6 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } } - @SuppressLint("InflateParams") - private class VolumeToast(context: Context) : Toast(context) { - private val progressBar: ProgressBar - fun setVolume(volume: Float) { - progressBar.progress = (100 * volume).roundToInt() - show() - } - - init { - duration = LENGTH_SHORT - val inflater = - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val view = inflater.inflate(R.layout.jukebox_volume, null) - progressBar = view.findViewById(R.id.jukebox_volume_progress_bar) as ProgressBar - setView(view) - setGravity(Gravity.TOP, 0, 0) - } - } - // The constants below are necessary so a MediaSession can be built from the Jukebox Service override fun isCurrentMediaItemDynamic(): Boolean { return false @@ -748,15 +685,15 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } override fun getMaxSeekToPreviousPosition(): Long { - return SEEK_START_AFTER_SECONDS * 1000L + return Settings.seekInterval.toLong() } override fun getSeekBackIncrement(): Long { - return SEEK_INCREMENT_SECONDS * 1000L + return Settings.seekInterval.toLong() } override fun getSeekForwardIncrement(): Long { - return SEEK_INCREMENT_SECONDS * 1000L + return Settings.seekInterval.toLong() } override fun isLoading(): Boolean { @@ -779,6 +716,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { return AudioAttributes.DEFAULT } + override fun setVolume(volume: Float) {} + override fun getVideoSize(): VideoSize { return VideoSize(0, 0) } @@ -824,7 +763,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { } override fun getDeviceInfo(): DeviceInfo { - return DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 1) + return DEVICE_INFO } override fun getPlayerError(): PlaybackException? { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxNotificationActionFactory.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxNotificationActionFactory.kt deleted file mode 100644 index 73bad2cb..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxNotificationActionFactory.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * JukeboxNotificationActionFactory.kt - * Copyright (C) 2009-2022 Ultrasonic developers - * - * Distributed under terms of the GNU GPLv3 license. - */ - -package org.moire.ultrasonic.service - -import android.annotation.SuppressLint -import android.app.PendingIntent -import android.content.ComponentName -import android.content.Intent -import android.os.Bundle -import android.view.KeyEvent -import androidx.core.app.NotificationCompat -import androidx.core.graphics.drawable.IconCompat -import androidx.media3.common.Player -import androidx.media3.common.util.Util -import androidx.media3.session.CommandButton -import androidx.media3.session.MediaNotification -import androidx.media3.session.MediaSession -import org.moire.ultrasonic.app.UApp - -/** - * This class creates Intents and Actions to be used with the Media Notification - * of the Jukebox Service - */ -@SuppressLint("UnsafeOptInUsageError") -class JukeboxNotificationActionFactory : MediaNotification.ActionFactory { - override fun createMediaAction( - mediaSession: MediaSession, - icon: IconCompat, - title: CharSequence, - command: Int - ): NotificationCompat.Action { - return NotificationCompat.Action( - icon, title, createMediaActionPendingIntent(mediaSession, command.toLong()) - ) - } - - override fun createCustomAction( - mediaSession: MediaSession, - icon: IconCompat, - title: CharSequence, - customAction: String, - extras: Bundle - ): NotificationCompat.Action { - return NotificationCompat.Action( - icon, title, null - ) - } - - override fun createCustomActionFromCustomCommandButton( - mediaSession: MediaSession, - customCommandButton: CommandButton - ): NotificationCompat.Action { - return NotificationCompat.Action(null, null, null) - } - - @Suppress("MagicNumber") - override fun createMediaActionPendingIntent( - mediaSession: MediaSession, - command: Long - ): PendingIntent { - val keyCode: Int = toKeyCode(command) - val intent = Intent(Intent.ACTION_MEDIA_BUTTON) - intent.component = ComponentName(UApp.applicationContext(), JukeboxMediaPlayer::class.java) - intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode)) - return if (Util.SDK_INT >= 26 && command == Player.COMMAND_PLAY_PAUSE.toLong()) { - return PendingIntent.getForegroundService( - UApp.applicationContext(), keyCode, intent, PendingIntent.FLAG_IMMUTABLE - ) - } else { - PendingIntent.getService( - UApp.applicationContext(), - keyCode, - intent, - if (Util.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0 - ) - } - } - - private fun toKeyCode(action: @Player.Command Long): Int { - return when (action.toInt()) { - Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, - Player.COMMAND_SEEK_TO_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT - Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, - Player.COMMAND_SEEK_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS - Player.COMMAND_STOP -> KeyEvent.KEYCODE_MEDIA_STOP - Player.COMMAND_SEEK_FORWARD -> KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - Player.COMMAND_SEEK_BACK -> KeyEvent.KEYCODE_MEDIA_REWIND - Player.COMMAND_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - else -> KeyEvent.KEYCODE_UNKNOWN - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt index eeba3b52..a22111be 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxUnimplementedFunctions.kt @@ -8,7 +8,6 @@ package org.moire.ultrasonic.service import android.annotation.SuppressLint -import android.app.Service import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView @@ -26,7 +25,7 @@ import androidx.media3.common.Tracks */ @Suppress("TooManyFunctions") @SuppressLint("UnsafeOptInUsageError") -abstract class JukeboxUnimplementedFunctions : Service(), Player { +abstract class JukeboxUnimplementedFunctions : Player { override fun setMediaItems(mediaItems: MutableList) { TODO("Not yet implemented") @@ -140,10 +139,6 @@ abstract class JukeboxUnimplementedFunctions : Service(), Player { TODO("Not yet implemented") } - override fun release() { - TODO("Not yet implemented") - } - override fun getCurrentTracks(): Tracks { // TODO Dummy information is returned for now, this seems to work return Tracks.EMPTY @@ -228,20 +223,4 @@ abstract class JukeboxUnimplementedFunctions : Service(), Player { override fun clearVideoTextureView(textureView: TextureView?) { TODO("Not yet implemented") } - - override fun setDeviceVolume(volume: Int) { - TODO("Not yet implemented") - } - - override fun increaseDeviceVolume() { - TODO("Not yet implemented") - } - - override fun decreaseDeviceVolume() { - TODO("Not yet implemented") - } - - override fun setDeviceMuted(muted: Boolean) { - TODO("Not yet implemented") - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt index 3c5d4df3..8c64d9a0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerLifecycleSupport.kt @@ -32,7 +32,7 @@ import timber.log.Timber class MediaPlayerLifecycleSupport : KoinComponent { private lateinit var ratingManager: RatingManager private val playbackStateSerializer by inject() - private val mediaPlayerController by inject() + private val mediaPlayerManager by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() private var created = false @@ -64,7 +64,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { return } - mediaPlayerController.onCreate { + mediaPlayerManager.onCreate { restoreLastSession(autoPlay, afterRestore) } @@ -81,7 +81,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { Timber.i("Restoring %s songs", it!!.songs.size) - mediaPlayerController.restore( + mediaPlayerManager.restore( it, autoPlay, false @@ -110,7 +110,7 @@ class MediaPlayerLifecycleSupport : KoinComponent { if (intent == null) return val intentAction = intent.action - if (intentAction == null || intentAction.isEmpty()) return + if (intentAction.isNullOrEmpty()) return Timber.i("Received intent: %s", intentAction) @@ -146,15 +146,15 @@ class MediaPlayerLifecycleSupport : KoinComponent { val state = extras.getInt("state") if (state == 0) { - if (!mediaPlayerController.isJukeboxEnabled) { - mediaPlayerController.pause() + if (!mediaPlayerManager.isJukeboxEnabled) { + mediaPlayerManager.pause() } } else if (state == 1) { - if (!mediaPlayerController.isJukeboxEnabled && - Settings.resumePlayOnHeadphonePlug && !mediaPlayerController.isPlaying + if (!mediaPlayerManager.isJukeboxEnabled && + Settings.resumePlayOnHeadphonePlug && !mediaPlayerManager.isPlaying ) { - mediaPlayerController.prepare() - mediaPlayerController.play() + mediaPlayerManager.prepare() + mediaPlayerManager.play() } } } @@ -183,18 +183,18 @@ class MediaPlayerLifecycleSupport : KoinComponent { onCreate(autoStart) { when (keyCode) { KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, - KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause() - KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.seekToPrevious() - KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.seekToNext() - KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop() - KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play() - KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() - KeyEvent.KEYCODE_1 -> mediaPlayerController.legacySetRating(1) - KeyEvent.KEYCODE_2 -> mediaPlayerController.legacySetRating(2) - KeyEvent.KEYCODE_3 -> mediaPlayerController.legacySetRating(3) - KeyEvent.KEYCODE_4 -> mediaPlayerController.legacySetRating(4) - KeyEvent.KEYCODE_5 -> mediaPlayerController.legacySetRating(5) - KeyEvent.KEYCODE_STAR -> mediaPlayerController.legacyToggleStar() + KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerManager.togglePlayPause() + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerManager.seekToPrevious() + KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerManager.seekToNext() + KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerManager.stop() + KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerManager.play() + KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerManager.pause() + KeyEvent.KEYCODE_1 -> mediaPlayerManager.legacySetRating(1) + KeyEvent.KEYCODE_2 -> mediaPlayerManager.legacySetRating(2) + KeyEvent.KEYCODE_3 -> mediaPlayerManager.legacySetRating(3) + KeyEvent.KEYCODE_4 -> mediaPlayerManager.legacySetRating(4) + KeyEvent.KEYCODE_5 -> mediaPlayerManager.legacySetRating(5) + KeyEvent.KEYCODE_STAR -> mediaPlayerManager.legacyToggleStar() else -> { } } @@ -222,17 +222,17 @@ class MediaPlayerLifecycleSupport : KoinComponent { // We can receive intents when everything is stopped, so we need to start onCreate(autoStart) { when (action) { - Constants.CMD_PLAY -> mediaPlayerController.play() + Constants.CMD_PLAY -> mediaPlayerManager.play() Constants.CMD_RESUME_OR_PLAY -> // If Ultrasonic wasn't running, the autoStart is enough to resume, // no need to call anything - if (isRunning) mediaPlayerController.resumeOrPlay() + if (isRunning) mediaPlayerManager.resumeOrPlay() - Constants.CMD_NEXT -> mediaPlayerController.seekToNext() - Constants.CMD_PREVIOUS -> mediaPlayerController.seekToPrevious() - Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause() - Constants.CMD_STOP -> mediaPlayerController.stop() - Constants.CMD_PAUSE -> mediaPlayerController.pause() + Constants.CMD_NEXT -> mediaPlayerManager.seekToNext() + Constants.CMD_PREVIOUS -> mediaPlayerManager.seekToPrevious() + Constants.CMD_TOGGLEPAUSE -> mediaPlayerManager.togglePlayPause() + Constants.CMD_STOP -> mediaPlayerManager.stop() + Constants.CMD_PAUSE -> mediaPlayerManager.pause() } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt similarity index 87% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt index 2be0f26f..a580b3d3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt @@ -16,8 +16,6 @@ import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player -import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT -import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Rating @@ -50,12 +48,13 @@ private const val CONTROLLER_SWITCH_DELAY = 500L private const val VOLUME_DELTA = 0.05f /** - * The implementation of the Media Player Controller. + * The Media Player Manager can forward commands to the Media3 controller as + * well as switch between different player interfaces (local, remote, cast etc). * This class contains everything that is necessary for the Application UI * to control the Media Player implementation. */ @Suppress("TooManyFunctions") -class MediaPlayerController( +class MediaPlayerManager( private val playbackStateSerializer: PlaybackStateSerializer, private val externalStorageMonitor: ExternalStorageMonitor, val context: Context @@ -97,15 +96,15 @@ class MediaPlayerController( * We run the event through RxBus in order to throttle them */ override fun onTimelineChanged(timeline: Timeline, reason: Int) { - val start = controller?.currentTimeline?.getFirstWindowIndex(isShufflePlayEnabled) + val start = timeline.getFirstWindowIndex(isShufflePlayEnabled) Timber.w("On timeline changed. First shuffle play at index: %s", start) deferredPlay?.let { Timber.w("Executing deferred shuffle play") it() deferredPlay = null } - - RxBus.playlistPublisher.onNext(playlist.map(MediaItem::toTrack)) + val playlist = Util.getPlayListFromTimeline(timeline, false).map(MediaItem::toTrack) + RxBus.playlistPublisher.onNext(playlist) } override fun onPlaybackStateChanged(playbackState: Int) { @@ -179,11 +178,8 @@ class MediaPlayerController( fun onCreate(onCreated: () -> Unit) { if (created) return externalStorageMonitor.onCreate { reset() } - if (activeServerProvider.getActiveServer().jukeboxByDefault) { - switchToJukebox(onCreated) - } else { - switchToLocalPlayer(onCreated) - } + + createMediaController(onCreated) rxBusSubscription += RxBus.activeServerChangingObservable.subscribe { oldServer -> if (oldServer != OFFLINE_DB_ID) { @@ -195,8 +191,7 @@ class MediaPlayerController( if (controller is JukeboxMediaPlayer) { // When the server changes, the Jukebox should be released. // The new server won't understand the jukebox requests of the old one. - releaseJukebox(controller) - controller = null + switchToLocalPlayer() } } @@ -246,6 +241,22 @@ class MediaPlayerController( Timber.i("MediaPlayerController started") } + private fun createMediaController(onCreated: () -> Unit) { + mediaControllerFuture = MediaController.Builder( + context, + sessionToken + ).buildAsync() + + mediaControllerFuture?.addListener({ + controller = mediaControllerFuture?.get() + + Timber.i("MediaController Instance received") + controller?.addListener(listeners) + onCreated() + Timber.i("MediaPlayerController creation complete") + }, MoreExecutors.directExecutor()) + } + private fun playerStateChangedHandler() { val currentPlaying = controller?.currentMediaItem?.toTrack() ?: return @@ -262,6 +273,10 @@ class MediaPlayerController( } } + fun addListener(listener: Player.Listener) { + controller?.addListener(listener) + } + private fun clearBookmark() { // This method is called just before we update the cachedMediaItem, // so in fact cachedMediaItem will refer to the track that has just finished. @@ -336,7 +351,6 @@ class MediaPlayerController( @Synchronized fun play(index: Int) { controller?.seekTo(index, 0L) - // FIXME CHECK ITS NOT MAKING PROBLEMS controller?.prepare() controller?.play() } @@ -538,7 +552,7 @@ class MediaPlayerController( @Synchronized fun canSeekToPrevious(): Boolean { - return controller?.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS) == true + return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS) == true } @Synchronized @@ -548,7 +562,7 @@ class MediaPlayerController( @Synchronized fun canSeekToNext(): Boolean { - return controller?.isCommandAvailable(COMMAND_SEEK_TO_NEXT) == true + return controller?.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT) == true } @Synchronized @@ -580,102 +594,49 @@ class MediaPlayerController( @set:Synchronized var isJukeboxEnabled: Boolean - get() = controller is JukeboxMediaPlayer - set(jukeboxEnabled) { - if (jukeboxEnabled) { - switchToJukebox {} + get() = PlaybackService.actualBackend == PlayerBackend.JUKEBOX + set(shouldEnable) { + if (shouldEnable) { + switchToJukebox() } else { - switchToLocalPlayer {} + switchToLocalPlayer() } } - private fun switchToJukebox(onCreated: () -> Unit) { - if (controller is JukeboxMediaPlayer) return - val currentPlaylist = playlist - val currentIndex = controller?.currentMediaItemIndex ?: 0 - val currentPosition = controller?.currentPosition ?: 0 + private fun switchToJukebox() { + if (isJukeboxEnabled) return + scheduleSwitchTo(PlayerBackend.JUKEBOX) DownloadService.requestStop() controller?.pause() controller?.stop() - val oldController = controller - controller = null // While we switch, the controller shouldn't be available - - // Stop() won't work if we don't give it time to be processed - Handler(Looper.getMainLooper()).postDelayed({ - if (oldController != null) releaseLocalPlayer(oldController) - setupJukebox { - controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition) - onCreated() - } - }, CONTROLLER_SWITCH_DELAY) } - private fun switchToLocalPlayer(onCreated: () -> Unit) { - if (controller is MediaController) return - val currentPlaylist = playlist + private fun switchToLocalPlayer() { + if (!isJukeboxEnabled) return + scheduleSwitchTo(PlayerBackend.LOCAL) + controller?.stop() + } + + private fun scheduleSwitchTo(newBackend: PlayerBackend) { + val currentPlaylist = playlist.toList() val currentIndex = controller?.currentMediaItemIndex ?: 0 val currentPosition = controller?.currentPosition ?: 0 - controller?.stop() - val oldController = controller - controller = null // While we switch, the controller shouldn't be available Handler(Looper.getMainLooper()).postDelayed({ - if (oldController != null) releaseJukebox(oldController) - setupLocalPlayer { - controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition) - onCreated() - } + // Change the backend + PlaybackService.setBackend(newBackend) + // Restore the media items + controller?.setMediaItems(currentPlaylist, currentIndex, currentPosition) }, CONTROLLER_SWITCH_DELAY) } private fun releaseController() { - when (controller) { - null -> return - is JukeboxMediaPlayer -> releaseJukebox(controller) - is MediaController -> releaseLocalPlayer(controller) - } - } - - private fun setupLocalPlayer(onCreated: () -> Unit) { - mediaControllerFuture = MediaController.Builder( - context, - sessionToken - ).buildAsync() - - mediaControllerFuture?.addListener({ - controller = mediaControllerFuture?.get() - - Timber.i("MediaController Instance received") - controller?.addListener(listeners) - onCreated() - Timber.i("MediaPlayerController creation complete") - }, MoreExecutors.directExecutor()) - } - - private fun releaseLocalPlayer(player: Player?) { - player?.removeListener(listeners) - player?.release() + controller?.removeListener(listeners) + controller?.release() if (mediaControllerFuture != null) MediaController.releaseFuture(mediaControllerFuture!!) Timber.i("MediaPlayerController released") } - private fun setupJukebox(onCreated: () -> Unit) { - val jukeboxFuture = JukeboxMediaPlayer.requestStart() - jukeboxFuture?.addListener({ - controller = jukeboxFuture.get() - onCreated() - controller?.addListener(listeners) - Timber.i("JukeboxService creation complete") - }, MoreExecutors.directExecutor()) - } - - private fun releaseJukebox(player: Player?) { - val jukebox = player as JukeboxMediaPlayer? - jukebox?.removeListener(listeners) - jukebox?.requestStop() - Timber.i("JukeboxService released") - } - /** * This function calls the music service directly and * therefore can't be called from the main thread @@ -700,10 +661,6 @@ class MediaPlayerController( controller?.volume = gain } - fun setVolume(volume: Float) { - controller?.volume = volume - } - /* * Sets the rating of the current track */ @@ -841,4 +798,6 @@ class MediaPlayerController( enum class InsertionMode { CLEAR, APPEND, AFTER_CURRENT } + + enum class PlayerBackend { JUKEBOX, LOCAL } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 4ba976ed..5acf05be 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -18,7 +18,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.DownloadService -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.executeTaskWithToast @@ -28,7 +28,7 @@ import org.moire.ultrasonic.util.executeTaskWithToast */ @Suppress("LongParameterList") class DownloadHandler( - val mediaPlayerController: MediaPlayerController, + val mediaPlayerManager: MediaPlayerManager, private val networkAndStorageChecker: NetworkAndStorageChecker ) : CoroutineScope by CoroutineScope(Dispatchers.IO) { private val maxSongs = 500 @@ -150,16 +150,16 @@ class DownloadHandler( networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() val insertionMode = when { - append -> MediaPlayerController.InsertionMode.APPEND - playNext -> MediaPlayerController.InsertionMode.AFTER_CURRENT - else -> MediaPlayerController.InsertionMode.CLEAR + append -> MediaPlayerManager.InsertionMode.APPEND + playNext -> MediaPlayerManager.InsertionMode.AFTER_CURRENT + else -> MediaPlayerManager.InsertionMode.CLEAR } if (playlistName != null) { - mediaPlayerController.suggestedPlaylistName = playlistName + mediaPlayerManager.suggestedPlaylistName = playlistName } - mediaPlayerController.addToPlaylist( + mediaPlayerManager.addToPlaylist( songs, autoPlay, shuffle, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt index 675979f7..3ec2cee6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -19,7 +19,7 @@ import org.koin.java.KoinJavaComponent.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.domain.Track -import org.moire.ultrasonic.service.MediaPlayerController +import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile import org.moire.ultrasonic.util.FileUtil.getCompleteFile import org.moire.ultrasonic.util.FileUtil.getPartialFile @@ -235,8 +235,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo private fun findFilesToNotDelete(): Set { val filesToNotDelete: MutableSet = HashSet(5) - val mediaController = inject( - MediaPlayerController::class.java + val mediaController = inject( + MediaPlayerManager::class.java ) val playlist = mainScope.future { mediaController.value.playlist }.get() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index e1f421f0..d3aa70f9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -128,6 +128,9 @@ object Settings { var seekInterval by StringIntSetting(getKey(R.string.setting_key_increment_time), 5000) + val seekIntervalMillis: Long + get() = (seekInterval / 1000).toLong() + @JvmStatic var mediaButtonsEnabled by BooleanSetting(getKey(R.string.setting_key_media_buttons), true) diff --git a/ultrasonic/src/main/res/layout/jukebox_volume.xml b/ultrasonic/src/main/res/layout/jukebox_volume.xml deleted file mode 100644 index 81adf6ad..00000000 --- a/ultrasonic/src/main/res/layout/jukebox_volume.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 0d215da8..c06ae0b5 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -48,7 +48,6 @@ Vzdálené ovládání není dostupné v offline módu. Vzdálené ovládání zapnuto. Hudba přehrávána na serveru. Vzdálené ovládání není podporováno. Aktualizujte svůj Subsonic server. - Hlasitost vzdáleného přístroje Ekvalizér Jukebox vypnut Jukebox zapnut diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index bb3b8559..728688b9 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -61,7 +61,6 @@ Fernbedienungs-Modus is Offline nicht verfügbar. Fernbedienung ausgeschaltet. Musik wird auf dem Server wiedergegeben. Fernbedienungs Modus wird nicht unterstützt. Bitte den Subsonic Server aktualisieren. - Entfernte Lautstärke Equalizer Jukebox Aus Jukebox An diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 04ac76bd..1a34b0e9 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -62,7 +62,6 @@ Control remoto no disponible en modo fuera de línea. Control remoto encendido. La música se reproduce en el servidor. Control remoto no soportado. Por favor actualiza tu servidor de Subsonic. - Volumen remoto Ecualizador Apagar Jukebox Encender Jukebox diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index b538dd39..735457b5 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -61,7 +61,6 @@ Le mode jukebox n\'est pas disponible en mode déconnecté. Mode jukebox activé. La musique est jouée sur le serveur Le mode jukebox n\'est pas pris en charge. Mise à jour du serveur Subsonic requise. - Volume sur serveur distant Égaliseur Désactiver le mode jukebox Activer le mode jukebox diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index ed40d385..f85bf6b3 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -54,7 +54,6 @@ A távvezérlés nem lehetséges kapcsolat nélküli módban! Távvezérlés bekapcsolása. A zenelejátszás a kiszolgálón történik. A távvezérlés nem támogatott. Kérjük, frissítse a Subsonic kiszolgálót! - Hangerő távvezérlése Equalizer Jukebox ki Jukebox be diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index c5e1dd60..0b778491 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -45,7 +45,6 @@ Il controllo remoto non è disponibile nella modalità offline. Controllo remoto abilitato. La musica verrà riprodotta sul server. Il controllo remoto non è supportato. Per favore aggiorna la versione del server Airsonic. - Volume remoto Equalizzatore Jukebox spento Jukebox acceso diff --git a/ultrasonic/src/main/res/values-ja/strings.xml b/ultrasonic/src/main/res/values-ja/strings.xml index afe2dcca..b1ee02f1 100644 --- a/ultrasonic/src/main/res/values-ja/strings.xml +++ b/ultrasonic/src/main/res/values-ja/strings.xml @@ -42,7 +42,6 @@ リモートコントロールがオンになりました。サーバーで音楽が再生されます。 リモートコントロールがオフになりました。モバイル端末で音楽が再生されます。 リモートコントロールがサポートされていません。Subsonicサーバーをアップグレードしてください。 - リモート音量 ジュークボックス ON 歌詞 アルバムを表示 diff --git a/ultrasonic/src/main/res/values-nb-rNO/strings.xml b/ultrasonic/src/main/res/values-nb-rNO/strings.xml index 2d0addec..5d530829 100644 --- a/ultrasonic/src/main/res/values-nb-rNO/strings.xml +++ b/ultrasonic/src/main/res/values-nb-rNO/strings.xml @@ -339,7 +339,6 @@ Fjernkontroll er avskrudd. Skru på jukebox-modus i Brukere > Innstillinger på din Subsonic-tjener. Fjernkontroll avskrudd. Musikk spilles på enheten. Fjernkontroll støttes ikke. Oppgrader din Subsonic-tjener. - Fjernkontroll Jukebox avslått Jukebox påslått Omstokking diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index b92aab0f..882ba982 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -63,7 +63,6 @@ Afstandsbediening is niet beschikbaar in offline-modus. Afstandsbediening ingeschakeld; muziek wordt afgespeeld op de server. Afstandsbediening wordt niet ondersteund. Werk je Subsonic-server bij. - Afstandsbedieningvolume Equalizer Jukebox uitgeschakeld Jukebox ingeschakeld diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index c75f5e26..c5f5cef2 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -47,7 +47,6 @@ Pilot jest niedostępny w trybie offline. Tryb pilota jest włączony. Muzyka jest odtwarzana na serwerze. Tryb pilota jest niedostępny. Proszę uaktualnić serwer Subsonic. - Zdalna głośność Korektor dźwięku Jukebox wyłączony Jukebox włączony diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index a6bf5731..b61c8b15 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -62,7 +62,6 @@ Controle remoto não está disponível no modo offline. Controle remoto ligado. Música tocada no servidor. Controle remoto não suportado. Atualize seu servidor Subsonic. - Volume Remoto Equalizador Jukebox Desligado Jukebox Ligado diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index d7c7f4b9..682ecc02 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -47,7 +47,6 @@ Controle remoto não está disponível no modo offline. Controle remoto ligado. Música tocada no servidor. Controle remoto não suportado. Atualize seu servidor Subsonic. - Volume Remoto Equalizador Jukebox Desligado Jukebox Ligado diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index 05cb3e20..93eb988a 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -59,7 +59,6 @@ Пульт дистанционного управления недоступен в автономном режиме. Включен пульт управления. Музыка играет на сервере. Пульт дистанционного управления не поддерживается. Пожалуйста, обновите ваш дозвуковой сервер. - Удаленная громкость Эквалайзер Jukebox выключен Jukebox включен diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index fea8126c..50265302 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -60,7 +60,6 @@ 离线模式不支持远程控制。 已打开远程控制,音乐将在服务端播放。 远程控制不支持,请升级您的 Subsonic 服务器。 - 远程音量 均衡器 关闭点唱机 开启点唱机 diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index 8fdfccfd..6b7b0983 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -141,7 +141,6 @@ 固定 傳送 聊天 - 遠端音量 頭像 您真的要刪除目前選取的項目嗎? 無法理解答覆,請檢查伺服器位址。 diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 12c534f1..394091ac 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -63,7 +63,6 @@ Remote control is not available in offline mode. Turned on remote control. Music is played on server. Remote control is not supported. Please upgrade your Subsonic server. - Remote Volume Equalizer Jukebox Off Jukebox On From 5ec0d8a96bb5d92685b73bb89e603eb0c54656a1 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sat, 20 May 2023 13:38:12 +0000 Subject: [PATCH 51/69] Add hasFragileUserData=true --- ultrasonic/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml index 7c0c1954..266ccd79 100644 --- a/ultrasonic/src/main/AndroidManifest.xml +++ b/ultrasonic/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ Date: Sat, 20 May 2023 14:32:27 +0000 Subject: [PATCH 52/69] Refactor rating controls in Session --- .../ultrasonic/util/SimpleServiceBinder.java | 39 ------ .../ultrasonic/adapters/TrackViewHolder.kt | 13 +- .../ultrasonic/fragment/PlayerFragment.kt | 3 +- .../ultrasonic/fragment/SearchFragment.kt | 34 ++++- .../fragment/TrackCollectionFragment.kt | 86 ++++++------ .../playback/AutoMediaBrowserCallback.kt | 125 +++++++++++------- .../playback/CustomNotificationProvider.kt | 63 +-------- .../ultrasonic/playback/PlaybackService.kt | 34 ++++- .../ultrasonic/service/DownloadService.kt | 4 +- 9 files changed, 196 insertions(+), 205 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/SimpleServiceBinder.java diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/SimpleServiceBinder.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/SimpleServiceBinder.java deleted file mode 100644 index 47360a1b..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/SimpleServiceBinder.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic 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. - - Subsonic 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 Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.util; - -import android.os.Binder; - -/** - * @author Sindre Mehus - */ -public class SimpleServiceBinder extends Binder -{ - private final S service; - - public SimpleServiceBinder(S service) - { - this.service = service; - } - - public S getService() - { - return service; - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 79b74069..313c6dd9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -11,6 +11,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.MutableLiveData import androidx.media3.common.HeartRating +import androidx.media3.common.StarRating import androidx.recyclerview.widget.RecyclerView import com.google.android.material.progressindicator.CircularProgressIndicator import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -139,7 +140,17 @@ class TrackViewHolder(val view: View) : updateStatus(it.state, it.progress) } - // Timber.v("Setting song done") + // Listen for rating updates + rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe { + // Ignore updates which are not for the current song + if (it.id != song.id) return@subscribe + + if (it.rating is HeartRating) { + updateSingleStar(it.rating.isHeart) + } else if (it.rating is StarRating) { + updateFiveStars(it.rating.starRating.toInt()) + } + } } // This is called when the Holder is recycled and receives a new Song diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index bdc08eec..a0ca0c75 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -225,7 +225,7 @@ class PlayerFragment : fiveStar5ImageView = view.findViewById(R.id.song_five_star_5) } - @Suppress("LongMethod", "DEPRECATION") + @Suppress("LongMethod") @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { cancellationToken = CancellationToken() @@ -235,6 +235,7 @@ class PlayerFragment : val width: Int val height: Int + @Suppress("DEPRECATION") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val bounds = windowManager.currentWindowMetrics.bounds width = bounds.width() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index e8a26e58..bd70f431 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -15,8 +15,11 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.appcompat.widget.SearchView +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.viewModelScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -55,8 +58,7 @@ import timber.log.Timber /** * Initiates a search on the media library and displays the results - * - * TODO: Implement the search field without using the deprecated OptionsMenu calls + * TODO: Switch to material3 class */ class SearchFragment : MultiListFragment(), KoinComponent { private var searchResult: SearchResult? = null @@ -80,7 +82,13 @@ class SearchFragment : MultiListFragment(), KoinComponent { super.onViewCreated(view, savedInstanceState) cancellationToken = CancellationToken() setTitle(this, R.string.search_title) - setHasOptionsMenu(true) + + // Register our options menu + (requireActivity() as MenuHost).addMenuProvider( + menuProvider, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) listModel.searchResult.observe( viewLifecycleOwner @@ -141,12 +149,24 @@ class SearchFragment : MultiListFragment(), KoinComponent { } /** - * This method creates the search bar above the recycler view + * This provide creates the search bar above the recycler view */ - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + private val menuProvider: MenuProvider = object : MenuProvider { + override fun onPrepareMenu(menu: Menu) { + setupOptionsMenu(menu) + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.search, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return true + } + } + fun setupOptionsMenu(menu: Menu) { val activity = activity ?: return val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager - inflater.inflate(R.menu.search, menu) val searchItem = menu.findItem(R.id.search_item) searchView = searchItem.actionView as SearchView val searchableInfo = searchManager.getSearchableInfo(requireActivity().componentName) @@ -275,7 +295,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { id = item.id, name = item.name, parentId = item.id, - isArtist = (item is Artist) + isArtist = false ) } else { SearchFragmentDirections.searchToAlbumsList( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 9e03df83..f771710f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -12,8 +12,11 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LiveData import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewModelScope @@ -114,7 +117,13 @@ open class TrackCollectionFragment( setupButtons(view) registerForContextMenu(listView!!) - setHasOptionsMenu(true) + + // Register our options menu + (requireActivity() as MenuHost).addMenuProvider( + menuProvider, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) // Create a View Manager viewManager = LinearLayoutManager(this.context) @@ -257,41 +266,39 @@ open class TrackCollectionFragment( } } - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - playAllButton = menu.findItem(R.id.select_album_play_all) + private val menuProvider: MenuProvider = object : MenuProvider { + override fun onPrepareMenu(menu: Menu) { + playAllButton = menu.findItem(R.id.select_album_play_all) - if (playAllButton != null) { - playAllButton!!.isVisible = playAllButtonVisible + if (playAllButton != null) { + playAllButton!!.isVisible = playAllButtonVisible + } + + shareButton = menu.findItem(R.id.menu_item_share) + + if (shareButton != null) { + shareButton!!.isVisible = shareButtonVisible + } } - shareButton = menu.findItem(R.id.menu_item_share) - - if (shareButton != null) { - shareButton!!.isVisible = shareButtonVisible - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.select_album, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val itemId = item.itemId - if (itemId == R.id.select_album_play_all) { - playAll() - return true - } else if (itemId == R.id.menu_item_share) { - shareHandler.createShare( - this, getSelectedSongs(), - refreshListView, cancellationToken!!, - navArgs.id - ) - return true + override fun onCreateMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.select_album, menu) } - return false + override fun onMenuItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.select_album_play_all) { + playAll() + return true + } else if (item.itemId == R.id.menu_item_share) { + shareHandler.createShare( + this@TrackCollectionFragment, getSelectedSongs(), + refreshListView, cancellationToken!!, + navArgs.id + ) + return true + } + return false + } } override fun onDestroyView() { @@ -379,20 +386,17 @@ open class TrackCollectionFragment( private fun selectAllOrNone() { val someUnselected = viewAdapter.selectedSet.size < childCount - - selectAll(someUnselected, true) + selectAll(someUnselected) } - private fun selectAll(selected: Boolean, toast: Boolean) { + private fun selectAll(selected: Boolean) { var selectedCount = viewAdapter.selectedSet.size * -1 selectedCount += viewAdapter.setSelectionStatusOfAll(selected) // Display toast: N tracks selected - if (toast) { - val toastResId = R.string.select_album_n_selected - Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0))) - } + val toastResId = R.string.select_album_n_selected + Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0))) } @Synchronized @@ -575,7 +579,7 @@ open class TrackCollectionFragment( setTitle(R.string.main_videos) listModel.getVideos(refresh2) } else if (id == null || getRandomTracks) { - // There seems to be a bug in ViewPager when resuming the Actitivy that subfragments + // There seems to be a bug in ViewPager when resuming the Activity that sub-fragments // arguments are empty. If we have no id, just show some random tracks setTitle(R.string.main_songs_random) listModel.getRandom(size, append) @@ -636,10 +640,6 @@ open class TrackCollectionFragment( R.id.song_menu_download -> { downloadBackground(false, songs) } - R.id.select_album_play_all -> { - // TODO: Why is this being handled here?! - playAll() - } R.id.song_menu_share -> { if (item is Track) { shareHandler.createShare( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index 456f23ef..3abb29df 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -9,8 +9,6 @@ package org.moire.ultrasonic.playback import android.annotation.SuppressLint import android.os.Bundle -import android.widget.Toast -import android.widget.Toast.LENGTH_SHORT import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata @@ -20,6 +18,7 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES import androidx.media3.common.Rating +import androidx.media3.session.CommandButton import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession @@ -27,7 +26,6 @@ import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult import androidx.media3.session.SessionResult.RESULT_SUCCESS import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.FutureCallback import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope @@ -49,7 +47,6 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.RatingManager -import org.moire.ultrasonic.util.MainThreadExecutor import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.buildMediaItem import org.moire.ultrasonic.util.toMediaItem @@ -92,7 +89,6 @@ private const val DISPLAY_LIMIT = 100 private const val SEARCH_LIMIT = 10 // List of available custom SessionCommands -const val SESSION_CUSTOM_SET_RATING = "SESSION_CUSTOM_SET_RATING" const val PLAY_COMMAND = "play " /** @@ -119,6 +115,24 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : private val isOffline get() = ActiveServerProvider.isOffline() private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId + private var customCommands: List + internal var customLayout = ImmutableList.of() + + init { + customCommands = + listOf( + // This button is used for an unstarred track, and its action will star the track + getHeartCommandButton( + SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON, Bundle.EMPTY) + ), + // This button is used for an starred track, and its action will unstar the track + getHeartCommandButton( + SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF, Bundle.EMPTY) + ) + ) + customLayout = ImmutableList.of(customCommands[0]) + } + /** * Called when a {@link MediaBrowser} requests the root {@link MediaItem} by {@link * MediaBrowser#getLibraryRoot(LibraryParams)}. @@ -176,11 +190,10 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : val connectionResult = super.onConnect(session, controller) val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() - /* - * TODO: Currently we need to create a custom session command, see https://github.com/androidx/media/issues/107 - * When this issue is fixed we should be able to remove this method again - */ - availableSessionCommands.add(SessionCommand(SESSION_CUSTOM_SET_RATING, Bundle())) + for (commandButton in customCommands) { + // Add custom command to available session commands. + commandButton.sessionCommand?.let { availableSessionCommands.add(it) } + } return MediaSession.ConnectionResult.accept( availableSessionCommands.build(), @@ -188,6 +201,28 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : ) } + override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { + if (!customLayout.isEmpty() && controller.controllerVersion != 0) { + // Let Media3 controller (for instance the MediaNotificationProvider) + // know about the custom layout right after it connected. + session.setCustomLayout(customLayout) + } + } + + private fun getHeartCommandButton(sessionCommand: SessionCommand): CommandButton { + val willHeart = + (sessionCommand.customAction == PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON) + return CommandButton.Builder() + .setDisplayName("Love") + .setIconResId( + if (willHeart) R.drawable.ic_star_hollow + else R.drawable.ic_star_full + ) + .setSessionCommand(sessionCommand) + .setEnabled(true) + .build() + } + override fun onGetItem( session: MediaLibraryService.MediaLibrarySession, browser: MediaSession.ControllerInfo, @@ -201,12 +236,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : // Create LRU Cache of MediaItems, fill it in the other calls // and retrieve it here. - if (mediaItem != null) { - return Futures.immediateFuture( + return if (mediaItem != null) { + Futures.immediateFuture( LibraryResult.ofItem(mediaItem, null) ) } else { - return Futures.immediateFuture( + Futures.immediateFuture( LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) ) } @@ -234,40 +269,13 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : var customCommandFuture: ListenableFuture? = null when (customCommand.customAction) { - SESSION_CUSTOM_SET_RATING -> { - /* - * It is currently not possible to edit a MediaItem after creation so the isRated value - * is stored in the track.starred value - * See https://github.com/androidx/media/issues/33 - */ - val track = mediaPlayerManager.currentMediaItem?.toTrack() - if (track != null) { - customCommandFuture = onSetRating( - session, - controller, - HeartRating(!track.starred) - ) - Futures.addCallback( - customCommandFuture, - object : FutureCallback { - override fun onSuccess(result: SessionResult) { - track.starred = !track.starred - // This needs to be called on the main Thread - // TODO: This is a looping reference - libraryService.onUpdateNotification(session) - } - - override fun onFailure(t: Throwable) { - Toast.makeText( - mediaPlayerManager.context, - "There was an error updating the rating", - LENGTH_SHORT - ).show() - } - }, - MainThreadExecutor() - ) - } + PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON -> { + customCommandFuture = onSetRating(session, controller, HeartRating(true)) + updateCustomHeartButton(session, true) + } + PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> { + customCommandFuture = onSetRating(session, controller, HeartRating(false)) + updateCustomHeartButton(session, false) } else -> { Timber.d( @@ -281,19 +289,21 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : return customCommandFuture return super.onCustomCommand(session, controller, customCommand, args) } - override fun onSetRating( session: MediaSession, controller: MediaSession.ControllerInfo, rating: Rating ): ListenableFuture { - if (session.player.currentMediaItem != null) + val mediaItem = session.player.currentMediaItem + if (mediaItem != null) { + mediaItem.toTrack().starred = (rating as HeartRating).isHeart return onSetRating( session, controller, - session.player.currentMediaItem!!.mediaId, + mediaItem.mediaId, rating ) + } return super.onSetRating(session, controller, rating) } @@ -303,6 +313,9 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : mediaId: String, rating: Rating ): ListenableFuture { + // TODO: Through this methods it is possible to set a rating on an arbitrary MediaItem. + // Right now the ratings are submitted, yet the underlying track is only updated when + // coming from the other onSetRating(session, controller, rating) return serviceScope.future { Timber.i(controller.packageName) // This function even though its declared in AutoMediaBrowserCallback.kt is @@ -324,7 +337,6 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : * and thereby customarily it is required to rebuild it.. * See also: https://stackoverflow.com/questions/70096715/adding-mediaitem-when-using-the-media3-library-caused-an-error */ - override fun onAddMediaItems( mediaSession: MediaSession, controller: MediaSession.ControllerInfo, @@ -1276,4 +1288,15 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : null } } + + fun updateCustomHeartButton( + session: MediaSession, + isHeart: Boolean + ) { + val command = if (isHeart) customCommands[1] else customCommands[0] + // Change the custom layout to contain the right heart button + customLayout = ImmutableList.of(command) + // Send the updated custom layout to controllers. + session.setCustomLayout(customLayout) + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt index 0f283267..50fa4c4f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt @@ -7,79 +7,22 @@ package org.moire.ultrasonic.playback import android.content.Context -import androidx.core.app.NotificationCompat -import androidx.media3.common.HeartRating import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider -import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession -import androidx.media3.session.SessionCommand import com.google.common.collect.ImmutableList import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.moire.ultrasonic.R -import org.moire.ultrasonic.service.MediaPlayerManager -import org.moire.ultrasonic.util.toTrack @UnstableApi class CustomNotificationProvider(ctx: Context) : DefaultMediaNotificationProvider(ctx), KoinComponent { - /* - * It is currently not possible to edit a MediaItem after creation so the isRated value - * is stored in the track.starred value. See https://github.com/androidx/media/issues/33 - * TODO: Once the bug is fixed remove this circular reference! - */ - private val mediaPlayerManager by inject() - - override fun addNotificationActions( - mediaSession: MediaSession, - mediaButtons: ImmutableList, - builder: NotificationCompat.Builder, - actionFactory: MediaNotification.ActionFactory - ): IntArray { - val tmp: MutableList = mutableListOf() - /* - * TODO: - * It is currently not possible to edit a MediaItem after creation so the isRated value - * is stored in the track.starred value - * See https://github.com/androidx/media/issues/33 - */ - val rating = mediaPlayerManager.currentMediaItem?.toTrack()?.starred?.let { - HeartRating( - it - ) - } - if (rating is HeartRating) { - tmp.add( - CommandButton.Builder() - .setDisplayName("Love") - .setIconResId( - if (rating.isHeart) R.drawable.ic_star_full - else R.drawable.ic_star_hollow - ) - .setSessionCommand( - SessionCommand( - SESSION_CUSTOM_SET_RATING, - HeartRating(rating.isHeart).toBundle() - ) - ) - .setExtras(HeartRating(rating.isHeart).toBundle()) - .setEnabled(true) - .build() - ) - } - return super.addNotificationActions( - mediaSession, - ImmutableList.copyOf((mediaButtons + tmp)), - builder, - actionFactory - ) - } - + // By default the skip buttons are not shown in compact view. + // We add the COMMAND_KEY_COMPACT_VIEW_INDEX to show them + // See also: https://github.com/androidx/media/issues/410 override fun getMediaButtons( session: MediaSession, playerCommands: Player.Commands, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 6d0f0db6..6dd656a3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -68,7 +68,7 @@ class PlaybackService : private var equalizer: EqualizerController? = null private val activeServerProvider: ActiveServerProvider by inject() - private lateinit var librarySessionCallback: MediaLibrarySession.Callback + private lateinit var librarySessionCallback: AutoMediaBrowserCallback private var rxBusSubscription = CompositeDisposable() @@ -132,6 +132,13 @@ class PlaybackService : setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext())) + // TODO: Remove minor code duplication with updateBackend() + val desiredBackend = if (activeServerProvider.getActiveServer().jukeboxByDefault) { + MediaPlayerManager.PlayerBackend.JUKEBOX + } else { + MediaPlayerManager.PlayerBackend.LOCAL + } + player = if (activeServerProvider.getActiveServer().jukeboxByDefault) { Timber.i("Jukebox enabled by default") getJukeboxPlayer() @@ -139,6 +146,8 @@ class PlaybackService : getLocalPlayer() } + actualBackend = desiredBackend + // Create browser interface librarySessionCallback = AutoMediaBrowserCallback(this) @@ -148,6 +157,11 @@ class PlaybackService : .setBitmapLoader(ArtworkBitmapLoader()) .build() + if (!librarySessionCallback.customLayout.isEmpty()) { + // Send custom layout to legacy session. + mediaLibrarySession.setCustomLayout(librarySessionCallback.customLayout) + } + // Set a listener to update the API client when the active server has changed rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { // Set the player wake mode @@ -209,6 +223,7 @@ class PlaybackService : player.addListener(listener) mediaLibrarySession.player = player + actualBackend = newBackend } @@ -281,7 +296,14 @@ class PlaybackService : } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - updateWidgetTrack(mediaItem?.toTrack()) + // Since we cannot update the metadata of the media item after creation, + // we cannot set change the rating on it + // Therefore the track must be our source of truth + val track = mediaItem?.toTrack() + if (track != null) { + updateCustomHeartButton(track.starred) + } + updateWidgetTrack(track) cacheNextSongs() } @@ -291,6 +313,10 @@ class PlaybackService : } } + private fun updateCustomHeartButton(isHeart: Boolean) { + librarySessionCallback.updateCustomHeartButton(mediaLibrarySession, isHeart) + } + private fun cacheNextSongs() { if (actualBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) return Timber.d("PlaybackService caching the next songs") @@ -394,6 +420,10 @@ class PlaybackService : private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic.error" private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages" + const val CUSTOM_COMMAND_TOGGLE_HEART_ON = + "org.moire.ultrasonic.HEART_ON" + const val CUSTOM_COMMAND_TOGGLE_HEART_OFF = + "org.moire.ultrasonic.HEART_OFF" private const val NOTIFICATION_ID = 3009 } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index 946ce471..808c3bbe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -11,6 +11,7 @@ import android.app.Notification import android.app.Service import android.content.Intent import android.net.wifi.WifiManager +import android.os.Binder import android.os.Build import android.os.Handler import android.os.IBinder @@ -39,7 +40,6 @@ import org.moire.ultrasonic.util.FileUtil.getCompleteFile import org.moire.ultrasonic.util.FileUtil.getPartialFile import org.moire.ultrasonic.util.FileUtil.getPinnedFile import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.SimpleServiceBinder import org.moire.ultrasonic.util.Storage import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification @@ -452,3 +452,5 @@ class DownloadService : Service(), KoinComponent { } } } + +class SimpleServiceBinder(val service: S) : Binder() From 315271390f5daf104c4927829e946f8b70e11595 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 20 May 2023 21:28:28 +0200 Subject: [PATCH 53/69] Add setting to control the max bitrate when pinning --- .../org/moire/ultrasonic/service/DownloadTask.kt | 4 ++-- .../kotlin/org/moire/ultrasonic/util/Settings.kt | 13 +++++++++---- ultrasonic/src/main/res/values/setting_keys.xml | 1 + ultrasonic/src/main/res/values/strings.xml | 1 + ultrasonic/src/main/res/xml/settings.xml | 15 +++++++++++---- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt index 4cd4c7df..e36975b8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt @@ -92,8 +92,8 @@ class DownloadTask( // Attempt partial HTTP GET, appending to the file if it exists. val (inStream, isPartial) = musicService.getDownloadInputStream( downloadTrack.track, fileLength, - Settings.maxBitRate, - downloadTrack.pinned + if (downloadTrack.pinned) Settings.maxBitRatePinning else Settings.maxBitRate, + downloadTrack.pinned && Settings.pinWithHighestQuality ) inputStream = inStream diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index d3aa70f9..5b179ec1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -33,18 +33,23 @@ object Settings { val maxBitRate: Int get() { return if (Util.isNetworkRestricted()) { - maxMobileBitRate + maxBitRateMobile } else { - maxWifiBitRate + maxBitRateWifi } } - private var maxWifiBitRate + private var maxBitRateWifi by StringIntSetting(getKey(R.string.setting_key_max_bitrate_wifi)) - private var maxMobileBitRate + private var maxBitRateMobile by StringIntSetting(getKey(R.string.setting_key_max_bitrate_mobile)) + var maxBitRatePinning + by StringIntSetting(getKey(R.string.setting_key_max_bitrate_pinning)) + val pinWithHighestQuality: Boolean + get() = (maxBitRatePinning == 0) + @JvmStatic val preloadCount: Int get() { diff --git a/ultrasonic/src/main/res/values/setting_keys.xml b/ultrasonic/src/main/res/values/setting_keys.xml index c18a9a92..0e135e5e 100644 --- a/ultrasonic/src/main/res/values/setting_keys.xml +++ b/ultrasonic/src/main/res/values/setting_keys.xml @@ -12,6 +12,7 @@ showTrackNumber maxBitrateWifi maxBitrateMobile + maxBitratePinning cacheSize customCacheLocation cacheLocation diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 394091ac..4f888cbb 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -233,6 +233,7 @@ Max Bitrate - Mobile Unlimited Max Bitrate - Wi-Fi + Max Bitrate - When pinning a song permanently Max Songs Respond to phone, headset and Bluetooth media buttons Media Buttons diff --git a/ultrasonic/src/main/res/xml/settings.xml b/ultrasonic/src/main/res/xml/settings.xml index ee11bda0..692117c5 100644 --- a/ultrasonic/src/main/res/xml/settings.xml +++ b/ultrasonic/src/main/res/xml/settings.xml @@ -167,18 +167,25 @@ a:title="@string/settings.network_title" app:iconSpaceReserved="false"> + Date: Sat, 20 May 2023 19:37:27 +0000 Subject: [PATCH 54/69] Fix StarRating when setting a rating through AutoMediaController --- .../moire/ultrasonic/playback/AutoMediaBrowserCallback.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index 3abb29df..8ca449af 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -18,6 +18,7 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES import androidx.media3.common.Rating +import androidx.media3.common.StarRating import androidx.media3.session.CommandButton import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService @@ -296,7 +297,11 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : ): ListenableFuture { val mediaItem = session.player.currentMediaItem if (mediaItem != null) { - mediaItem.toTrack().starred = (rating as HeartRating).isHeart + if (rating is HeartRating) { + mediaItem.toTrack().starred = rating.isHeart + } else if (rating is StarRating) { + mediaItem.toTrack().userRating = rating.starRating.toInt() + } return onSetRating( session, controller, From a8961e8e96b05ebd2accee6b7effaafe51f474fa Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sun, 21 May 2023 12:18:11 +0000 Subject: [PATCH 55/69] Merge 4.4.1 to dev --- .../metadata/android/en-US/changelogs/119.txt | 10 +++++ .../metadata/android/en-US/changelogs/120.txt | 10 +++++ gradle.properties | 3 ++ ultrasonic/build.gradle | 4 +- ultrasonic/minify/proguard-retrofit.pro | 41 ++++++++++++++++--- 5 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/119.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/120.txt diff --git a/fastlane/metadata/android/en-US/changelogs/119.txt b/fastlane/metadata/android/en-US/changelogs/119.txt new file mode 100644 index 00000000..be4a20f9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/119.txt @@ -0,0 +1,10 @@ +Features: +- This releases focuses on shuffled playback. The view of the playlist will now present itself in the order it will actually play. You can toggle the shuffle mode to create a new order, while the past playback history will be preserved. +- Use Coroutines for triggering the download or playback of music through the context menus +- Enable Artists pictures by Default + +Bug fixes: +- Remove an unhelpful popup that "ID must be set" +- Shuffle mode doesn't always play all tracks +- Shuffle mode starts with the first track most of the time + diff --git a/fastlane/metadata/android/en-US/changelogs/120.txt b/fastlane/metadata/android/en-US/changelogs/120.txt new file mode 100644 index 00000000..be4a20f9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/120.txt @@ -0,0 +1,10 @@ +Features: +- This releases focuses on shuffled playback. The view of the playlist will now present itself in the order it will actually play. You can toggle the shuffle mode to create a new order, while the past playback history will be preserved. +- Use Coroutines for triggering the download or playback of music through the context menus +- Enable Artists pictures by Default + +Bug fixes: +- Remove an unhelpful popup that "ID must be set" +- Shuffle mode doesn't always play all tracks +- Shuffle mode starts with the first track most of the time + diff --git a/gradle.properties b/gradle.properties index 7ab3793a..e2c4770f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,3 +20,6 @@ android.nonFinalResIds=true # It can be removed if it makes problems org.gradle.unsafe.configuration-cache=true +# TODO Renable on day (check that Retrofit, Jackson, and Imageloader are working) +android.enableR8.fullMode=false + diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 8aab2f76..361616e1 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { applicationId "org.moire.ultrasonic" - versionCode 117 - versionName "4.3.4" + versionCode 120 + versionName "4.4.1" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk diff --git a/ultrasonic/minify/proguard-retrofit.pro b/ultrasonic/minify/proguard-retrofit.pro index f6dd5b8f..8e85c3d0 100644 --- a/ultrasonic/minify/proguard-retrofit.pro +++ b/ultrasonic/minify/proguard-retrofit.pro @@ -1,10 +1,41 @@ -#### From retrofit +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod -# Retain generic type information for use by reflection by converters and adapters. --keepattributes Signature -# Retain service method parameters. --keepclassmembernames,allowobfuscation interface * { +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations + +# Keep annotation default values (e.g., retrofit2.http.Field.encoded). +-keepattributes AnnotationDefault + +# Retain service method parameters when optimizing. +-keepclassmembers,allowshrinking,allowobfuscation interface * { @retrofit2.http.* ; } + # Ignore annotation used for build tooling. -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions$* + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> + +# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). +-keep,allowobfuscation,allowshrinking interface retrofit2.Call +-keep,allowobfuscation,allowshrinking class retrofit2.Response + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation \ No newline at end of file From 22fda501f49e0da577abf1447fe1fb55b4f6f0e3 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 21 May 2023 14:14:45 +0200 Subject: [PATCH 56/69] Ensure correct thread when accepting a rating --- .../moire/ultrasonic/adapters/TrackViewHolder.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 313c6dd9..2efea271 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -142,13 +142,15 @@ class TrackViewHolder(val view: View) : // Listen for rating updates rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe { - // Ignore updates which are not for the current song - if (it.id != song.id) return@subscribe + launch(Dispatchers.Main) { + // Ignore updates which are not for the current song + if (it.id != song.id) return@launch - if (it.rating is HeartRating) { - updateSingleStar(it.rating.isHeart) - } else if (it.rating is StarRating) { - updateFiveStars(it.rating.starRating.toInt()) + if (it.rating is HeartRating) { + updateSingleStar(it.rating.isHeart) + } else if (it.rating is StarRating) { + updateFiveStars(it.rating.starRating.toInt()) + } } } } From 5e4ec56ae7f58d6fa6ee07b387e7fe976c57cd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Garc=C3=ADa=20Amor?= Date: Tue, 23 May 2023 12:54:54 +0200 Subject: [PATCH 57/69] Changes applicationId for GitLab builds --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b79f63b0..3f9d9721 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -74,7 +74,9 @@ Unit Tests: Assemble Release: stage: Build - script: ./gradlew assembleRelease + script: + - sed -i 's/applicationId \"org.moire.ultrasonic\"/applicationId "org.moire.ultrasonic.gitlab"/' ultrasonic/build.gradle + - ./gradlew assembleRelease artifacts: name: ultrasonic-release-unsigned-${CI_COMMIT_SHA} paths: From 0a6a12c70abbcc6929774df386aa4cb82fae48e3 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 24 May 2023 19:31:38 +0000 Subject: [PATCH 58/69] Update dependency com.android.tools.build:gradle to v8.0.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd4fa3e3..d21ccf60 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ gradle = "8.1.1" navigation = "2.5.3" -gradlePlugin = "8.0.1" +gradlePlugin = "8.0.2" androidxcore = "1.10.1" ktlint = "0.43.2" ktlintGradle = "11.3.2" From 7a453dbd303a0581b1a2aa716f8afa39ebd37bd7 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Mon, 29 May 2023 08:11:09 +0000 Subject: [PATCH 59/69] Correct transition reason --- .../kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt index bb67ab9b..dfc06cd5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt @@ -505,7 +505,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player { listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) { it.onMediaItemTransition( currentMedia, - Player.MEDIA_ITEM_TRANSITION_REASON_SEEK + Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ) } } From 77d3f8c11b919e40c2d948c4ad2fdc24da4fc92e Mon Sep 17 00:00:00 2001 From: Newson Parker <2434843612@qq.com> Date: Sun, 21 May 2023 01:12:15 +0000 Subject: [PATCH 60/69] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (426 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hans/ --- ultrasonic/src/main/res/values-zh-rCN/strings.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 50265302..769c22ae 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -67,7 +67,7 @@ 保存播放列表 关闭屏幕常亮 开启屏幕常亮 - 显示专辑 + 转到专辑 随机 随机播放模式已启用 随机播放模式已禁用 @@ -280,7 +280,7 @@ 测试连接 亮色 暗色 - Black + 黑色 主题 允许自签名 HTTPS 证书 强制原始密码认证 @@ -337,7 +337,7 @@ 看看我从 %s 分享的这首音乐 分享歌曲通过 分享 - 显示艺术家 + 转到艺术家 数年 调试选项 将调试日志写入文件 @@ -448,4 +448,5 @@ 捷克语 德语 葡萄牙语(巴西) + 最大比特率 - 永久固定歌曲时 \ No newline at end of file From 45ca0966fd2fd3a88c53e8abe5847059ddd195a9 Mon Sep 17 00:00:00 2001 From: Newson Parker <2434843612@qq.com> Date: Sun, 21 May 2023 01:14:49 +0000 Subject: [PATCH 61/69] Translated using Weblate (Chinese (Traditional)) Currently translated at 69.0% (294 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hant/ --- .../src/main/res/values-zh-rTW/strings.xml | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index 6b7b0983..0610cc46 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -84,7 +84,7 @@ 8 秒 使用自訂緩衝路徑 緩衝路径 - 錯誤緩衝路徑,使用預設緩衝路徑 + 錯誤緩衝路徑,使用預設緩衝路徑。 緩衝大小 100 MB 1 GB @@ -129,7 +129,7 @@ 已停用 註記 刪除 - 顯示專輯 + 轉至專輯 簡體中文(中國) 儲存播放清單 書籤設置在 %s。 @@ -232,4 +232,68 @@ 在 Android 系統下次掃描裝置內音樂時生效。 播放時顯示正在播放介面 在媒體庫介面開始播放後切換到正在播放介面 + 50 + 3 首歌 + 1 + 20 + 75 + 分鐘 + 500 首歌 + 112 Kbps + 1 首歌 + 3 + 40 + 500 + 160 Kbps + 0:00 + 120 秒 + 小時 + 黑色 + 認證 + 2 首歌 + 10 + 伺服器地址 + 60 秒 + 5 + 100 首歌 + 明色 + 96 Kbps + 0 KB + 覆寫當前語言 + 5 首歌 + 250 + 192 Kbps + 80 Kbps + 25 + 30 + 100 + 影片 + 禁用 + 50 首歌 + %d kbps + 10 首歌 + 256 Kbps + 32 Kbps + 320 Kbps + 64 Kbps + 網路延時 + 105 秒 + 45 秒 + 75 秒 + 90 秒 + 通知 + 網路 + 其他設定 + 15 + 伺服器顏色 + 0 B + 0.00 GB + 用戶名 + 暗色 + -:-- + 0.00 MB + 1000 首歌 + 密碼 + + 128 Kbps \ No newline at end of file From 296308cebf07a84a441842e4cb06e54aef5635c3 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 22 May 2023 11:10:19 +0000 Subject: [PATCH 62/69] Translated using Weblate (Spanish) Currently translated at 100.0% (426 of 426 strings) Translation: Ultrasonic/app Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/ --- ultrasonic/src/main/res/values-es/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index 1a34b0e9..39adfa95 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -452,4 +452,5 @@ Funciones soportadas No se puede reanudar la reproducción Presione el botón de reproducción en la notificación de medios si aún está presente; de lo contrario, abra la aplicación para iniciar la reproducción y vuelva a conectar la sesión al controlador + Tasa de bits máxima: al fijar una canción de forma permanente \ No newline at end of file From 4c049671db627f6393f83f87bce810bd4f058c8b Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 1 Jun 2023 10:31:36 +0200 Subject: [PATCH 63/69] Apply assistant changes to gradle file --- ultrasonic/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 361616e1..3d72f36b 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -14,7 +14,7 @@ android { minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk - resConfigs 'cs', 'de', 'en', 'es', 'fr', 'gl', 'hu', 'it', 'ja', 'nb-rNO', 'nl', 'pl', 'pt', 'pt-rBR', 'ru', 'zh-rCN', 'zh-rTW' + resourceConfigurations += ['cs', 'de', 'en', 'es', 'fr', 'gl', 'hu', 'it', 'ja', 'nb-rNO', 'nl', 'pl', 'pt', 'pt-rBR', 'ru', 'zh-rCN', 'zh-rTW'] } bundle.language.enableSplit = false From 25f3ff0bd36ee512d993b16315e21b8772ae076b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 1 Jun 2023 08:48:58 +0000 Subject: [PATCH 64/69] Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.0 --- config/detekt/detekt.yml | 6 +++++- gradle/libs.versions.toml | 2 +- .../kotlin/org/moire/ultrasonic/service/PlaybackState.kt | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index ef02adeb..0bd0b7cb 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -52,7 +52,11 @@ style: active: true ForbiddenComment: active: true - values: ['FIXME:', 'STOPSHIP:'] + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' WildcardImport: active: true MaxLineLength: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d21ccf60..5378fd7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ gradlePlugin = "8.0.2" androidxcore = "1.10.1" ktlint = "0.43.2" ktlintGradle = "11.3.2" -detekt = "1.22.0" +detekt = "1.23.0" preferences = "1.2.0" media3 = "1.0.2" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackState.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackState.kt index a40e7b05..d57a5b86 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackState.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackState.kt @@ -14,6 +14,6 @@ data class PlaybackState( var repeatMode: Int = 0 ) : Serializable { companion object { - const val serialVersionUID = -293487987L + private const val serialVersionUID = -293487987L } } From c1013f6b80be363e418c831de773f44295e5ca90 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 1 Jun 2023 10:28:49 +0200 Subject: [PATCH 65/69] Fix play all in Track collection random view --- .../moire/ultrasonic/fragment/TrackCollectionFragment.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index f771710f..5491bca0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -351,13 +351,11 @@ open class TrackCollectionFragment( val isArtist = navArgs.isArtist - // Need a valid id to download stuff - val id = navArgs.id ?: return - - if (hasSubFolders) { + // Need a valid id to recurse sub directories stuff + if (hasSubFolders && navArgs.id != null) { downloadHandler.fetchTracksAndAddToController( fragment = this, - id = id, + id = navArgs.id!!, append = append, autoPlay = !append, shuffle = shuffle, From e35a33edde4ae39c0782c45b22dfef323d524f91 Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 1 Jun 2023 10:29:30 +0200 Subject: [PATCH 66/69] Use App context when toasting from background tasks, use App context to resolve error messages --- .../java/org/moire/ultrasonic/util/BackgroundTask.java | 2 +- .../org/moire/ultrasonic/fragment/EditServerFragment.kt | 2 +- .../org/moire/ultrasonic/fragment/PlayerFragment.kt | 8 ++++---- .../org/moire/ultrasonic/util/CommunicationError.kt | 7 ++++--- .../kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt | 5 +++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java index 8ae51dee..bf59e1c8 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java @@ -58,7 +58,7 @@ public abstract class BackgroundTask implements ProgressListener protected String getErrorMessage(Throwable error) { - return CommunicationError.getErrorMessage(error, activity); + return CommunicationError.getErrorMessage(error); } @Override diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index e16e0d53..73d3a0ba 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -401,7 +401,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { Timber.w(exception) ErrorDialog.Builder(requireContext()) .setTitle(R.string.error_label) - .setMessage(getErrorMessage(exception, context)) + .setMessage(getErrorMessage(exception)) .show() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index a0ca0c75..cc46a670 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -78,6 +78,7 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.api.subsonic.models.AlbumListType +import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags @@ -662,7 +663,6 @@ class PlayerFragment : parentId = track.parent, isAlbum = true ) - findNavController().navigate(action) return true } @@ -822,16 +822,16 @@ class PlayerFragment : musicService.createPlaylist(null, playlistName, entries) }.invokeOnCompletion { if (it == null || it is CancellationException) { - Util.toast(context, R.string.download_playlist_done) + Util.toast(UApp.applicationContext(), R.string.download_playlist_done) } else { Timber.e(it, "Exception has occurred in savePlaylistInBackground") val msg = String.format( Locale.ROOT, "%s %s", resources.getString(R.string.download_playlist_error), - CommunicationError.getErrorMessage(it, context) + CommunicationError.getErrorMessage(it) ) - Util.toast(context, msg) + Util.toast(UApp.applicationContext(), msg) } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt index f81e7b92..acdd03da 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.SubsonicRESTException +import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage import timber.log.Timber @@ -46,14 +47,14 @@ object CommunicationError { ErrorDialog( context = context, - message = getErrorMessage(error, context) + message = getErrorMessage(error) ).show() } @JvmStatic @Suppress("ReturnCount") - fun getErrorMessage(error: Throwable, context: Context?): String { - if (context == null) return "Couldn't get Error message, Context is null" + fun getErrorMessage(error: Throwable): String { + val context = UApp.applicationContext() if (error is IOException && !Util.hasUsableNetwork()) { return context.resources.getString(R.string.background_task_no_network) } else if (error is FileNotFoundException) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt index c0a95bd9..ad7580c3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.moire.ultrasonic.R +import org.moire.ultrasonic.app.UApp import timber.log.Timber object CoroutinePatterns { @@ -40,7 +41,7 @@ fun CoroutineScope.executeTaskWithToast( // Setup a handler when the job is done job.invokeOnCompletion { val toastString = if (it != null && it !is CancellationException) { - CommunicationError.getErrorMessage(it, fragment.context) + CommunicationError.getErrorMessage(it) } else { successString() } @@ -49,7 +50,7 @@ fun CoroutineScope.executeTaskWithToast( if (toastString == null) return@invokeOnCompletion launch(Dispatchers.Main) { - Util.toast(fragment.context, toastString) + Util.toast(UApp.applicationContext(), toastString) } } From 3bd3607220a177679d1edfbe03b112f5dccce32b Mon Sep 17 00:00:00 2001 From: tzugen Date: Thu, 1 Jun 2023 23:21:43 +0200 Subject: [PATCH 67/69] Remove unused fragment param --- .../kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt | 4 ++-- .../kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 5acf05be..1a653a50 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -46,7 +46,7 @@ class DownloadHandler( var successString: String? = null // Launch the Job - executeTaskWithToast(fragment, { + executeTaskWithToast({ val tracksToDownload: List = tracks ?: getTracksFromServer(isArtist, id!!, isDirectory, name, isShare) @@ -104,7 +104,7 @@ class DownloadHandler( ) { var successString: String? = null // Launch the Job - executeTaskWithToast(fragment, { + executeTaskWithToast({ val songs: MutableList = getTracksFromServer(isArtist, id, isDirectory, name, isShare) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt index ad7580c3..8e0d1154 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt @@ -31,7 +31,6 @@ object CoroutinePatterns { } fun CoroutineScope.executeTaskWithToast( - fragment: Fragment, task: suspend CoroutineScope.() -> Unit, successString: () -> String? ): Job { @@ -63,7 +62,7 @@ fun CoroutineScope.executeTaskWithModalDialog( successString: () -> String ) { // Create the job - val job = executeTaskWithToast(fragment, task, successString) + val job = executeTaskWithToast(task, successString) // Create the dialog val builder = InfoDialog.Builder(fragment.requireContext()) From 79ac73020be8cb5b3f3f9e684e08c9d0445f1d33 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 1 Jun 2023 21:31:52 +0000 Subject: [PATCH 68/69] Update dependency org.mockito.kotlin:mockito-kotlin to v5 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5378fd7a..057f57a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ picasso = "2.8" junit4 = "4.13.2" junit5 = "5.9.3" mockito = "5.3.1" -mockitoKotlin = "4.1.0" +mockitoKotlin = "5.0.0" kluent = "1.73" apacheCodecs = "1.15" robolectric = "4.10.3" From eb3aa0d202eeb24cf34a2fc4d0642e8361b21f56 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 3 Jun 2023 11:11:28 +0200 Subject: [PATCH 69/69] Release 4.5.0 --- fastlane/metadata/android/en-US/changelogs/122.txt | 10 ++++++++++ .../metadata/android/en-US/full_description.txt | 14 +++++++------- ultrasonic/build.gradle | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/122.txt diff --git a/fastlane/metadata/android/en-US/changelogs/122.txt b/fastlane/metadata/android/en-US/changelogs/122.txt new file mode 100644 index 00000000..e691579a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/122.txt @@ -0,0 +1,10 @@ +Features: +- Revamp management of ratings. Tracks can be starred from the notification in Android 13, and the changes will show up everywhere immediately. +- Add a setting to control the maximum bitrate when pinning music (can be used to avoid downloading lossless files like flac). +- Modernize the Jukebox player. +- The hardware keys can be used to set the Jukebox volume. +- The current playlist shows a spinner when loading takes some time + +Bug fixes: +- Request correct bluetooth permission on Android 13 (needed to pause/play on connect) +- Update dependencies (OkHttp, Material) \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 18d5f968..cdec6bb2 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,17 +1,17 @@ Ultrasonic is a Subsonic (and compatible servers) client to Android. You can use Ultrasonic to connect with your server and listen music. Main features: -* Thin -* Fast -* Material theme with dark and light variants +* Small size & fast +* Material You theme with dark and light variants * Multiple server support -* Offline Mode +* Download tracks for offline playback * Bookmarks * Playlists on server -* Random play +* Shuffled playback * Jukebox mode -* Server chat -* And much more!!! +* And much more!! + +Note: Ultrasonic uses semantic release versions. Releases with a zero in the last digit introduce new features or significant changes, all other releases focus on fixing bugs. The source code is available with GPL license in GitLab: https://gitlab.com/ultrasonic/ultrasonic If you have any issue, please post in: https://gitlab.com/ultrasonic/ultrasonic/issues diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 3d72f36b..bc68203f 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { applicationId "org.moire.ultrasonic" - versionCode 120 - versionName "4.4.1" + versionCode 122 + versionName "4.5.0" minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk