From 88341c8b868397cebe9329a0e1cd4c1dbcfb2ada Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Wed, 11 Oct 2023 17:17:11 +0200 Subject: [PATCH] Fossil/Skagen Hybrids: Add new navigation app --- .gitmodules | 6 +- .../main/assets/fossil_hr/navigationApp.wapp | Bin 0 -> 38077 bytes .../AbstractAppManagerFragment.java | 4 +- .../devices/qhybrid/QHybridCoordinator.java | 7 +- .../devices/qhybrid/QHybridSupport.java | 6 + .../fossil_hr/FossilHRWatchAdapter.java | 55 ++++++++ .../gadgetbridge/util/FileUtils.java | 25 +++- app/src/main/res/values/strings.xml | 2 + ...watchface.sh => build_fossil_hr_gbapps.sh} | 12 +- external/fossil-hr-gbapps | 1 + external/fossil-hr-watchface | 1 - external/pack.py | 124 ++++++++++++++++++ 12 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 app/src/main/assets/fossil_hr/navigationApp.wapp rename external/{build_fossil_hr_watchface.sh => build_fossil_hr_gbapps.sh} (68%) create mode 160000 external/fossil-hr-gbapps delete mode 160000 external/fossil-hr-watchface create mode 100644 external/pack.py diff --git a/.gitmodules b/.gitmodules index 19b292ea4..cc9db9855 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "fossil-hr-watchface"] - path = external/fossil-hr-watchface - url = https://codeberg.org/Freeyourgadget/fossil-hr-watchface [submodule "jerryscript"] path = external/jerryscript url = https://github.com/jerryscript-project/jerryscript +[submodule "fossil-hr-gbapps"] + path = external/fossil-hr-gbapps + url = https://codeberg.org/Freeyourgadget/fossil-hr-gbapps diff --git a/app/src/main/assets/fossil_hr/navigationApp.wapp b/app/src/main/assets/fossil_hr/navigationApp.wapp new file mode 100644 index 0000000000000000000000000000000000000000..4510dd019830a4617ecea6c963342f61d86180c7 GIT binary patch literal 38077 zcmeI5X>gs@ndjg4zE^rBy|49Jw!Gn8Mz*|S%Ny7%-Yskc#%AAyEM3b2SrU?LW0tTr zm_QZ?OCTW$Nk~lSkS3i}S0~le>FTO3_~f3N>JKwr^KquAnwk%pn)y7;@Bf^0uC9db zEYnp(PY3GiInQ(U=RDh4?)Cqkl|=gQ!`Gvz0{dKZv^r=*Tg3yol~coa?^n=QS|R=SDo(Q^9rB8 zqpc3wiD=_}K3CI5YgZ|5UC>dtB%WKIo3zfYZ*A?08oQ#Vt|;q@^0^Ij)2`CAt~t$J zbBnHd)4Q%{>6$;IYr)K}g{@tSW_2x|y;sd?K6Ry1slL8C9{N8wBZ{sqbwt0pHtC3e zv$edfZe$aEb zJEFfk*O9LMsJySq>L+m=H#V+-q38H~I*&Qer!Jor3xIr9ER3QpQQTi@nX@`dqRr(` zR!4Qw=6XOHZEpBvzxcy{b7?P*Xs=W(=%-G)i1G0GbTR9(6n?h>mx1#+e>>XBE7qo!|1A4xiWaIn3v2J|FS;}$Pd<-7)2`r8^ZZZt)3)P(k|bsL$@o8IJo{HO zHa;KoS(%hs)Vi*?zN?gevJc7AdTG8%qVnh0L>fp$mi_1Q5tgV_5+BmFtRt)On@i;P zPs#V=q5b-Lwf`~sS|{Vr;1TD!^#LvlS{s=Ro<|03m`{J_B2%{R2K^*KlR^eQ0o z_c0KOYwb!(U3KM8w$^V;f2Aon`6? zeokI5$+haN*gA=irhiUf8k@xD^W=YeHD%HF71z)geD4RI=X02Tot(#;6GXNgZ>|H> z$D30?L!$&Zv2i2FajB&CKHX+|yLZ0P>&oT2xpP-+_dNBwF8w}#Eq(g__g@E}uBUH( z=u>@b{IZ4V4%QRd_!KDHDmmFws_U;`nr|7$FLCP{pqo|4$y!g*I7IDv{Nq2W@)a-TIVl1f3fZt$@U1VyB2fn%a!>r zO(mE6cRPM;DV6){TGH)F{aTZulC_7@^Plvj)9{pGE?=JRg?D_;lOcH&kHyRA$ITOd zs-Ao@WjE!gKHd*xLMslCKFAzzBL8L}LXjR}3`beN+sM0}GIv713;I2jxflBV(7H)u zZ&y4>JABL!_JAM5kCnF8ccg#ck!-DBnKpcsY;D}!v^m?F_qH@|kEZ>~q5qT{Z0?NO z_jbnZ`#MYQ`#Y2NuFi7%jh%Jv2RiH94|b;QhdLYDZ|ZDpzqzxi{g%$G{cvaAerxBn z_9LCm?MFL{_S-tAA8))ZzVkv+j&Hv(y&NCD&{B>MUYJpi_g$D-j(1*YEyvehm{pFq zUYK2uJ1@*B$7?UlEyo=f=9S}R7p^GBi!RJB$MY^MD95cA7MA1U!lH8AbYXEhuH(=4 zXbF?SPPHwy@hg31drXD!JD>2v+?*vadLxBU-;T*;-y1 z*IiflPo-Wd zv8Jf<=)iclA!#%MWngR^OVK+#bbO${91RQ|A8v^HAodLPR+6YP-Va=ZZr1jB(q5gs6XX#H>XGUXZ$HprsyIr@ZMkrW`TEyGl%IMHo z_xOo{vF;vX$Wqi-X^e&k`&?1*$H0dwXR7#MMdNdfYKl${pRRO^v13Jadf?co7&g)~ zK2EEQE>Ol3tX(BXwdJDQS#)2ZzThHm=NZ(sOZEG*%hv z>pnGB8SU;J>=_&L{GJh2PM#Xn82W}!9UH84kN1qsaAo@|LzU66Fb&c0NM%S3H>tPN z17ib}F57J4=mb{iq-1NXd$dAp1E(wSk-1PpGbu+Jd41Gll2(e2R_YijqhQ2mdd7QC z*oe!l!thAh#+^lu<=t8g9&3p|Rni zEYPF`j71Y~iYDe5M*~ntPK}+Yi+T|8an^RMGTz;LYIGFw_G=eMeax(zPcQlX7s(%r zW@*+Y4W@|Ql}ASgM$z7s`5Tj@Wq#K&)ZY|o$@)i!QS%v2zn!{vyZL6($$>s}j`bYu zi>5hEueMniF}BgO=y=~L$!pmxz!jm+*oom$qy|~Db*S`=Rho>a&PHU?Aqvcd*9X1NhPT&aAnb)!|wxgJkV8v0D@(b1{}Syzp1 z`1tW|7G>O~?fbVy>NzG^s}^^2tjVeJWxlmjlY>d=36uSNi#81Lq1ozT@dw6E2ycue zzum_N;I&Ckx0Cm}XzddICsMF=iyD{8UJq2xL_eGM+uuH%ml~4t^e8Ep8vZel8%pK2 zs8o&{{vLWF<(%gw)&6?jneP9sEA< zuY(^0e+hUT_yX`D;IqIX;Fo}-z{m5`(WBIPlseWWphP8K5j7@tMkk7jQ|LRQW3<&loj4{!FO{fM=71oMvxvPFpPls6 zO}pz!yBu~Bl%q6TsW>LTe1A^VWHeElRKHYbe~EHSaZ-xQ(22Czd&Q;o(LMCJ(el+| zIZk~IwUJnw88E)}^nE8~ucEJnth$IXyOv{SV5QX}EvpBE>Ra`0w=vfz^^BW2>7$lw z>P?z&X#?{(N}UZ+qSE4qFH7H(YO04Yd)EfDFW;JP)hLt9#{c;m;)fgars$UZ8p@U^ zJ2B%hpTIkZZ;o!tw^>>7wYIcYXL8vs(ZPJXZ&x|uz4~$ZrShHmo5(O8OY-4N*$_x6;OOvq$yRI!xXP+K?Bh z&ksirTe?2KnNlZ7<5Q?}6WA~11_O&JO_B z0B?*=QF3jRP!E}*TwJ2Jh7!FaC&FW_Vp%x8CKis>0}ohdr1W@#j^}%$G3sw7g@lww zNy8^vMk->kcxzN5t(3SOmv>R>5mu!{YJ}3;N$rH@W>ZR<>1kI47Bd&K?aaKiJ}H%E zvnpCO(PhJLiq=ve=>gZ;LdUI2kKhCNLg>qwaa=+>N;iV7g1Q(e84+EYAr5o^mqM2u ziQWv{A9WX(wwlC4U*RAuVQgCgn7+4%;K@8o#CU+oC$`YF*sGiYW(9nPs>Y94#xsRk&id zqjZGYaf73-g(mBA6iE<8Toz6C=Qha%UPyICYfkP!l7u6X@wDX58cbI0F2mJ=^0lsa zL%}*nfhN0%R+{u0Rhnd$^BzZSn4l_3L7N*Ad;#^XWPNf!Ix>Nm>EnS(6j|CWG{_5wIsG%kmm$w-8 z>y&TA@2SMsu|X55xV+h@U!w$e3!kboU=3Lw(z1S=jQ&+hf#J7R3anvT9@5Q}+GzBz zP^!p7dU{PNZs@cUFH^IHm`$y`9L=ES21~z0&6#;fx7MWMhV@qB%ha5ehxBa5a+T3v zggz$^>ABF?8T}med3i`*0e!8}Uw}S859tNa*BJeI=nM0ZUYxHs>YqhR7~?8S;}^E#L81U8gErzK@6-}KaC$>ZmFlDRg`GAG#;_MIuGf! z`ISa}GFnG3%Pg&^o9KbPfLg&Pmp9}gy)j>E)W@PtlwV?Le7i* zi&z#Kb{cGZ9@5w43ygY-80RBmou3g4{WRJ`yC2YoqObRXUHOlR`Q9T&`~flLyV0$* z{SNIYj{O#p7`Wjr$|;6^6L?SldUQW|l3!x_2Fcm6{3}rf8Y@^n!5-pa((tl8gx7zB zGMX{_4cm7-O6>DEv{7iiL=Kev1=9kjo$*=zkh^soo}$C|y_!}({Qr31ZPI<5J2G;i*md8t4y z3$(px&EpK)P$1zM-YA=%$0^_EjMMH+va~2hY9?#KULjfNOxy1J?nc1Fi=? z1MCDo1>6LD61WA3)MZx#&jPmr&j7Clo&sJE90TqEBC**n;4p9x@FZ{_5UI|(fCIn- zz<%H%Ad;Wm4D1CS2KE4t0MU-@HXz!P5jke}0`CIe4ZH`4MrFi}85)*707T=m2Z6T& zj{$E1_5p7K9tR!-!jA%872?Z{9*=V#!)nLKM;m ztta|X%z`!xnsh~Ljn%-KEJ|ciNShp#q0NCd2O4R~NLqq67aE>VmdE5LF6Kd*2SxI` zD0%Y>{`vha&7;_>LiTFnkS+Sp=^tXPj=;0s@C-c_l)l!&RQR66{f!IGxWM9U3QUFX zP2AtOK-a-c=WAgq`o2Vek%+Xp25w4_-#<~)Kb#NjrNl|;TbiP&i;R;s)a+u^68dzp zvT^=7x~&S9F{_(e!*WXbrRehbQ~IzPNq#9hM==+zQO++#myf+zA9eYq==l1J*1#@b zik^iYrbsQeyP!^SQQ1}Oa;aRRmP_Ul^`MP)m(NcxmoE!gzD#}c)63)% z6FIA}qFbu9z=+v`Wd$oPg{{sfa#qvE=4vglY!X&n3R{y;G66>3f_RSvt_s>q+F(Q*Qs}_$!5c~Rct3OE>4rHjR;qA#Wl3{SkCsOcpQONxhD`x z!6$QVVxGzwAtPmDQ#_Ni%^`m6w1?kc9qpqaJ>V{Rls$W zL=7Ui7FPkS}F6f z%>)itWwV98uIS7cr>a?~6%M0RSKA#s1ZV`Rt@$1r_q5@lpUjmd&=0lYv_C|c=JVJR zs6DNUZAl8(PNwaH`q=}V2Gll2<=AT2+_koBSJ=8ZT-y@m?_&lWpwfFQ6vsJI;-I!! z+PaXZ4U0IaO^@J>Ky6^OO>x^Z_2+6IP>O>N<#I#^mDQpRHe0sYAB3jXHFC9Op|)-D z6aqp~fNmw2v%wJu*#f%!M;IS%+Hs4@kNW&3^3@l*CYXuW$_JOk&=VU{%R%1+ZJks? z`WL6jMyi!lU1_39XoUsO0+nCq^B?p1WuK3=P=1E2Sp6q_euL-z|{ zv66DwzqA9o>>#iJ7Av5C1^RO6KpnIcz(O|jYtXNR4zz;@V6hB3HdQct1$rChf!gpJ z4i##kSqpIBxB1KUW_l_3 zPEKwX{+a~?`=ek%Jv+(($~Fm?CCa`FUB?+fWJCuS?~*bfSbU$9J8t};<)Z88-;b2e zoPK{`>9Xl%t&{!;hdi_DkgG3gqSr6tkw42n#wY(d9z+_hAoo3=##b=DQh~QjwtlasP5iL@sr+gSMxq|khNrBD2^!ddxa1)-+V1d?&#G?x(; zPgyN_EorRtXhCP6gCd_OU6ucoZc1Cb@}#)d(yNm_f^Z7X+B#hgz9!jaRB<>iHj`eP z$eY9|b{@O3(b8zUd8IhTm&jAgLyMx-pwxQCt_T6Imf@kySBt;bQ(OLcEl~a#++E-iKfb!24Z_5_QPoJ}r-cz$fiPJAb0m@U$W6M)#^3*Rw2g*asV+-2u=Bv=Lr}EVD z*aa|~?5V-zpR4{_e)?5v0gG=?3(G8+EruTaw1>s(v z0oHnMT|No_ePQ9te-4_j$2ZfKJhW_N)l+|mK4ewD%NVdS>G#0RV;ApQN|xdWmXcj~ zPbq0NdU)Q_6CU70^e}kqH^bI>@@v@t)-d&~{GX{*VvBs*Lo?>M6$|F+L~2QARUAVN z&QQo3tVu6ZS`G#Af2bxcqpU#WgoiT5n*MPQ7Xq~;eICvSx-j|}DJ?MS3~OX?9)tqS zWQ-&{4|q72e2Ln9;0h7JQ$(`32V9~#2Pl!f%R@}3g`RhKC|sg`n}^^Qx*h>nC@UOA zDGq~6VN_mc?RU0Z(SnxuMEYmK?{450D7g9B%CGlO zaw2@4hYAXWukr9I4_A4(!NU#@6^f|54*I$ucpI=Acm&u3JPhmw-VCe&4*~mu2Y>^>F5tt! zeZZ5zJ-}h$E+CubVh3;xcs=kG@LJ#*;5Oh{;MKs#fgz|RNV8B@f$THT<;tN6P*alCeX*9pH{->@!Wox-|>bqwno);X+u zkb@u>K~93)1UU+F733_)U68{dmqAYV`f&w0-tF-q=Q}+f^dRU((37AyL65fk@+xVmR(m|ynUx+7c4&pigPm&kc(7y3 zJRa=aQjZ5axY*;tPA>F#u%q+Ar`lS0H_0B)ql~Qa9uML5RC_+j-p`?2tv}FOP4yR3 z{K-^*Gu0nW^;e(sXSM#0{wMiEjdzN_oa9fL-z0x4*_rB-#iv@oEk4eMC&xV$pIgCC zc&K%_SbU)Mx>!6Rd8&;&MyBAar^GEG@Nw{;34iA5prYnWJk_uFymisjipdRJ3i~$Q zCRvh}37Nj<&mLwdNa2+Hn|_GwK5R$+s>gMHp>XLdhO^M5U$itrge(6uOJf+JKLhJA ziKLzsl}ja+cpNGQ(M}rU6q5)|#xPDXi=_WhgD9hu(8nxm;~>j~+2O*QB|}s&m3C@t zC;hfJY){Im0v{P4_mHiHl$%$H9a#08#@uKVC1t+iR1v@CVi-ZB;c~qe!9vw39pjfm z*gnAa(e^CPc9H~p# zDL*vCH}*d4M7#cJ;MxOV&_W~)6xy~DGOF-C^K`qNQB93q0do^AqO2)K)lNZVygC0NM`l(zGP|4+`FYMtq@FvW zV|Gbf9RB_9a*6)Xy*%dM=m3b=?A`TG{=7IG%<2 zzP(zUIqs!**B_(FcDwd%bQ-Th39Y)uV`j-XfCID{$>G`gAGcy|&f>v#>PUuWCF0pu zT9mv?=EOBQFWUvDvlXZtZ%rn&7uM4~+xbZn&;xtQr&V$5DEU6(N~Cg{?(_qSNdazE zQmUtCCM}X8N$FvSYbB{cYobWrGC67|xr87lv65s-_{|PaOrKu#OIt&rC4w6YmA1uLfWdcH?WmSUHzhqnyM*U8lN zH5SF$oRoI97`1G-3+l~wzJ6ZLlG>=S&TcyqLe8ns-DSQ&3k2*sN3pHdi#ctCTaV() z@f=_OLmX1`2|?I5(Tl48TiN`H+Mt`^!Wq_o+z6A@_}sn?;2Co1_SY1s><-zF{j zDA_3hDLbeCBE#=VOYwMiGLgQ^_>mOdB-ob!2jE!P;)g)&v%5>sGiP}4KJpRsY}w6R z_W`;+|Ga-agUg;b#br<2Ff6H?dT{ai7XBEDv{9b$L!k43KV`f~8?>K5LmJ&2^p?O* zTyo%#$&rk^^B87K8O>ch5HED@3vW!DjBD~Fl=&_+Y4~@5(#dy#CKEc5k{6(iWCNMe zX_p;#ILUE-@(pMclBFY#*P!5s8Oft?HsLXh+OXKb?kW@+I7|KrDJ&c%hm0oc=k8On zyJ5?eJ;dy9v{ENn+XJLdL#qRq?1oo7 zl|MDzQ*>mWTND=^h%2&iqR5JlJDBD`wIy7(O*rdCzoOXLVLFYJbl1_eQliL?itF?3 zL9uiL%!24~fPrEge9Sxfz*^6L9!xuw- zqOE)O=_X5dMU8E zNExzk*chAxMI17WK_-_fH;c&V+|@Qvh_<<86E7!d+I#ESf}M@YtxMP?s!pI0;`AyT z!!xw2OP8m~+en!&0XGAmu(G6RPg||6q#graL)k~k+fJD?;6VHShlk{8zlmk|BG|OqvY1#%dv< z-#*J^%LTe{*HU)r<{hM6#mIDP<}%fiHC#lBOSQ{rpoP88#UhdRgiM$Vzr8BTgqSN< ziJ+Q5t&%-M16HqNS`)2VYw~V22x2`g82ZEDL_e;*nm(*gKep=Q4UKwM;zy_%YQ*-< zC}6liO2q*;XHv!kcw-yTz$5WaS_rhQtt7=#Jc)6^hOL5KCu4T~dYDGKO*`MF(k+pl zB87DZLq6!VCI318{H}~NjwsRmBii!Wy&MzqBv5qW;cN2CIC_77Nse_EE*Ma&L zp@pR3`3_c`HRRR0xC}3xn^<#^0K59rHPQ8-yBQ45<#I63>*?7-di(|(FOJ2&<@H=z zsow2LPxUBB>*ubSm1p)razxrmjlv3d9c~xnq`zb}HZezMr?*&%v+!aYwA0GfF_;B$ z90ge}VWJ&RxVwSf8j0NwZ~#E+VRH9dYJe2sF0_792dR4;Xh&e-8+vwIa3yo&mec~6 z9!g-tm|KZ&sa21@Qs_R2X))_YKw~!xE=--RcD3!dTLyPbgYFqs5_>=MapCGBXi)4H z0!p@2W$cl}y_8W{%29;DyU7v099*&vZu;Hf;a!YKHgXj>P~SB(o4Lm5V6qa~T5F$F zaXTqnHM`YQo~PJh)a(RI)->JV%IUt;j+Z!mu!FCo(3QW@Lyzld%&hPcAT}`F?D7kb zXIp4Tw%6l2AhNKj1~&~plFv#wwD4)QbTyPCvdVUF1T1d7IA%7!n&&a~dbR6|dNziF z_Cv10DLB7*=*m&v#!-vQD!aKIWnAvaMPq{x7Os+~Hia8M;ET=M^C|GHmapcc;Ir9v z_zu5W1=~EXcU(;GfU4EI%5wSED5bUPT36A*ZL_l)XzP_C7bYi?3#~G^$q{m6@*z3$ za%Fzw9_W%p9D?)wIiW>)mit@@^k^X|7vUk%wvnB06iNOAjF5KyeYPmw+=3j61gBMmx6U+Mu|HYDo<&arPhDi@3qXqIy*rxu<5Y9*lPX$c!x=jnx`SBw#@ z7vHUfH0_}pT=T*{RP)7$!5Un8cCrSSUJcdY8q07Eu91w?;Oh608eDdM^b3YR2*+CR zAFW9XHm;7*X_TxUyxzw6%M%n?UOjrZ5?b-=;BXDjgUecMO0GT24EeOLZ&p`ZLpCP$ zCRw?L{4gGkax3Gi_r1$5Z?^Rf6v@seYNWnIFObU>dosq1%(81g&`8ahNrk`c6LjFN zU5$AHb`r=76#Mc5KEj?#v*FhvsNHS3sJMVK4GNNEFFIPl**)0{8O1X*p)*pohDSqQ z+}BIE_^8u?MfIVy+t4erQH=8qAkn)$c}BJw$s3eKrY*v+K87+43NfByKxB@x-ytPw z`>y)d6W|a>WICeK4(>flFlx%O(X=OoKen<>lzpF6BYl4WOeynIO0Z|7?1xq+R+_me zI`}G)g?&$F(r@%d|K$>AcK<;Q*bfG%k&kG&1k|R~g)FXvGBi5XcL&lx2Wk)uMqidr z32^MC@_DPna8oyMSe`N!otkKJI*UwyNLfwR2Tyi(>9(y3{@i({SOlv=h^>%11kh%D z-nNo5OK({UqmX`+l+4MSK$b)=+suT$4#YI+Jv*&y^YyBw6f$UGIs34;@QAv7Ur5Vu z0Se5-(CEhkV64IscN6EtG)`|Zx`h5Vmc>1Gy3#Mhr39-DSfN8aSY1M zq$-p+1nej003`?1p1#?W4N_ts@Z@Azc)TE=l9XUX4~JTo>Y^5u%)>k~dl% zda^g8ajKO1K0%#>4(jI7#-y={H#04qbZ;F+voj)d2h7?|mu@nuIJ>}7D41qwTwi1q z4hzy-ph#(!0Kp4zTPGJzv6Zm960KmH)mfG}erav5goBJlK>_`1-P@r^CFQ#1j+Ptl z9kVcdU3k`7}NDczPUM`WK@gk8H>X=m#t^e1O}m>S)rW%-oD_jdVz< zZyfSX5_g{c0Z@(!AK-78&XbbV+a_4wbdWpKSMPOb$(NsF{m6{&spRg1H$WN(-KLuv=kp;mfwywb;eCU zuR@be8Ap?Jl%h~)NLoc2`)@ZdafQxarfehD-z+V6Hl$>ivCno#i*>hqS**Q%Bd`YA11EWbJ(PD|U@x=4!l!rx zv;06={LDl9rYJ3&U#WVS!lm#IsVu*G7d(*j*Zjd6TGXu|VUqFapgR~sQWW+({vD)8 zEIpjS>U-`U$~m-zvIvU!F3%uakIlFBVCE^7o)TDnTPY}Zq`VAVD=06)s%UHD*Y+?e zS$bJ~S$_BISCr*YBoRlbqcLISO;Yd)b~__n;r?y8Y(Ey>Rt=9J%dt+qS%PA99))H zdLu6&zbL!`TwWmH(la8ADlYxQ69_lUE&FdWY1~x{@&NJ#p#^C(o}h|L%a2dstSElK za{pxczp#AxJuLtK!1CJzJNN71lYb+8=>Ot@-DkNJrVc^D;1hBnl87889^;3R*YAcw zMet?RNueJ0g+@~)8FOeDcDswRU8x22QcMgwdD@?AuVkuhm+hwJc6(wbz}-XWEZpz1 zWF`<+YG~($X(xra+ue$(>BQZYj6AuhznxB-A+Zx{XV$)ExfmpSK;o`LzhX2Qt(Pr@ zL7TYWvh#3v&i=PwVE|(N1*iw!n7lR#tm|PJgX^j5b_weU*JZ^39#ksmZJJc?w)H zMgBRWt5-jfr=Zm?uS1*ZM4k+sU4VzYhMGL&#je#uUSl;+hLcdFI?5)Wqi|Y*LspZg zw78)>Nt;>;E#@_OI+@qVLY~_9_4GwRom*7%nrrg3Gf!&rI7_J3Q+=&QXmk2Ro;jC5 zZ>gbQWos~_N^!VOAk>=N#X2j|T0>cDlvyv0j{W_I43R{a%HMc-SE4iJEy+Tmk zsa|Q*Zm$R>e2dbjFecou-6e-n3zu$%c?pMO_FWRSCLEqtc>``gX%V<5d_DPrzu+n7 zPI_!>y$kOI4{LOne!N2uZ!K4w#EanBW%BLRt0rGxt*y3Bd-m1biT)XztF+o zm$iN1?EyN??|x#pcpZZN^3)Tp<#rlfNq2Z;BCJQ0&b`LcCiqT{h zF(%5Nxd!jTJTz-fKp2VIR8|LI;EysN}CvxO1?f^3_)9WO|-~xH_6v~rqI6omwzQeo(_j|@uFsypO zWbrv(RF3XIl#(~uk~ms``lU17^STy7+O@6Ll%ofLY{vD2PloH3j~{`uM|;Ze@cC%3 zwbW^ELrS)BRd3sT`FiTRne674Lwwfzng4em?FO(;dU$7-0FrEMueRX92I~>8J!bzA zP`ZV+gU`>;$#vue&ObqD@7My=yEc2@ zwhhiyQ`Um++s@L$)xgJqTY*pLr^f6jijXijRfr_9c!w4z)0<+sOo~Y&|(O z;~IktNHbLJ6T)amq2g1Z%2EXCBa9301Jzjt*Xjr*@KaEojdIjstA|g6j;*e?Av)R* z)!D(C(kAKTz!$~$q1V^`==AHM=ETmNejQc2^fxS@=)x}Qs>ikTjBl*j>%V2?xcsR( z6Zn?Vh+FKnp*uTx2b#_h{sjMR@ZmdX37W3kbnUq9-6(M4e987O_!9dCd#9y%739n5 zAsz+$E??r`=vBRmUXICXhxd8GH#~qpxk8V*T}1Ev$wWxs^?Qc&&MoUnp4^*r;R+$& z^6pxb(ywO9)uW*N&W!yK2*C~}-5fr~G?h8Dpn+f4a^G(Q>XCCMcYs?6(A1-T>pnovH9<<9Lz}rlmlL1PA5*YDMxH|??@F5P0k^)^AB3CPlGyc<)fIG8a zBy#G?cLra9zTCSg&fLq$%Y}&IOubfI?n4x3<}vQ}R8kaY;xX*@4PsH8c_-kp@6m|j z3_Iq$N|{f4;l2(hVr?%%dT=wAmhn|hXM`sXO_jnN+cB!lvhX*-Wr%_H=<%CC1(uj& zdxG{hkS(K*Z0%9ow}IaL+7mj|GHf%$W0yZc_(kCOuaytBm1O(|zo&=L_5~A}*mt42 z@JJ>WV;cf?`^F*C?=K3FU##8-yd)`q&b&G^`#&+Vc5^-X-xZB6{El$_v;4o(YGyVS zEx>kaJTl^M>d`OZ4bp5j_Z3EHw~yq>O1bT5IJ9?RtgK52P-Q>1Fm71|`!&H#nru(v z2xz2RJIGadwo5(Ob7uQz2ddxQ8=%Yq%Jc&d0V}|pfxW=Pz#iZcU^nnKcy&K8PH!i5 zA6j@P@E%&fi`MTZ=N?Ml3A_*b9pv8+ybb&T@^8x@1Uo`$puICXM(M+px+P@DI+B|xr(>JhK=*5+hbW^%ohzU$PY&`|S0f9QZ;#{-&>9Co zdex(QT)kwnjcJ?1?d6eg4=y^2l>$LRg>UK%5l_WxZr9#c-jY5 zceiUOEeLKuq_NiEK0BgX=kn~1T(`7Wk*9!n4|U~N*L&#k4BTGsfU8`w!Jsf~roORn zcelHB$D4uKOPz~g{Fb9iSdM<5F8O2b!yqgmSC7`_@D zjkA{tT1Di={;{8hJ|B6pe*|ce$B`L*CpKL?fz#(6>od@hBl}S!B;ERu+@SgPl0khS zH?j!&#cIo!x-3?Em;Xo!mdt zamE>!9IhTKwQ#;9!>#a4=G-~ClDpeBgB+Y(F!9e@N!%RMGB@Y%oaE-*hQb11eaDg# zr;U0oQygciHyCZiXcGA$QhUi8ByT@?4*_qqGPpYZYg+ncuT%<&tCQ;5qZO1|-!I6H zT1lK;hO4u0D&XquA?4kaz}2O=I?i`FnYWU+)7$-&0HzO+bCjHJa*jan0lyXe81NQw z=kDxZh0;kDJi^@>hgKywlR|KrU5n93>w%KamZ4#uIEOw&4h{|+Qv+Y*`fz$C&=?Q< z?%P4#b=Z?89G*Sh5?pHK5p+GLvPV-=^tR+i-IACB%dPFFl%NzAf!BjCxj8AUeTNC$ zo>HNV=Kc_DQw%AioSAT-eMd+T+@3B;8Bv%N+?*ZCNV&cCOliIS-NRqh%{wzwFZ)LT zsAWI?A$9AthfF9NrD{}f3MM6&F|&W^$DSsEp{}au^CmEUux8?41p5CqE!hcfubmYT z7oeHj3pno1++Hx{xI1%uD|~smHo3iE%ANcQt;fJ}sYdkJ~ z$nDAfX%67@iIYRm0}URT+r#}?8F*xF5BFy{{4lq7oyXBIb9=Zy%ZDFkC2)U+iyvsd z{fxKz1gB4Qoj&CfMT1AC-8((*^a%cX9DdlQ9L=)w@Wb5R+rTfryE}oq%jE6`d(IA@ z8jlv2?`>MLaF%TGEzB7dmDp<^bQXWkBZ?Vb-QN^VV^NsMSzGw1 Yh_D?4{jhO(Xy?dCQ}mze|K`2_124A3UjP6A literal 0 HcmV?d00001 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java index 480a65850..cae5dd197 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java @@ -74,7 +74,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol; -import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GridAutoFitLayoutManager; @@ -544,6 +543,9 @@ public abstract class AbstractAppManagerFragment extends Fragment { if ((mGBDevice.getType() != DeviceType.FOSSILQHYBRID) || (!selectedApp.isOnDevice()) || ((selectedApp.getType() != GBDeviceApp.Type.WATCHFACE) && (selectedApp.getType() != GBDeviceApp.Type.APP_GENERIC))) { menu.removeItem(R.id.appmanager_app_download); } + if (mGBDevice.getType() == DeviceType.FOSSILQHYBRID && selectedApp.getName().equals("workoutApp")) { + menu.removeItem(R.id.appmanager_app_delete); + } if (mGBDevice.getType() == DeviceType.PEBBLE) { switch (selectedApp.getType()) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java index 99a7a0145..c2c1569bf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java @@ -310,13 +310,11 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator { return device.getType() == DeviceType.FOSSILQHYBRID; } - @Override public int getDeviceNameResource() { return R.string.devicetype_qhybrid; } - @Override public int getDefaultIconResource() { return R.drawable.ic_device_zetime; @@ -326,4 +324,9 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator { public int getDisabledIconResource() { return R.drawable.ic_device_zetime_disabled; } + + @Override + public boolean supportsNavigation() { + return isHybridHR(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java index 3c757b304..7f91178a3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java @@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NavigationInfoSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; @@ -840,4 +841,9 @@ public class QHybridSupport extends QHybridBaseSupport { } } } + + @Override + public void onSetNavigationInfo(NavigationInfoSpec navigationInfoSpec) { + ((FossilHRWatchAdapter) watchAdapter).onSetNavigationInfo(navigationInfoSpec); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java index e77c9426d..ac491b8f1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java @@ -23,6 +23,7 @@ import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.reque import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicControlRequest.MUSIC_PHONE_REQUEST; import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicControlRequest.MUSIC_WATCH_REQUEST; import static nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil.convertDrawableToBitmap; +import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID; import static nodomain.freeyourgadget.gadgetbridge.util.StringUtils.shortenPackageName; import android.app.Service; @@ -49,6 +50,7 @@ import android.os.Messenger; import android.os.RemoteException; import android.widget.Toast; +import androidx.core.app.NotificationCompat; import androidx.core.content.res.ResourcesCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -96,6 +98,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicContr import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.CommuteActionsActivity; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilFileReader; +import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilHRInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration; import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample; @@ -106,6 +109,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NavigationInfoSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.Weather; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; @@ -164,6 +168,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.WidgetsPutRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.workout.WorkoutRequestHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.FactoryResetRequest; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -193,6 +198,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { } private boolean saveRawActivityFiles = false; + private boolean notifiedAboutMissingNavigationApp = false; HashMap appIconCache = new HashMap<>(); String lastPostedApp = null; @@ -480,6 +486,8 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { // renderWidgets(); // dunno if there is any point in doing this at start since when no watch is connected the QHybridSupport will not receive any intents anyway + updateBuiltinAppsInCache(); + queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZED)); } @@ -2067,4 +2075,51 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { } return null; } + + public void onSetNavigationInfo(NavigationInfoSpec navigationInfoSpec) { + String installedAppsJson = getDeviceSupport().getDevice().getDeviceInfo("INSTALLED_APPS").getDetails(); + if (installedAppsJson == null || !installedAppsJson.contains("navigationApp")) { + if (!notifiedAboutMissingNavigationApp) { + notifiedAboutMissingNavigationApp = true; + NotificationCompat.Builder ncomp = new NotificationCompat.Builder(getContext(), NOTIFICATION_CHANNEL_ID) + .setContentTitle(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_title)) + .setContentText(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_text)) + .setTicker(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_text)) + .setSmallIcon(R.drawable.ic_notification) + .setAutoCancel(true); + GB.notify((int) System.currentTimeMillis(), ncomp.build(), getContext()); + GB.toast(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_text), Toast.LENGTH_LONG, GB.WARN); + } + return; + } + try { + JSONObject navJson = new JSONObject() + .put("push", new JSONObject() + .put("set", new JSONObject() + .put("navigationApp._.config.info", new JSONObject() + .put("distance", navigationInfoSpec.distanceToTurn) + .put("eta", navigationInfoSpec.ETA) + .put("instruction", navigationInfoSpec.instruction) + .put("nextAction", navigationInfoSpec.nextAction) + ) + ) + ); + + queueWrite(new JsonPutRequest(navJson, this)); + } catch (JSONException e) { + LOG.error("JSON exception: ", e); + } + } + + private void updateBuiltinAppsInCache() { + FossilFileReader fileReader; + try { + fileReader = new FossilFileReader(FileUtils.getUriForAsset("fossil_hr/navigationApp.wapp", getContext()), getContext()); + if (FossilHRInstallHandler.saveAppInCache(fileReader, fileReader.getBackground(), fileReader.getPreview(), getDeviceSupport().getDevice().getDeviceCoordinator(), getContext())) { + LOG.info("Successfully copied navigationApp for Fossil Hybrids to cache"); + } + } catch (IOException e) { + LOG.warn("Could not copy navigationApp to cache", e); + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java index 93b05e573..94a9779e6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java @@ -22,6 +22,8 @@ import android.content.Context; import android.net.Uri; import android.os.Environment; +import androidx.annotation.NonNull; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; @@ -39,7 +41,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import androidx.annotation.NonNull; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBEnvironment; @@ -337,6 +338,7 @@ public class FileUtils { public static String makeValidFileName(String name) { return name.replaceAll("[\0/:\\r\\n\\\\]", "_"); } + /** *Returns extension of a file * @param file string filename @@ -349,4 +351,25 @@ public class FileUtils { } return extension; } + + /** + * Returns a Uri referencing a temporary file with the contents of the given asset + * @param assetPath relative path to the assets file + * @param context current context for getting AssetManager + * @return Uri that points to the created temporary file + * @throws IOException thrown when a file could not be created or opened + */ + public static Uri getUriForAsset(String assetPath, Context context) throws IOException { + File tempFile = File.createTempFile("tmpfile" + System.currentTimeMillis(), null); + tempFile.deleteOnExit(); + FileOutputStream fos = new FileOutputStream(tempFile); + InputStream asset = context.getAssets().open(assetPath); + byte[] buffer = new byte[1024]; + int read; + while ((read = asset.read(buffer)) != -1) { + fos.write(buffer, 0, read); + } + fos.close(); + return Uri.fromFile(tempFile); + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 574a65259..de0401877 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2386,4 +2386,6 @@ Select whether device uses Celsius or Fahrenheit scale. Celsius Fahrenheit + Navigation app not installed on watch + Navigation started but navigationApp not installed on watch. Please install it from the App Manager. diff --git a/external/build_fossil_hr_watchface.sh b/external/build_fossil_hr_gbapps.sh similarity index 68% rename from external/build_fossil_hr_watchface.sh rename to external/build_fossil_hr_gbapps.sh index 28a8acf3d..82a341c4e 100755 --- a/external/build_fossil_hr_watchface.sh +++ b/external/build_fossil_hr_gbapps.sh @@ -4,8 +4,8 @@ gcc_version="$(gcc -v 2>&1 | grep -oe '^gcc version [0-9][0-9\.]*[0-9]' | sed 's (( gcc_version > 11 )) && git apply ../patches/jerryscript-gcc-12-build-fix.patch python3 tools/build.py --jerry-cmdline-snapshot ON popd -pushd fossil-hr-watchface -export jerry=../jerryscript/build/bin/jerry-snapshot +pushd fossil-hr-gbapps/watchface +export jerry=../../jerryscript/build/bin/jerry-snapshot $jerry generate -f '' open_source_watchface.js -o openSourceWatchface.bin $jerry generate -f '' widget_date.js -o widgetDate.bin $jerry generate -f '' widget_weather.js -o widgetWeather.bin @@ -19,4 +19,10 @@ $jerry generate -f '' widget_chanceofrain.js -o widgetChanceOfRain.bin $jerry generate -f '' widget_uv.js -o widgetUV.bin $jerry generate -f '' widget_custom.js -o widgetCustom.bin popd -mv fossil-hr-watchface/*.bin ../app/src/main/assets/fossil_hr/ +mv fossil-hr-gbapps/watchface/*.bin ../app/src/main/assets/fossil_hr/ +pushd fossil-hr-gbapps/navigationApp +mkdir -p build/files/{code,config,display_name,icons,layout} +$jerry generate -f '' app.js -o build/files/code/navigationApp +python3 ../../pack.py -i build/ -o navigationApp.wapp +popd +mv fossil-hr-gbapps/navigationApp/navigationApp.wapp ../app/src/main/assets/fossil_hr/ diff --git a/external/fossil-hr-gbapps b/external/fossil-hr-gbapps new file mode 160000 index 000000000..0d8312b39 --- /dev/null +++ b/external/fossil-hr-gbapps @@ -0,0 +1 @@ +Subproject commit 0d8312b39771e08aa7bd1a23c114beaffae0ef11 diff --git a/external/fossil-hr-watchface b/external/fossil-hr-watchface deleted file mode 160000 index 24247ae23..000000000 --- a/external/fossil-hr-watchface +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 24247ae23e1b903ddcc1e4e9cfb4ad7280a77db2 diff --git a/external/pack.py b/external/pack.py new file mode 100644 index 000000000..4326724da --- /dev/null +++ b/external/pack.py @@ -0,0 +1,124 @@ +# File downloaded from https://github.com/dakhnod/Fossil-HR-SDK/ + +import sys +import os +import json +import crc32c +import getopt + +class Packer: + def __init__(self): + self.file_block = bytearray() + + def put_int(self, content, length=4): + self.file_block.extend(content.to_bytes(length, 'little')) + + def pack(self, input_dir_path, output_file_path): + start_path = os.getcwd() + + if not os.path.isdir(input_dir_path): + print('cannot find dir %s' % input_dir_path) + exit() + os.chdir(input_dir_path) + + with open('app.json', 'r') as json_file: + app_meta = json.load(json_file) + + os.chdir('files') + + all_files = [] + dir_sizes = {} + + for files_dir_list in [('code', False), ('icons', False), ('layout', True), ('display_name', True), ('config', True)]: + dir_size = 0 + files_dir = files_dir_list[0] + append_null = files_dir_list[1] + files = os.listdir(files_dir) + os.chdir(files_dir) + for file in sorted(files): + print(f'packing {file}') + with open(file, 'rb')as f: + contents = bytearray(f.read()) + if append_null: + contents.append(0) + file_size = contents.__len__() + all_files.append({ + 'filename': file, + 'contents': contents, + 'size': file_size + }) + dir_size = dir_size + file_size + file.__len__() + 4 # null byte + size bytes + os.chdir(os.pardir) + dir_sizes[files_dir] = dir_size + + offset_code = 88 + offset_icons = offset_code + dir_sizes['code'] + offset_layout = offset_icons + dir_sizes['icons'] + offset_display_name = offset_layout + dir_sizes['layout'] + offset_config = offset_display_name + dir_sizes['display_name'] + offset_file_end = offset_config + dir_sizes['config'] + + self.file_block.extend([int(octet) for octet in app_meta['version'].split('.')]) + + self.put_int(0) + self.put_int(0) + self.put_int(offset_code) + self.put_int(offset_icons) + self.put_int(offset_layout) + self.put_int(offset_display_name) + self.put_int(offset_display_name) + self.put_int(offset_config) + self.put_int(offset_file_end) + self.put_int(0) + self.put_int(0) + self.put_int(0) + self.put_int(0) + self.put_int(0) + self.put_int(0) + self.put_int(0) + self.put_int(0) + self.put_int(0) + + for file in all_files: + filename = file['filename'] + self.put_int(filename.__len__() + 1, 1) + self.file_block.extend(filename.encode('utf-8')) + self.put_int(0, 1) # null byte ending + self.put_int(file['size'], 2) + self.file_block.extend(file['contents']) + + os.chdir(start_path) + + identifier = all_files[0]['filename'] + + full_file = bytearray() + full_file.extend([0xFE, 0x15]) # file handle + full_file.extend([0x03, 0x00]) # file version + full_file.extend(int(0).to_bytes(4, 'little')) # file offset + full_file.extend(self.file_block.__len__().to_bytes(4, 'little')) # file size + full_file.extend(self.file_block) + full_file.extend(crc32c.crc32c(self.file_block).to_bytes(4, 'little')) + + if output_file_path is None: + output_file_path = identifier + + with open(output_file_path, 'wb') as output_file: + output_file.write(full_file) + + + +def main(): + packer = Packer() + input_dir_path = None + output_file_path = None + args, remainder = getopt.getopt(sys.argv[1:], 'i:o:', ['input=', 'output=']) + for key, value in args: + if key in ['-i', '--input']: + input_dir_path = value + elif key in ['-o', '--output']: + output_file_path = value + packer.pack(input_dir_path, output_file_path) + + +if __name__ == '__main__': + main()