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 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 Uw1RnykEUGWvs)lTB4Nwl>#=K4C*&A9W6 zqOJAu=2YJhwRMx#&xp3Jin)Gu)YcAzzbV?fCg%FasIBb=|AJ`i+L-H`;+R2yQ--6K zH(LGkqUCil*GHnZwi*0$qOHv_*SEw`gMLxKBXyco3ve(E1Of)~kco--}ug2Ce@fYP~LK z{rget4MFQ~MXj$6TK`_udSlS~cca!rLF?a%S`P=Ue=BM|614tC)cX3M_1B`-Hw3M} z615%;T7M~OePhu2Ow@XN(E1Be>zjhspO0GK614to)OtK<{h6rsuAud&qt<(Z)~`pc zZwp$#7Pa0Nw0@w0=5j{Zi2S$*A?mg4T~mtq%vSKOD6_8nk{SYJDtd{gBT`GZeI8ULHto zjYEbvzz!u?5r;w%{kj7Q7UEC{33gwCm2oJP33g9{^~9mjlVBf8u--Tnz-ZoarYu0bvVJHknZJ-C% zvlX;i8;8Q$s13BjTK5KR*2SR!M}dQY2XK`2aVV^h+LVqX72IY+910tp^BDeUb5$G) zSHWTG*W*z5=CAm_U!2pt8ZTUE;8)8|Hu>MHUxr&f5w*H6Xfy96^h!II747?j|J7#B zT3TOHR7%6(d?={q0rW_iT^a#kGEzc>AwQ=YW@)hk9kD_0N<0RlIOvHPZg#AmOJjBk;c#%jPf5q%v}= zi{?+FHZ`PEhvo-Sn}JmS16TMet&0Yu;u?b&&D*Yc8XO~HOM-tnYQ4=2v7$*vL&fvo zh+5wYY1c)QjD)JFwMhm-rA;ysnx&}CT|q0#4+U)&KcK{<;JQ0`yWM$*)lNli_6M!C zIUTjZ5P3Cfb9>O{lkv8o&FgWGUH7TD+saQzjF`{FTZ1-l#POibXX7o_^SP+iO;-DS z)M|%4^Eabb7)5VJt;T{jXQEaZOZ#Hcgt}-BCHP*O;mYnl;P*7U3}!RkKziG)cic_d z=HB#&u==Bl90UA|WJ7B>p4y$gdVYK6f-|1KN%h?@{R5mUj;itC~_ z*SKkdsZhbLt*04|T3zEKY}Q&&$QacO`CZlB$(ZQ%YwK&%y};K5geqUbQ#Sg&m^WWQ46h2Khw_1Bz+k4hdB*$v?gy#W zI61Vr+Mrk2GgkSP^=vZY7&*@}RohIwmFZb?=?Y9c%)OIdHZk>oA}Y@`cMB`Mkoxt|8{Z!{C3o4 zQ_$*nysY1jT4DIZr*Q_n9dT;>Zqx?l|C{mJp!GMSRyYN|nVbXPG+O&T$KUFm2-d%o zF!r5%>-^U^ zSpA)US=oFixiY@vywS?FLF?}(cgA-OQ*f+(Kg+=84?CRVkE|Ehgf{<%dhPW6BdeE2 zm)%18$MM32H64N6$)u6DFC&pJ#Ivz%{wx+o;V8L3O!eBvS2RVf)j-hZom9sS^R``M z9Ig6Bst+3$@*J(eurH+gjX|q7QXLF`-Reb!RDQ+2pr~-&Av^DMxtd*e$YFEcrLwxD z%y7F79)Vm&p5K``z#YLPJuq!0$MY}+Uzz;N1AB0!no(H zR!2f3eYC=nHSQ7`2&++PbBl8dld4E$WHs}9S2QWt?1mHKFmkH0GA(f->|PEr*U9*G!uIs^4MiAPD^)NGs~t) zBF7jF5~m&rTHl-6NNT?V*CaD_}={W2`GqFUrfv>Ex<4sa0u{B=_YcwP^)QNs@Opj9 zMd(@9<_HwK?cg*EpE#aM3cP7bQOf;q4{itfIo-X`U? zqtxO?L6vnElPQL#zTOlRcUhA~7A0-q2swn_XVZP$&vOu{D9Z?5-h(>Ldr{_5xKZR8 z7Y;PW$#@y}eDv=)k-m%X<2(6YzMJo79+(g2h52Egm@npy`C}fLPv({R9ZPfV^NxrA z;&sXEl-Dhx}0a?=u}_ zy2x~r=_b=rrmIY6neH+jX1dIDn&~#vai;4`=b7#^AIN+m^NGwiG9Sr&CG(lgcQPN! zd@1v(%(pTh%X}^Kxy<)6AIy9)^U2INGaq&Nb1|<)r3aGvG0HIQ%&#xQ&rJc#{2uw! zEe|dyFD{WMm&%(<<V5L8Tizjmy5-?z_KS?z0Y;(+^BqCs0lRWh5Z)&@CNh7r z@u~JVmi$!hitYu0dAm;xHai%!g6(RB&;_zKK%Vymt@ijGhyzQ1+mrl`Hp7-^17pUs z;tT+FBSToDgW57@e<(bV_(^KEpD7gd5) zr{B#Pyn?rg8{-idbA8|Abkle*y&hG>X~5+SOj)rqJUMbl3)2)@+>2$J++b) zK`<6f2E!lqc7>?NQ%u?$6yUT$g4@Fmsm%9SLunSdF*6PC8#IZc0xe>q5LH4^aqpIU zAn{4U9Xx##HN_J`?}d9fr!ek;XX>slj^fXO6YVi*Y0(AVW|nXIE;ouio)juKC$h?U zY3VV}9Pdc6&gP2|k)EO+4Zak7Cm!!0&Y=x{l8N&NJ`%H{W^+hvCGCfCu%}9g)I>faHBF)gQ!&oUG@KI*{8cQgK zHi#CXM5B{<%$~OxN376o;G%BInlxyD!AEVyOe&)_tuQke4#|7~@l1=k#36BGp^Kq& z`8sG_R$GJCYtohj*2eNY%>NG`W%W$oH2sf!nFJ#*q*)(J)-fl?W3bochma|fV#hUk zFqg>}fqP>3RK`DT6kmxRvvcor@-j2(TFJ8Et6WpI*skF-5s|6@*G*e1%nUwBLpOJa6MR2ycIbF`E04-%9%>qJm1DBLw&XLxNo~E1W0f5)`ls>2g^ic+ z&Wp}0=XubQ+EVLb*Y4x8)CBX6d6ELF-l8rLX!zuw>hBCptG5NDYVY7CxmN`~6&9#r1*WNE8AmfICi*o$LW4GK#Z>lGp@ga#`8FF?ge= z!CJf%>1%EtoCCy^Xp^nJKCtr)<=Wj1DAmL=Y}L#gPhWzVGiqI6{~8uEPkr+kBIM+mjJaO`U6= z^n3Vjo=Mv{cy_m?dCK$FJ+Jo6r&6NTPMhzvCN&WfX8D=qP`o)wzGsqqhjnF@h;oA! z7THHOR?j5|5AHu!nYcK3gbNG&VEcnAxNJyQ5I<$(LRY)K zi`~O^H5~4eUQ5f&{V(~>>dw<9XuJ@GSoKon~ z;Gm$ayP;{jm9h5BacAZxu2^hCQUY;{S*oeK z%GkBn(Jj7~lZ`Gqqfh?x6?}I83oo$)19-@f{osJgckiIGm@m%k+&}ld1J$3ULHx)! zOII8wK&G-~K$)M>2aaz9-y_+s*)zuh1AJ^!p3HaYW-Cgd`!lmqkW_4&#H2tYIFmsF zTQb$zgmKO)_Z)M}Y_mm5`RU}dCxh1FfEEQzIq#MWPvnmMLM*>2C>{Xiuz<>r31~bL z=V+?chYb$Wb3A}2J3VW)?xO)-Ok#O5Xhm#5tHUW#sqKFJW;$r~VAOiXkc92=v!04n zpSZYeQoEV8D-YV!Ue0$}Z6a#@a?m=J7Vv&Kc=_>86mce{XS_wlsh{&-5HVN4`X zjo7{s&kTpqfceaM{;7MHh+euWLK1^H0!YQeXJ&CD_c#&dfM|wO@)n5~cu`%Q!T=^# zf%L48skU@Mz%kXR=suB1oWSHTI zV#hc{?Jz1>!bP?};s!x9<1Hj|1nIN3U~i7pA&OUe+dQHuiD3I!+!PSPYBa!>#-RXv z#s|-qMua5{7OFuJNXjWk4^-PZ3c>_0gl;Wkrughp6;WVG#&ep?5Ck#Qn%f*`Q7pHs zW486U({H|Ft<%+*nHKZ;b@?kh5B0S_cZ=p<<^3SW_?M$KSK|B^BBE29zw&M0$N;u) z*FR3Uq_`&C!-<$7YD7H7qJMvu{MkQp7bOSEqzKcy(Xrl)V0JnXe_~THPD@;s95Q2Z z%pY|2ID9`vvfO=xGKNnXWeo=sN-;ZfQ10+Kn~Mz>rf~<)L`HG^z2)p>qY|(=?NK7( z#$$oT`OT?mJOwkAoi8X(WW{5s69pmiq7_HQbWG67)!)bLNv3{rG>jRBT0NT*TK!lU zILnL9pBn9O|HF1Cys@1)tya^C8-F~-XlGMg_C(qd*i~AB6Hr%x_Rn8sxddkWzE#BM1cjPQTt0XtHN3KFw`?>^*?Av?$<&mRZ0ctm!i zr!!T^ha2n*htj+w8E_P{FB7iyXjC|TrKBHOo;fMI@czw7^+!q&jez5OY>0R{oF0zv z>yDfN0?!8{FiUX&Y)zPgc}H}HHW>6&g!hjor5>38E^O;y9A-4U9*FO_!GuG4v7McG z`*Z1gXoJ7cN3N#@R=)thHXaYIBN4zkc)rJmFwfxVK;Zu<-a`S-i$aGKKrYheuo(^< z%59=UAM0vEe;iE2Tynj~iVmmQ1%d=X667a9&F!wXIzH!qk5O6c*PQ=Xb{>8K&i`L? zf}L^xOU?+N`+t#OcOgPBJ(el}EA#D0zSV(vl&2PuVB(t>vBL1G9* zQV(IepB(!fTgg3R9%;a*(NXL@4q<)(_lm>l%`0vUgm{%LoW)k$%hyr$J>}1s0jj<+ z67yI)TXPE!FE$AzzUju20!gm3^b+5o(}{0c8jAbbiEnAeu7OU(C3)P5!jM_*+k*(v zdbroO65*A>)@2)TN=?KAZn@XqF05^v5bbK;-p)0?MbZ5__Y0lANQ?gQI+O70_JAomfx_09Y~Gbt^=*fHJ% zzBb=SQb4-TP8eera~~tleUxl3%Y7sOq&`zxGMp5E3}>sR?^|$)x7qwh8*$;sKafG5 zA@1iO9W+Dy7ze!82|~25G9Ot7;_)1Oap@ZHGp&DycnlGbUxv64$1CWNj!?HYK5ZB3 zcoMk+h#kgZp79Lvs1RAk31S)3-EA_e@ujDJ-rN05%h1Mv^nQ}R@|f_4LRERxv@u7) zYp$kEj5&WfZBL!(^xKgZdG;{s*zb_%7xB&k7I_6|vwtG5(Q_Q$VHy9t4I}X~?V23{ zSLipdS64p=m2{qt-=D3y%kO{I(l&~!s7s^I^wfHj!Gh&(=QpkMNQmX-1LNCPq0OAg1uGbSML!`rk{3IH1n{E-G*RwyDy;6H&i^JEExn7MoJ*w<1SO zd?S*S{&kFi`fKo!`m2#x;#V*qYRg-wzZCsY3(}R&M3R`^jHEPu0lrxOCeb2`zkQxs z%1!xP9ON8KgMT&-;=#818*z|xFRlKWILLXIR=+lu&5Lo6vo5Xe@eMXhe>4tq+ND96 zQP%Jyagft44F-omy1ftwIqlM5a0R3qFpeo9o((Od{QyfxzgT)0D*=p@bFM?K;0^+o zj)Ad6bzDZkI5vgrkaxJ1fZ@9a#*U?d0EEPRAjn`+*`AMsoONliEYE>)x-HkC8(=~M zTa#es2&;i`7A{_P>TFHtpaAOZszf$XCwg;ppvK-5cKvLw&H?pnJ+r{-Z0<+)04Et7DMKB1;MPFxIy<-|)hyfKS?^C;7%k+9OAcB{6O$V? zEsJy^@vP;pNF*YC7V|mvE$5z=dtf$*OXL!( zp&m{0m4gCyraC(($yy^F*z?oaJGtMa{VOpYT2iGvwp^0eX8CQtv4{*eqI~&)F?M0H z8@JDEU6 z&DMv;nesy5-frb|4lJ|8RDUj<+-m5)o;?B;`F1KOsJ25^(#9%Icm~%!vxGxNI>)or zoda8H--Swi(Q4R@SVk#L^^@-ENw!jkaz-@nev4stpd38m>ECVvAl-bvZR1$7863)? z748ypE5o5A7I%*Q>O2=mB0M}>GJfq+gx6=g$6<>xCzN6*{cfPg^w}k&>DK10N3v=Yq`KR}h#-2y;%hlZ;$KBPj%u{;2 zTj?Fk-2!!;!zR+!6x*_AoNU?tU-k(s`w7UT^E`u;Z}{snWtaSqUZ#9fE1~VK98LEx znGM6XDfD@#QujwW9h#TQWJ$`=>qrwn7>f|_#Zuv=c;Uj;By+n)@A70>rJwi(ReBf8 zq~u{@xClpud?$%(Jk^|*FzvaY`|@*0N5#dFPS>%W4e93q5GS3{|qwPWfwVIg&;!Y#f5JW8yXa1`~Zo50G35aC+-F1bck9Y^- zZZ3XK0uchlSv@O`_yFQ;p4|k94j|6rF+kXL0>s%n&N%kb0CC1nBp^dvxph}xh7HBg zc?OJ_JDnkuuCiRi>kkCBoDx~g!HTOjMbs>5jBNs>UUA9~?pacoNlix+L^RL=h#b^Sv8FR^H92w6Px>sX9lVCE9`Zy@s$#oIqrnCH;y~8oR0Jp zBc0<;w0hbd@D@*g!GCO|!sVKNY^3?9r;t`Dv#5C~X!TV5SW4ghSjscvr$>mK-W+;38v=)+5E?P#+LGjm78B8B z?>Oay9x=B4+XPlPawElatt2xk#cpl;3o&S7x8&cj-;lU9CHXhRaA{MHnd6BlH?doG zXj5_mE|J(BRvt7L75njl5hJH04Hu>M{*;l#K7QnDYFpZFYMa_hQtuF_Ia*YdRBDvj z`^*W(uI?+enp^D(yN*LwxZ7GJ6@Ngz;*NC9zxw(XV$;=7AX8;snn4bPdCm`ZvKb{g zo*&X7xtA#Q*7O5AoViGwIQhS$cw{FOkjcO^aJF+E z11Dk926Nc_KxPB&Thk8}k=a0-HOIgwuOYP?<$TmiIYh@`B&?&uk?FuSoYa7^Pi6yc zlJ7D4nKRlX&!fYU$v~U45-_~URG`gC2?U$SL`ZE9Q&ygx0F1K|a0ZY~K$|lL(Ac0c z+8j`XM#r4d=DYzo1$0K6(>!qob3O)b&M_sA5V}h3ltXBTkQs~<37~S$$KVLws;jwLF$Z0Q+r)VZ$jd^c<-=!aL>qlF6_FASrmenP@csP7F9SEuwhjNf6 z#qa9O`lj+KZBnm1BRie!Ri%@?AYDsZNhf>NPukN+f_XZq?NlIbmN@A19;>Qe@F-jT z5xbJ4O;V|zj#|CsC$-h2({dQclqZGqOIwyu>f+9R70P_SevAvVaAP3V$fKVo?(A2{ z9`PeM{h0K6YaqeZW1FsK6VFJ$*kuRM66Jh|SckCz1RTjzl%B?lsCytKO?lGl14K!T zfDvmPop%*=l2C%2j_wCY`5 zzXqe3C}a+`<>VlT6D;wmJLXI74AL+oW%?)c|oFT;piKuwvwWB5U3 znZ2WyL4BFM6Mx~zh|J1~!I03HSvjH&v6or7M^{|zWf+PbVlT6I@EjhH**nSFh`r3- zHz%=|Svh;-*bSKRiTOb6Wmb-Qki}kxk-m-C%j})>ZNwh?(!f5HMZ_L^y=bE>BKA0J zh&IXDh`rlWoB2TOA^zz1omZYwkzuHmh`s&k8Ym30cSmZYRwDN9OzoSK*y9)>o{3tC z*!xgwGaraO#2@|8nyb2YYN)|bC!y|p(lzYUgAVzT4({R5AjDq&MXlXQ>?I@IQep9f z|7wc6 zi|q_QIY9v&Xy_V$#=o?1D!=u}!u;amfnS}U@-O=8ojaE&J$?81&Rc)}mmgi2dS&v&{81*PGk~du_T19s zMN5(AVdlitbe?i=OwLW6Fm&+WbRD_$lUdO5G@BpvA_Q>SO9 TryzxUPM++S|E>4m{_y_+FSf*A 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()